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

Огляд

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

Примітка

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

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

../../_images/dodge_preview.gif

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

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

Запустіть Godot і створіть новий проєкт. Потім завантажте: download: dodge_assets.zip <files / dodge_assets.zip>. Тут містяться зображення і звуки, які ви будете використовувати для створення гри. Разархівіруйте ці файли в папку вашого проєкту.

Примітка

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

Ця гра розроблена під портретний формат, тому нам потрібно налаштувати розмір ігрового вікна. Виберіть меню "Project -> Project Settings -> Display -> Window" і задайте полю "Width" (Ширина) значення `` 480``, а полю "Height" (Висота) значення `` 720``.

Також в цій секції, під опціями "Stretch", встановіть `` Mode`` на "2d" і `` Aspect`` на "keep". Це гарантує, що гра послідовно масштабується на екранах різного розміру.

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

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

../../_images/filesystem_dock.png

Сцена Гравця

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

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

Для початку нам потрібно вибрати кореневої вузол [Root Node] для об'єкта player. У загальному випадку, кореневий вузол сцени повинен відображати бажану функціональність об'єкта - чим об'єкт є. Натисніть кнопку "Other Node" і додайте вузол: ref: Area2D <class_Area2D> в сцену.

../../_images/add_node.png

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

За допомогою `` Area2D`` ми можемо виявляти об'єкти, які перекривають або стикаються з гравцем. Змініть ім'я вузла на `` Player``, двічі клацнувши по ньому. Тепер, коли ми встановили кореневої вузол сцени, ми можемо додавати додаткові вузли, щоб привнести більше функціоналу.

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

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

Натисніть на вузол Player і додайте йому в нащадки вузол AnimatedSprite (Анімація спрайту). AnimatedSprite оброблятиме зовнішній вигляд і анімацію для нашого гравця. Зауважте, що поруч із вузлом є символ попередження. AnimatedSprite вимагає ресурсу SpriteFrames (кадри спрайту), який являє собою список кадрів, які він може відображати. Щоб створити його, знайдіть властивість Frames (Кадри) у Інспекторі та натисніть "[порожній]" -> "Нові 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

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

../../_images/player_coll_shape.png

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

../../_images/player_scene_nodes.png

Не забудьте зберегти сцену знову після цих змін.

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

Тепер нам треба додати трохи функціоналу, який ми не можемо отримати за допомогою вбудованих вузлів і тому ми додамо скрипт. Клацніть на вузлі `` Player`` і натисніть кнопку "Attach Script":

../../_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 напрямки для перевірки. Дії введення визначені в Параметрах проєкту в розділі "Карта введення". Тут ви можете визначити власні події та призначити їм різні клавіші, події миші чи інші входи. Для цієї демонстрації ми будемо використовувати події за замовчуванням, які призначені клавішам зі стрілками на клавіатурі.

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

func _process(delta):
    var velocity = Vector2()  # The player's movement vector.
    if Input.is_action_pressed("ui_right"):
        velocity.x += 1
    if Input.is_action_pressed("ui_left"):
        velocity.x -= 1
    if Input.is_action_pressed("ui_down"):
        velocity.y += 1
    if Input.is_action_pressed("ui_up"):
        velocity.y -= 1
    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite.play()
    else:
        $AnimatedSprite.stop()
public override void _Process(float delta)
{
    var velocity = new Vector2(); // The player's movement vector.

    if (Input.IsActionPressed("ui_right"))
    {
        velocity.x += 1;
    }

    if (Input.IsActionPressed("ui_left"))
    {
        velocity.x -= 1;
    }

    if (Input.IsActionPressed("ui_down"))
    {
        velocity.y += 1;
    }

    if (Input.IsActionPressed("ui_up"))
    {
        velocity.y -= 1;
    }

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

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

Почнемо з того, що встановимо значення `` velocity`` в`` (0, 0) `` - за замовчуванням гравець рухатися не повинен. Потім, ми перевіряємо кожен введення і додаємо / віднімаємо значення з `` velocity``, щоб отримати загальний напрямок. Наприклад, якщо ви одночасно утримуєте `` right`` і `` down``, отриманий вектор `` velocity`` буде `` (1, 1) ``. В цьому випадку, оскільки ми додаємо горизонтальне і вертикальний рух, гравець буде рухатися * швидше *, ніж якби він переміщався тільки по горизонталі.

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

Порада

Якщо ви ніколи раніше не використовували векторну математику, або вам потрібно освіжити знання, ви можете побачити пояснення по використанню векторів у Godot у Vector math. Це добре знати, але це не буде потрібно для решти цього уроку.

Ми також перевіряємо, чи програвач рухається, щоб ми могли викликати `` play () `` або `` stop () `` на AnimatedSprite.

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

Порада

У GDScript $ повертає вузол на відносний шлях від поточного вузла, або повертає null якщо вузол не знайдено. Оскільки AnimatedSprite є нащадком поточного вузла, ми можемо використовувати $AnimatedSprite.

Тепер, коли у нас є напрямок руху, ми можемо оновити позицію гравця. Ми також можемо використовувати `` clamp () , щоб він не покинув екран. * Clamping * означає обмеження руху діапазоном. Додайте наступне в кінець функції `` _process (переконайтеся, що він не має відступу під 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) і переконайтеся, що можете рухати гравця по екрані у всіх напрямках.

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

Якщо ви отримуєте помилку на панелі "Зневаджувач", яка говорить

Attempt to call function 'play' in base 'null instance' on a null instance

це, ймовірно, означає, що ви неправильно написали ім'я вузла AnimatedSprite. Імена вузлів залежать від регістру і `` $ NodeName`` повинен відповідати імені, яке ви бачите в дереві сцен.

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

Тепер, коли гравець може рухатися, нам потрібно змінити, яку анімацію грає AnimatedSprite, виходячи з її напрямку. У нас є анімація "прогулянка", яка показує гравцеві, що йде праворуч. Цю анімацію слід перевернути горизонтально, використовуючи властивість `` flip_h '' для руху ліворуч. У нас також є анімація "вгору", яку слід перевернути вертикально "flip_v" для руху вниз. Розмістимо цей код в кінці функції `` _process () ``:

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

Примітка

Булеві призначення в наведеному вище коді - це звичайна стенограма для програмістів. Оскільки ми робимо тест порівняння (булевий), а також * присвоюємо * булеве значення, ми можемо робити і те й інше. Розглянемо цей код порівняно з однорядним булевим призначенням вище:

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" в коді.

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

hide()
Hide();

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

Ми хочемо виявляти зіткнення гравця з ворогами, але ворогів ми ще не створили! Це добре, тому що ми будемо використовувати сигнали Godot.

Додайте наступний код в початок скрипта, після extends Area2d:

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

[Signal]
public delegate void Hit();

Цей рядок визначає власний сигнал під назвою "hit" ("удар"), який буде випромінюватися з нашого гравця, коли він буде стикатися з ворогом. Ми будемо використовувати Area2D для виявлення зіткнення. Виберіть вузол Player і натисніть на вкладку "Вузол" поруч із вкладкою Інспектор, щоб побачити список сигналів, які гравець може випромінювати:

../../_images/player_signals.png

Зверніть увагу, що наш користувацький сигнал "hit" там також присутнiй! Оскільки наші противники будуть вузлами `` RigidBody2D``, нам потрібен сигнал body_entered(body: Node). Цей сигнал буде викликатися, коли тіло (body) контактує з гравцем. Натисніть "Підключити.." i з'явится вікно "Підключити сигнал". Нам не потрібно змінювати будь-які з цих налаштувань, тож знову натиснiть "Підключити". Godot автоматично створить функцію у скрипті вашого гравця.

../../_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 більше одного разу.

Примітка

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

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

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

Сцена ворога

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

Ми створимо сцену Mob, яку ми зможемо потім використати, для створення будь-якої кількості незалежних мобів в грі.

Примітка

Перегляньте Instancing (вставка екземпляра, Інстансування), щоб дізнатися більше про створення екземплярів (інстансування).

Налаштування вузлів

Клацніть Сцена -> Нова сцена та додайте наступні вузли:

  • RigidBody2D (тверде тіло 2D) (названий Mob)

    • :ref:`AnimatedSprite <class_AnimatedSprite>`(Анімація спрайту)

    • :ref:`CollisionShape2D <class_CollisionShape2D>`(Форма зіткнення 2D)

    • VisibilityNotifier2D (Видимість)

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

У властивостях RigidBody2D встановіть значення Gravity Scale (сила гравітації) на 0, щоб моб не падав вниз. Крім того, в розділі PhysicsBody2D натисніть властивість Mask і зніміть прапорець у першому полі. Завдяки цьому в натовпі моби не будуть стикатися один з одним.

../../_images/set_collision_mask.png

Налаштуйте AnimatedSprite так, як ви налаштовували його для гравця. На цей раз, у нас є 3 анімації: ``fly``(літати), ``swim``(плавати), і ``walk``(ходити). В папці art є по два зображення для кожної анімації.

Встановіть "Speed (FPS)" на 3 для всіх анімацій.

../../_images/mob_animations.gif

Встановіть властивість Playing в Інспекторі на "On" ("Вкл").

Ми вибиратимемо одну з цих анімації випадковим чином, тому моби будуть мати певну різноманітність.

Як і зображення гравця, ці зображення мобів потрібно зменшити. Встановіть в AnimatedSprite властивість Scale на (0.75, 0.75).

Як і в сцені Player, додайте CapsuleShape2D для зіткнення. Щоб вирівняти фігуру із зображенням, потрібно встановити властивість Rotation Degrees``(градус поворота) на ``90 (під "Transform" в Інспекторі).

Зберегти сцену.

Скрипт ворога

Додайте скрипт до 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.

}

Коли наш ворог створюється, він обирає випадкове значення між min_speed і max_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.

Примітка

Ви повинні використовувати, randomize() якщо хочете, щоб ваша послідовність "випадкових" чисел була щоразу різною, коли ви запускаєте сцену. Ми будемо використовувати randomize() в нашій Main сцені, тому тут це не потрібно.

Останній фрагмент - це змусити мобів видаляти себе, коли вони залишають екран. Підключіть сигнал screen_exited() вузла VisibilityNotifier2D і додайте цей код:

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

На цьому завершуємо сцену Mob.

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

Тепер настав час зібрати все це до купи. Створіть нову сцену та додайте Node <class_Node>`(Вузол) з ім'ям ``Main`. Переконайтеся, що ви створили Node (Вузол), а не Node2D (Вузол2D). Натисніть кнопку «Створити екземпляр» (іконка ланцюжка) і виберіть збережене 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. Цей вузол автоматично обертатиметься та рухатиметься по шляху, тому ми можемо використовувати його для вибору випадкової позиції та напрямку вздовж шляху.

Ваше сцена має виглядати так:

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

Виберіть вузол Main і побачите в Інспекторі під "Script Variables" (Властивості скрипта) властивість Mob.

Ви можете змінити значення цієї властивості двома шляхами:

  • Перетягнути Mob.tscn з панелі "Файлова система" у властивість Mob.

  • Клацнути стрілочку вниз поряд з "[empty]"(пусто) і вибрати "Load" (Завантажити). Виберіть Mob.tscn.

Далі виберіть вузол Player на вкладці Сцена і виберіть вкладку "Вузол". Переконайтесь, що на вкладці "Вузол" вибрано "Сигнали".

Ви повинні побачити список сигналів для вузла Player. Знайдіть у списку сигнал hit і двічі клацніть по ньому (або клацніть по ньому правою клавішею мишки і виберіть "Під'єднати..."). Це відкриє діалогове вікно підключення сигналу. Ми хочемо створити нову функцію з назвою game_over, яка буде працювати з тим, що має відбуватися, коли гра закінчується. Введіть "game_over" у поле "Метод отримувач" внизу вікна "З’єднати сигнал з методом". Додайте наступний код до нової функції, а також функцію new_game, щоб налаштувати все для нової гри:

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

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

public void NewGame()
{
    _score = 0;

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

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

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

У функції _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);
}

Важливо

Чому PI? У функціях, що вимагають кутів, GDScript використовує радіани , а не градуси. Якщо вам зручніше працювати з градусами, вам потрібно буде скористатися функціями deg2rad() та rad2deg() для перетворення між ними.

Тестування сцени

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

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

Також визначимо Main, як нашу "Головну Сцену" - вона буде запускатися автоматично при кожному запуску гри. Натисніть кнопку "Відтворити" і виберіть Main.tscn при появі запиту.

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

Переконавшись, що все працює видаліть виклик new_game() з _ready().

HUD (інтерфейс)

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

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

HUD повинен відобразити наступну інформацію:

  • Рахунок, змінюваний ScoreTimer.

  • Повідомлення, наприклад "Game Over" ("Гра закінчена"), або "Get Ready!" ("Готуйся!")

  • Кнопка "Start"("Старт"), щоб розпочати гру.

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

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

  • Label названа ScoreLabel (Мітка рахунку).

  • Label названа ``Message``( повідомлення).

  • Button названа ``StartButton``(кнопка старту).

  • Timer названий ``MessageTimer``(таймер повідомлення).

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

  1. У розділі "Custom Fonts" ("Спеціальні шрифти") виберіть "New DynamicFont" ("Новий динамічний шрифт")

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

../../_images/custom_font2.png

Зробивши це зі ScoreLabel ви можете натиснути стрілку вниз поряд з властивістю DynamicFont і вибрати "Копіювати", а потім "Вставити" його в тому ж місці на других вузлах Control.

Примітка

Прив'язки та поля: вузли 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

На MessageTimer, встановіть Wait Time (Час затримки) на 2 і встановіть властивість One Shot на "Увімкнено".

Тепер додайте скрипт до 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();
}

Ця функція буде викликатися тоді, коли ми хочемо показати повідомлення тимчасово, наприклад, "Приготуйся".

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 щоразу, коли рахунок змінюється.

Підключіть сигнал 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 до Main

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

../../_images/completed_main_scene.png

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

На вкладці Вузол приєднайте сигнал HUD start_game до функції new_game() вузла Main ввівши "new_game" в "Receiver Method" у вікні "Під'єднання сигналу". Переконайтеся, що в скрипті поряд з 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);

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

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

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

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