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

Огляд

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

Примітка

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

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

../../_images/dodge_preview.gif

Why 2D? 3D games are much more complex than 2D ones. You should stick to 2D until you have a good understanding of the game development process and how to use Godot.

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

Launch Godot and create a new project. Then, download dodge_assets.zip. This contains the images and sounds you’ll be using to make the game. Unzip these files in your project folder.

Примітка

For this tutorial, we will assume you are familiar with the Godot editor. If you haven’t read Сцени та вузли, do so now for an explanation of setting up a project and using the editor.

This game is designed for portrait mode, so we need to adjust the size of the game window. Click on Project -> Project Settings -> Display -> Window and set «Width» to 480 and «Height» to 720.

Also in this section, under the «Stretch» options, set Mode to «2d» and Aspect to «keep». This ensures that the game scales consistently on different sized screens.

Організація проекту

In this project, we will make 3 independent scenes: Player, Mob, and HUD, which we will combine into the game’s Main scene. In a larger project, it might be useful to create folders to hold the various scenes and their scripts, but for this relatively small game, you can save your scenes and scripts in the project’s root folder, identified by res://. You can see your project folders in the FileSystem Dock in the lower left corner:

../../_images/filesystem_dock.png

Сцена Гравця

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

Структура вузлів

To begin, we need to choose a root node for the player object. As a general rule, a scene’s root node should reflect the object’s desired functionality - what the object is. Click the «Other Node» button and add an Area2D node to the scene.

../../_images/add_node.png

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

With Area2D we can detect objects that overlap or run into the player. Change the node’s name to Player by double-clicking on it. Now that we’ve set the scene’s root node, we can add additional nodes to give it more functionality.

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

../../_images/lock_children.png

Збережіть сцену. Клацніть Сцена -> Зберегти, або натисніть Ctrl+S на Windows/Linux, або Cmd + S на macOS.

Примітка

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

  • GDScript: Класи (вузли) використовують PascalCase, змінні та функції використовують snake_case, а константи використовують ALL_CAPS (Дивіться Посібник зі стилю GDScript ).
  • C#: Класи, експортовані змінні та методи використовують PascalCase, приватні поля використовують _camelCase, локальні змінні та параметри використовують camelCase (Дивіться Настанови по стилю C# ). Будьте обережні, вводячи назви методів саме під час підключення сигналів.

Анімація спрайта

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». Click again to open the «SpriteFrames» panel:

../../_images/spriteframes_panel.png

On the left is a list of animations. Click the «default» one and rename it to «walk». Then click the «New Animation» button to create a second animation named «up». Find the player images in the «FileSystem» tab - they’re in the art folder you unzipped earlier. 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 for the corresponding animation:

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

Make sure to save the scene again after these changes.

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

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

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

Примітка

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

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

Якщо ви використовуєте C#, вам потрібно (пере)будувати збірки проектів, коли ви хочете бачити нові змінні експорту, чи сигнали. Цю збірку можна вручну запустити, натиснувши слово «Mono» внизу вікна редактора, щоб відкрити панель Mono, а потім натиснути кнопку «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 напрямки для перевірки. Дії введення визначені в Параметрах проекту в розділі «Карта введення». Тут ви можете визначити власні події та призначити їм різні клавіші, події миші чи інші входи. Для цієї демонстрації ми будемо використовувати події за замовчуванням, які призначені клавішам зі стрілками на клавіатурі.

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 у Vector math. Це добре знати, але це не буде потрібно для решти цього уроку.

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

Click «Play Scene» (F6) and confirm you can move the player around the screen in all directions.

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

If you get an error in the «Debugger» panel that says

Attempt to call function 'play' in base 'null instance' on a 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;
}

Play the scene again and check that the animations are correct in each of the directions.

Порада

A common mistake here is to type the names of the animations wrong. The animation names in the SpriteFrames panel must match what you type in the code. If you named the animation "Walk", you must also use a capital «W» in the code.

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

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

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

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

Note the green icon indicating that a signal is connected to this function. Add this code to the function:

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

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

У властивостях 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”.

We’ll select one of these animations randomly so that the mobs will have some variety.

Як і зображення гравця, ці зображення мобів потрібно зменшити. Встановіть в 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)];
}

First, we get the list of animation names from the AnimatedSprite’s frames property. This returns an Array containing all three animation names: ["walk", "swim", "fly"].

We then need to pick a random number between 0 and 2 to select one of these names from the list (array indices start at 0). randi() % n selects a random integer between 0 and 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 на «включено» і задайте Position (позиція) в StartPosition на (240, 450).

Поява мобів

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

../../_images/path2d_buttons.png

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

../../_images/grid_snap_button.png

Важливо

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

../../_images/draw_path2d.gif

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

Тепер, коли шлях визначений, додайте вузол PathFollow2D (слідувати за шляхом 2D) нащадком 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».

You can assign this property’s value in two ways:

  • 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

Let’s test the scene to make sure everything is working. Add this to _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 (інтерфейс)

Останній фрагмент, який потрібен нашій грі, - це інтерфейс користувача: інтерфейс для відображення таких речей, як рахунок, повідомлення «game over» («гра закінчена») та кнопка перезапуску. Створіть нову сцену та додайте вузол CanvasLayer з іменем HUD. «HUD» означає «head-up display» , інформаційний дисплей, який з’являється у вигляді перекриття поверх ігрового вікна.

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

The HUD needs to display the following information:

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

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

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

  • Label названа ScoreLabel (Мітка рахунку).
  • Label named Message.
  • Button названа ``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» («Шрифт / Дані шрифту») виберіть «Завантажити» та виберіть файл «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 (контрольні) мають положення та розмір, але вони також мають прив’язки (якорі) та поля. Прив’язки визначають початок координат - опорну точку для країв вузла. Поля оновлюються автоматично під час переміщення, чи зміни розміру, контрольного вузла. Вони представляють відстань від країв контрольного вузла до його прив’язки. Детальнішу інформацію дивіться Design interfaces with the Control nodes.

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

../../_images/ui_anchor.png

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

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

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

Message

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

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

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

Примітка

Коли вам потрібно зробити паузу на короткий час, альтернативою використанню вузла Timer є використання функції SceneTree (Дерева Сцени) create_timer(). Це може бути дуже корисно для такої затримки, як, наприклад, у наведеному вище коді, де ми хочемо зачекати трохи часу, перш ніж показувати кнопку «Старт».

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

Ця функція викликається Main щоразу, коли рахунок змінюється.

Connect the timeout() signal of MessageTimer and the pressed() signal of StartButton and add the following code to the new functions:

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 до Main

Now that we’re done creating the HUD scene, go back to Main. Instance the HUD scene in Main like you did the Player scene. The scene tree should look like this, so make sure you didn’t miss anything:

../../_images/completed_main_scene.png

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

In the Node tab, connect the HUD’s start_game signal to the new_game() function of the Main node by typing «new_game» in the «Receiver Method» in the «Connect a Signal» window. Verify that the green connection icon now appears next to func new_game() in the script.

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

Тепер ви готові грати! Натисніть кнопку «Відтворити проект». Вам буде запропоновано вибрати головну сцену, тому виберіть 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.

In the Mob scene, select the root node and click the «Node» tab next to the Inspector (the same place where you find the node’s signals). Next to «Signals», click «Groups» and you can type a new group name and click «Add».

../../_images/group_tab.png

Now all mobs will be in the «mobs» group. We can then add the following line to the game_over() function in Main:

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

The call_group() function calls the named function on every node in a group - in this case we are telling every mob to delete itself.

Завершальна обробка

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

Тло

The default gray background is not very appealing, so let’s change its color. One way to do this is to use a ColorRect node. Make it the first node under Main so that it will be drawn behind the other nodes. ColorRect only has one property: Color. Choose a color you like and select «Layout» -> «Full Rect» so that it covers the screen.

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()``.

Гарячі клавіші

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

Тепер, коли з’явиться кнопка запуску, ви можете, або натиснути на неї, або натиснути пробіл, щоб розпочати гру.

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

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