Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Разработка сцены с мобом

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

Давайте разработаем самих монстров в новой сцене. Структура узлов будет похожа на сцену player.tscn.

Создайте сцену, корнем которой снова является узел CharacterBody3D. Назовите его Mob. Добавьте узел Node3D в качестве его дочернего узла, назовите его Pivot. Перетащите файл mob.glb из панели Файловая система на узел Pivot, чтобы добавить 3D-модель монстра в сцену.

../../_images/drag_drop_mob.webp

Вы можете переименовать только что созданный узел mob в Character.

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

Нам нужна форма столкновения, чтобы наше тело работало. Щёлкните правой кнопкой мыши на узле Mob, корне сцены, и выберите Добавить дочерний узел.

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

Добавьте CollisionShape3D.

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

В инспекторе назначьте BoxShape3D свойству Shape.

../../_images/08.create_box_shape3D.jpg

Мы должны изменить его размер, чтобы он лучше соответствовал 3D-модели. Вы можете сделать это интерактивно, щёлкая и перетаскивая оранжевые точки.

Параллелепипед должен касаться пола и быть немного тоньше, чем модель. Физические движки работают таким образом, что если сфера игрока коснется даже угла параллепипеда, произойдет столкновение. Если параллепипед будет слишком большой по сравнению с 3D-моделью, вы можете умереть на расстоянии от монстра, и игра будет казаться несправедливой по отношению к игрокам.

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

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

Удаление монстров за пределами экрана

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

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

Примечание

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

При работе с GDScript вам не нужно беспокоиться об этом. Основная причина использования пулов заключается в том, чтобы избежать зависания в языках со сборщиком мусора, таких как C# или Lua. GDScript использует другую технику управления памятью, подсчет ссылок, которая не имеют такой проблемы. Вы можете узнать больше об этом здесь Управление памятью.

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

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

Измените его размер с помощью оранжевых точек, пока он не покроет всю 3D-модель.

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

Программирование движения мобов

Давайте реализуем движение монстра. Мы сделаем это в два этапа. Сначала мы напишем скрипт для узла Mob, который определит функцию для инициализации монстра. Затем мы запрограммируем механизм случайного появления в сцене main.tscn и вызовем функцию оттуда.

Добавьте скрипт к Mob.

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

Вот код движения для начала. Мы определяем два свойства, min_speed и max_speed, чтобы определить случайный диапазон скорости, который мы в будущем используем для задания значения CharacterBody3D.velocity.

extends CharacterBody3D

# Minimum speed of the mob in meters per second.
@export var min_speed = 10
# Maximum speed of the mob in meters per second.
@export var max_speed = 18


func _physics_process(_delta):
    move_and_slide()

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

Нам нужно определить ещё одну функцию для расчёта CharacterBody3D.velocity. Эта функция повернёт монстра в сторону игрока и случайным образом вычислит угол его движения и скорость.

В качестве аргументов функция принимает start_position, позицию появления моба, и player_position.

Мы расположим моба с помощью start_position и повернем его к игроку с помощью метода look_at_from_position(), а также вычисляем угол, поворачивая его на случайную величину вокруг оси Y. Ниже, randf_range() выводит случайное значение между -PI / 4 радианами и PI / 4 радианами.

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -45 and +45 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

Мы получили случайную позицию, теперь нам нужно получить random_speed. В данном случае пригодится функция randi_range(), так как она возвращает случайные целые числа в определённом диапазоне, поэтому мы используем min_speed и max_speed в качестве предельных значений. random_speed просто является целым числом, и мы просто используем его для умножения на CharacterBody3D.velocity. После умножения random_speed на CharacterBody3D.velocity, мы поворачиваем Vector3 CharacterBody3D.velocity в сторону игрока.

func initialize(start_position, player_position):
    # ...

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

Уход с экрана

Нам всё еще нужно уничтожать мобов, когда они покидают экран. Для этого мы подключим сигнал нашего узла VisibleOnScreenNotifier3D screen_exited к Mob.

Вернитесь в 3D окно просмотра, нажав на ярлык 3D в верхней части редактора. Вы также можете нажать Ctrl + F2 (Opt + 2 на macOS).

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

Выберите узел VisibleOnScreenNotifier3D и в правой части интерфейса перейдите во вкладку Узел. Дважды щёлкните на сигнале screen_exited().

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

Подключите сигнал к Mob

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

Это добавит новую функцию _on_visible_on_screen_notifier_3d_screen_exited(). Из неё вызовите метод queue_free(). Это уничтожит экземпляр моба, который его вызвал.

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

Наш монстр готов вступить в игру! В следующей части вы будете порождать монстров на игровом уровне.

Вот, для справки, полный сценарий mob.gd.

extends CharacterBody3D

# Minimum speed of the mob in meters per second.
@export var min_speed = 10
# Maximum speed of the mob in meters per second.
@export var max_speed = 18

func _physics_process(_delta):
    move_and_slide()

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -45 and +45 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()