Designing the mob scene¶
В этой части вы будете кодировать монстров, которых мы будем называть мобами. В следующем уроке мы будем распределять их случайным образом по всей игровой области.
Давайте разработаем самих монстров в новой сцене. Структура узлов будет похожа на сцену Player.
Создайте сцену, корнем которой снова является узел KinematicBody. Назовите его Mob. Добавьте узел Spatial в качестве его дочернего узла, назовите его Pivot. Перетащите файл mob.glb
из панели FileSystem на узел Pivot, чтобы добавить 3D-модель монстра в сцену. Вы можете переименовать только что созданный узел mob в Character.
Нам нужна форма столкновения, чтобы наше тело работало. Щёлкните правой кнопкой мыши на узле Mob, корне сцены, и выберите Add Child Node.
Добавьте CollisionShape.
В инспекторе назначьте BoxShape свойству Shape.
Мы должны изменить его размер, чтобы он лучше соответствовал 3D-модели. Вы можете сделать это интерактивно, щёлкая и перетаскивая оранжевые точки.
Габаритный контейнер должен касаться пола и быть немного тоньше, чем модель. Физические движки работают таким образом, что если сфера игрока коснется даже угла габаритного контейнера, произойдет столкновение. Если габаритный контейнер будет слишком большой по сравнению с 3D-моделью, вы можете умереть на расстоянии от монстра, и игра будет казаться несправедливой по отношению к игрокам.
Обратите внимание, что мой габаритный контейнер выше, чем монстр. В этой игре это нормально, потому что мы смотрим на сцену сверху и используем фиксированную перспективу. Формы столкновений не обязательно должны точно соответствовать модели. Их форма и размер должны определяться тем, как игра ощущается при тестировании.
Removing monsters off-screen¶
Мы собираемся порождать монстров через регулярные промежутки времени на игровом уровне. Если мы не будем осторожны, их количество может увеличиться до бесконечности, а мы этого не хотим. Каждый экземпляр монстра имеет стоимость памяти и обработки, и мы не хотим платить за это, когда монстр находится за пределами экрана.
Как только монстр покидает экран, он нам больше не нужен, поэтому мы можем его удалить. В Godot есть узел, который определяет, когда объекты покидают экран, VisibilityNotifier, и мы собираемся использовать его для уничтожения наших мобов.
Примечание
Когда вы постоянно создаёте экземпляр объекта в играх, существует техника, которую можно использовать, чтобы избежать затрат на постоянное создание и уничтожение экземпляров, называемая объединением. Она заключается в предварительном создании массива объектов и повторном их использовании.
При работе с GDScript вам не нужно беспокоиться об этом. Основная причина использования пулов заключается в том, чтобы избежать зависания в языках с мусорными коллекциями, таких как C# или Lua. GDScript использует другую технику управления памятью, подсчет ссылок, которая не имеет этого предостережения. Вы можете узнать больше об этом здесь Управление памятью.
Выберите узел Mob и добавьте VisibilityNotifier в качестве его дочернего элемента. Появится еще один квадрат, на этот раз розовый. Когда это поле полностью покинет экран, узел издаст сигнал.
Измените его размер с помощью оранжевых точек, пока он не покроет всю 3D-модель.
Coding the mob's movement¶
Давайте реализуем движение монстра. Мы сделаем это в два этапа. Сначала мы напишем скрипт на узле Mob, который определит функцию для инициализации монстра. Затем мы закодируем механизм случайного появления в сцене Main и вызовем функцию оттуда.
Attach a script to the Mob.
Вот код движения для начала. Мы определяем два свойства, min_speed
и max_speed
, чтобы определить случайный диапазон скорости. Затем мы определяем и инициализируем velocity
.
extends KinematicBody
# 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
var velocity = Vector3.ZERO
func _physics_process(_delta):
move_and_slide(velocity)
public class Mob : KinematicBody
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
// Minimum speed of the mob in meters per second
[Export]
public int MinSpeed = 10;
// Maximum speed of the mob in meters per second
[Export]
public int MaxSpeed = 18;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
MoveAndSlide(_velocity);
}
}
Аналогично игроку, мы перемещаем моба каждый кадр, вызывая метод KinematicBody
's move_and_slide()
. На этот раз мы не обновляем velocity
каждый кадр: мы хотим, чтобы монстр двигался с постоянной скоростью и покинул экран, даже если он врежется в препятствие.
Вы можете увидеть предупреждение в GDScript о том, что возвращаемое значение из move_and_slide()
не используется. Это ожидаемо. Вы можете просто проигнорировать предупреждение или, если хотите скрыть его полностью, добавить комментарий # warning-ignore:return_value_discarded
прямо над строкой move_and_slide(velocity)
. Подробнее о системе предупреждений GDScript см. в Система предупреждений GDScript.
Нам нужно определить ещё одну функцию для расчёта начальной скорости. Эта функция повернёт монстра в сторону игрока и случайным образом вычислит угол его движения и скорость.
В качестве аргументов функция принимает start_position
, позицию появления моба, и player_position
.
Мы располагаем моба с помощью start_position
и поворачиваем его к игроку с помощью метода look_at_from_position()
, а также вычисляем угол, поворачивая его на случайную величину вокруг оси Y. Ниже, rand_range()
выводит случайное значение между -PI / 4
радианами и PI / 4
радианами.
# We will call this function from the Main scene.
func initialize(start_position, player_position):
# We position the mob and turn it so that it looks at the player.
look_at_from_position(start_position, player_position, Vector3.UP)
# And rotate it randomly so it doesn't move exactly toward the player.
rotate_y(rand_range(-PI / 4, PI / 4))
// We will call this function from the Main scene
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
// We position the mob and turn it so that it looks at the player.
LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
// And rotate it randomly so it doesn't move exactly toward the player.
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
}
Затем мы снова вычисляем случайную скорость с помощью rand_range()
и используем её для вычисления скорости.
Мы начинаем с создания 3D-вектора, направленного вперёд, умножаем его на наш random_speed
, и, наконец, поворачиваем его с помощью метода Vector3
класса rotated()
.
func initialize(start_position, player_position):
# ...
# We calculate a random speed.
var random_speed = rand_range(min_speed, max_speed)
# We calculate a forward velocity that represents the speed.
velocity = Vector3.FORWARD * random_speed
# We then rotate the vector based on the mob's Y rotation to move in the direction it's looking.
velocity = velocity.rotated(Vector3.UP, rotation.y)
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
// ...
// We calculate a random speed.
float randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
// We calculate a forward velocity that represents the speed.
_velocity = Vector3.Forward * randomSpeed;
// We then rotate the vector based on the mob's Y rotation to move in the direction it's looking
_velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
}
Leaving the screen¶
Нам всё еще нужно уничтожать мобов, когда они покидают экран. Для этого мы подключим сигнал нашего узла VisibilityNotifier screen_exited
к Mob.
Вернитесь в 3D окно просмотра, нажав на ярлык 3D в верхней части редактора. Вы также можете нажать Ctrl + F2 (Alt + 2 на macOS).
Выберите узел VisibilityNotifier и в правой части интерфейса перейдите в панель Node. Дважды щёлкните на сигнале screen_exited().
Подключите сигнал к Mob.
Это вернёт вас в редактор скриптов и добавит новую функцию _on_VisibilityNotifier_screen_exited()
. Из неё вызовите метод queue_free()
. Это уничтожит экземпляр моба, когда поле VisibilityNotifier покинет экран.
func _on_VisibilityNotifier_screen_exited():
queue_free()
// We also specified this function name in PascalCase in the editor's connection window
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
Наш монстр готов вступить в игру! В следующей части вы будете порождать монстров на игровом уровне.
Вот, для справки, полный сценарий Mob.gd
.
extends KinematicBody
# 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
var velocity = Vector3.ZERO
func _physics_process(_delta):
move_and_slide(velocity)
func initialize(start_position, player_position):
look_at_from_position(start_position, player_position, Vector3.UP)
rotate_y(rand_range(-PI / 4, PI / 4))
var random_speed = rand_range(min_speed, max_speed)
velocity = Vector3.FORWARD * random_speed
velocity = velocity.rotated(Vector3.UP, rotation.y)
func _on_VisibilityNotifier_screen_exited():
queue_free()
public class Mob : KinematicBody
{
// Minimum speed of the mob in meters per second
[Export]
public int MinSpeed = 10;
// Maximum speed of the mob in meters per second
[Export]
public int MaxSpeed = 18;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
MoveAndSlide(_velocity);
}
// We will call this function from the Main scene
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
var randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
_velocity = Vector3.Forward * randomSpeed;
_velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
}
// We also specified this function name in PascalCase in the editor's connection window
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
}