Прыжки и раздавливание монстров

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

Во-первых, мы должны изменить несколько настроек, связанных с физическими взаимодействиями. Ознакомьтесь с физические слои.

Управление физическими взаимодействиями

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

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

Если это вас смущает, не волнуйтесь, через секунду мы увидим три примера.

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

По умолчанию все физические тела и зоны имеют значения всех масок и слоев равных 1. Это означает, что все они сталкиваются друг с другом.

Физические слои представлены числами, но мы можем дать им имена, чтобы отслеживать, что есть что.

Установка имен слоев

Давайте дадим нашим физическим слоям имя. Перейдите в раздел Project -> Project Settings (Проект -> Настройки проекта).

изображение0

В левом меню перейдите вниз по ссылке Имена слоя -> Физика 3D. Справа вы увидите список слоёв с полем рядом с каждым из них. Там вы можете задать их имена. Назовите первые три слоя player, enemies, и world, соответственно.

изображение1

Теперь мы можем назначить их нашим физическим узлам.

Назначение слоев и масок

В сцене Main выберите узел Ground. В инспекторе разверните раздел Collision. Там вы увидите слои и маски узла в виде сетки кнопок.

изображение2

Земля является частью мира, поэтому мы хотим, чтобы она была частью третьего слоя. Нажмите на кнопку с подсветкой, чтобы выключить первый слой Layer и включить третий. Затем отключите Mask, щёлкнув по ней.

изображение3

Как я уже упоминал, свойство Mask позволяет узлу слушать взаимодействие с другими физическими объектами, но нам не нужно, чтобы у него были столкновения. Свойству Ground не нужно ничего слушать; оно просто находится там, чтобы предотвратить падение существ.

Обратите внимание, что вы можете нажать кнопку "..." в правой части свойств, чтобы увидеть список именованных флажков.

изображение4

Далее следуют Player и Mob. Откройте player.tscn, дважды щёлкнув по файлу в панели Файловая система.

Выберите узел Player и установите его свойство Collision -> Mask на "enemies" и "world". Вы можете не изменять свойство Layer, так как первый слой - это слой "игрока" (player).

изображение5

Затем откройте сцену Mob, дважды щёлкнув на mob.tscn и выбрав узел Mob.

Установите его Collision -> Layer на "enemies" и снимите его Collision -> Mask, оставив маску пустой.

изображение6

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

Примечание

Мобам не нужно маскировать слой "world", потому что они движутся только в плоскости XZ. Мы не применяем к ним гравитацию.

Прыжки

Сама механика прыжков требует всего двух строк кода. Откройте скрипт Player. Нам нужно значение для контроля силы прыжка и обновления _physics_process() для кодирования прыжка.

После строки, определяющей fall_acceleration, в верхней части скрипта добавьте строку jump_impulse.

#...
# Vertical impulse applied to the character upon jumping in meters per second.
@export var jump_impulse = 20

Внутри _physics_process(), добавьте следующий код перед строкой, где мы вызвали блок кода с move_and_slide().

func _physics_process(delta):
    #...

    # Jumping.
    if is_on_floor() and Input.is_action_just_pressed("jump"):
        target_velocity.y = jump_impulse

    #...

Это всё, что вам нужно для прыжка!

Метод is_on_floor() является инструментом класса CharacterBody3D. Он возвращает значение true, если тело столкнулось с полом в этом кадре. Вот почему мы применяем гравитацию к Player : так мы сталкиваемся с полом, а не парим над ним как монстры.

Если персонаж стоит на полу и игрок нажимает кнопку "прыжок", мы мгновенно придаем ему большую вертикальную скорость. В играх очень важно, чтобы управление было отзывчивым, а такие мгновенные прибавки скорости, хотя и нереалистичны, но ощущаются отлично.

Обратите внимание, что ось +Y направлен вверх. Это отличается от 2D, где ось +Y направлена вниз.

Раздавливание монстров

Далее добавим механику давки. Мы заставим персонажа подпрыгивать над монстрами и одновременно убивать их.

Нам нужно обнаружить столкновения с монстром и отличить их от столкновений с полом. Для этого мы можем использовать функцию тегов Godot group.

Снова откройте сцену mob.tscn и выберите узел Mob. Перейдите в панель Узел справа, чтобы увидеть список сигналов. В панели Узел есть две вкладки: Сигналы, которую вы уже использовали, и Группы, которая позволяет присваивать узлам теги.

Щёлкните по нему, чтобы открыть поле, в котором можно написать имя тега. Введите в поле "mob" и нажмите кнопку Добавить новую группу.

изображение7

В панели Сцена появится иконка, указывающая на то, что узел является частью по крайней мере одной группы.

изображение8

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

Программирование механики давки

Вернитесь к скрипту Player, чтобы запрограммировать давку и отскок.

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

# Vertical impulse applied to the character upon bouncing over a mob in
# meters per second.
@export var bounce_impulse = 16

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

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

При таком коде, если на данном кадре не произошло ни одного столкновения, цикл не будет выполняться.

func _physics_process(delta):
    #...

    # Iterate through all collisions that occurred this frame
    for index in range(get_slide_collision_count()):
        # We get one of the collisions with the player
        var collision = get_slide_collision(index)

        # If there are duplicate collisions with a mob in a single frame
        # the mob will be deleted after the first collision, and a second call to
        # get_collider will return null, leading to a null pointer when calling
        # collision.get_collider().is_in_group("mob").
        # This block of code prevents processing duplicate collisions.
        if collision.get_collider() == null:
            continue

        # If the collider is with a mob
        if collision.get_collider().is_in_group("mob"):
            var mob = collision.get_collider()
            # we check that we are hitting it from above.
            if Vector3.UP.dot(collision.get_normal()) > 0.1:
                # If so, we squash it and bounce.
                mob.squash()
                target_velocity.y = bounce_impulse
                # Prevent further duplicate calls.
                break

Это большое количество новых функций. Вот дополнительная информация о них.

Обе функции get_slide_collision_count() и get_slide_collision() происходят из класса CharacterBody3D и связаны с move_and_slide().

get_slide_collision() возвращает объект KinematicCollision3D, который содержит информацию о том, где и как произошло столкновение. Например, мы используем его свойство get_collider, чтобы проверить, столкнулись ли мы с "мобом", вызвав is_in_group(): collision.get_collider().is_in_group("mob").

Примечание

Метод is_in_group() доступен для каждого Node.

Чтобы проверить, что мы приземляемся на монстра, мы используем векторное скалярное произведение (dot product): Vector3.UP.dot(collision.get_normal()) > 0.1. Нормаль столкновения - это трёхмерный вектор, перпендикулярный плоскости, в которой произошло столкновение. Скалярное произведение позволяет нам сравнить его с направлением вверх.

При использовании скалярных произведений результат больше, чем 0, когда два вектора находятся под углом менее 90 градусов. Значение больше, чем 0.1 говорит нам о том, что мы находимся примерно над монстром.

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

Мы вызываем одну неопределённую функцию, mob.squash() поэтому мы должны добавить её в класс Mob.

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

# Emitted when the player jumped on the mob.
signal squashed

# ...


func squash():
    squashed.emit()
    queue_free()

Примечание

При использовании C# Godot будет автоматически создавать соответствующие события для всех сигналов, заканчивающихся на EventHandler, см. C# сигналы.

Мы будем использовать сигнал для добавления очков к счёту в следующем уроке.

С помощью этого вы сможете убивать монстров, прыгая на них. Вы можете нажать F5, чтобы попробовать игру и установить main.tscn в качестве основной сцены вашего проекта.

Однако игрок еще не умрёт. Мы поработаем над этим в следующей части.