前言
注意到Godot这款游戏引擎是之前用rust的bevy游戏引擎写游戏时太过于生理不适,让我在网上去寻找别的方式来开发游戏,经过一番Google,我最后得到了Cocos2d,unity,unreal engine,Godot这几个比较成熟又比较有人气及有成功商业化例子的引擎,一开始我选择了Unity,但是我不会C#,这就比较难受了,unreal engine对于我的用途有点过于庞大了,Cocos2d虽然合适,但后面对3D支持貌似不太好,于是最后将目光投向了Godot。
插曲
但是Godot又不是完全不用学东西,GDScript是Godot自己创造的一套编程语言,轻量且完全对于游戏开发量身定制,不过好在比较好学,但正巧在考虑Godot时他正在Preview 4.0版本,对于4.0版本有着很多的优化和改动,所以待到4.0推出后我正式入坑了Godot。
为什么要选择Godot + Rust混合开发?
1. 将游戏逻辑与关键算法分离
GDScript我现在一般会用于编写游戏逻辑(如控制,画面刷新等),将一些不太适合GDScript的算法分离开,做到分开维护,这样让工作变得井井有条。
2. 最大化利用机器性能
将关键算法利用编译型语言编译成原生库可以最大程度利用性能,避免脚本语言边运行边解释的性能损耗。
3. 利用编程语言现有的生态
Rust目前有很多性能强的优秀crate,开箱即用,可以避免二次开发,减轻开发负担。
4. 作为rustacean的执念
作为一个rust程序员,用rust编写程序的执念相当强,所以也算一点私心(
准备工作
- Godot Engine 4.x
- VSCode + rust-analyzer
由于4.0发生了许多的变动,例如跨语言调用由GDNative变成了GDExtension,本文章主
要探讨GDExtension,之后的Godot相关也会注重于4.X版本
开始编码
首先建立一个我们要开发的Godot游戏的工程,笔者这里已经提前建立好了,我将文件夹结构组织如下
- HelloWorld/
- assets/
- class/
- rust/
- scene/
其中我们将把rust工程存放于rust文件夹中
然后我们进入rust文件夹,使用命令建立一个lib工程,并使用vscode打开他
1
2
3
|
cargo new hello --lib
cd hello
code .
|
编辑cargo.toml,将该工程设置为编译为c动态库,在[dependencies]
上加入
1
2
|
[lib]
crate-type = ["cdylib"]
|
并在[dependencies]
下加入godot crate
1
2
|
[dependencies]
godot = { git = "https://github.com/godot-rust/gdext", branch = "master" }
|
打开lib.rs,可以看到cargo已经帮你提前写好的库模板,将其删除,加入我们自己的代码。
首先godot现在使用GDExtension加载外部库,所以我首先需要在rust中给godot暴露出一个进入点。
1
2
3
4
5
6
|
use godot::prelude::*;
struct HelloWorld;
#[gdextension]
unsafe impl ExtensionLibrary for HelloWorld {}
|
godot-rust团队通过方便的宏为我们隐去了大部分繁琐的操作,只需要建立一个空结构体,将ExtensionLibrary trait附加在该结构体上,并标注#[gdextension]
即可
在GDScript中,每一个脚本都是一个类,一个个类在游戏中起着自己的作用,相应的,rust也需要遵守这样的游戏规则,但是rust中并没有类,类是数据和方法的结合体,在rust中我们可以用结构体去替代,在这里我们新建一个结构体,来假装我们有一个GD脚本。
1
2
3
4
5
6
7
8
|
#[derive(GodotClass)]
#[class(base=Node2D)]
struct ImAClass {
#[base]
any_name_you_like: Base<Node2D>,
this_is_your_variable: isize
}
|
通过godot crate的宏,我们能将结构体以GD类的方式暴露给Godot,上两行属性标注表明了这个结构体是一个继承了类类型Node2D的GD类,成员中我们必须要声明一个表明基类的成员变量,这样Godot才可以通过这个结构体访问这个基类,之后就可以随性所欲的声明自己需要的变量,例如,速度等。
在Godot中,类具有许多虚方法,如_init,_process等需要用户自己去实现,那么我们的结构体也不例外,所以我们需要对其实现相关的函数
1
2
3
4
5
6
7
8
9
10
11
|
#[godot_api]
impl Node2DVirtual for ImAClass {
fn init(node: Base<Node2D>) -> Self {
godot_print!("Hello World!");
Self {
this_is_your_variable: 0,
any_name_you_like: node
}
}
}
|
在godot中,普通的println!()无法使用,需要使用godot crate提供的godot_print!()宏。除了godot的内置函数外,我们如果还想实现自己的方法该怎么办呢?答案是其实和上面一样
1
2
3
4
5
6
7
8
9
|
use godot::buitin::*;
#[godot_api]
impl ImAClass {
#[func]
fn say_your_name(name: GodotString) -> GodotString {
return GodotString::from(format!("Hello {} from rust", name.to_string()));
}
}
|
注意,我们这里看到了新东西!这个GodotString是什么呀?嘿嘿,你需要意识到rust和GDScript是有着不同类型系统的两套语言,所以你需要做一定的类型转换才能让两套语言“畅聊无阻”,在这里我们将godot中的string通过to_string()方法转换为了rust中的字符串类型,然后通过rust的format!()宏将rust字符串转变为了Godot中的string类型。#[func]将我们的say_your_name函数暴露给了godot,以供我们调用
最后总代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
use godot::builtin::*;
use godot::prelude::*;
struct HelloWorld;
#[gdextension]
unsafe impl ExtensionLibrary for HelloWorld {}
#[derive(GodotClass)]
#[class(base=Node2D)]
struct ImAClass {
#[base]
any_name_you_like: Base<Node2D>,
this_is_your_variable: isize,
}
#[godot_api]
impl Node2DVirtual for ImAClass {
fn init(base: Base<Self::Base>) -> Self {
godot_print!("Hello World!");
Self {
any_name_you_like: base,
this_is_your_variable: 0,
}
}
}
#[godot_api]
impl ImAClass {
#[func]
fn say_your_name(name: GodotString) -> GodotString {
return GodotString::from(format!("Hello {} from rust", name.to_string()));
}
}
|
最后Cargo,启动!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
PS > cargo build
Compiling glam v0.23.0
Compiling godot-ffi v0.1.0 (https://github.com/godot-rust/gdext?branch=master#09324179)
Compiling godot-core v0.1.0 (https://github.com/godot-rust/gdext?branch=master#09324179)
Compiling godot v0.1.0 (https://github.com/godot-rust/gdext?branch=master#09324179)
Compiling hello v0.1.0 (D:\CodeDir\HelloWorld\rust\hello)
warning: fields `any_name_you_like` and `this_is_your_variable` are never read
--> src\lib.rs:13:5
|
11 | struct ImAClass {
| -------- fields in this struct
12 | #[base]
13 | any_name_you_like: Base<Node2D>,
| ^^^^^^^^^^^^^^^^^
14 |
15 | this_is_your_variable: isize,
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: associated function `say_your_name` is never used
--> src\lib.rs:32:8
|
31 | impl ImAClass {
| ------------- associated function in this implementation
32 | fn say_your_name(name: GodotString) -> GodotString {
| ^^^^^^^^^^^^^
warning: `hello` (lib) generated 2 warnings
Finished dev [unoptimized + debuginfo] target(s) in 31.59s
|
接下来,我们需要在Godot里调用这个库,首先我们打开godot工程,在工程根目录下创建hello.gdextension。
这里推荐以 模块名.extension 的方式创建文件,便于管理
输入以下内容
1
2
3
4
5
6
7
8
9
10
11
12
13
|
[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = 4.1
[libraries]
linux.debug.x86_64 = "res://rust/hello/target/debug/libhello.so"
linux.release.x86_64 = "res://rust/hello/target/release/libhello.so"
windows.debug.x86_64 = "res://rust/hello/target/debug/hello.dll"
windows.release.x86_64 = "res://rust/hello/target/release/hello.dll"
macos.debug = "res://rust/hello/target/debug/libhello.dylib"
macos.release = "res://rust/hello/target/release/libhello.dylib"
macos.debug.arm64 = "res://rust/hello/target/debug/libhello.dylib"
macos.release.arm64 = "res://rust/hello/target/release/libhello.dylib"
|
然后看一看工程根目录下.godot/extension_list.cfg中有没有我们的gdextension文件,有即导入成功。
最后我们来到了最激动的实验验收阶段!新建场景添加一个Node2D场景,一气呵成!
1
2
3
4
5
|
func _ready():
var rust = ImAClass.new()
var message_from_rust = rust.say_your_name("KZNR")
print(message_from_rust)
pass
|
结果验收
最后输出:
1
2
3
4
5
|
Godot Engine v4.1.1.stable.mono.official.bd6af8e0e - https://godotengine.org
Vulkan API 1.3.205 - Forward+ - Using Vulkan Device #0: NVIDIA - NVIDIA GeForce RTX 3050 Ti Laptop GPU
Hello World!
Hello KZNR from rust
|
至此,godot调用rust大功告成。