简介
本文以你的第一个 2D 游戏 — Godot Engine (4.5) 简体中文文档为蓝本撰写,godot-rust提供了该教程的一个rust的实现dodge-the-creeps,与本文方法会稍有不同。如果你对rust、godot等内容比较生疏或感到疑惑,可以先阅读godot-rust入门文档。
本文搭建在godot-rust(gdext)创建项目的基础之上,如果你对该部分内容感到生疏,可以先阅读这部分内容,为接下来的操作做好准备。
注意,本文使用的godot版本为4.5.1,godot版本的变化可能会导致一些内容失效。
本文操作均在Windows上执行。
正文
创建主场景
在rust中创建主场景:
use godot::prelude::*;
#[derive(GodotClass)]
#[class(base=Node)]
struct Main {
base: Base<Node>,
}
#[godot_api]
impl INode for Main {
fn init(base: Base<Node>) -> Self {
Self { base }
}
}
我们之所以使用 Node 而不是 Node2D,是因为这个节点会作为处理游戏逻辑的容器使用。本身是不需要 2D 功能的。
编译rust
cargo build
回到godot编辑器,如同之前一样创建新场景:
保存main场景,这时你的文件系统看上去应该像这样:
在Main场景下实例化你的Player使其成为一个子节点:
为Main添加子节点:
Timer更名为MobTimer—— 控制怪物产生的频率Timer更名为ScoreTimer—— 计算游玩秒数,即分数Timer更名为StartTimer—— 在游戏开始之前给出延迟Marker2D更名为StartPosition—— 表示玩家的起始位置
在每个Timer节点的检查器面板中设置Wait Time属性:
MobTimer:0.5sScoreTimer:1sStartTimer:2s
注意,我们并没有为Main编组。
在StartTimer的检查器面板中,启用One Shot属性,StartPosition的Position属性设置为x = 240, y = 450。
生成怪物
在主场景中我们需要实现生成怪物的逻辑,它是从游戏屏幕边缘随机位置生成,为Main添加子节点Path2D并命名为MobPath,选中MobPath,在工具栏你应该会发现一些新的按钮。
推荐将使用智能吸附和使用栅格吸附这两个功能打开
使用按住ctrl + 鼠标左键方式,如下图所示,顺时针方式为你的游戏边框生成一条路径:
完成后你应该能在你的编辑器中看到如下所示带箭头的一个闭合矩形,大小和你的游戏屏幕一致:
原文提到“以顺时针的顺序绘制路径,否则小怪会向外而非向内生成!”,并非指顺时针这个方向是什么特别约定或者对godot引擎来说有何特别之处,是由于其生成怪物算法依赖了上图中路径的箭头方向
再为Path2D子节点添加子节点PathFollow2D并命名为MobSpawnLocation,此时你的节点结构看上去应该像这样:
编写主场景代码
在rust中向Main添加两个字段,并更新init()函数:
#[derive(GodotClass)]
#[class(base=Node)]
struct Main {
#[export]
mob_scene: OnEditor<Gd<PackedScene>>,
score: i64,
base: Base<Node>,
}
#[godot_api]
impl INode for Main {
fn init(base: Base<Node>) -> Self {
Self {
mob_scene: OnEditor::default(),
score: 0,
base,
}
}
}
编译rust
cargo build
回到godot编辑器,在编辑器里给mob_scene赋值:
- 将
mob.tscn从文件系统面板拖放到Mob Scene属性里。 - 单击
[空]旁边的下拉箭头按钮,选择“加载”。选择mob.tscn
我们先在Main中添加一个game_over方法:
#[godot_api]
impl Main {
fn game_over(&mut self) {
self.base().get_node_as::<Timer>("ScoreTimer").stop();
self.base().get_node_as::<Timer>("MobTimer").stop();
}
}
将Player的hit()信号连接到game_over(),注意,在这之前请将Player和Player的hit()信号添加pub标识,让其在rust中外部可见:
pub struct Player
......
impl Player {
#[signal]
pub fn hit();
......
}
#[godot_api]
impl INode for Main {
......
fn ready(&mut self) {
self.base()
.get_node_as::<Player>("Player")
.signals()
.hit()
.connect_other(self, Self::game_over);
}
}
开始下一步之前我们在impl Main模块中准备一个new_game()方法以供备用,在这之前请将player的start()方法标记为pub,就像前面一步一样。
impl Main {
......
fn new_game(&mut self) {
self.score = 0;
// 获得我们定义的初始位置,即(240.0, 450.0)
let position = self
.base()
.get_node_as::<Marker2D>("StartPosition")
.get_position();
// 展现godot-rust通过bind机制从GD对象调用Player定义的方法
let mut mob = self.base().get_node_as::<Player>("Player");
{
let mut player = mob.bind_mut(); // 在区域内绑定,并在区域内释放
player.start(position);
}
self.base().get_node_as::<Timer>("StartTimer").start();
}
}
现在处理三个计时器Timer,在impl Main模块中添加三个handle(),在此之前你需要将你的Mob标记为pub让其在rust内外部可见:
fn on_score_timer_timeout(&mut self) {
self.score += 1; // 每次ScoreTimer按照设置的等待时间跳一次,加一分
}
fn on_start_timer_timeout(&mut self) {
self.base().get_node_as::<Timer>("MobTimer").start(); // MobTimer开始计时
self.base().get_node_as::<Timer>("ScoreTimer").start(); // ScoreTimer开始计时
}
fn on_mob_timer_timeout(&mut self) {
let mut rng = rand::rng();
let mut mob = self.mob_scene.instantiate_as::<Mob>(); // 生成一个Mob实例
// 在边框路径上随机获得一个点
let mut mob_spawn_location = self
.base()
.get_node_as::<PathFollow2D>("MobPath/MobSpawnLocation");
mob_spawn_location.set_progress_ratio(rng.random_range(0.0..=1.0));
// 将Mob实例放到随机点上
mob.set_position(mob_spawn_location.get_position());
// 根据随机点的方向顺时针正交出向游戏屏幕内的方向,并使这个方向在正负PI/4范围随机化
let mut direction = mob_spawn_location.get_rotation() + PI as f32 / 2.0;
direction += rng.random_range((-PI / 4.0)..=(PI / 4.0)) as f32;
// 将随机化的方向放置入mob实例
mob.set_rotation(direction);
// 随机一个速度,使速度和mob的朝向一致,并将这个速度设置为mob的线性速度
let velocity = Vector2::new(rng.random_range(150.0..=250.0), 0.0);
mob.set_linear_velocity(velocity.rotated(direction));
// 向场景加入这个mob实例
self.base_mut().add_child(&mob);
}
原文中的randf()、randf_range()辅助函数其实在godot-rust 4.5已经有了,你也可以使用这两个函数来辅助你生成随机数
在ready()中将Timer的信号连接到上述handle:
fn ready(&mut self) {
......
self.base()
.get_node_as::<Timer>("MobTimer")
.signals()
.timeout()
.connect_other(self, Self::on_mob_timer_timeout);
self.base()
.get_node_as::<Timer>("ScoreTimer")
.signals()
.timeout()
.connect_other(self, Self::on_score_timer_timeout);
self.base()
.get_node_as::<Timer>("StartTimer")
.signals()
.timeout()
.connect_other(self, Self::on_start_timer_timeout);
}
测试主场景
在ready()最后添加代码:
fn ready(&mut self) {
self.new_game();
}
将main.tscn设置为主场景
如果没有这个选项,说明main已经为主场景。
回忆一下,我们在StartTimer计时结束,也就是2s后才启动的MobTimer,以0.5s速率生成一个Mob,因此测试刚开始时需要耐心稍等第一个的敌人才会出现。
当确认场景没有问题,将self.new_game()从ready()中删除,因为我们要在其他地方开始新游戏。