godot-rust(gdext)你的第一个2D游戏 - 6

81 阅读6分钟

简介

本文以你的第一个 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

新建Hud场景

CanvasLayer节点可以让我们在游戏的其他部分的上一层绘制 UI 元素,这样它所显示的信息就不会被任何游戏元素(如玩家或敌人)所覆盖。

Hud添加子节点:

  • Label重命名为ScoreLabel —— 显示分数
  • Label重命名为Message —— 显示消息
  • Button重命名为StartButton —— 开始按钮
  • Timer重命名为MessageTimer —— 信息显示计时器

此时场景结构应该像这样:

Hud场景结构

ScoreLabelMessageStartButton三个子节点按照以下方法做修改字体操作。在godot编辑器的右边检查器面板中下滑到Control,找到Theme Overrides->Fonts,点击向下箭头点击加载,准备的字体文件在res://fonts/中。

无标题.jpg

三个子节点都修改完字体后,保存。

还有一些设置需要操作:

ScoreLabel

  • 检查器->Label->Text->0
  • 检查器->Control->Font Sizes->64px
  • 检查器->Label->Horizontal Alignment->Center
  • 检查器->Label->Vertical Alignment->Center
  • 工具栏->锚点预设->顶部居中(见下图)

设置ScoreLabel锚点

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->启用

配置完成后,你的场景应该有了一个初步的样子:

游戏信息HUD

回到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中:

将Hud也实例化到Main中

回到Rust的Main,我们需要在代码层面将HudMain连接起来

Hudstart_game()信号连接到Mainnew_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);
    }
}

Mainnew_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");
    }
}

Maingame_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

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

请不要忘记在Mainready() 中移除对 new_game() 的调用。否则你的游戏将自动开始。

编译rust

cargo build

回到godot编辑器中测试项目,你可以开始玩游戏了

删除旧的小怪(Mob

当你结束上一把游戏,迫不及待地点击开始下一把时,有一定几率你会发现上一把的怪物还停留在你的屏幕中,我们需要在开始游戏清除掉它们。

回到godot编辑器,选中Mob场景,在编辑器右边检查器旁边的节点->分组中创建新的mobs分组

节点分组

mobs分组

那么你创建的所有mob都会在mobs这个分组内

查看mobs分组

回到rust,在Mainnew_game()中添加代码

fn new_game(&mut self) {
    ......
    // 调用场景树中mobs分组内全部子场景的queue_free()方法
    self.base()
        .get_tree()
        .unwrap()
        .call_group("mobs", "queue_free", &[]);
}

这样,在游戏开始时,所有还留存的mob实例都将释放,你的屏幕内不再有残留的怪物

游戏在这一点上大部分已经完成。在下一部分和最后一部分中,我们将通过添加背景,循环音乐和一些键盘快捷键来对其进行一些润色。

参考

  1. 你的第一个 2D 游戏 — Godot Engine (4.5) 简体中文文档
  2. godot-rust(gdext)创建项目 - 掘金
  3. godot - Rust
  4. demo-projects/dodge-the-creeps at master · godot-rust/demo-projects · GitHub