Использование CharacterBody2D/3D

Введение

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

Примечание

Хотя в примерах этого документа используется CharacterBody2D, те же концепции применимы и в 3D.

Что такое тело персонажа?

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

Примечание

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

Совет

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

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

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

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

Обрабатывать физическое движение тела следует в обратном вызове _physics_process().

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

move_and_collide

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

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

move_and_collide наиболее полезен, когда вам нужно просто переместить тело и обнаружить столкновение, но не нужна автоматическая реакция на столкновение. Например, если вам нужно, чтобы пуля рикошетила от стены, вы можете напрямую изменить угол её скорости при обнаружении столкновения. См. пример ниже.

move_and_slide

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

При вызове move_and_slide() функция использует ряд свойств узла для расчета поведения скольжения. Эти свойства можно найти в Инспекторе или задать в коде.

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

    Это свойство представляет собой вектор скорости тела в пикселях в секунду. move_and_slide() автоматически изменит это значение при столкновении.

  • motion_mode - значение по умолчанию: MOTION_MODE_GROUNDED

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

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

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

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

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

  • wall_min_slide_angle - значение по умолчанию: 0.261799 (в радианах, эквивалентно 15 градусам)

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

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

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

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

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

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

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

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

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

Примечание

get_slide_collision_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.get_normal())

# using move_and_slide
move_and_slide()

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

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

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

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

Примеры

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

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

Если вы загрузили пример проекта, этот пример находится в "basic_movement.tscn".

В этом примере добавьте CharacterBody2D с двумя дочерними элементами: Sprite2D и CollisionShape2D. Используйте Godot "icon.svg" в качестве текстуры Sprite2D (перетащите его из дока файловой системы в свойство Texture элемента Sprite2D). В свойстве Shape элемента CollisionShape2D выберите "New RectangleShape2D" и измените размер прямоугольника так, чтобы он поместился поверх изображения спрайта.

Примечание

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

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

extends CharacterBody2D

var speed = 300

func get_input():
    var input_dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    velocity = input_dir * speed

func _physics_process(delta):
    get_input()
    move_and_collide(velocity * delta)

Запустите эту сцену, и вы увидите, что move_and_collide() работает как и ожидалось, перемещая тело вдоль вектора скорости. Теперь посмотрим, что произойдёт, если добавить препятствия. Добавьте StaticBody2D с прямоугольной формой столкновения. Для видимости можно использовать Sprite2D, Polygon2D или включить "Visible Collision Shapes" в меню "Debug".

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

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

Попробуйте изменить функцию на move_and_slide() и запустить снова.

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

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

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

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

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

extends CharacterBody2D

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

func get_input():
    # Add these actions in Project Settings -> Input Map.
    var input_dir = Input.get_axis("backward", "forward")
    velocity = transform.x * input_dir * speed
    if Input.is_action_just_pressed("shoot"):
        shoot()

func shoot():
    # "Muzzle" is a Marker2D placed at the barrel of the gun.
    var b = Bullet.instantiate()
    b.start($Muzzle.global_position, rotation)
    get_tree().root.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()
        move_and_slide()

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

extends CharacterBody2D

var speed = 750

func start(_position, _direction):
    rotation = _direction
    position = _position
    velocity = Vector2(speed, 0).rotated(rotation)

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

func _on_VisibilityNotifier2D_screen_exited():
    # Deletes the bullet when it exits the screen.
    queue_free()

Действие происходит в _physics_process(). После использования move_and_collide(), если происходит столкновение, возвращается объект KinematicCollision2D (в противном случае возвращается null).

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

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

../../_images/k2d_bullet_bounce.gif

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

Давайте рассмотрим ещё один популярный пример: 2D-платформер. move_and_slide() идеально подходит для быстрого создания и запуска функционального контроллера персонажа. Если вы скачали пример проекта, вы найдёте его в файле "platformer.tscn".

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

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

extends CharacterBody2D

var speed = 300.0
var jump_speed = -400.0

# Get the gravity from the project settings so you can sync with rigid body nodes.
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")


func _physics_process(delta):
    # Add the gravity.
    velocity.y += gravity * delta

    # Handle Jump.
    if Input.is_action_just_pressed("jump") and is_on_floor():
        velocity.y = jump_speed

    # Get the input direction.
    var direction = Input.get_axis("ui_left", "ui_right")
    velocity.x = direction * speed

    move_and_slide()
../../_images/k2d_platform.gif

В этом коде мы используем move_and_slide(), как описано выше, — для перемещения тела вдоль вектора скорости, скользя по любым поверхностям столкновения, таким как земля или платформа. Мы также используем is_on_floor() для проверки допустимости прыжка. Без этого вы бы могли "прыгать" в воздухе; это отлично подходит для Flappy Bird, но не для платформера.

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