简介
本文以你的第一个 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添加以下子节点:
AnimatedSprite2DCollisionShape2DVisibleOnScreenNotifier2D
同样将Mob编组,这时你的Mob节点看上去应该像这样:
选中Mob节点,在检查器的RigidBody2D面板中把Gravity Scale设置为0,这样可以防止怪物自由落体。
在RigidBody2D下方的CollisionObject2D面板中,展开Collision分组并取消选中Mask属性里的1。
这将确保怪物们不会相互碰撞。
原文中这句话中文翻译有歧义,通常我们会理解成字面意思,即怪物之间不会发生碰撞。但通过手册物理介绍条目可知,取消遮罩层1是Mob忽略Layer 1中任何碰撞检测,推测这是优化设置,但注意Mob自己依然在Layer 1
选中AnimatedSprite2D子节点,和之前一样操作设置三个动画,fly、swim、walk,将三个动画的对应动画速度值都调整为3 FPS,从文件夹art中拖动对应的图片:
然后将AnimatedSprite2D的Scale属性设为x = 0.75, y = 0.75。
像在Player一样,选中CollisionShape2D,为其添加一个CapsuleShape2D,并在Transform->Rotation为90。由于三个动画大小不一,我们折中调整碰撞形状大小。
保存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);
}
敌人场景就创建完了,接下来我们需要一个游戏场景把Player和Mob联系起来。