Ваша Первая Игра

Обзор

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

Примечание

Этот проект представляет собой введение в движок Godot. Предполагается, что у вас уже есть опыт программирования. Если вы новичок в программировании, вам нужно начать отсюда: Написание скриптов.

Игра называется «Увернись от Крипов!». Ваш персонаж должен двигаться и избегать врагов как можно дольше. Вот предварительная демонстрация финального результата:

../../_images/dodge_preview.gif

Почему 2D? 3D-игры намного сложнее, чем 2D. Вы должны придерживаться 2D, пока не получите хорошее представление о процессе разработки игр.

Настройка проекта

Запустите Godot и создайте новый проект. Затем загрузите dodge_assets.zip - изображения и звуки, которые вы будете использовать для создания игры. Разархивируйте эти файлы в папку вашего проекта.

Примечание

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

В данной игре будет использоваться портретный вид, поэтому нам нужно настроить размер игрового окна. Нажмите «Проект» -> «Настройки проекта» -> «Дисплей» -> «Окно» и установите значения полей «Ширина» - 480 и «Высота» - 720.

Организация Проекта

В этом проекте мы создадим 3 независимые сцены: Player, Mob и HUD, которые мы будем комбинировать со сценой Main. В более крупном проекте было бы полезно сделать папки для хранения различных сцен и их скриптов, но для этой, относительно небольшой игры, вы можете сохранять свои сцены и скрипты в корневой папке, называемой res://. Вы можете видеть папки проекта в доке FileSystem (Файловая Система) в верхнем левом углу:

../../_images/filesystem_dock.png

Сцена Игрока

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

Структура нода

Для начала нажмите кнопку «Добавить/Создать новый узел» и добавьте в сцену узел :ref:`Area2D <class_Area2D>“.

../../_images/add_node.png

С помощью Area2D мы можем обнаруживать объекты, которые перекрывают или сталкиваются с игроком. Измените его имя на Player, нажав на имя узла. Это корневой узел сцены. Мы можем добавить к игроку дополнительные узлы для расширения его функциональности.

Прежде чем мы добавим детей на узел Player, мы хотим убедиться, что нажав на них мы не переместим их и не изменим их размер. Выберите узел и нажмите на значок справа от блокировки; его всплывающая подсказка гласит: «Гарантирует, что дети объекта не могут быть выбраны.»

../../_images/lock_children.png

Сохраните сцену. Выберите меню «Сцена» -> «Сохранить» или нажмите «Ctrl+S» на Windows/Linux или Command+S на Mac.

Примечание

Для этого проекта мы будем следовать правилам именования Godot.

  • GDScript: Классы (узлы) используют PascalCase, переменные и функции - snake_case, константы - ALL_CAPS (см. Руководство по стилю GDScript).
  • C#: Классы, экспортируемые переменные и методы используют PascalCase, приватные поля - _camelCase, локальные переменные и параметры - сamelCase (см. C# style guide). Будьте осторожны, набирайте имена методов точно при подключении сигналов.

Анимация Спрайтов

Click on the Player node and add an AnimatedSprite node as a child. The AnimatedSprite will handle the appearance and animations for our player. Notice that there is a warning symbol next to the node. An AnimatedSprite requires a SpriteFrames resource, which is a list of the animations it can display. To create one, find the Frames property in the Inspector and click «[empty]» -> «New SpriteFrames». This should automatically open the SpriteFrames panel.

../../_images/spriteframes_panel.png

Слева находится список анимаций. Щелкните на «default» и переименуйте на «right». Затем щелкните кнопку «Add» для создания второй анимации под именем «up». Для каждой анимации, перетащите на сторону «Animation Frames» два изображения с именами playerGrey_up[1/2] и playerGrey_walk[1/2]:

../../_images/spriteframes_panel2.png

Изображения игрока немного великоваты для игрового окна, потому нам надо их слегка уменьшить. Щелкните на узле AnimatedSprite и установите параметр Scale в (0.5, 0.5). Вы можете найти его в Инспекторе, под заголовком Node2D.

../../_images/player_scale.png

Наконец, добавьте CollisionShape2D в качестве дочернего узла в Player. Это позволит задать игроку «Hitbox» или, иначе говоря, границы зоны определения столкновений. Для этого персонажа лучше всего подходит узел CapsuleShape2D, поэтому далее идем в Инспектор, и в разделе «Shape» щелкаем «[empty]»» -> «New CapsuleShape2D». Подгоните размер фигуры под размер изображения:

../../_images/player_coll_shape.png

Когда вы закончите, сцена с вашим Player будет выглядеть вот так:

../../_images/player_scene_nodes.png

Движение Игрока

Теперь нам надо добавить немного функционала, который мы не можем получить с помощью встроенных узлов и поэтому мы добавим скрипт. Щелкните на узле Player и нажмите кнопку «Add Script»:

../../_images/add_script_button.png

В окне настроек скрипта вы можете оставить все настройки по умолчанию. Просто нажмите «Create»:

Примечание

Если вы создаете скрипт на C# или другом языке, выберете нужный вам язык в выпадающем меню language перед тем как создать его.

../../_images/attach_node_window.png

Примечание

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

Начните с объявления переменных - членов, которые понадобятся этому объекту:

extends Area2D

export var speed = 400  # How fast the player will move (pixels/sec).
var screen_size  # Size of the game window.
public class Player : Area2D
{
    [Export]
    public int Speed = 400; // How fast the player will move (pixels/sec).

    private Vector2 _screenSize; // Size of the game window.
}

Использование ключевого слова export у первой переменной speed позволяет устанавливать ее значение в Инспекторе. Это может быть полезно, если вы хотите изменять значения точно так же как и встроенные свойства узла. Щелкните на узел Player и вы увидите, что свойство появилось в разделе «Script Variables» в Инспекторе. Помните, что если изменить значение здесь, то оно перезапишет значение, установленное в скрипте.

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

Если вы используете C#, вам нужно (пере)собирать сборки проекта всякий раз, когда вы хотите увидеть новые экспортируемые переменные или сигналы. Эта сборка может быть запущена вручную путем нажатия на слово «Mono» в нижней части окна редактора, чтобы открыть Mono Panel, а затем на кнопку «Build Project».

../../_images/export_variable.png

Функция _ready () вызывается, когда узел входит в дерево сцены, что является хорошим моментом для определения размера игрового окна:

func _ready():
    screen_size = get_viewport_rect().size
public override void _Ready()
{
    _screenSize = GetViewport().GetSize();
}

Теперь мы можем использовать функцию _process() для определения того, что игрок будет делать. _process() вызывается каждый кадр, поэтому мы будем использовать ее для обновления состояния тех элементов нашей игры, которые будут часто изменяться. Для игрока, сделаем следующее:

  • Проверка ввода.
  • Перемещение в заданном направлении.
  • Воспроизведение соответствующей анимации.

Во-первых, нам нужно обрабатывать ввод с клавиатуры (нажимает ли игрок клавишу?). В этой игре у нас есть 4 направления движения, соответственно кнопок управляющих движением тоже будет 4. Назначить действия на определённые клавиши можно в настройках проекта в разделе «Список действий». Для данной демонстрации назначать клавиши не понадобится, потому что мы будем использовать действия назначенные по-умолчанию (стрелками на клавиатуре).

Вы можете определить, нажата ли клавиша с помощью функции Input.is_action_pressed() ``, которая возвращает ``true, если клавиша нажата, или false, если нет.

func _process(delta):
    var velocity = Vector2()  # The player's movement vector.
    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
    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite.play()
    else:
        $AnimatedSprite.stop()
public override void _Process(float delta)
{
    var velocity = new Vector2(); // The player's movement vector.

    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;
    }

    var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");

    if (velocity.Length() > 0)
    {
        velocity = velocity.Normalized() * Speed;
        animatedSprite.Play();
    }
    else
    {
        animatedSprite.Stop();
    }
}

Начнём с того, что установим значение velocity в``(0, 0)`` - по умолчанию игрок двигаться не должен. Затем, мы проверяем каждый ввод и добавляем/вычитаем значение из velocity, чтобы получить общее направление. Например, если вы одновременно удерживаете right и down, полученный вектор velocity будет ``(1, 1) ``. В этом случае, поскольку мы добавляем горизонтальное и вертикальное движение, игрок будет двигаться быстрее, чем если бы он перемещался только по горизонтали.

Можем избежать этого, если мы нормализуем скорость, что означает, что мы устанавливаем ее длину на 1 и умножаем на желаемую скорость. Это означает отсутствие более быстрого диагонального движения.

Совет

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

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

Совет

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

$ является сокращением для get_node(). Поэтому в приведенном выше коде $AnimatedSprite.play() это то же самое, что и get_node("AnimatedSprite").play().

Теперь, когда у нас есть направление движения, мы можем обновить позицию Player и использовать функцию clamp(), чтобы он не выходил за границы экрана, добавив ее в нижнюю часть функции _process:

position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)
Position += velocity * delta;
Position = new Vector2(
    x: Mathf.Clamp(Position.x, 0, _screenSize.x),
    y: Mathf.Clamp(Position.y, 0, _screenSize.y)
);

Совет

Зажимать (Clamping) значение означает ограничивать его в заданном диапазоне.

Нажмите кнопку «Play Scene» (F6) и убедитесь, что вы можете перемещать игрока по экрану во всех направлениях. Консоль вывода, которая открывается при воспроизведении сцены может быть закрыта с помощью кнопки Output` (которая должна быть выделена синим цветом) в нижней левой части нижней панели.

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

Если в панели «Отладчик» вы получили сообщение об ошибке, которая ссылается на «несуществующий экземпляр», это, скорее всего, означает, что вы ввели неправильное имя узла. Имена узлов чувствительны к регистру, а $NodeName или get_node("NodeName") должны совпадать с именем, которое вы видите в дереве сцены.

Выбор анимации

Теперь, когда игрок может двигаться, нам нужно изменять анимацию AnimatedSprite в зависимости от направления движения. У нас есть анимация «вправо», которую нужно перевернуть горизонтально, используя свойство flip_h для левого движения, и анимация «вверх», которую нужно перевернуть вертикально с помощью flip_v для движения вниз. Давайте поместим следующий код в конец нашей функции _process():

if velocity.x != 0:
    $AnimatedSprite.animation = "right"
    $AnimatedSprite.flip_v = false
    # See the note below about boolean assignment
    $AnimatedSprite.flip_h = velocity.x < 0
elif velocity.y != 0:
    $AnimatedSprite.animation = "up"
    $AnimatedSprite.flip_v = velocity.y > 0
if (velocity.x != 0)
{
    animatedSprite.Animation = "right";
    // See the note below about boolean assignment
    animatedSprite.FlipH = velocity.x < 0;
    animatedSprite.FlipV = false;
}
else if(velocity.y != 0)
{
    animatedSprite.Animation = "up";
    animatedSprite.FlipV = velocity.y > 0;
}

Примечание

Логическое присваивания в коде выше, является общим сокращением для программистов. Рассмотрим-ка вот этот код в сравнении с сокращенным присваиванием выше:

if velocity.x < 0:
    $AnimatedSprite.flip_h = true
else:
    $AnimatedSprite.flip_h = false
if velocity.x < 0:
    animatedSprite.FlipH = true
else:
    animatedSprite.FlipH = false

Снова запустите сцену и убедитесь, что анимации правильны для каждого из направлений. Когда вы будете уверены, что движение работает правильно, добавьте эту строку в функцию _ready(), чтобы игрок был скрыт, когда игра начнётся:

hide()
Hide();

Подготовка к столкновениям

Мы хотим, чтобы Player обнаруживал столкновение с врагом, но мы еще не сделали никаких врагов! Это нормально, потому что мы будем использовать такой функционал Godot, как сигнал.

Добавьте следующий код в начало скрипта, после extends Area2d:

signal hit
// Don't forget to rebuild the project so the editor knows about the new signal.

[Signal]
public delegate void Hit();

Это определяет пользовательский сигнал под названием «hit», который наш игрок будет излучать (отправлять), когда он сталкивается с противником. Мы будем использовать Area2D для обнаружения столкновения. Выберите нод Player и щелкните вкладку «Node» рядом со вкладкой «Inspector», чтобы просмотреть список сигналов, которые может излучать игрок:

../../_images/player_signals.png

Обратите внимание, что наш пользовательский сигнал «hit» там также есть! Поскольку наши противники будут узлами RigidBody2D, нам нужен сигнал body_entered(Object body); он будет вызываться, когда тело (body) контактирует с игроком. Нажмите «Подключить..», а затем снова «Подключить» в окне «Подключение сигнала». Нам не нужно изменять какие-либо из этих настроек - Godot автоматически создаст функцию, называемую _on_Player_body_entered в скрипте вашего игрока.

Совет

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

Добавьте этот код в функцию:

func _on_Player_body_entered(body):
    hide()  # Player disappears after being hit.
    emit_signal("hit")
    $CollisionShape2D.set_deferred("disabled", true)
public void OnPlayerBodyEntered(PhysicsBody2D body)
{
    Hide(); // Player disappears after being hit.
    EmitSignal("Hit");
    GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
}

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

Примечание

Отключение зоны столкновения может привести к ошибке, если это произойдет во время обработки столкновений движка. Использование call_deferred() позволяет нам заставить Godot ждать отключения фигуры, пока это не будет безопасно.

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

func start(pos):
    position = pos
    show()
    $CollisionShape2D.disabled = false
public void Start(Vector2 pos)
{
    Position = pos;
    Show();
    GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
}

Сцена Врага

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

Сделаем это в сцене Mob, которую затем мы можем инстанцировать, чтобы создать любое количество независимых мобов в игре.

Настройка узла

Нажмите «Сцена» -> «Новая сцена», и так мы создадим Моба.

Сцена Моба будет использовать следующие узлы:

  • RigidBody2D (названный Mob)
    • AnimatedSprite
    • CollisionShape2D
    • VisibilityNotifier2D (названный Visibility)

Не забудьте настроить дочерние узлы, чтобы они не могли быть выбраны, как вы это делали со сценой игрока.

В свойствах RigidBody2D установите в поле Gravity Scale значение 0, чтобы моб не упал вниз. Кроме того, в разделе PhysicsBody2D выберите свойство Mask и снимите первый флажок. Это гарантирует, что мобы не столкнутся друг с другом.

../../_images/set_collision_mask.png

Настройте AnimatedSprite как вы сделали это для игрока. На этот раз у нас есть 3 анимации: fly, swim и walk. Установите свойство Playing в Инспекторе на «On» и настройте параметр «Speed (FPS)», как показано ниже. Мы будем выбирать одну из этих анимаций случайным образом, чтобы мобы имели некоторое разнообразие.

../../_images/mob_animations.gif

fly должно быть настроен на 3 FPS, а swim и walk на 4 FPS.

Также как изображения игрока, изображения мобов нужно уменьшить. Установите для AnimatedSprite в свойстве Scale значение (0.75, 0.75).

Как и в сцене Player, добавьте узел CapsuleShape2D для расчета столкновений. Чтобы выровнять фигуру с изображением, вам нужно установить в свойство Rotation Degrees значение 90 под `` Node2D``.

Скрипт врага

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

extends RigidBody2D

export var min_speed = 150  # Minimum speed range.
export var max_speed = 250  # Maximum speed range.
var mob_types = ["walk", "swim", "fly"]
public class Mob : RigidBody2D
{
    // Don't forget to rebuild the project so the editor knows about the new export variables.

    [Export]
    public int MinSpeed = 150; // Minimum speed range.

    [Export]
    public int MaxSpeed = 250; // Maximum speed range.

    private String[] _mobTypes = {"walk", "swim", "fly"};
}

Во время появления моба, выберем случайное значение между min_speed и max_speed для определения того, как быстро каждый моб будет двигаться (было бы скучно, если бы все они двигались с одинаковой скоростью). Также у нас есть массив, содержащий имена трех анимаций, которые мы будем выбирать случайным образом. Убедитесь, что имена анимаций в скрипте совпадают с именами анимаций в SpriteFrame resource.

Теперь давайте посмотрим на остальную часть скрипта. В функции _ready() мы случайным образом выбираем один из трех типов анимации:

func _ready():
    $AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
// C# doesn't implement GDScript's random methods, so we use 'System.Random' as an alternative.
static private Random _random = new Random();

public override void _Ready()
{
    GetNode<AnimatedSprite>("AnimatedSprite").Animation = _mobTypes[_random.Next(0, _mobTypes.Length)];
}

Примечание

Если хотите, чтобы последовательность «случайных» чисел была различной при каждом запуске сцены, вам нужно использовать функцию randomize(). Мы будем использовать randomize() в нашей сцене Main, поэтому сейчас нам это не понадобится. Функция randi() % n - стандартный способ получить случайное целое число между 0 и n-1.

И наконец, заставим мобов удалять себя, когда они покидают экран. Подключите сигнал screen_exited() узла Visibility и добавьте следующий код:

func _on_Visibility_screen_exited():
    queue_free()
public void OnVisibilityScreenExited()
{
    QueueFree();
}

На этом заканчиваем работу над сценой Mob.

Главная сцена

Пришло время собрать все это вместе. Создайте новую сцену и добавьте узел Node с именем Main. Нажмите кнопку «Инстанцировать» и выберите сохраненную ранее сцену Player.tscn.

../../_images/instance_scene.png

Примечание

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

Теперь добавьте следующие узлы в виде дочерних элементов Main и назовите их как показано ниже (значения указаны в секундах):

  • Timer (назвать MobTimer) - чтобы контролировать частоту появления мобов
  • Timer (назвать ScoreTimer) - чтобы каждую секунду увеличивать счет
  • Timer (назвать StartTimer) - чтобы дать задержку перед стартом игры
  • Position2D (назвать StartPosition) - чтобы указать начальную позицию игрока

Задайте значение Wait Time для каждого из узлов Timer следующим образом:

  • MobTimer: 0.5
  • ScoreTimer: 1
  • StartTimer: 2

Кроме того, установите для свойства One Shot узла StartTimer значение «On» и для свойства Position узла StartPosition установите значение (240, 450).

Добавление мобов

Узел Main будет порождать новых мобов, и мы хотим, чтобы они появлялись в случайном месте на краю экрана. Добавьте узел Path2D с именем MobPath как дочерний элемент узла Main. Когда вы выберете Path2D, вы увидите несколько новых кнопок в верхней части редактора:

../../_images/path2d_buttons.png

Выберите среднюю («Добавить Точку») и нарисуйте путь, щелкнув, чтобы добавить точки на указанные углы. Чтобы точки привязывались к сетке убедитесь, что пункт «Привязывать к сетке» включен. Эта опция может быть найдена под кнопкой «Опции привязывания», левее кнопки «Закрепить», в виде ряда из трёх вертикальных точек.

../../_images/draw_path2d.gif

Важно

Нарисуйте путь в порядке по часовой стрелке, иначе ваши мобы будут появляться с направлением наружу, а не внутрь!

Поместив точку «4» на изображение, нажмите кнопку «Закрыть кривую», и она будет завершена.

Теперь, когда путь определен, добавьте узел PathFollow2D как дочерний элемент MobPath и назовите его MobSpawnLocation. Этот узел будет автоматически вращаться и следовать по пути при его перемещении, поэтому мы можем использовать его для выбора случайной позиции и направления вдоль пути.

Главный скрипт

Добавьте скрипт к узлу Main. В начале скрипта мы пишем export (PackedScene) чтобы мы могли выбрать сцену Mob, экземпляр которой мы хотим сделать.

extends Node

export (PackedScene) var Mob
var score

func _ready():
    randomize()
public class Main : Node
{
    // Don't forget to rebuild the project so the editor knows about the new export variable.

    [Export]
    public PackedScene Mob;

    private int _score;

    // We use 'System.Random' as an alternative to GDScript's random methods.
    private Random _random = new Random();

    public override void _Ready()
    {
    }

    // We'll use this later because C# doesn't support GDScript's randi().
    private float RandRange(float min, float max)
    {
        return (float)_random.NextDouble() * (max - min) + min;
    }
}

Перетащите Mob.tscn из панели «FileSystem» в свойство Mob под переменными скрипта на узле Main.

Далее, щёлкните на Player и соедините hit-сигнал. Мы хотим сделать новую функцию с названием game_over, которая будет обрабатывать то, что происходит когда игра заканчивается. Введите «game_over» в поле «Метод в ноде» снизу от окна «Соединение сигнала». Вставьте следующий код, а также функцию new_game чтобы настроить всё для новой игры:

func game_over():
    $ScoreTimer.stop()
    $MobTimer.stop()

func new_game():
    score = 0
    $Player.start($StartPosition.position)
    $StartTimer.start()
public void GameOver()
{
    GetNode<Timer>("MobTimer").Stop();
    GetNode<Timer>("ScoreTimer").Stop();
}

public void NewGame()
{
    _score = 0;

    var player = GetNode<Player>("Player");
    var startPosition = GetNode<Position2D>("StartPosition");
    player.Start(startPosition.Position);

    GetNode<Timer>("StartTimer").Start();
}

Теперь присоедините сигнал timeout() каждого из узлов Timer (StartTimer, ScoreTimerMobTimer) к основному скрипту. StartTimer запустит другие два таймера. ScoreTimer будет увеличивать счет на 1.

func _on_StartTimer_timeout():
    $MobTimer.start()
    $ScoreTimer.start()

func _on_ScoreTimer_timeout():
    score += 1
public void OnStartTimerTimeout()
{
    GetNode<Timer>("MobTimer").Start();
    GetNode<Timer>("ScoreTimer").Start();
}

public void OnScoreTimerTimeout()
{
    _score++;
}

В функции _on_MobTimer_timeout() мы создадим экземпляр противника, выберем случайное начальное местоположение вдоль Path2D и заставим его двигаться. Узел PathFollow2D будет автоматически поворачивать его в нужном направлении, поэтому мы воспользуемся этим свойством, чтобы выбрать направление противника и его позицию.

Обратите внимание, что новый экземпляр должен быть добавлен в сцену с помощью функции add_child().

Теперь кликните на MobTimer в окне сцены, затем откройте окно инспектора, перейдите на вкладку Узел и кликнете на timeout() и присоедините сигнал.

Добавьте следующий код:

func _on_MobTimer_timeout():
    # Choose a random location on Path2D.
    $MobPath/MobSpawnLocation.set_offset(randi())
    # Create a Mob instance and add it to the scene.
    var mob = Mob.instance()
    add_child(mob)
    # Set the mob's direction perpendicular to the path direction.
    var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
    # Set the mob's position to a random location.
    mob.position = $MobPath/MobSpawnLocation.position
    # Add some randomness to the direction.
    direction += rand_range(-PI / 4, PI / 4)
    mob.rotation = direction
    # Set the velocity (speed & direction).
    mob.linear_velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
    mob.linear_velocity = mob.linear_velocity.rotated(direction)
public void OnMobTimerTimeout()
{
    // Choose a random location on Path2D.
    var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
    mobSpawnLocation.SetOffset(_random.Next());

    // Create a Mob instance and add it to the scene.
    var mobInstance = (RigidBody2D)Mob.Instance();
    AddChild(mobInstance);

    // Set the mob's direction perpendicular to the path direction.
    float direction = mobSpawnLocation.Rotation + Mathf.Pi / 2;

    // Set the mob's position to a random location.
    mobInstance.Position = mobSpawnLocation.Position;

    // Add some randomness to the direction.
    direction += RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
    mobInstance.Rotation = direction;

    // Choose the velocity.
    mobInstance.SetLinearVelocity(new Vector2(RandRange(150f, 250f), 0).Rotated(direction));
}

Важно

В тригонометрических функциях, GDScript использует * радианы*, а не градусы. Если вам удобнее работать с градусами, вам нужно использовать функции deg2rad() и rad2deg() для преобразования между ними.

HUD

Финальная часть нашей игры - UI: интерфейс для отображения таких элементов, как игровой счет, сообщение «игра окончена» и кнопка перезапуска. Создайте новую сцену и добавьте Canvas Layer узел с именем``HUD``. «HUD» означает «heads-up display», информационный дисплей, отображающийся поверх игры.

Узел CanvasLayer позволяет нам изображать элементы UI поверх всей игры. Это значит, что отображаемая информация не перекрывается никакими игровыми элементами, такими как игрок или мобы.

HUD отображает следующую информацию:

  • Счет, измененный ScoreTimer.
  • Сообщение, например «Game Over» или «Get Ready!»
  • Кнопка «Start» чтобы начать игру.

Основной узел для элементов пользовательского интерфейса Control. Чтобы создать UI, мы будем использовать два вида Control nodes: Label и Button.

Создайте следующее в качестве потомка узла „“ HUD“„:

  • Label названный ScoreLabel.
  • Label названный MessageLabel.
  • Кнопка с именем StartButton.
  • Timer названный MessageTimer.

Click on the ScoreLabel and type a number into the Text field in the Inspector. The default font for Control nodes is small and doesn’t scale well. There is a font file included in the game assets called «Xolonium-Regular.ttf». To use this font, do the following for each of the three Control nodes:

  1. Под «Custom Fonts», выберите «New DynamicFont»
../../_images/custom_font1.png
  1. Нажмите на только что добавленный «DynamicFont», и в разделе «Font/Font Data», выберите «Load» и выберите файл «Xolonium-Regular.ttf» . Необходимо также установить размер шрифта Size. Значение 64 хорошо подходит.
../../_images/custom_font2.png

Примечание

Якоря и поля: Узлы Control имеют не только положение и размер, но также привязки(якоря) и поля. Якоря определяют начало координат - опорную точку для ребер узла. Поля обновляются автоматически при перемещении или изменении размера узла управления. Они представляют расстояние от краев Control - узла до его якоря. См.: ref: doc_design_interfaces_with_the_control_nodes для получения более подробной информации.

Организуйте узлы, как показано ниже. Нажмите кнопку «Якорь» для установки узла управления якорем:

../../_images/ui_anchor.png

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

ScoreLabel

  • Text : 0
  • Макет : «Top Wide (Вверху вшырь)»
  • Выравнивание : «Центр»

MessageLabel

  • Text: Уворачивайся от крипов!
  • Макет: «HCenter Wide»
  • Выравнивание : «Центр»

StartButton

  • Текст : Start
  • Слой: «Center Bottom»
  • Отступ :
    • Cверху: - 200
    • Внизу: -100

Теперь добавьте этот скрипт в HUD:

extends CanvasLayer

signal start_game
public class HUD : CanvasLayer
{
    // Don't forget to rebuild the project so the editor knows about the new signal.

    [Signal]
    public delegate void StartGame();
}

Сигнал start_game сообщает узлу Main, что кнопка была нажата.

func show_message(text):
    $MessageLabel.text = text
    $MessageLabel.show()
    $MessageTimer.start()
public void ShowMessage(string text)
{
    var messageLabel = GetNode<Label>("MessageLabel");
    messageLabel.Text = text;
    messageLabel.Show();

    GetNode<Timer>("MessageTimer").Start();
}

Эта функция будет вызывается, когда мы хотим показать временное сообщение, например, «приготовиться». На MessageTimer, измените Wait Time до 2 и измените One Shot установить на «On».

func show_game_over():
    show_message("Game Over")
    yield($MessageTimer, "timeout")
    $MessageLabel.text = "Dodge the\nCreeps!"
    $MessageLabel.show()
    yield(get_tree().create_timer(1), 'timeout')
    $StartButton.show()
async public void ShowGameOver()
{
    ShowMessage("Game Over");

    var messageTimer = GetNode<Timer>("MessageTimer");
    await ToSignal(messageTimer, "timeout");

    var messageLabel = GetNode<Label>("MessageLabel");
    messageLabel.Text = "Dodge the\nCreeps!";
    messageLabel.Show();

    GetNode<Button>("StartButton").Show();
}

Эта функция вызывается, когда игрок проигрывает. Она будет показывать надпись «Game Over» на 2 секунды, затем будет происходить возврат к основному экрану и появится кнопка «Start».

Примечание

Если вам нужно сделать паузу на короткое время, то альтернативой использованию узла Timer является использование функции SceneTree’s `create_timer()`. Это может быть очень полезно для задержки, как, например, в вышеприведенном коде, где мы хотим подождать немного времени, прежде чем показать кнопку «Start».

func update_score(score):
    $ScoreLabel.text = str(score)
public void UpdateScore(int score)
{
    GetNode<Label>("ScoreLabel").Text = score.ToString();
}

Эта функция вызывается из Main каждый раз, когда изменяется количество очков.

Соедините «timeout()» сигнал из «MessageTimer» и сигнал «pressed()» из «StartButton».

func _on_StartButton_pressed():
    $StartButton.hide()
    emit_signal("start_game")

func _on_MessageTimer_timeout():
    $MessageLabel.hide()
public void OnStartButtonPressed()
{
    GetNode<Button>("StartButton").Hide();
    EmitSignal("StartGame");
}

public void OnMessageTimerTimeout()
{
    GetNode<Label>("MessageLabel").Hide();
}

Подключение к главной HUD

Закончив создание HUD сцены, сохраните её и вернитесь к Main.``HUD`` сцену поместите в нижнюю часть дерева Main. Дерево должно выглядеть так, поэтому убедитесь, что вы ничего не пропустили:

../../_images/completed_main_scene.png

Теперь нам нужно подключить HUD функцию для Main скрипта. Для этого требуется несколько дополнений к Main сцены:

Во вкладке Узел подключите к HUD сигнал start_game и new_game() функций.

В new_game(), обновит счет дисплея и покажет сообщение «Get Ready»:

$HUD.update_score(score)
$HUD.show_message("Get Ready")
var hud = GetNode<HUD>("HUD");
hud.UpdateScore(_score);
hud.ShowMessage("Get Ready!");

В game_over() нам нужно вызвать соответствующую функцию HUD:

$HUD.show_game_over()
GetNode<HUD>("HUD").ShowGameOver();

Наконец добавьте в _on_ScoreTimer_timeout() для синхронизации отображения с меняющемся очками:

$HUD.update_score(score)
GetNode<HUD>("HUD").UpdateScore(_score);

Теперь вы готовы к игре! Нажмите на кнопку «Запустить проект» или F5. Вам будет предложено выбрать основную сцену, выбирайте Main.tscn.

Удаляем старых крипов

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

Для удаления оставшихся крипов, мы используем сигнал start_game который уже посылает узел HUD. Мы не можем использовать редактор для подключения сигнала к мобам так как мы хотим, поскольку, до запуска игры, в дереве сцены Main нет узлов Mob. Вместо этого мы используем код.

Начните с добавления новой функций Mob.gd. queue_free() удалит этот узел в конце текущего кадра.

func _on_start_game():
    queue_free()
public void OnStartGame()
{
    QueueFree();
}

Затем добавьте новую строку, в конец функции в _on_MobTimer_timeout(), в Main.gd.

$HUD.connect("start_game", mob, "_on_start_game")
GetNode("HUD").Connect("StartGame", mobInstance, "OnStartGame");

Эта строка говорит новому узлу Mob (определённому переменной mob ) отвечать на все сигналы start_game, отправленные узлом HUD, исполнением функции _on_start_game().

Завершающие штрихи

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

Фон

По умолчанию серый фон не очень привлекательный, так что давайте поменяем цвет на свой. Один из способов сделать это заключается в использовании ColorRect узла. Сделайте первый узел под Main так, что он будет тянутся за другими узлами. ColorRect имеет только одно свойство: Color. Выберите цвет, который вам нравится и измените размер ColorRect, так чтобы он покрывал экран.

Вы можете добавить фоновое изображение (если у вас оно есть), с помощью узла Sprite.

Звуковые эффекты

Звук и музыка могут быть самым эффективным способом, чтобы добавить больше привлекательности к игре. В папке активов вашей игры, есть два звуковых файла: «House In a Forest Loop.ogg» для фоновой музыки и «gameover.wav», когда игрок проигрывает.

Добавьте два AudioStreamPlayer узла, как дочерний для Main. Назовите одну из них Music, а другие DeathSound. На каждом из них, нажмите на кнопку Stream свойствах выберите «загрузить» и добавьте соответствующий звуковой файл.

Для воспроизведения музыки, добавьте $Music.play() функцию в new_game() и $Music.stop() в game_over().

Наконец, добавить $DeathSound.play() в game_over().

Сочетание клавиш

Поскольку в игру играют с помощью клавиатуры, было бы удобно, если бы мы могли начать игру, нажав любую клавишу. Одним из способов сделать это является использование свойства «Shortcut» узла Button.

In the HUD scene, select the StartButton and find its Shortcut property in the Inspector. Select «New Shortcut» and click on the «Shortcut» item. A second Shortcut property will appear. Select «New InputEventAction» and click the new «InputEvent». Finally, in the Action property, type the name ui_select. This is the default input event associated with the spacebar.

../../_images/start_button_shortcut.png

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

Файлы проекта

Вы можете загрузить завершённую версию этого проекта по этим ссылкам: