简介
本文以你的第一个 2D 游戏 — Godot Engine (4.5) 简体中文文档为蓝本撰写,godot-rust提供了该教程的一个rust的实现dodge-the-creeps,与本文方法会稍有不同。如果你对rust、godot等内容比较生疏或感到疑惑,可以先阅读godot-rust入门文档。
本文搭建在godot-rust(gdext)创建项目的基础之上,如果你对该部分内容感到生疏,可以先阅读这部分内容,为接下来的操作做好准备。
注意,本文使用的godot版本为4.5.1,godot版本的变化可能会导致一些内容失效。
本文操作均在Windows上执行。
正文
游戏信息显示(HUD)
我们的游戏需要用户界面来展现分数、提示信息、开始游戏等信息和交互。
在rust创建Hud
use godot::{
classes::{CanvasLayer, ICanvasLayer},
prelude::*,
};
#[derive(GodotClass)]
#[class(base=CanvasLayer)]
struct Hud {
base: Base<CanvasLayer>,
}
#[godot_api]
impl ICanvasLayer for Hud {
fn init(base: Base<CanvasLayer>) -> Self {
Self { base }
}
}
编译rust
cargo build
创建一个新场景,选择你刚编写的Hud
CanvasLayer节点可以让我们在游戏的其他部分的上一层绘制 UI 元素,这样它所显示的信息就不会被任何游戏元素(如玩家或敌人)所覆盖。
向Hud添加子节点:
Label重命名为ScoreLabel—— 显示分数Label重命名为Message—— 显示消息Button重命名为StartButton—— 开始按钮Timer重命名为MessageTimer—— 信息显示计时器
此时场景结构应该像这样:
对ScoreLabel、Message、StartButton三个子节点按照以下方法做修改字体操作。在godot编辑器的右边检查器面板中下滑到Control,找到Theme Overrides->Fonts,点击向下箭头点击加载,准备的字体文件在res://fonts/中。
三个子节点都修改完字体后,保存。
还有一些设置需要操作:
ScoreLabel:
检查器->Label->Text->0检查器->Control->Font Sizes->64px检查器->Label->Horizontal Alignment->Center检查器->Label->Vertical Alignment->Center工具栏->锚点预设->顶部居中(见下图)
Message:
检查器->Label->Text->Dodge the Creeps!检查器->Control->Font Sizes->64px检查器->Label->Horizontal Alignment->Center检查器->Label->Vertical Alignment->Center检查器->Label->Autowrap Mode->Word检查器->Control->Layout->Transform->Size->X->480px工具栏->锚点预设->居中
StartButton:
检查器->Button->Text->Start检查器->Control->Font Sizes->64px检查器->Control->Layout->Transform->Size->X->200px检查器->Control->Layout->Transform->Size->X->100px工具栏->锚点预设->底部居中检查器->Control->Layout->Transform->Position->Y->580px
MessageTimer:
检查器->Timer->Wait Time->2s检查器->Timer->One Shot->启用
配置完成后,你的场景应该有了一个初步的样子:
回到rust的Hud,添加一个start_game()信号,用来提示Main游戏开始
#[godot_api]
impl Hud {
#[signal]
pub fn start_game();
}
当游戏处于不同状态时,你肯定想向玩家发出提示,添加一个show_message()来实现它
#[godot_api]
impl Hud {
......
pub fn show_message(&mut self, text: &str) {
let mut message = self.base().get_node_as::<Label>("Message");
// 显示信息
message.set_text(text);
message.show();
// 计时2s
self.base().get_node_as::<Timer>("MessageTimer").start();
}
}
我们来处理Player死亡事件,游戏显示“Game Over”2秒,然后返回标题屏幕,等待稍许后显示“Start”按钮。
#[godot_api]
impl Hud {
......
pub async fn show_game_over(&mut self) { // 注意函数前有async关键字
self.show_message("Game Over");
self.base()
.get_node_as::<Timer>("MessageTimer")
.signals()
.timeout()
.to_future()
.await;
let mut message = self.base().get_node_as::<Label>("Message");
message.set_text("Dodge the Creeps!");
message.show();
self.base()
.get_tree()
.unwrap()
.create_timer(1.0)
.unwrap()
.signals()
.timeout()
.to_future()
.await;
self.base().get_node_as::<Button>("StartButton").show();
}
}
添加修改分数的方法
#[godot_api]
impl Hud {
......
pub fn update_score(&self, score: i64) {
self.base()
.get_node_as::<Label>("ScoreLabel")
.set_text(&score.to_string());
}
}
添加处理StartButton按下事件pressed()信号、MessageTimer到时事件timeout()信号的handle
#[godot_api]
impl Hud {
......
fn on_start_button_pressed(&mut self) {
self.base().get_node_as::<Button>("StartButton").hide();
self.signals().start_game().emit();
}
fn on_message_timer_timeout(&mut self) {
self.base().get_node_as::<Label>("Message").hide();
}
}
在ready()连接handle和信号
#[godot_api]
impl ICanvasLayer for Hud {
......
fn ready(&mut self) {
self.base()
.get_node_as::<Button>("StartButton")
.signals()
.pressed()
.connect_other(self, Self::on_start_button_pressed);
self.base()
.get_node_as::<Timer>("MessageTimer")
.signals()
.timeout()
.connect_other(self, Self::on_message_timer_timeout);
}
}
将HUD场景连接到Main场景
像将Player实例化到Main,中一样,我们需要将Hud也实例化到Main中:
回到Rust的Main,我们需要在代码层面将Hud和Main连接起来
将Hud的start_game()信号连接到Main的new_game()方法,这样即可以通过按下Hud的开始按钮来开始游戏
#[godot_api]
impl INode for Main {
......
fn ready(&mut self) {
......
self.base()
.get_node_as::<Hud>("Hud")
.signals()
.start_game()
.connect_other(self, Self::new_game);
}
}
在Main的new_game()中添加更新Hud的逻辑
fn new_game(&mut self) {
......
let mut hud = self.base().get_node_as::<Hud>("Hud");
{
let mut hud = hud.bind_mut();
hud.update_score(self.score);
hud.show_message("Get Ready");
}
}
在Main的game_over()中添加更新Hud的逻辑
fn game_over(&mut self) {
......
let mut hud = self.base().get_node_as::<Hud>("Hud");
spawn(async move { // 必须使用godot自带的spwan,严禁使用tokio等类似的rust运行时
hud.bind_mut().show_game_over().await;
});
}
这里和教程原文有比较大的差异
# hud.tscn
func show_game_over():
show_message("Game Over")
# Wait until the MessageTimer has counted down.
await $MessageTimer.timeout
$Message.text = "Dodge the Creeps!"
$Message.show()
# Make a one-shot timer and wait for it to finish.
await get_tree().create_timer(1.0).timeout
$StartButton.show()
......
# main.tscn
_on_score_timer_timeout():
......
$HUD.show_game_over()
可以看出GDScripte隐藏了很多细节,让await调用看上去非常贴合自然逻辑。godot-rust文档并未给出直接说明,如何在rust中复刻这一过程。经过大量查询godot引擎相关资料,结合docs.rs中的相关内容反复推敲,经实际测试,本文确定了上述rust实现方式。当然,经过稍加修改,你可以完全使用signal连接handle的方式来实现,完全不要去管rust的future。
在Main的on_score_timer_timeout()中添加更新Hud的逻辑
fn on_score_timer_timeout(&mut self) {
self.score += 1;
let mut hud = self.base().get_node_as::<Hud>("Hud");
{
let hud = hud.bind_mut();
hud.update_score(self.score);
}
}
请不要忘记在
Main的ready()中移除对new_game()的调用。否则你的游戏将自动开始。
编译rust
cargo build
回到godot编辑器中测试项目,你可以开始玩游戏了
删除旧的小怪(Mob)
当你结束上一把游戏,迫不及待地点击开始下一把时,有一定几率你会发现上一把的怪物还停留在你的屏幕中,我们需要在开始游戏清除掉它们。
回到godot编辑器,选中Mob场景,在编辑器右边检查器旁边的节点->分组中创建新的mobs分组
那么你创建的所有mob都会在mobs这个分组内
回到rust,在Main的new_game()中添加代码
fn new_game(&mut self) {
......
// 调用场景树中mobs分组内全部子场景的queue_free()方法
self.base()
.get_tree()
.unwrap()
.call_group("mobs", "queue_free", &[]);
}
这样,在游戏开始时,所有还留存的mob实例都将释放,你的屏幕内不再有残留的怪物
游戏在这一点上大部分已经完成。在下一部分和最后一部分中,我们将通过添加背景,循环音乐和一些键盘快捷键来对其进行一些润色。