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

Введение

Godot предлагает несколько объектов столкновения для обеспечения как обнаружения столкновения, так и реакций. Решить, какой из них лучше использовать в Вашем проекте, может быть затруднительно. Избежать проблем и упростить разработку можно, если понять, как работает каждый из них и каковы его достоинства и недостатки. В этом учебном пособии мы рассмотрим узел KinematicBody2D и покажем несколько примеров его использования.

Примечание

Этот документ предполагает, что вы знакомы с различными физическими телами Godot. Сначала прочитайте Введение в физику.

Что такое кинематическое тело?

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

Совет

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

Движение и столкновения

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

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

Вы должны перемещать кинематическое тело только в обратном вызове _physics_process().

Два метода перемещения служат разным целям, и далее в этом учебном пособии Вы увидите примеры их работы.

move_and_collide

Этот метод принимает один параметр: Vector2, указывающий относительное перемещение тела. Обычно это вектор скорости, умноженный на временной интервал кадра (delta). Если движок обнаружит столкновение где-либо вдоль этого вектора, тело немедленно прекратит движение. Если это произойдет, метод вернет объект KinematicCollision2D.

KinematicCollision2D — это объект, содержащий данные о столкновении и сталкивающемся объекте. С помощью этих данных можно вычислить реакцию на столкновение.

move_and_slide

Метод move_and_slide() предназначен для упрощения реализации реакции на столкновение в типовом случае, когда требуется, чтобы одно тело скользило по другому. Это особенно полезно в платформерах, или играх с видом сверху, например.

Совет

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

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

  • up_directionзначение по умолчанию: Vector2( 0, 0 )

    Этот параметр позволяет определить, какие поверхности движок должен считать полом. Установка этого параметра позволяет использовать методы is_on_floor(), is_on_wall() и is_on_ceiling() для определения типа поверхности, с которой контактирует тело. Значение по умолчанию означает, что все поверхности считаются стенами.

  • stop_on_slopeзначение по умолчанию: false

    Этот параметр предотвращает скольжение тела по склонам, когда оно стоит.

  • max_slidesзначение по умолчанию: 4

    Этот параметр задаёт максимальное число столкновений перед тем, как тело остановится. Слишком маленькое значение может полностью остановить перемещение.

  • floor_max_angleзначение по умолчанию: 0.785398 (в радианах, эквивалентно 45 градусам)

    Этот параметр - максимальный угол, после которого поверхность перестает считаться "полом"

  • infinite_inertiaзначение по умолчанию: true

Если этот параметр true, то тело может толкать узлы RigidBody2D, игнорируя их массу, но без обнаружения столкновений с ними. Если этот параметр false, тело столкнется с твёрдым телом и остановится.

move_and_slide_with_snap

Этот метод добавляет некоторые дополнительный функционал к move_and_slide(), добавляя параметр snap. Пока этот вектор находится в контакте с землей, тело будет оставаться прикрепленным к поверхности. Обратите внимание — это означает, что Вы должны отключить привязывание, например, когда прыгаете. Для этого можно установить snap в Vector2.ZERO или использовать move_and_slide().

Определение столкновений

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

При использовании move_and_slide() возможно возникновение нескольких столкновений, пока вычисляется ответ для скольжения. Для обработки этих столкновений используйте get_slide_count() и get_slide_collision():

# Using move_and_collide.
var collision = move_and_collide(velocity * delta)
if collision:
    print("I collided with ", collision.collider.name)

# Using move_and_slide.
velocity = move_and_slide(velocity)
for i in get_slide_count():
    var collision = get_slide_collision(i)
    print("I collided with ", collision.collider.name)

Примечание

get_slide_count() считает только те моменты, когда тело сталкивалось и изменяло направление.

Подробные сведения о том, какие данные о столкновениях возвращаются, см. в разделе KinematicCollision2D.

Какой метод перемещения следует использовать?

Основной вопрос от новых пользователей Godot: «Как определить, какую функцию движения использовать?» Часто ответ заключается в использовании move_and_slide(), потому что это «проще», но это не обязательно так. Один из способов думать об этом заключается в том, что move_and_slide() является частным случаем, а move_and_collide() — более общим. Например, следующие два фрагмента кода приводят к одной и той же реакции на столкновение:

../../_images/k2d_compare.gif
# using move_and_collide
var collision = move_and_collide(velocity * delta)
if collision:
    velocity = velocity.slide(collision.normal)

# using move_and_slide
velocity = move_and_slide(velocity)
// using MoveAndCollide
var collision = MoveAndCollide(velocity * delta);
if (collision != null)
{
    velocity = velocity.Slide(collision.Normal);
}
// using MoveAndSlide
velocity = MoveAndSlide(velocity);

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

В приведенном выше примере мы сохраняем скорость, которую возвращает move_and_slide(), обратно в переменную velocity. Это нужно потому, что при столкновении персонажа с окружением функция внутри пересчитывает скорость, чтобы отразить замедление.

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

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

Примеры

Чтобы увидеть эти примеры в действии, загрузите образец проекта: using_kinematic2d.zip.

Перемещение и стены

Если Вы загрузили образец проекта, то этот пример находится в файле «BasicMovent.tscn».

В этом примере добавьте KinematicBody2D с двумя дочерними элементами: Sprite и CollisionShape2D. Используйте иконку Godot «icon.png» в качестве текстуры Sprite (перетащите её из дока файловой системы в свойство Texture нашего Sprite). В свойстве Shape в CollisionShape2D выберите «Новый RectangleShape2D» и установите размер прямоугольника, чтобы он заполнил всё изображение спрайта.

Примечание

Посмотрите Перемещение в 2D пространстве примеры реализации 2D схем движения.

Присоедините скрипт к KinematicBody2D и добавьте следующий код:

extends KinematicBody2D

var speed = 250
var velocity = Vector2()

func get_input():
    # Detect up/down/left/right keystate and only move when pressed.
    velocity = Vector2()
    if Input.is_action_pressed('ui_right'):
        velocity.x += 1
    if Input.is_action_pressed('ui_left'):
        velocity.x -= 1
    if Input.is_action_pressed('ui_down'):
        velocity.y += 1
    if Input.is_action_pressed('ui_up'):
        velocity.y -= 1
    velocity = velocity.normalized() * speed

func _physics_process(delta):
    get_input()
    move_and_collide(velocity * delta)
using Godot;
using System;

public class KBExample : KinematicBody2D
{
    public int Speed = 250;
    private Vector2 _velocity = new Vector2();

    public void GetInput()
    {
        // Detect up/down/left/right keystate and only move when pressed
        _velocity = new Vector2();

        if (Input.IsActionPressed("ui_right"))
            _velocity.x += 1;

        if (Input.IsActionPressed("ui_left"))
            _velocity.x -= 1;

        if (Input.IsActionPressed("ui_down"))
            _velocity.y += 1;

        if (Input.IsActionPressed("ui_up"))
            _velocity.y -= 1;
    }

    public override void _PhysicsProcess(float delta)
    {
        GetInput();
        MoveAndCollide(_velocity * delta);
    }
}

Запустите сцену и Вы увидите, что move_and_collide() работает, как ожидалось, перемещая тело вдоль вектора скорости. Теперь давайте посмотрим, что происходит, когда Вы добавляете некоторые препятствия. Добавьте StaticBody2D с прямоугольной формой столкновения. Для визуализации Вы можете использовать спрайт, Polygon2D, или включить «Видимые Формы Столкновения» из меню «Отладка».

Запустите сцену еще раз и попытайтесь двигаться в препятствие. Вы увидите, что KinematicBody2D не может проникнуть в него. Теперь попробуйте двигаться в препятствие под углом, и обнаружите, что препятствие действует как клей — ощущение, что тело застревает.

Это происходит из-за отсутствия реакции на столкновение. move_and_collide() останавливает движение тела при столкновении. Нам нужно кодировать любую реакцию, которую мы хотим получить после столкновения.

Попробуйте изменить функцию на move_and_slide(velocity) и запустить снова. Обратите внимание, что из расчета скорости была удалена delta.

move_and_slide() обеспечивает реакцию на столкновения по умолчанию при скольжении тела вдоль объекта столкновения. Это применимо для большого количества типов игр, и может быть достаточным, чтобы получить то поведение, которое Вы хотите.

Отскок/отражение

Что, если Вам не нужна реакция на скользящее столкновение? К примеру («BounceandCollide.tscn» в проекте с примерами), у нас есть персонаж, стреляющий пулями, и мы хотим, чтобы пули отскакивали от стен.

В этом примере используются три сцены. Основная сцена содержит персонажа и стены. Bullet и Wall — это отдельные сцены, чтобы их можно было инстанцировать.

Персонаж управляется клавишами w и s для движения вперёд и назад. Для прицеливания используется указатель мыши. Ниже приведен код персонажа с использованием move_and_slide():

extends KinematicBody2D

var Bullet = preload("res://Bullet.tscn")
var speed = 200
var velocity = Vector2()

func get_input():
    # Add these actions in Project Settings -> Input Map.
    velocity = Vector2()
    if Input.is_action_pressed('backward'):
        velocity = Vector2(-speed/3, 0).rotated(rotation)
    if Input.is_action_pressed('forward'):
        velocity = Vector2(speed, 0).rotated(rotation)
    if Input.is_action_just_pressed('mouse_click'):
        shoot()

func shoot():
    # "Muzzle" is a Position2D placed at the barrel of the gun.
    var b = Bullet.instance()
    b.start($Muzzle.global_position, rotation)
    get_parent().add_child(b)

func _physics_process(delta):
    get_input()
    var dir = get_global_mouse_position() - global_position
    # Don't move if too close to the mouse pointer.
    if dir.length() > 5:
        rotation = dir.angle()
        velocity = move_and_slide(velocity)
using Godot;
using System;

public class KBExample : KinematicBody2D
{
    private PackedScene _bullet = (PackedScene)GD.Load("res://Bullet.tscn");
    public int Speed = 200;
    private Vector2 _velocity = new Vector2();

    public void GetInput()
    {
        // add these actions in Project Settings -> Input Map
        _velocity = new Vector2();
        if (Input.IsActionPressed("backward"))
        {
            _velocity = new Vector2(-Speed/3, 0).Rotated(Rotation);
        }
        if (Input.IsActionPressed("forward"))
        {
            _velocity = new Vector2(Speed, 0).Rotated(Rotation);
        }
        if (Input.IsActionPressed("mouse_click"))
        {
            Shoot();
        }
    }

    public void Shoot()
    {
        // "Muzzle" is a Position2D placed at the barrel of the gun
        var b = (Bullet)_bullet.Instance();
        b.Start(GetNode<Node2D>("Muzzle").GlobalPosition, Rotation);
        GetParent().AddChild(b);
    }

    public override void _PhysicsProcess(float delta)
    {
        GetInput();
        var dir = GetGlobalMousePosition() - GlobalPosition;
        // Don't move if too close to the mouse pointer
        if (dir.Length() > 5)
        {
            Rotation = dir.Angle();
            _velocity = MoveAndSlide(_velocity);
        }
    }
}

И код для пули:

extends KinematicBody2D

var speed = 750
var velocity = Vector2()

func start(pos, dir):
    rotation = dir
    position = pos
    velocity = Vector2(speed, 0).rotated(rotation)

func _physics_process(delta):
    var collision = move_and_collide(velocity * delta)
    if collision:
        velocity = velocity.bounce(collision.normal)
        if collision.collider.has_method("hit"):
            collision.collider.hit()

func _on_VisibilityNotifier2D_screen_exited():
    queue_free()
using Godot;
using System;

public class Bullet : KinematicBody2D
{
    public int Speed = 750;
    private Vector2 _velocity = new Vector2();

    public void Start(Vector2 pos, float dir)
    {
        Rotation = dir;
        Position = pos;
        _velocity = new Vector2(speed, 0).Rotated(Rotation);
    }

    public override void _PhysicsProcess(float delta)
    {
        var collision = MoveAndCollide(_velocity * delta);
        if (collision != null)
        {
            _velocity = _velocity.Bounce(collision.Normal);
            if (collision.Collider.HasMethod("Hit"))
            {
                collision.Collider.Call("Hit");
            }
        }
    }

    public void OnVisibilityNotifier2DScreenExited()
    {
        QueueFree();
    }
}

Работа происходит в _physics_process(). Вызов move_and_collide(), если были столкновения, возвращает объект KinematicCollision2D; в противном случае возвращается значение Nil.

Если есть столкновение, мы используем его normal для отражения velocity пули с помощью метода Vector2.bounce().

Если у сталкивающегося объекта (collider) есть метод hit, мы также вызываем и его. В проекте примера мы добавили эффект цветной вспышки на стене, чтобы это продемонстрировать.

../../_images/k2d_bullet_bounce.gif

Перемещения в платформере

Попробуем еще один популярный пример: 2D платформер. move_and_slide() идеально подходит для быстрого получения функционала прыжка и бега персонажа. Если Вы загрузили проект с примерами, то можете найти его в «Platformer.tscn».

В этом примере предполагается, что уровень создан из объектов StaticBody2D. Они могут быть любой формы и размера. В проекте примера мы используем Polygon2D для создания форм платформ.

Вот код для тела игрока:

extends KinematicBody2D

export (int) var run_speed = 100
export (int) var jump_speed = -400
export (int) var gravity = 1200

var velocity = Vector2()
var jumping = false

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 jump and is_on_floor():
        jumping = true
        velocity.y = jump_speed
    if right:
        velocity.x += run_speed
    if left:
        velocity.x -= run_speed

func _physics_process(delta):
    get_input()
    velocity.y += gravity * delta
    if jumping and is_on_floor():
        jumping = false
    velocity = move_and_slide(velocity, Vector2(0, -1))
using Godot;
using System;

public class KBExample : KinematicBody2D
{
    [Export] public int RunSpeed = 100;
    [Export] public int JumpSpeed = -400;
    [Export] public int Gravity = 1200;

    Vector2 velocity = new Vector2();
    bool jumping = false;

    public void GetInput()
    {
        velocity.x = 0;
        bool right = Input.IsActionPressed("ui_right");
        bool left = Input.IsActionPressed("ui_left");
        bool jump = Input.IsActionPressed("ui_select");

        if (jump && IsOnFloor())
        {
            jumping = true;
            velocity.y = JumpSpeed;
        }

        if (right)
            velocity.x += RunSpeed;
        if (left)
            velocity.x -= RunSpeed;
    }

    public override void _PhysicsProcess(float delta)
    {
        GetInput();
        velocity.y += Gravity * delta;
        if (jumping && IsOnFloor())
            jumping = false;
        velocity = MoveAndSlide(velocity, new Vector2(0, -1));
    }
}
../../_images/k2d_platform.gif

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

Также обратите внимание, что мы добавили Vector2 (0, -1) в качестве нормали к полу. Этот вектор указывает прямо вверх. В результате, если персонаж сталкивается с объектом, имеющим эту нормаль, объект будет считаться полом.

Использование нормали пола позволяет нам реализовать прыжки, используя is_on_floor(). Эта функция возвращает значение true только после такого столкновения move_and_slide(), при котором нормаль тела столкновения находится в пределах 45 градусов от заданного вектора пола. Можно управлять максимальным углом, задав floor_max_angle.

Этот угол также позволяет реализовать другие функции, такие как прыжки от стены, используя, например, is_on_wall().