简介
本文以你的第一个 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编辑器的检查器中发现这个字段,并可以手动调整:
在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编辑器内看到这个信号:
我们先需要一个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()方法并没有在任何位置被调用。