Ваша перша гра

Огляд

Цей урок допоможе вам створити перший проект Godot. Ви дізнаєтесь, як працює редактор Godot, як структурувати проект та як побудувати 2D гру.

Примітка

Цей проект - це вступ до редактора Godot. Він передбачає, що ви вже маєте певний досвід програмування. Якщо ви взагалі не програмуєте, вам слід почати звідси: Скриптинґ.

Гра називається «Dodge the Creeps!» («Ухиляйсь від Крипів»). Ваш персонаж повинен якомога довше рухатися і уникати ворогів. Ось попередній перегляд кінцевого результату:

../../_images/dodge_preview.gif

Чому 2D? 3D-ігри набагато складніші, ніж 2D. Вам слід дотримуватися 2D, поки ви добре не зрозумієте процес розробки гри.

Налаштування проекта

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

Примітка

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

Ця гра буде використовувати портретний режим, тому нам потрібно відрегулювати розмір вікна гри. Клацніть на Проект -> Параметри проекту -> Display (Дисплей) -> Window (Вікно) та встановіть «Width» («Ширина») на 480 та «Height» («Висота») на 720.

Organizing the project

У цьому проекті ми будемо робити 3 незалежні сцени: Player (Гравець), Mob (Ворог), і HUD (інтерфейс), які ми будемо об’єднувати в ігрову сцену Main (головну). У більшому проекті може бути корисним створення тек для розміщення різних сцен та їх скриптів, але для цієї порівняно невеликої гри ви можете зберігати свої сцени та скрипти в кореневій теці проекту, згаданої як res://. Ви можете бачити теки проектів на панелі Файлова система у нижньому лівому куті:

../../_images/filesystem_dock.png

Сцена Гравця

Перша сцена, яку ми зробимо, визначає об’єкт Player (Гравець) . Однією з переваг створення окремої сцени гравця є те, що ми можемо протестувати її окремо, ще до того, як ми створили інші частини гри.

Node structure

Для початку натисніть кнопку «Додати / створити новий вузол» та додайте до сцени вузол Area2D .

../../_images/add_node.png

Godot виведе попереджувальний значок поруч із вузлом у дереві сцени. Ви можете проігнорувати його наразі. Ми розглянемо цей значок пізніше.

З Area2D можна виявити об’єкти , які перекриваються, або стикаються з гравцем. Змініть його ім’я на Player, натиснувши ім’я вузла. Це кореневий вузол сцени. Ми можемо додати додаткові вузли до гравця, щоб розширити його функціональність.

Перш ніж додати будь-яких нащадків до вузла Player, ми хочемо переконатися, що ми їх випадково не перемістимо чи не змінимо розмір, натиснувши на них. Виберіть вузол і натисніть на значок праворуч від блокування; в підказці написано: «Гарантує, що нащадки об’єкта не можуть бути вибрані.»

../../_images/lock_children.png

Save the scene. Click Scene -> Save, or press Ctrl + S on Windows/Linux or Cmd + S on macOS.

Примітка

У цьому проекті ми будемо дотримуватися правил іменування Godot.

  • GDScript: Класи (вузли) використовують PascalCase, змінні та функції використовують snake_case, а константи використовують ALL_CAPS (Дивіться GDScript style guide ).
  • C#: Класи, експортовані змінні та методи використовують PascalCase, приватні поля використовують _camelCase, локальні змінні та параметри використовують camelCase (Дивіться 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

On the left is a list of animations. Click the «default» one and rename it to «right». Then click the «Add» button to create a second animation named «up». Drag the two images for each animation, named playerGrey_up[1/2] and playerGrey_walk[1/2], into the «Animation Frames» side of the panel:

../../_images/spriteframes_panel2.png

Зображення гравця трохи занадто великі для вікна гри, тому нам потрібно їх масштабувати. Клацніть на вузлі AnimatedSprite та встановіть властивість Scale (Масштаб) на (0.5, 0.5). Ви можете знайти її в Інспекторі під заголовком Node2D.

../../_images/player_scale.png

Нарешті створіть ще одного нащадка Player, додайте CollisionShape2D (Форма межі зіткнень). Він визначатиме «ударну коробку» гравця, або межі його зони зіткнення. Для цього персонажа найкраще підходить вузол CapsuleShape2D (Форма капсули 2D), тому поруч із пунктом «Shape» («Форма») в Інспекторі натисніть «[порожній]» «->» Нова CapsuleShape2D «. Використовуючи дві ручки розмірів, змініть розмір форми, щоб покрити спрайт:

../../_images/player_coll_shape.png

Коли ви закінчите, ваша сцена Player повинна виглядати так:

../../_images/player_scene_nodes.png

Переміщення гравця

Тепер нам потрібно додати певну функціональність, яку ми не можемо отримати із вбудованих вузлів, тому додамо скрипт. Клацніть вузол Player і натисніть кнопку «Додати скрипт»:

../../_images/add_script_button.png

У вікні налаштувань скрипту ви можете залишити налаштування за замовчуванням. Просто натисніть «Створити»:

Примітка

Якщо ви створюєте скрипт на C# , або іншій мові, виберіть мову зі спадного меню Мова, перед тим, як створити скрипт.

../../_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» («Змінні скриптів»). Пам’ятайте, що якщо ви зміните тут значення, воно перепише значення, записане в скрипті.

Попередження

If you’re using C#, you need to (re)build the project assemblies whenever you want to see new export variables or signals. This build can be manually triggered by clicking the word «Mono» at the bottom of the editor window to reveal the Mono Panel, then clicking the «Build Project» button.

../../_images/export_variable.png

Функція _ready() викликається , коли вузол входить в дерево сцени, чудовий момент , щоб задати розмір вікна гри:

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

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

  • Перевірити ввід.
  • Рухати в заданому напрямі.
  • Відтворити відповідну анімацію.

Спочатку нам потрібно перевірити ввід - чи гравець натискає клавішу? Для цієї гри у нас є 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 (вниз) одночасно, отриманий вектор буде (1, 1). У цьому випадку, оскільки ми додаємо горизонтальний та вертикальний рух, гравець рухатиметься швидше, ніж якби просто перемістився по горизонталі.

Ми можемо запобігти цьому, якщо нормалізуємо (normalize) швидкість, тобто, встановимо її довжину (length) на 1 і помножимо на потрібну швидкість. Це означає, що більше немає більш швидкого руху по діагоналі.

Порада

If you’ve never used vector math before, or need a refresher, you can see an explanation of vector usage in Godot at Vector math. It’s good to know but won’t be necessary for the rest of this tutorial.

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

Порада

In GDScript, $ returns the node at the relative path from the current node, or returns null if the node is not found. Since AnimatedSprite is a child of the current node, we can use $AnimatedSprite.

$``це скорочення для ``get_node(). Так що в наведеному вище коді $AnimatedSprite.play() це те саме, що і get_node("AnimatedSprite").play().

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:

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 () означає тривалість кадру - тобто часу, необхідного для завершення попереднього кадру. Використання цього значення гарантує, що ваш рух залишатиметься послідовним, навіть якщо частота кадрів змінюється.

Click «Play Scene» (F6) and confirm you can move the player around the screen in all directions. The console output that opens upon playing the scene can be closed by clicking Output (which should be highlighted in blue) in the lower left of the Bottom Panel.

Попередження

Якщо ви отримаєте помилку на панелі «Зневаджувач», яка посилається на «null instance» («нульовий екземпляр»), це, ймовірно, означає, що ви неправильно написали ім’я вузла. Імена вузлів чутливі до регістру і $Ім'яВузла, чи get_node("Ім'яВузла") має збігатися з ім’ям , яке ви бачите в дереві сцени.

Вибір анімації

Тепер, коли гравець може рухатися, нам потрібно, щоб залежно від напрямку руху він відтворював різну анімацію. У нас є анімація «right» («права»), яку слід відтворювати при горизонтальному русі, використовуючи властивість flip_h для руху вліво, і анімація «up» («вгору»), яку слід перевернути вертикально з допомогою 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";
    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. Consider this code versus the shortened 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

Відтворіть сцену ще раз і перевірте правильність анімації в кожному з напрямків. Коли ви переконаєтеся, що рух відтворюється правильно, додайте цей рядок до _ready(), щоб гравець був схований під час гри:

hide()
Hide();

Підготовка до зіткнень

We want Player to detect when it’s hit by an enemy, but we haven’t made any enemies yet! That’s OK, because we’re going to use Godot’s signal functionality to make it work.

Додайте наступне у верхній частині скрипту після 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 і натисніть на вкладку «Вузол» поруч із вкладкою Інспектор, щоб побачити список сигналів, які гравець може випромінювати:

../../_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( Object body ) signal; this will be emitted when a body contacts the player. Click «Connect..» and then «Connect» again on the «Connecting Signal» window. We don’t need to change any of these settings - Godot will automatically create a function in your player’s script. This function will be called whenever the signal is emitted - it handles the signal.

Порада

Під час підключення сигналу замість того, щоб 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 більше одного разу.

Примітка

Відключення області зіткнення може призвести до помилки, якщо це станеться під час опрацювання редактором зіткнень. Використання set_deferred() дозволяє нам почекати, і вимкнути область, коли це буде безпечно.

Остання деталь нашого гравця - функція, яку ми можемо викликати для появи гравця при запуску нової гри.

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

Сцена ворога

Тепер настав час зробити ворогів, від яких гравцеві доведеться ухилятися. Їх поведінка буде не дуже складною: вони будуть час від часу виникати по краях екрана і рухатися у випадковому напрямку по прямій лінії, а потім зникати за межами екрану.

We will build this into a Mob scene, which we can then instance to create any number of independent mobs in the game.

Node setup

Натисніть Сцена -> Нова сцена і створіть сцену Mob.

Сцена Mob використовуватиме такі вузли:

  • RigidBody2D (тверде тіло 2D) (названий Mob)
    • :ref:`AnimatedSprite <class_AnimatedSprite>`(Анімація спрайту)
    • :ref:`CollisionShape2D <class_CollisionShape2D>`(Форма зіткнення 2D)
    • VisibilityNotifier2D (named Visibility)

Не забудьте налаштувати нащадків, щоб вони не могли бути обрані, як ви робили це зі сценою гравця.

In the RigidBody2D properties, set Gravity Scale to 0, so the mob will not fall downward. In addition, under the PhysicsBody2D section, click the Mask property and uncheck the first box. This will ensure the mobs do not collide with each other.

../../_images/set_collision_mask.png

Налаштуйте AnimatedSprite так, як ви налаштовували її для гравця. На цей раз, у нас є 3 анімації: fly, swim, і walk. Встановіть властивість Playing (відтворення) в інспекторі на «Увімкнено» і відрегулюйте налаштування «Частота (кадри за секунду (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"};
}

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). We also have an array containing the names of the three animations, which we’ll use to select a random one. Make sure you’ve spelled these the same in the script and in the SpriteFrames 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.

Main scene

Тепер настав час зібрати все це разом. Створіть нову сцену та додайте 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 на «включено» і задайте Position (позиція) в StartPosition на (240, 450).

Spawning mobs

Головний вузол породжує нових мобів, і ми хочемо, щоб вони з’являлись у випадковому місці на краю екрана. Додайте вузол Path2D (Шлях 2D), нащадком Main і назвіть MobPath. Вибравши Path2D, ви побачите нові кнопки вгорі редактора:

../../_images/path2d_buttons.png

Виберіть середню («Додати точку») та намалюйте шлях, клацнувши, щоб додати точки у вказаних кутах. Щоб точки прикріпилися до сітки (ґратки), переконайтеся, що іконка «Використати до ґратки» включена. Цю опцію можна знайти зліва від кнопки «Блокування», виглядає як магніт з кількома лініями, що перетинаються.

../../_images/draw_path2d.gif

Важливо

Малюйте доріжку за годинниковою стрілкою , інакше ваші моби будуть виникати спрямовані назовні, а не всередину!

Після розміщення четвертої точки на зображенні натисніть кнопку «Закрити криву», і ваша крива буде завершена.

Now that the path is defined, add a PathFollow2D node as a child of MobPath and name it MobSpawnLocation. This node will automatically rotate and follow the path as it moves, so we can use it to select a random position and direction along the path.

Головний скрипт

Add a script to Main. At the top of the script, we use export (PackedScene) to allow us to choose the Mob scene we want to instance.

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 з панелі «Файлова система» у властивість Mob під Script Variables (змінними скрипту) Main вузла.

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 «Connecting Signal» window. Add the following code, as well as a new_game function to 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();
}

Тепер підключіть сигнал timeout() кожного з вузлів Timer ( StartTimer, ScoreTimer і MobTimer) до головного скрипту. 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++;
}

In _on_MobTimer_timeout(), we will create a mob instance, pick a random starting location along the Path2D, and set the mob in motion. The PathFollow2D node will automatically rotate as it follows the path, so we will use that to select the mob’s direction as well as its position.

Зауважте, що новий екземпляр потрібно додати до сцени за допомогою 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.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 (інтерфейс)

The final piece our game needs is a UI: an interface to display things like score, a «game over» message, and a restart button. Create a new scene, and add a CanvasLayer node named HUD. «HUD» stands for «heads-up display», an informational display that appears as an overlay on top of the game view.

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

HUD показує наступну інформацію:

  • Рахунок, змінюваний ScoreTimer.
  • Повідомлення, наприклад «Game Over» («Гра закінчена»), або «Get Ready!» («Готуйся!»)
  • Кнопка «Start»(«Старт»), щоб розпочати гру.

Основний вузол для елементів інтерфейсу - Control. Для створення нашого інтерфейсу ми будемо використовувати два типи цього вузла : Label та Button.

Додайте вузлу HUD таких нащадків:

  • Label названа ScoreLabel (Мітка рахунку).
  • Label названа ``MessageLabel``(мітка повідомлення).
  • Button названа ``StartButton``(кнопка старту).
  • Timer названий ``MessageTimer``(таймер повідомлення).

Клацніть на ScoreLabel і введіть число в поле Text в інспекторі. Шрифт за замовчуванням для вузлів Control невеликий і не дуже масштабується. У ігрових активах є файл шрифту, який називається «Xolonium-Regular.ttf» (даний шрифт підтримує кирилицю). Щоб використовувати цей шрифт, виконайте наступне для кожного з трьох Control вузлів:

  1. У розділі «Custom Fonts» («Спеціальні шрифти») виберіть «New DynamicFont» («Новий динамічний шрифт»)
../../_images/custom_font1.png
  1. Клацніть на доданому «DynamicFont», а під «Font/Font Data» («Шрифт / Дані шрифту») виберіть «Завантажити» та виберіть файл «Xolonium-Regular.ttf». Ви також повинні встановити розмір шрифта в полі Size. Добре працює значення 64.
../../_images/custom_font2.png

Примітка

Anchors and Margins: Control nodes have a position and size, but they also have anchors and margins. Anchors define the origin - the reference point for the edges of the node. Margins update automatically when you move or resize a control node. They represent the distance from the control node’s edges to its anchor. See Design interfaces with the Control nodes for more details.

Впорядкуйте вузли, як показано нижче. Натисніть кнопку «Макет», щоб встановити макет вузла керування:

../../_images/ui_anchor.png

Ви можете перетягнути вузли, щоб розмістити їх вручну, або, для більш точного розміщення, скористайтеся такими налаштуваннями:

ScoreLabel (Мітка рахунку)

  • Text : 0
  • Макет : «Згори за шириною»
  • Align (вирівнювання): «Center»

MessageLabel (Мітка повідомлення)

  • Text : Dodge the Creeps! (Ухились від крипів!)
  • Макет : «Гор. за центром за шириною»
  • Align (вирівнювання): «Center»
  • Autowrap (Авто-згортання) : «Увімкнено»

StartButton (Кнопка Старту)

  • Text : Start (Старт)
  • Макет : «За центром внизу «
  • Margin (Поле) :
    • Top (зверху): -200
    • Bottom (Знизу): -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 на «Увімкнено».

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

This function is called when the player loses. It will show «Game Over» for 2 seconds, then return to the title screen and, after a brief pause, show the «Start» button.

Примітка

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 delay, such as in the above code, where we want to wait a little bit of 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():
    $MessageLabel.hide()
public void OnStartButtonPressed()
{
    GetNode<Button>("StartButton").Hide();
    EmitSignal("StartGame");
}

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

Підключення HUD до Main

Тепер, коли ми закінчили створення сцени HUD, збережіть її та поверніться до Main. Вставте HUD сцену в Main, як ви вставили сцену Player, і помістіть її в нижню частину дерева. Повне дерево має виглядати так, тож переконайтеся, що ви нічого не пропустили:

../../_images/completed_main_scene.png

Тепер нам потрібно підключити функції HUD до нашого скрипту Main. Для цього потрібно кілька доповнень до сцени Main:

На вкладці Вузол підключіть сигнал start_game від HUD до функції вузла Main 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();

Finally, add this to _on_ScoreTimer_timeout() to keep the display in sync with the changing score:

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

Тепер ви готові грати! Натисніть кнопку «Відтворити проект». Вам буде запропоновано вибрати головну сцену, тому виберіть Main.tscn.

Видалення старих крипів

Якщо ви дограєте до кінця гри, а потім починаєте нову гру, крипи з попередньої гри все ще залишаються на екрані. Було б краще, якби вони всі зникли на початку нової гри.

We’ll use the start_game signal that’s already being emitted by the HUD node to remove the remaining creeps. We can’t use the editor to connect the signal to the mobs in the way we need because there are no Mob nodes in the Main scene tree until we run the game. Instead we’ll use code.

Start by adding a new function to Mob.gd. queue_free() will delete the current node at the end of the current frame.

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

Потім в Main.gd додайте новий рядок наприкінці функції _on_MobTimer_timeout().

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

This line tells the new Mob node (referenced by the mob variable) to respond to any start_game signal emitted by the HUD node by running its _on_start_game() function.

Finishing up

We have now completed all the functionality for our game. Below are some remaining steps to add a bit more «juice» to improve the game experience. Feel free to expand the gameplay with your own ideas.

Тло

Сіре тло за замовчуванням не дуже привабливе, тому давайте змінимо його колір. Один із способів зробити це - використати вузол ColorRect. Перемістіть його на самий верх нащадків, щоби він був одразу після вузла Main, щоби малювався позаду інших вузлів. ColorRect має тільки одну властивість:Color. Виберіть потрібний вам колір і змініть розмір ColorRect так, щоб він покривав екран.

Ви також можете додати фонове зображення, якщо воно у вас є, за допомогою вузла Sprite.

Sound effects

Звук і музика можуть бути єдиним найефективнішим способом додати привабливість до гри. У теці ваших ігрових ресурсів у вас є два звукових файли: «House In a Forest Loop.ogg» для фонової музики та «gameover.wav» - коли гравець програє.

Add two AudioStreamPlayer nodes as children of Main. Name one of them Music and the other DeathSound. On each one, click on the Stream property, select «Load», and choose the corresponding audio file.

Щоб відтворити музику, додайте $Music.play() у функцію new_game() та $Music.stop() у функцію game_over().

Нарешті, додайте $DeathSound.play() у функцію``game_over()``.

Комбінації клавіш

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. One way to do this is using 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 «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

Now when the start button appears, you can either click it or press Space to start the game.

Файли проекту

Ви можете знайти завершену версію цього проекту в таких місцях: