游戏主场景
现在是时候将我们所做的一切整合到一个可玩的游戏场景中了。
创建新场景并添加一个 Node 节点,命名为 Main
。(我们之所以使用 Node 而不是 Node2D,是因为这个节点会作为处理游戏逻辑的容器使用。本身是不需要 2D 功能的。)
点击实例化按钮(由链条图标表示)并选择保存的 Player.tscn
。
现在, 将以下节点添加为 Main
的子节点, 并按如下所示对其进行命名(值以秒为单位):
Timer(名为
MobTimer
)——控制怪物产生的频率Timer(名为
ScoreTimer
)——每秒增加分数Timer(名为
StartTimer
)——在开始之前给出延迟Position2D(名为
StartPosition
)——表示玩家的起始位置
如下设置每个 Timer
节点的 Wait Time
属性:
MobTimer
:0.5
ScoreTimer
:1
StartTimer
:2
此外,将 StartTimer
的 One Shot
属性设置为“启用”,并将 StartPosition
节点的 Position
设置为 (240, 450)
。
生成怪物
Main
节点将产生新的生物, 我们希望它们出现在屏幕边缘的随机位置. 添加一个名为 MobPath
的 Path2D 节点作为 Main
的子级. 当你选择 Path2D
时, 你将在编辑器顶部看到一些新按钮:
选择中间的按钮(“添加点”),然后通过点击给四角添加点来绘制路径。要使点吸附到网格,请确保同时选中“使用网格吸附”和“使用吸附”。这些选项可以在“锁定”按钮左侧找到,图标为一个磁铁加三个点或一些交叉线。
重要
以顺时针的顺序绘制路径,否则小怪会向外而非向内生成!
在图像上放置点 4
后, 点击 闭合曲线
按钮, 你的曲线将完成.
现在已经定义了路径, 添加一个 PathFollow2D 节点作为 MobPath
的子节点, 并将其命名为 MobSpawnLocation
. 该节点在移动时, 将自动旋转并沿着该路径, 因此我们可以使用它沿路径来选择随机位置和方向.
您的场景应如下所示:
Main 脚本
将脚本添加到 Main
. 在脚本的顶部, 我们使用 export (PackedScene)
来允许我们选择要实例化的 Mob
场景.
GDScriptC#C++
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()
的调用,以便随机数生成器在每次运行游戏时生成不同的随机数:
GDScriptC#C++
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();
}
单击 Main
节点,就可以在“检查器”的“Script Variables”(脚本变量)下看到 Mob Scene
属性。
有两种方法来给这个属性赋值:
Drag
Mob.tscn
from the “FileSystem” dock and drop it in the Mob Scene property.单击“[空]”旁边的下拉箭头按钮,选择“加载”。选择
Mob.tscn
。
在场景树中选择 Player
节点, 然后选择 节点(Node)
选项卡(位于右侧属性旁), 确保已选择 信号(Signals)
.
你可以看到 Player
的信号列表. 找到 hit
信号并双击(或右键选择 “连接信号…”). 我们将在打开的界面创建 game_over
函数, 用来处理游戏结束时发生的事情. 在 连接信号到方法
窗口底部的 接收方法
框中键入 game_over
. 添加以下代码, 以及 new_game
函数以设置新游戏的所需内容:
GDScriptC#C++
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();
}
现在将每个 Timer
节点( StartTimer
, ScoreTimer
和 MobTimer
)的 timeout()
信号连接到 main
脚本。 StartTimer
将启动其他两个计时器.。 ScoreTimer
将使得分加1。
GDScriptC#C++
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);
}
在 _on_MobTimer_timeout()
中, 我们先创建小怪实例,然后沿着 Path2D
路径随机选取起始位置,最后让小怪移动。PathFollow2D
节点将沿路径移动,并会自动旋转,所以我们将使用它来选择怪物的方位和朝向。生成小怪后,我们会在 150.0
和 250.0
之间选取随机值,表示每只小怪的移动速度(如果它们都以相同的速度移动,那么就太无聊了)。
注意,必须使用 add_child()
将新实例添加到场景中。
GDScriptC#C++
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 使用弧度而不是度数。圆周率(Pi)表示转半圈的弧度,约为 3.1415
(还有等于 2 * PI
的 TAU
)。如果您更喜欢使用度数,则需使用 deg2rad()
和 rad2deg()
函数在两种单位之间进行转换。
测试场景
让我们测试这个场景,确保一切正常。请将对 new_game
的调用添加至 _ready()
:
GDScriptC#C++
func _ready():
randomize()
new_game()
public override void _Ready()
{
NewGame();
}
// This code goes in `main.cpp`.
void Main::_ready() {
new_game();
}
让我们同时指定 Main
作为我们的“主场景”——游戏启动时自动运行的场景。按下“运行”按钮,当弹出提示时选择 Main.tscn
。
小技巧
如果你已经将别的场景设置为“主场景”了,你可以在文件系统面板上右键点击 Main.tscn
并选择“设为主场景”。
你应该可以四处移动游戏角色,观察敌人的生成,以及玩家被敌人击中时会消失。
当你确定一切正常时,在 _ready()
中移除对 new_game()
的调用。
我们的游戏还缺点啥?缺用户界面。在下一课中,我们将会添加标题界面并且显示玩家的分数。