Введение в физику

При разработке игры часто нужно знать, когда два объекта в игре пересекаются или сталкиваются. Это называется обнаружением столкновений. При обнаружении столкновения, как правило, требуется, чтобы что-либо произошло. Это называется реакцией на столкновение.

Godot предлагает ряд объектов столкновений в 2D и 3D как для обнаружения столкновений, так и для реагирования на них. Определение, какие из них использовать в Вашем проекте, может оказаться сложным. Избежать проблем и упростить разработку можно, если понимать, как работает каждый из них и каковы его достоинства и недостатки.

В этом руководстве вы узнаете:

  • Четыре типа объектов столкновений в Godot

  • Как работает каждый объект столкновений

  • Когда и почему выбирать один тип среди других

Примечание

В примерах этого документа будут использоваться 2D объекты. Каждый 2D физический объект и форма столкновений имеют прямой эквивалент в 3D и в большинстве случаев работают одинаковыми способами.

Объекты столкновений

Godot предлагает четыре типа объектов столкновений, все из которых расширяют CollisionObject2D. Последние три из перечисленных ниже являются физическими телами и дополнительно расширяют PhysicsBody2D.

  • Area2D

    Узлы Area2D обеспечивают обнаружение и влияние. Они могут определять, когда объекты перекрываются, и испускать сигналы при входе или выходе тел. Кроме того, Area2D можно использовать для переопределения физических свойств, таких, как сила тяжести или упругость, в определенной области.

  • StaticBody2D

    Статическое тело — это тело, которое не перемещается физическим движком. Оно участвует в обнаружении столкновений, но не движется в ответ на столкновение. Чаще всего используется для объектов, являющихся частью среды, или не требующих какого-либо динамического поведения.

  • RigidBody2D

    Это узел реализует симуляцию 2D физики. Вы не управляете RigidBody2D напрямую, а прикладываете к нему силы (гравитация, импульсы и т. д.), и физический движок вычисляет результирующее движение. Подробнее об использовании твёрдых тел.

  • CharacterBody2D

    Тело, которое обеспечивает обнаружение столкновений, но не физику. Все движения и реакции на столкновения должны быть реализованы в коде.

Физический материал

Статичные и твёрдые тела можно настроить на использование PhysicsMaterial. Это позволяет регулировать трение и отскок объекта, а также определять, является ли он поглощающим и/или шероховатым.

Виды и формы коллизий (столкновений)

Физическое тело может содержать любое количество Shape2D объектов в качестве потомков. Эти формы используются для определения границ столкновения объекта и обнаружения контакта с другими объектами.

Примечание

Чтобы обнаруживать столкновения, объекту должен быть назначен хотя бы один Shape2D.

Наиболее распространенным способом назначения формы является добавление CollisionShape2D или CollisionPolygon2D как дочернего элемента объекта. Такие узлы позволят Вам рисовать форму непосредственно в рабочем пространстве редактора.

Важно

Будьте осторожны и никогда не масштабируйте формы столкновений в редакторе. Свойство «Масштаб» в инспекторе должно оставаться (1, 1). При изменении размера формы столкновения всегда следует использовать параметры размера, а не параметры масштаба Node2D. Масштабирование формы может привести к неожиданному поведению при столкновении.

../../_images/player_coll_shape.webp

Обратный вызов для физических процессов

Физический движок работает с фиксированной частотой (по умолчанию 60 итераций в секунду). Эта частота обычно отличается от частоты кадров, которая варьируется в зависимости от отображаемого объекта и доступных ресурсов.

Важно, чтобы весь код, связанный с физикой, выполнялся с этой фиксированной частотой. Поэтому Godot различает between physics and idle processing (обработку физики и обработку простоя). Код, выполняющийся в каждом кадре, называется обработкой простоя, а код, выполняемый на каждом такте физики, называется обработкой физики. Godot предоставляет два разных обратных вызова, по одному для каждой частоты обработки.

Обратный вызов физики, Node._physics_process(), вызывается перед каждым шагом физики. Любой код, которому необходим доступ к свойствам тела, должен быть запущен здесь. Этому методу будет передан параметр delta, представляющий собой число с плавающей запятой, равное времени, прошедшему в секундах с момента последнего шага. При использовании частоты обновления физики по умолчанию 60 Гц она обычно будет равна 0,01666... (но не всегда, об этом ниже).

Примечание

Рекомендуется всегда использовать параметр delta в Ваших физических вычислениях, чтобы игра вела себя правильно, если Вы измените скорость обновления физики, или если устройство игрока будет тормозить.

Слои и маски столкновений

Одна из самых мощных, но часто неправильно понимаемых функций столкновений — это система слоёв столкновений. Эта система позволяет создавать сложные взаимодействия между различными объектами. Ключевыми понятиями являются слои и маски. Каждый объект CollisionObject2D имеет 32 различных физических слоя, с которыми он может взаимодействовать.

Рассмотрим каждое из свойств по очереди:

  • collision_layеr

    Здесь описываются слои, на которых отображается объект. По умолчанию все тела находятся на слое 1.

  • collision_mаsk

    Здесь описываются слои, которые будут использоваться для сканирования телом столкновений. Если объект не находится в одном из слоев маски, тело игнорирует его. По умолчанию все тела сканируют слой 1.

Эти свойства можно настроить с помощью кода или путем их редактирования в инспекторе.

Отслеживать назначение каждого слоя может быть сложно, поэтому может оказаться полезным присваивать им имена. Имена можно задать в разделе Project Settings > Layer Names > 2D Physics.

../../_images/physics_layer_names.webp

Пример GUI

В игре есть четыре типа узлов: «Стены», «Игрок», «Враг» и «Монета». И Игрок, и Враг должны сталкиваться со стенами. Узел Игрок должен обнаруживать столкновения как с Враг, так и с Монета, но Враг и Монета должны игнорировать друг друга.

Начните с именования слоев 1-4: «walls», «player», «enemies» и «coins» и поместите каждый тип узла в соответствующий слой с помощью свойства «Слой». Затем задайте свойство «Маска» каждого узла, выбрав слои, с которыми он должен взаимодействовать. Например, настройки игрока будут выглядеть следующим образом:

../../_images/player_collision_layers.webp ../../_images/player_collision_mask.webp

Пример кода

В вызовах функций слои указываются в виде битовой маски. Если функция включает все слои по умолчанию, маска слоя будет задана как 0xffffffff. В зависимости от ваших предпочтений, в коде маски слоёв могут быть представлены в двоичной, шестнадцатеричной или десятичной форме.

Эквивалент кода приведенного выше примера, в котором включены слои 1, 3 и 4, будет следующим:

# Example: Setting mask value for enabling layers 1, 3 and 4

# Binary - set the bit corresponding to the layers you want to enable (1, 3, and 4) to 1, set all other bits to 0.
# Note: Layer 32 is the first bit, layer 1 is the last. The mask for layers 4, 3 and 1 is therefore:
0b00000000_00000000_00000000_00001101
# (This can be shortened to 0b1101)

# Hexadecimal equivalent (1101 binary converted to hexadecimal).
0x000d
# (This value can be shortened to 0xd.)

# Decimal - Add the results of 2 to the power of (layer to be enabled - 1).
# (2^(1-1)) + (2^(3-1)) + (2^(4-1)) = 1 + 4 + 8 = 13
#
# We can use the `<<` operator to shift the bit to the left by the layer number we want to enable.
# This is a faster way to multiply by powers of 2 than `pow()`.
# Additionally, we use the `|` (binary OR) operator to combine the results of each layer.
# This ensures we don't add the same layer multiple times, which would behave incorrectly.
(1 << 1 - 1) | (1 << 3 - 1) | (1 << 4 - 1)

# The above can alternatively be written as:
# pow(2, 1 - 1) + pow(2, 3 - 1) + pow(2, 4 - 1)

Вы также можете устанавливать биты независимо, вызывая set_collision_layer_value(layer_number, value) или set_collision_mask_value(layer_number, value) для любого заданного CollisionObject2D следующим образом:

# Example: Setting mask value to enable layers 1, 3, and 4.

var collider: CollisionObject2D = $CollisionObject2D  # Any given collider.
collider.set_collision_mask_value(1, true)
collider.set_collision_mask_value(3, true)
collider.set_collision_mask_value(4, true)

Экспорт аннотаций можно использовать для экспорта битовых масок в редакторе с удобным GUI (графическим интерфейсом):

@export_flags_2d_physics var layers_2d_physics

Дополнительные аннотации экспорта доступны для слоёв рендеринга и навигации как в 2D, так и в 3D. См. Экспорт битовых флагов.

Area2D

Узлы областей обеспечивают обнаружение ** и **влияние. Они могут обнаруживать перекрытия объектов и испускать сигналы при входе или выходе тел. Области также могут использоваться для переопределения физических свойств, таких как сила тяжести или упругости в определенной области.

Есть три основных варианта использования Area2D:

  • Переопределение физических параметров (таких как гравитация) в данной области.

  • Обнаружение, когда другие тела входят или выходят из области или какие тела находятся в настоящее время в области.

  • Проверка других областей на перекрытие.

По умолчанию области также получают ввод с помощью мыши и сенсорного экрана.

StaticBоdy2D

Статическое тело — это тело, которое не перемещается физическим движком. Оно участвует в обнаружении столкновений, но не движется в ответ на столкновение. Однако, оно может передавать движение или вращение столкнувшемуся телу, как если бы оно двигалось, используя его свойства``constant_linear_velocity`` и constant_angular_velocity.

Узлы StaticBody2D чаще всего используются для объектов, которые являются частью среды, или которым не требуется какое-либо динамическое поведение.

Примеры использования StaticBody2D:

  • Платформы (включая движущиеся платформы)

  • Конвейерные ленты

  • Стены и другие препятствия

RigidBоdy2D

Реализует симуляцию 2D-физики. Вы не управляете RigidBody2D напрямую. Вместо этого к нему применяются силы, и физический движок вычисляет результирующее движение, включая столкновения с другими телами и реакции на столкновения, такие как отскок, поворот и т.д.

Поведение твёрдого тела можно изменить с помощью таких свойств, как «Масса», «Трение» или «Отскок», которые можно задать в инспекторе.

Поведение тела также зависит от свойств мира, установленных в Project Settings > Physics, или путем ввода Area2D, который переопределяет глобальные свойства физики.

Когда твёрдое тело находится в состоянии покоя и какое-то время не двигалось, оно «засыпает». Спящее тело ведёт себя как статическое тело, и его силы не рассчитываются физическим движком. Тело будет просыпаться либо при приложении сил, либо при столкновении, либо при помощи кода.

Использование RigidBody2D

Одним из преимуществ использования жесткого тела является то, что различные поведения можно получить «бесплатно», без написания какого-либо кода. Например, если бы Вы делали игру в стиле «Angry Birds» с падающими блоками, Вам нужно было бы просто создать различные RigidBody2D и скорректировать их свойства. Штабелирование, падение и отскок будут автоматически рассчитываться с помощью физического движка.

Однако если вы хотите иметь некоторый контроль над телом, вам следует быть осторожным — изменение position, linear_velocity или других физических свойств твердого тела может привести к неожиданному поведению. Если вам нужно изменить какие-либо физические свойства, вы должны использовать обратный вызов _integrate_forces() вместо _physics_process(). В этом обратном вызове у вас есть доступ к телу PhysicsDirectBodyState2D, что позволяет безопасно изменять свойства и синхронизировать их с физическим движком.

Например, вот код космического корабля в стиле «Asterioids»:

extends RigidBody2D

var thrust = Vector2(0, -250)
var torque = 20000

func _integrate_forces(state):
    if Input.is_action_pressed("ui_up"):
        state.apply_force(thrust.rotated(rotation))
    else:
        state.apply_force(Vector2())
    var rotation_direction = 0
    if Input.is_action_pressed("ui_right"):
        rotation_direction += 1
    if Input.is_action_pressed("ui_left"):
        rotation_direction -= 1
    state.apply_torque(rotation_direction * torque)

Обратите внимание, что мы не устанавливаем свойства linear_velocity или angular_velocity напрямую, а, скорее, прикладываем силы (thrust и torque) к телу и позволяем физическому движку рассчитать результирующее движение.

Примечание

Когда жесткое тело переходит в спящий режим, функция _integrate_forces() не вызывается. Чтобы переопределить это поведение, необходимо сохранить тело в рабочем состоянии, создав столкновение, применив к нему силу или отключив свойство can_sleep. Помните, что это может негативно сказаться на производительности.

Отслеживание контактов

По умолчанию твердые тела не отслеживают контакты, поскольку это может потребовать огромного объема памяти, если в сцене находится много тел. Чтобы включить отчеты о контактах, задайте для свойства max_contacts_reported ненулевое значение. Затем контакты можно получить с помощью PhysicsDirectBodyState2D.get_contact_count() и связанных функций.

Мониторинг контактов через сигналы можно включить с помощью свойства contact_monitor. Список доступных сигналов см. в разделе RigidBody2D.

ПерсонажТело2D

Тела CharacterBody2D обнаруживают столкновения с другими телами, но не подвержены влиянию физических свойств, таких как гравитация или трение. Вместо этого пользователь должен управлять ими с помощью кода. Физический движок не будет перемещать тело персонажа.

При перемещении тела персонажа не следует напрямую задавать его position. Вместо этого используются методы move_and_collide() или move_and_slide(). Эти методы перемещают тело вдоль заданного вектора и мгновенно останавливают его при столкновении с другим телом. После столкновения тела с другим телом реакцию на столкновение необходимо закодировать вручную.

Реакция на столкновение ресурсов

После столкновения вам может потребоваться, чтобы тело подпрыгивало, скользило вдоль стены или изменяло свойства объекта, в который оно попало. Способ обработки реакции на столкновение зависит от того, какой метод вы использовали для перемещения CharacterBody2D.

move_and_collide

При использовании move_and_collide() функция возвращает объект KinematicCollision2D, который содержит информацию о столкновении и столкнувшемся теле. Эту информацию можно использовать для определения реакции.

Например, если нужно найти точку в пространстве, где произошло столкновение:

extends PhysicsBody2D

var velocity = Vector2(250, 250)

func _physics_process(delta):
    var collision_info = move_and_collide(velocity * delta)
    if collision_info:
        var collision_point = collision_info.get_position()

Или отскочить от объекта, с которым столкнулись:

extends PhysicsBody2D

var velocity = Vector2(250, 250)

func _physics_process(delta):
    var collision_info = move_and_collide(velocity * delta)
    if collision_info:
        velocity = velocity.bounce(collision_info.get_normal())

move_and_slide

Скольжение является общей реакцией на столкновение; представьте себе игрока, движущегося вдоль стен в игре с видом сверху, или бегающего вверх и вниз по склонам в платформере. Хотя этот ответ и можно закодировать самостоятельно после использования move_and_collide(), move_and_slide() обеспечивает более удобный способ реализации скользящего движения без написания большого количества кода.

Предупреждение

move_and_slide() автоматически учитывает временной шаг в своих расчётах, поэтому вам не следует** умножать вектор скорости на delta. Это не относится к gravity, поскольку это ускорение, зависящее от времени и требующее масштабирования с помощью delta.

Например, используйте следующий код для создания персонажа, который может ходить по земле (включая уклоны) и прыгать, когда стоит на земле:

extends CharacterBody2D

var run_speed = 350
var jump_speed = -1000
var gravity = 2500

func get_input():
    velocity.x = 0
    var right = Input.is_action_pressed('ui_right')
    var left = Input.is_action_pressed('ui_left')
    var jump = Input.is_action_just_pressed('ui_select')

    if is_on_floor() and jump:
        velocity.y = jump_speed
    if right:
        velocity.x += run_speed
    if left:
        velocity.x -= run_speed

func _physics_process(delta):
    velocity.y += gravity * delta
    get_input()
    move_and_slide()

Дополнительные сведения по использованию move_and_slide() см. в разделе Кинематический персонаж (2D), включая демонстрационный проект с подробным кодом.