Главная сцена игры¶
Итак, настало время перенести всё, что мы сделали вместе, на играбельную игровую сцену.
Создайте новую сцену и добавьте Node с именем Main
. (Причиной, по которой мы используем Node, а не Node2D, является то, что узел будет контейнером для обработки игровой логики. Это не требует именно двумерного функционала.)
Click the Instance button (represented by a chain link icon) and select your saved
Player.tscn
.

Теперь добавьте следующие узлы в виде дочерних элементов 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)
.
Добавление мобов¶
Узел Main будет порождать новых мобов, и мы хотим, чтобы они появлялись в случайном месте на краю экрана. Добавьте узел Path2D с именем MobPath
как дочерний элемент узла Main
. Когда вы выберете Path2D
, вы увидите несколько новых кнопок в верхней части редактора:

Выберите среднюю ("Add Point") и нарисуйте путь щелчками мыши, чтобы добавить точки в показанных углах. Чтобы точки привязывались к сетке, убедитесь, что выбраны "Use Grid Snap" и "Use Snap". Эти опции можно найти слева от кнопки "Lock", они отображаются в виде магнита рядом с точками и пересекающимися линиями соответственно.

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

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

Главный скрипт¶
Добавьте скрипт к узлу Main
. В верхней части скрипта мы пишем export (PackedScene)
, что позволяет нам выбрать сцену Mob, экземпляр которой мы хотим сделать.
extends Node
export(PackedScene) var mob_scene
var score
public class Main : Node
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
#pragma warning disable 649
// We assign this in the editor, so we don't need the warning about not being assigned.
[Export]
public PackedScene MobScene;
#pragma warning restore 649
public int Score;
}
// Copy `player.gdns` to `main.gdns` and replace `Player` with `Main`.
// Attach the `main.gdns` file to the Main node.
// Create two files `main.cpp` and `main.hpp` next to `entry.cpp` in `src`.
// This code goes in `main.hpp`. We also define the methods we'll be using here.
#ifndef MAIN_H
#define MAIN_H
#include <AudioStreamPlayer.hpp>
#include <CanvasLayer.hpp>
#include <Godot.hpp>
#include <Node.hpp>
#include <PackedScene.hpp>
#include <PathFollow2D.hpp>
#include <RandomNumberGenerator.hpp>
#include <Timer.hpp>
#include "hud.hpp"
#include "player.hpp"
class Main : public godot::Node {
GODOT_CLASS(Main, godot::Node)
int score;
HUD *_hud;
Player *_player;
godot::Node2D *_start_position;
godot::PathFollow2D *_mob_spawn_location;
godot::Timer *_mob_timer;
godot::Timer *_score_timer;
godot::Timer *_start_timer;
godot::AudioStreamPlayer *_music;
godot::AudioStreamPlayer *_death_sound;
godot::Ref<godot::RandomNumberGenerator> _random;
public:
godot::Ref<godot::PackedScene> mob_scene;
void _init() {}
void _ready();
void game_over();
void new_game();
void _on_MobTimer_timeout();
void _on_ScoreTimer_timeout();
void _on_StartTimer_timeout();
static void _register_methods();
};
#endif // MAIN_H
Мы также добавляем здесь вызов randomize()
, чтобы случайный генератор чисел генерировал различные случайные числа каждый раз, когда запускается игра :
func _ready():
randomize()
public override void _Ready()
{
GD.Randomize();
}
// This code goes in `main.cpp`.
#include "main.hpp"
#include <SceneTree.hpp>
#include "mob.hpp"
void Main::_ready() {
_hud = get_node<HUD>("HUD");
_player = get_node<Player>("Player");
_start_position = get_node<godot::Node2D>("StartPosition");
_mob_spawn_location = get_node<godot::PathFollow2D>("MobPath/MobSpawnLocation");
_mob_timer = get_node<godot::Timer>("MobTimer");
_score_timer = get_node<godot::Timer>("ScoreTimer");
_start_timer = get_node<godot::Timer>("StartTimer");
// Uncomment these after adding the nodes in the "Sound effects" section of "Finishing up".
//_music = get_node<godot::AudioStreamPlayer>("Music");
//_death_sound = get_node<godot::AudioStreamPlayer>("DeathSound");
_random = (godot::Ref<godot::RandomNumberGenerator>)godot::RandomNumberGenerator::_new();
_random->randomize();
}
Click the Main
node and you will see the Mob Scene
property in the Inspector
under "Script Variables".
Значение этого свойства можно присвоить двумя способами:
Drag
Mob.tscn
from the "FileSystem" dock and drop it in the Mob Scene property.Нажмите стрелочку вниз рядом с "[пусто]" ("[empty]") и выберите "Загрузить" ("Load"). Затем выберите
Mob.tscn
.
Затем выберите узел Player
("Игрок") в панели "Сцена" ("Scene") и откройте вкладку "Узел" ("Node") в инспекторе. Убедитесь, что во вкладке "Узел" выбрана вкладка "Сигналы" ("Signals").
Вы должны увидеть список сигналов для узла 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();
}
// This code goes in `main.cpp`.
void Main::game_over() {
_score_timer->stop();
_mob_timer->stop();
}
void Main::new_game() {
score = 0;
_player->start(_start_position->get_position());
_start_timer->start();
}
Теперь присоедините сигнал timeout()
каждого из узлов Timer (StartTimer
, ScoreTimer
и MobTimer
) к главному скрипту. StartTimer
запустит два других таймера. ScoreTimer
будет увеличивать счет на 1.
func _on_ScoreTimer_timeout():
score += 1
func _on_StartTimer_timeout():
$MobTimer.start()
$ScoreTimer.start()
public void OnScoreTimerTimeout()
{
Score++;
}
public void OnStartTimerTimeout()
{
GetNode<Timer>("MobTimer").Start();
GetNode<Timer>("ScoreTimer").Start();
}
// This code goes in `main.cpp`.
void Main::_on_ScoreTimer_timeout() {
score += 1;
}
void Main::_on_StartTimer_timeout() {
_mob_timer->start();
_score_timer->start();
}
// Also add this to register all methods and the mob scene property.
void Main::_register_methods() {
godot::register_method("_ready", &Main::_ready);
godot::register_method("game_over", &Main::game_over);
godot::register_method("new_game", &Main::new_game);
godot::register_method("_on_MobTimer_timeout", &Main::_on_MobTimer_timeout);
godot::register_method("_on_ScoreTimer_timeout", &Main::_on_ScoreTimer_timeout);
godot::register_method("_on_StartTimer_timeout", &Main::_on_StartTimer_timeout);
godot::register_property("mob_scene", &Main::mob_scene, (godot::Ref<godot::PackedScene>)nullptr);
}
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.
When we spawn a mob, we'll pick a random value between 150.0
and
250.0
for how fast each mob will move (it would be boring if they were
all moving at the same speed).
Обратите внимание, что новый экземпляр должен быть добавлен в сцену с помощью функции add_child()
.
func _on_MobTimer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instance()
# Choose a random location on Path2D.
var mob_spawn_location = get_node("MobPath/MobSpawnLocation")
mob_spawn_location.offset = randi()
# Set the mob's direction perpendicular to the path direction.
var direction = mob_spawn_location.rotation + PI / 2
# Set the mob's position to a random location.
mob.position = mob_spawn_location.position
# Add some randomness to the direction.
direction += rand_range(-PI / 4, PI / 4)
mob.rotation = direction
# Choose the velocity for the mob.
var velocity = Vector2(rand_range(150.0, 250.0), 0.0)
mob.linear_velocity = velocity.rotated(direction)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
public void OnMobTimerTimeout()
{
// Note: Normally it is best to use explicit types rather than the `var`
// keyword. However, var is acceptable to use here because the types are
// obviously Mob and PathFollow2D, since they appear later on the line.
// Create a new instance of the Mob scene.
var mob = (Mob)MobScene.Instance();
// Choose a random location on Path2D.
var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
mobSpawnLocation.Offset = GD.Randi();
// 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.
mob.Position = mobSpawnLocation.Position;
// Add some randomness to the direction.
direction += (float)GD.RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
mob.Rotation = direction;
// Choose the velocity.
var velocity = new Vector2((float)GD.RandRange(150.0, 250.0), 0);
mob.LinearVelocity = velocity.Rotated(direction);
// Spawn the mob by adding it to the Main scene.
AddChild(mob);
}
// This code goes in `main.cpp`.
void Main::_on_MobTimer_timeout() {
// Create a new instance of the Mob scene.
godot::Node *mob = mob_scene->instance();
// Choose a random location on Path2D.
_mob_spawn_location->set_offset((real_t)_random->randi());
// Set the mob's direction perpendicular to the path direction.
real_t direction = _mob_spawn_location->get_rotation() + (real_t)Math_PI / 2;
// Set the mob's position to a random location.
mob->set("position", _mob_spawn_location->get_position());
// Add some randomness to the direction.
direction += _random->randf_range((real_t)-Math_PI / 4, (real_t)Math_PI / 4);
mob->set("rotation", direction);
// Choose the velocity for the mob.
godot::Vector2 velocity = godot::Vector2(_random->randf_range(150.0, 250.0), 0.0);
mob->set("linear_velocity", velocity.rotated(direction));
// Spawn the mob by adding it to the Main scene.
add_child(mob);
}
Важно
Почему PI
? В функциях, требующих углы, Godot использует радианы, а не градусы. Число Пи представляет собой пол-оборота в радианах, примерно 3.1415
(также есть переменная TAU
, которая равна 2 * PI
) Если вам удобнее работать с градусами, вам нужно использовать функции deg2rad()
и rad2deg()
для преобразования между ними.
Тестирование сцены¶
Давайте протестируем сцену, чтобы убедиться, что все работает. Добавьте в _ready()
вызов new_game
:
func _ready():
randomize()
new_game()
public override void _Ready()
{
NewGame();
}
// This code goes in `main.cpp`.
void Main::_ready() {
new_game();
}
Также давайте назначим сцену Main
в качестве нашей "Главной сцены", которая запускается автоматически при запуске игры. Нажмите кнопку "Play" и выберите Main.tscn
при появлении запроса.
Совет
If you had already set another scene as the "Main Scene", you can right
click Main.tscn
in the FileSystem dock and select "Set As Main Scene".
У вас должна быть возможность перемещать игрока, видеть, как появляются мобы, и видеть, как игрок исчезает, когда его бьет моб.
Когда вы убедитесь, что всё работает, удалите вызов new_game()
из _ready()
.
Чего не хватает нашей игре? Какого-нибудь пользовательского интерфейса. В следующем уроке мы добавим заглавный экран и отобразим очки игрока.