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

54 阅读5分钟

简介

本文以你的第一个 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,向Player添加两个字段,并更新init方法:

#[derive(GodotClass)]
#[class(base=Area2D)]
struct Player {
    #[export] // 向godot暴露这个字段
    speed: real,
    screen_size: Vector2,

    base: Base<Area2D>,
}
#[godot_api]
impl IArea2D for Player {
    fn init(base: Base<Area2D>) -> Self {
        Self {
            speed: 400.0,
            screen_size: Vector2::ZERO,
            base,
        }
    }
}

rust编译后,你可以在godot编辑器的检查器中发现这个字段,并可以手动调整:

向godot暴露字段

impl IArea2D for Player模块中重写ready()方法

当节点进入场景树时,ready() 函数被调用,这是查看游戏窗口大小的好时机:

fn ready(&mut self) {
    self.screen_size = self.base().get_viewport_rect().size;
}

我们需要重写的另一个方法是process()

现在我们可以使用process()函数定义玩家将执行的操作。process()在每一帧都被调用,因此我们将使用它来更新我们希望会经常变化的游戏元素。对于玩家(即Player)而言,我们需要执行以下操作:

  • 检查输入。
  • 沿给定方向移动。
  • 播放合适的动画。

重写process()之前我们对需要godot捕获的输入进行配置,点击项目->项目设置->输入映射

输入映射

添加新输入映射名称

添加新的输入映射

为映射添加事件

添加事件

选择键盘按键->Right,点击确定

选择键盘右箭头事件

我们一共要配置四个输入映射:

  • move_right映射到键盘按键->右箭头键
  • move_left 映射到键盘按键->左箭头键
  • move_up 映射到键盘按键->上箭头键
  • move_down 映射到键盘按键->向下箭头键

配置四个输入映射

我们只将一个键映射到每个输入动作,但你可以将多个键、操纵杆按钮或鼠标按钮映射到同一个输入动作。

回到rust,在impl IArea2D for Player模块中重写process()方法

fn process(&mut self, delta: f32) {
        let mut velocity = Vector2::ZERO;  // 初始化一个零向量,即默认Player无移动

        let input = Input::singleton(); // 获得输入单例
        if input.is_action_pressed("move_right") { // 与godot中设置名称保持一致
            // 添加向右向量
            velocity += Vector2::RIGHT;
        }
        if input.is_action_pressed("move_left") { // 与godot中设置名称保持一致
            // 添加向左向量
            velocity += Vector2::LEFT;
        }
        if input.is_action_pressed("move_down") { // 与godot中设置名称保持一致
            // 添加向下向量
            velocity += Vector2::DOWN;
        }
        if input.is_action_pressed("move_up") { // 与godot中设置名称保持一致
            // 添加向上向量
            velocity += Vector2::UP;
        }

        // 获得Player节点下的AnimatedSprite2D节点,注意传入函数的参数与godot中设置的名称一致
        let mut animated_sprite = self
            .base()
            .get_node_as::<AnimatedSprite2D>("AnimatedSprite2D");

        // 判断是否有移动
        if velocity.length() > 0.0 {
            // 将移动向量长度归一化,始终保持“1”,比如你同时按下“上”和“右”时
            velocity = velocity.normalized() * self.speed;
            animated_sprite.play(); // 播放动画
        } else {
            animated_sprite.stop(); // 停止动画
        }
        
        // 计算Player当前位置
        let mut position = self.base().get_position() + velocity * delta;
        // 使用clamp()方法来方式Player离开屏幕
        position = position.clamp(Vector2::ZERO, self.screen_size);
        // 更新Player位置
        self.base_mut().set_position(position);
    }

编译rust

cargo build

回到godot编辑器,按F6或者在编辑器右上方(检查器的上方)点击运行当前场景

运行当前场景

确认你能够在屏幕中沿任一方向移动Player

选择动画

如果进行了测试,细心的你会发现Player在移动时播放的动画是固定,即你在前面设置的walk。但这不符合我们日常体验,正常情况下Player应随前进方向的不同而播放对应的动画。在本文中我们设定walk是向右走,向左时就将walk水平翻转,up是向上走,向下时就将up垂直翻转。

在rust的animated_sprite.play()这行上方添加播放动画的判断逻辑

if velocity.x != 0.0 { // 当有向横向移动时
    animated_sprite.set_animation("walk"); // 设置为动画为walk,注意与你在godot中名称一致
    animated_sprite.set_flip_v(false); // 恢复垂直翻转,否则在向下移动后动画播放会稍许奇怪
    animated_sprite.set_flip_h(velocity.x < 0.0); // 根据左右方向判断是否水平翻转
} else {
    animated_sprite.set_animation("up"); // 设置为动画为up,注意与你在godot中名称一致
    animated_sprite.set_flip_v(velocity.y > 0.0) // 根据上下方向判断是否垂直翻转
}
animated_sprite.play(); // 原播放代码处

编译rust

cargo build

再进行测试,你应该发现Player随移动方向变化,动画播放也随之发生变化。

确认Player能够正常工作后,在ready()函数最后面添加代码,在游戏开始时隐藏Player

self.base_mut().hide();

准备碰撞

游戏中我们希望能够检测Player是否被敌人碰撞,虽然目前游戏中我们还没有敌人。定义一个信号,注意不是在impl IArea2D for Player模块中,而是impl Player,同时#[godot_api]不能忘记。

#[godot_api]
impl Player {
    #[signal]
    fn hit(); // 被碰撞的信号
}

编译后你可以在godot编辑器内看到这个信号:

Player的hit信号

我们先需要一个handle来处理发生碰撞后要干什么,在impl Player模块中加入以下代码:

fn on_body_entered(&mut self, _body: Gd<Node2D>) {
    self.base_mut().hide(); // Player隐藏
    self.signals().hit().emit(); // 发出hit信号
    
    // 获得Player节点下的CollisionShape2D节点,注意传入函数的参数与godot中设置的名称一致
    // 禁用碰撞形状
    self.base()
        .get_node_as::<CollisionShape2D>("CollisionShape2D")
        .set_deferred("disabled", &true.to_variant());
}

然后将碰撞信号和上面的handle连接起来,我们在ready()的最后面添加以下代码:

self.signals()
    .body_entered()
    .connect_self(Self::on_body_entered);

最后,为Player添加一个游戏开始方法,用于启动游戏,在impl Player模块中加入以下代码:

fn start(&mut self, pos: Vector2) {
    self.base_mut().set_position(pos); // 重置Player位置
    self.base_mut().show(); // 将Player显示出来
    
    // 启用碰撞形状
    self.base()
        .get_node_as::<CollisionShape2D>("CollisionShape2D")
        .set_deferred("disabled", &false.to_variant());
}

当然,目前start()方法并没有在任何位置被调用。

参考

  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