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

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

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

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

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

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

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

Примечание

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

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

Godot предлагает четыре вида физических тел, расширяющих CollisionObject2D:

  • Area2D

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

Остальные три тела расширяют PhysicsBody2D:

  • StaticBody2D

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

  • RigidBody2D

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

  • KinematicBody2D

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

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

Static bodies and rigid bodies can be configured to use a physics material. This allows adjusting the friction and bounce of an object, and set if it's absorbent and/or rough.

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

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

Примечание

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

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

Важно

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

../../_images/player_coll_shape1.png

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

The physics engine may spawn multiple threads to improve performance, so it can use up to a full frame to process physics. Because of this, the value of a body's state variables such as position or linear velocity may not be accurate for the current frame.

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

Примечание

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

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

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

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

  • collision_layer

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

  • collision_mask

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

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

Отслеживать, для чего используется каждый слой, может быть затруднительно, поэтому может оказаться полезным назначить имена используемым слоям. Имена можно присвоить в меню Настройки проекта - > Имена слоев.

../../_images/physics_layer_names.png

Пример GUI

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

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

../../_images/player_collision_layers.png ../../_images/player_collision_mask.png

Пример кода

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

Код, эквивалентный приведенному выше примеру, в котором были включены уровни 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 20 is the first bit, layer 1 is the last. The mask for layers 4,3 and 1 is therefore
0b00000000000000001101
# (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 be enabled-1).
# (2^(1-1)) + (2^(3-1)) + (2^(4-1)) = 1 + 4 + 8 = 13
pow(2, 1) + pow(2, 3) + pow(2, 4)

Area2D

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

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

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

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

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

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

StaticBody2D

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

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

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

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

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

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

RigidBody2D

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

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

На поведение тела также влияют свойства мира, заданные в Свойства проекта - > Физика, или вход в Area2D, переопределяющего глобальные свойства физики.

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

Режимы твёрдого тела

Твёрдое тело может быть установлено в один из четырех режимов:

  • Твёрдое — тело ведет себя как физический объект. Оно сталкивается с другими телами и реагирует на приложенные к нему силы. Это режим по умолчанию.

  • Статичное — тело ведет себя как StaticBody2D и не двигается.

  • Персонаж — аналогично режиму «Твёрдое», но тело не может вращаться.

  • Кинематическое — Тело ведет себя как KinematicBody2D и должно перемещаться кодом.

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

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

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

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

extends RigidBody2D

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

func _integrate_forces(state):
    if Input.is_action_pressed("ui_up"):
        applied_force = thrust.rotated(rotation)
    else:
        applied_force = Vector2()
    var rotation_dir = 0
    if Input.is_action_pressed("ui_right"):
        rotation_dir += 1
    if Input.is_action_pressed("ui_left"):
        rotation_dir -= 1
    applied_torque = rotation_dir * torque
class Spaceship : RigidBody2D
{
    private Vector2 _thrust = new Vector2(0, 250);
    private float _torque = 20000;

    public override void _IntegrateForces(Physics2DDirectBodyState state)
    {
        if (Input.IsActionPressed("ui_up"))
            SetAppliedForce(_thrust.Rotated(Rotation));
        else
            SetAppliedForce(new Vector2());

        var rotationDir = 0;
        if (Input.IsActionPressed("ui_right"))
            rotationDir += 1;
        if (Input.IsActionPressed("ui_left"))
            rotationDir -= 1;
        SetAppliedTorque(rotationDir * _torque);
    }
}

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

Примечание

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

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

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

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

KinematicBody2D

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

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

Реакция кинематического тела на столкновение

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

move_and_collide

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

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

extends KinematicBody2D

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.position
class Body : KinematicBody2D
{
    private Vector2 _velocity = new Vector2(250, 250);

    public override void _PhysicsProcess(float delta)
    {
        var collisionInfo = MoveAndCollide(_velocity * delta);
        if (collisionInfo != null)
        {
            var collisionPoint = collisionInfo.GetPosition();
        }
    }
}

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

extends KinematicBody2D

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.normal)
class Body : KinematicBody2D
{
    private Vector2 _velocity = new Vector2(250, 250);

    public override void _PhysicsProcess(float delta)
    {
        var collisionInfo = MoveAndCollide(_velocity * delta);
        if (collisionInfo != null)
            _velocity = _velocity.Bounce(collisionInfo.Normal);
    }
}

move_and_slide

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

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

move_and_slide() автоматически включает временной интервал в свои расчеты, поэтому не умножайте вектор скорости на delta.

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

extends KinematicBody2D

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

var velocity = Vector2()

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()
    velocity = move_and_slide(velocity, Vector2(0, -1))
class Body : KinematicBody2D
{
    private float _runSpeed = 350;
    private float _jumpSpeed = -1000;
    private float _gravity = 2500;

    private Vector2 _velocity = new Vector2();

    private void GetInput()
    {
        _velocity.x = 0;

        var right = Input.IsActionPressed("ui_right");
        var left = Input.IsActionPressed("ui_left");
        var jump = Input.IsActionPressed("ui_select");

        if (IsOnFloor() && jump)
            _velocity.y = _jumpSpeed;
        if (right)
            _velocity.x += _runSpeed;
        if (left)
            _velocity.x -= _runSpeed;
    }

    public override void _PhysicsProcess(float delta)
    {
        _velocity.y += _gravity * delta;
        GetInput();
        _velocity = MoveAndSlide(velocity, new Vector2(0,-1));
    }
}

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