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

38 阅读3分钟

简介

本文以你的第一个 2D 游戏 — Godot Engine (4.5) 简体中文文档为蓝本撰写,godot-rust提供了该教程的一个rust的实现dodge-the-creeps,与本文方法会稍有不同。如果你对rust、godot等内容比较生疏或感到疑惑,可以先阅读godot-rust入门文档

本文搭建在godot-rust(gdext)创建项目的基础之上,如果你对该部分内容感到生疏,可以先阅读这部分内容,为接下来的操作做好准备。

注意,本文使用的godot版本为4.5.1,godot版本的变化可能会导致一些内容失效

本文操作均在Windows上执行。

正文

创建敌人场景

如你在创建Player时所做的工作一样,在rust中创建Mob

#[derive(GodotClass)]
#[class(base=RigidBody2D)]
struct Mob {
    base: Base<RigidBody2D>,
}
#[godot_api]
impl IRigidBody2D for Mob {
    fn init(base: Base<RigidBody2D>) -> Self {
        Self { base }
    }
}

编译rust

cargo build

回到godot编辑器,在左上角点击场景->新建场景->其他节点,创建Mob

创建Mob

Mob添加以下子节点:

  • AnimatedSprite2D
  • CollisionShape2D
  • VisibleOnScreenNotifier2D

同样将Mob编组,这时你的Mob节点看上去应该像这样:

Mob和子节点

选中Mob节点,在检查器的RigidBody2D面板中把Gravity Scale设置为0,这样可以防止怪物自由落体。

修改Mob的Gravity Scale

RigidBody2D下方的CollisionObject2D面板中,展开Collision分组并取消选中Mask属性里的1

确保Mob之前不会碰撞

这将确保怪物们不会相互碰撞。

原文中这句话中文翻译有歧义,通常我们会理解成字面意思,即怪物之间不会发生碰撞。但通过手册物理介绍条目可知,取消遮罩层1Mob忽略Layer 1中任何碰撞检测,推测这是优化设置,但注意Mob自己依然在Layer 1

选中AnimatedSprite2D子节点,和之前一样操作设置三个动画,flyswimwalk,将三个动画的对应动画速度值都调整为3 FPS,从文件夹art中拖动对应的图片:

fly动画

swim动画

walk动画

然后将AnimatedSprite2DScale属性设为x = 0.75, y = 0.75

调整AnimatedSprite2D的Scale

像在Player一样,选中CollisionShape2D,为其添加一个CapsuleShape2D,并在Transform->Rotation90。由于三个动画大小不一,我们折中调整碰撞形状大小。

调整CapsuleShape2D的Rotation

调增CapsuleShape2D的碰撞形状

保存Mob场景,这时你的文件系统面板中应该像这样:

保存Mob

编写敌人代码

原文的gdscript代码是这样的:

func _ready():
    var mob_types = Array($AnimatedSprite2D.sprite_frames.get_animation_names())
    $AnimatedSprite2D.animation = mob_types.pick_random()
    $AnimatedSprite2D.play()

这段代码的逻辑是,从AnimatedSprite2D中获取全部动画名称,随机选择一个动画并播放。但rust中获得的mob_types数据结构中并没有pick_random()这个方法。在godot-rust给出的demo中使用了这样的代码:

let mut sprite = self
    .base()
    .get_node_as::<AnimatedSprite2D>("AnimatedSprite2D");
let anim_names = sprite.get_sprite_frames().unwrap().get_animation_names();
let anim_names = anim_names.to_typed_array();
let animation_name = anim_names.pick_random().unwrap();

而实际上编译器会在to_typed_array()方法这里报错。

代码无效

不着急,我们自己实现一个pick_random(),为rust添加rand crate

cargo add rand

为godot-rust编写一个trait:

use godot::{builtin::PackedArray, meta::PackedArrayElement};
use rand::Rng;

pub trait PickRandom<T: PackedArrayElement> {
    fn pick_random(&self) -> T;
}
impl<T: PackedArrayElement> PickRandom<T> for PackedArray<T> {
    fn pick_random(&self) -> T {
        let mut rng = rand::rng();
        let index = rng.random_range(0..self.len());

        self.get(index).unwrap()
    }
}

我们的ready()方法就应该是这样:

fn ready(&mut self) {
    let mut sprite = self
        .base()
        .get_node_as::<AnimatedSprite2D>("AnimatedSprite2D");

    let random_name = sprite
        .get_sprite_frames()
        .unwrap()
        .get_animation_names()
        .pick_random();

    sprite.set_animation(random_name.arg());
    sprite.play();
}

最后,我们需要在Mob离开屏幕时删除自己,这需要调用VisibleOnScreenNotifier2D节点的screen_exited()信号。我们先写handle

#[godot_api]
impl Mob {
    fn on_screen_exited(&mut self) {
        self.base_mut().queue_free();
    }
}

ready()后面将信号handle连接起来:

fn ready(&mut self) {
    ......

    self.base()
        .get_node_as::<VisibleOnScreenNotifier2D>("VisibleOnScreenNotifier2D")
        .signals()
        .screen_exited()
        .connect_other(self, Self::on_screen_exited);
}

敌人场景就创建完了,接下来我们需要一个游戏场景把PlayerMob联系起来。

参考

  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