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

Обзор

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

Примечание

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

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

../../_images/dodge_preview.gif

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

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

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

Примечание

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

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

Также в этой секции, под опциями «Stretch», установите Mode на «2d» и Aspect на «keep». Это гарантирует, что игра последовательно масштабируется на экранах разного размера.

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

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

../../_images/filesystem_dock.png

Сцена Игрока

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

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

Для начала нам нужно выбрать корневой узел [Root Node] для объекта player. В общем случае, корневой узел сцены должен отображать желаемую функциональность объекта - чем объект является. Нажмите кнопку «Other Node» и добавьте узел Area2D в сцену.

../../_images/add_node.png

Godot отобразит значок предупреждения рядом с этим узлом в древе сцены. Пока что не обращайте внимание на это. Мы рассмотрим это позже.

С помощью 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#). Будьте осторожны, набирайте имена методов точно при подключении сигналов.

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

Кликните на узел Player и добавьте дочерний узел AnimatedSprite. AnimatedSprite будет обрабатывать внешний вид и анимации для нашего игрока. Обратите внимание на символ предупреждения рядом с узлом. AnimatedSprite требует ресурс SpriteFrames, который представляет собой список отображаемых анимаций. Чтобы его создать, найдите свойство Frames в окне инспектора и кликните на «[empty]»-> «New SpriteFrames». Далее, в том же месте, нажмите кнопку <SpriteFrames>. Панель «SpriteFrames» откроется автоматически:

../../_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

После этих изменений обязательно сохраните сцену еще раз.

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

Now we need to add some functionality that we can’t get from a built-in node, so we’ll add a script. Click the Player node and click the «Attach Script» button:

../../_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().Size;
}

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

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

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

You can detect whether a key is pressed using Input.is_action_pressed(), which returns true if it’s pressed or false if it isn’t.

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

We start by setting the velocity to (0, 0) - by default, the player should not be moving. Then we check each input and add/subtract from the velocity to obtain a total direction. For example, if you hold right and down at the same time, the resulting velocity vector will be (1, 1). In this case, since we’re adding a horizontal and a vertical movement, the player would move faster diagonally than if it just moved horizontally.

We can prevent that if we normalize the velocity, which means we set its length to 1, then multiply by the desired speed. This means no more fast diagonal movement.

Совет

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

We also check whether the player is moving so we can call play() or stop() on the AnimatedSprite.

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

Совет

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

Now that we have a movement direction, we can update the player’s position. We can also use clamp() to prevent it from leaving the screen. Clamping a value means restricting it to a given range. Add the following to the bottom of the _process function (make sure it’s not indented under the else):

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

Совет

Параметр «delta» в функции «_process()» означает длительность кадра, то есть время, потраченное на завершение обработки предыдущего кадра. Благодаря использованию этого значения скорость движения будет постоянной даже при изменении частоты кадров.

Нажмите «Воспроизвести сцену» (F6) и удостоверьтесь, что вы можете перемещать игрока по экрану во всех направлениях.

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

Если вы получаете ошибку в панели «Отладчик», которая говорит

Попытка вызова функции 'play' в основании 'null instance' на нулевом экземпляре

this likely means you spelled the name of the AnimatedSprite node wrong. Node names are case-sensitive and $NodeName must match the name you see in the scene tree.

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

Now that the player can move, we need to change which animation the AnimatedSprite is playing based on its direction. We have the «walk» animation, which shows the player walking to the right. This animation should be flipped horizontally using the flip_h property for left movement. We also have the «up» animation, which should be flipped vertically with flip_v for downward movement. Let’s place this code at the end of the _process() function:

if velocity.x != 0:
    $AnimatedSprite.animation = "walk"
    $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 = "walk";
    animatedSprite.FlipV = false;
    // See the note below about boolean assignment
    animatedSprite.FlipH = velocity.x < 0;
}
else if (velocity.y != 0)
{
    animatedSprite.Animation = "up";
    animatedSprite.FlipV = velocity.y > 0;
}

Примечание

The boolean assignments in the code above are a common shorthand for programmers. Since we’re doing a comparison test (boolean) and also assigning a boolean value, we can do both at the same time. Consider this code versus the one-line boolean assignment above:

if velocity.x < 0:
    $AnimatedSprite.flip_h = true
else:
    $AnimatedSprite.flip_h = false
if (velocity.x < 0)
{
    animatedSprite.FlipH = true;
}
else
{
    animatedSprite.FlipH = false;
}

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

Совет

Общей ошибкой является неправильное именование анимаций. Имена анимаций в панели SpriteFrames должны совпадать с именами анимаций в вашем коде. Если вы назвали анимацию «Walk», вы должны также использовать заглавную букву «W» в коде.

When you’re sure the movement is working correctly, add this line to _ready(), so the player will be hidden when the game starts:

hide()
Hide();

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

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

Add the following at the top of the script, after 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

Notice our custom «hit» signal is there as well! Since our enemies are going to be RigidBody2D nodes, we want the body_entered(body: Node) signal. This signal will be emitted when a body contacts the player. Click «Connect..» and the «Connect a Signal» window appears. We don’t need to change any of these settings so click «Connect» again. Godot will automatically create a function in your player’s script.

../../_images/player_signal_connection.png

Обратите внимание на зеленый значок, указывающий на то, что сигнал подключен к этой функции. Добавьте этот код в функцию:

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 по нескольку раз.

Примечание

Disabling the area’s collision shape can cause an error if it happens in the middle of the engine’s collision processing. Using set_deferred() tells Godot to wait to disable the shape until it’s safe to do so.

The last piece is to add a function we can call to reset the player when starting a new game.

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

Сцена Врага

Now it’s time to make the enemies our player will have to dodge. Their behavior will not be very complex: mobs will spawn randomly at the edges of the screen, choose a random direction, and move in a straight line.

We’ll create a Mob scene, which we can then instance to create any number of independent mobs in the game.

Примечание

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

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

Click Scene -> New Scene and add the following nodes:

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

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

../../_images/set_collision_mask.png

Set up the AnimatedSprite like you did for the player. This time, we have 3 animations: fly, swim, and walk. There are two images for each animation in the art folder.

Adjust the «Speed (FPS)» to 3 for all animations.

../../_images/mob_animations.gif

Set the Playing property in the Inspector to “On”.

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

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

As in the Player scene, add a CapsuleShape2D for the collision. To align the shape with the image, you’ll need to set the Rotation Degrees property to 90 (under «Transform» in the Inspector).

Save the scene.

Скрипт врага

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

extends RigidBody2D

export var min_speed = 150  # Minimum speed range.
export var max_speed = 250  # Maximum speed range.
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.

}

When we spawn a mob, we’ll pick a random value between min_speed and max_speed for how fast each mob will move (it would be boring if they were all moving at the same speed).

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

func _ready():
    var mob_types = $AnimatedSprite.frames.get_animation_names()
    $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()
{
    var animSprite = GetNode<AnimatedSprite>("AnimatedSprite");
    var mobTypes = animSprite.Frames.GetAnimationNames();
    animSprite.Animation = mobTypes[_random.Next(0, mobTypes.Length)];
}

Сперва мы получаем список имён анимаций из AnimatedSprite –> свойство frames. Оно возвращает нам список, содержащий имена всех трёх анимаций: ["walk", "swim", "fly"].

Дальше нам нужно выбрать случайное число между 0 и 2, чтобы выбрать имя одной из анимаций из списка (индексы членов списка начинаются с 0). Функция randi() % n выбирает случайное целое число между 0 и n-1.

Примечание

You must use randomize() if you want your sequence of «random» numbers to be different every time you run the scene. We’re going to use randomize() in our Main scene, so we won’t need it here.

The last piece is to make the mobs delete themselves when they leave the screen. Connect the screen_exited() signal of the VisibilityNotifier2D node and add this code:

func _on_VisibilityNotifier2D_screen_exited():
    queue_free()
public void OnVisibilityNotifier2DScreenExited()
{
    QueueFree();
}

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

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

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

../../_images/instance_scene.png

Теперь добавьте следующие узлы в виде дочерних элементов 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/grid_snap_button.png

Важно

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

../../_images/draw_path2d.gif

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

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

Your scene should look like this:

../../_images/main_scene_nodes.png

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

Добавьте скрипт к узлу 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;
    }
}

Click the Main node and you will see the Mob property in the Inspector under «Script Variables».

Значение этого свойства можно присвоить двумя способами:

  • Drag Mob.tscn from the «FileSystem» panel and drop it in the Mob property .
  • Click the down arrow next to «[empty]» and choose «Load». Select Mob.tscn.

Next, click on the Player and connect the hit signal. We want to make a new function named game_over, which will handle what needs to happen when a game ends. Type «game_over» in the «Receiver Method» box at the bottom of the «Connect a Signal» window and click «Connect». Add the following code to the new function, as well as a new_game function that will set everything up for a 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();
}

Now connect the timeout() signal of each of the Timer nodes (StartTimer, ScoreTimer , and MobTimer) to the main script. StartTimer will start the other two timers. ScoreTimer will increment the score by 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().

func _on_MobTimer_timeout():
    # Choose a random location on Path2D.
    $MobPath/MobSpawnLocation.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.Offset = _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.LinearVelocity = new Vector2(RandRange(150f, 250f), 0).Rotated(direction);
}

Важно

Why PI? In functions requiring angles, GDScript uses radians, not degrees. If you’re more comfortable working with degrees, you’ll need to use the deg2rad() and rad2deg() functions to convert between the two.

Testing the scene

Давайте протестируем сцену, чтобы убедиться, что все работает. Добавьте в _ready() следующие строки:

func _ready():
    randomize()
    new_game()
    public override void _Ready()
    {
        NewGame();
    }
}

Let’s also assign Main as our «Main Scene» - the one that runs automatically when the game launches. Press the «Play» button and select Main.tscn when prompted.

You should be able to move the player around, see mobs spawning, and see the player disappear when hit by a mob.

When you’re sure everything is working, remove the call to new_game() from _ready().

HUD

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

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

The HUD needs to display the following information:

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

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

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

  • Label названный ScoreLabel.
  • Label named Message.
  • Кнопка с именем 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:

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

Once you’ve done this on the ScoreLabel, you can click the down arrow next to the DynamicFont property and choose «Copy», then «Paste» it in the same place on the other two Control nodes.

Примечание

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

Организуйте узлы, как показано ниже. Нажмите кнопку «Layout (Макет)», чтобы задать макет для узла Control :

../../_images/ui_anchor.png

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

ScoreLabel

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

Message

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

StartButton

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

On the MessageTimer, set the Wait Time to 2 and set the One Shot property to «On».

Теперь добавьте этот скрипт в 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):
    $Message.text = text
    $Message.show()
    $MessageTimer.start()
public void ShowMessage(string text)
{
    var message = GetNode<Label>("Message");
    message.Text = text;
    message.Show();

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

This function is called when we want to display a message temporarily, such as «Get Ready».

func show_game_over():
    show_message("Game Over")
    # Wait until the MessageTimer has counted down.
    yield($MessageTimer, "timeout")

    $Message.text = "Dodge the\nCreeps!"
    $Message.show()
    # Make a one-shot timer and wait for it to finish.
    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 message = GetNode<Label>("Message");
    message.Text = "Dodge the\nCreeps!";
    message.Show();

    await ToSignal(GetTree().CreateTimer(1), "timeout");
    GetNode<Button>("StartButton").Show();
}

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

Примечание

When you need to pause for a brief time, an alternative to using a Timer node is to use the SceneTree’s create_timer() function. This can be very useful to add delays such as in the above code, where we want to wait some time before showing the «Start» button.

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():
    $Message.hide()
public void OnStartButtonPressed()
{
    GetNode<Button>("StartButton").Hide();
    EmitSignal("StartGame");
}

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

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

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

../../_images/completed_main_scene.png

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

Во вкладке «Узел», соедините сигнал HUD-а start_game к функции главного узла new_game() введя «new_game» в поле «Метод-приёмник» в окне «Подключить сигнал к методу» . Убедитесь что рядом с функцией func 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.

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

If you play until «Game Over» and then start a new game right away, the creeps from the previous game may still be on the screen. It would be better if they all disappeared at the start of a new game. We just need a way to tell all the mobs to remove themselves. We can do this with the «group» feature.

В сцене Mob, выберите корневой узел и нажмите на вкладку «узел» рядом с инспектором (там же, где вы находили сигналы узлов). Дпльше на «Сигналы», нажмите на «Группы» и теперь вы можете ввести новое имя группы и нажать «Добавить».

../../_images/group_tab.png

Теперь все мобы будут в группе «mobs». Теперь мы можем добавить соответствующие строки к функции «game_over()» во вкладке «Main»:

get_tree().call_group("mobs", "queue_free")
GetTree().CallGroup("mobs", "queue_free");

Функция ``call_group()``вызывает каждую именную функцию на каждом узле в группе- в этом случае мы говорим каждому мобу удалять себя.

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

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

Фон

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

You could also add a background image, if you have one, by using a TextureRect node instead.

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

Звук и музыка могут быть самым эффективным способом, чтобы добавить больше привлекательности к игре. В папке активов вашей игры, есть два звуковых файла: «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().

Keyboard shortcut

Since the game is played with keyboard controls, it would be convenient if we could also start the game by pressing a key on the keyboard. We can do this with the «Shortcut» property of the Button node.

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 «InputEventAction». 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

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

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

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