注册属性

到目前为止,你已经学会了如何注册类和函数。这样就足以用 godot-rust 创建简单的应用程序了,但你可能希望让 Godot 能更直接地访问你对象的状态。

这时,属性就派上用场了。在 Rust 中,属性通常定义为结构体的字段。

另请参考 GDScript属性参考.

目录

注册变量

之前,我们定义了一个函数 Monster::get_name(),它可以用来获取名称,但在 GDScript 中使用时仍然需要写 obj.get_name()。有时候,你不需要额外的封装,而是希望直接访问字段。

gdext 库提供了 #[var] 属性来标记应当暴露为变量的字段。它的功能类似于 GDScript 中的 var 关键字。

从之前的结构体声明开始,我们现在将 #[var] 属性添加到 name 字段上。同时,我们将类型从 String 改为 GString,因为该字段现在直接与 Godot 交互。

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(init, base=Node3D)]
struct Monster {
    #[var]
    name: GString,
    hitpoints: i32,
}
}

这样做的效果是,name 现在作为 属性 在 Godot 中注册:

var monster = Monster.new()

# Write the property.
monster.name = "Orc"

# Read the property.
print(monster.name) # prints "Orc"

在 GDScript 中,属性是对 getter 和 setter 函数调用的语法糖。你也可以显式调用这些函数:

var monster = Monster.new()

# Write the property.
monster.set_name("Orc")

# Read the property.
print(monster.get_name()) # prints "Orc"

#[var]属性还可以接受参数,用来定制是否同时提供 getter 和 setter,以及它们的名称。如果你需要更复杂的逻辑,也可以编写 Rust 方法作为 getter 和 setter。详情请参阅API 文档

可见性

#[func] 函数一样,#[var] 字段不需要是 pub。这将 Godot 与 Rust 的可见性隔离开来。

在实践中,你仍然可以通过间接方法(例如 Godot 的反射 API)访问 #[var] 字段。但这时是经过刻意选择的;私有字段主要是防止 意外 错误或封装泄漏。

导出变量

#[var] 属性将字段暴露给 GDScript,但不会在 Godot 编辑器 UI 中显示它。

将属性暴露到编辑器 UI 中被称为 导出。与 GDScript 中的 @export 注解类似,gdext 通过 #[export] 属性提供导出功能。你可能会注意到命名上的一致性。

下面的代码不仅将 name 字段暴露给 GDScript,还会在编辑器中添加一个属性 UI。这样,你就可以为每个 Monster 实例单独命名,而无需编写任何代码!

#![allow(unused)]
fn main() {
#[derive(GodotClass)]
#[class(init, base=Node3D)]
struct Monster {
    #[export]
    name: GString,
    hitpoints: i32,
}
}

你可能注意到,#[var] 属性不见了。这是因为 #[export] 自动隐含了 #[var],所以 name 仍然可以像以前一样从 GDScript 中访问。

你还可以将这两个属性声明在同一个字段上。如果你提供了参数来定制它们,这是必须的。

枚举类型

你可以将 Rust 枚举导出为属性。导出的枚举会在编辑器中显示为一个下拉菜单,包含所有可用的选项。为了做到这一点,你需要派生三个trait:

  • GodotConvert用于定义如何在 Godot 中转换该类型。
  • Var 允许将它用作 #[var] 属性,这样它就可以从 Godot 访问。
  • Export 允许将它用作 #[export] 属性,这样它就会出现在编辑器 UI 中。

由于 Godot 本身没有专门的枚举类型,你可以将其映射为整数(例如: i64)或字符串(GString)。这可以通过 #[Godot] 属性的 via 键进行配置。

以下是导出枚举的示例:

#![allow(unused)]
fn main() {
#[derive(GodotConvert, Var, Export)]
#[godot(via = GString)]
pub enum Planet {
    Earth, // first enumerator is default.
    Mars,
    Venus,
}

#[derive(GodotClass)]
#[class(base=Node)]
pub struct SpaceFarer {
    #[export]
    favorite_planet: Planet,
}
}

上面的代码将在编辑器 UI 中显示如下:

Exported enum in the Godot editor UI

重构 Rust 枚举可能会影响已经序列化的场景,因此,如果你选择整数或字符串作为底层表示,请谨慎处理:

  • 整数可以在不破坏现有场景的情况下重命名枚举变体,但新的变体必须严格添加到末尾,现有的变体不能删除或重排序。
  • 字符串允许自由的重排序和删除(如果未使用),并且调试更加容易。但你不能重命名它们,并且它们占用的空间略多(只有在你有数万个枚举值时才需要考虑)。

当然,你始终可以调整现有的场景文件,但这涉及到手动查找和替换,通常容易出错。

GDScript中的枚举

枚举在 Godot 中并不是头等对象(First-class citizen)。即使你在 GDScript 中定义了它们,它们主要是常量的语法糖。 这段声明:

enum Planet {
    EARTH,
    VENUS,
    MARS,
}

@export var favorite_planet: Planet

大致等同于:

const EARTH = 0
const VENUS = 1
const MARS = 2

@export_enum("EARTH", "VENUS", "MARS") var favorite_planet = Planet.EARTH

然而,枚举不是类型安全的,你可以这样做:

var p: Planet = 5

此外,除非你用字符串值初始化常量,否则你无法检索它们的名称,这会让调试更加困难。并且没有反射机制,比如“获取枚举值的数量”或“遍历所有枚举值”。如果你有选择,建议保持枚举类型在 Rust 中定义。

高级用法

#[var]#[export] 属性都接受参数,允许进一步定制属性在 Godot 中的注册方式。有关详细信息,请查阅 API 文档

PackedArray 可变性

Packed*Array 类型使用写时复制语义,意味着每个新实例都可以看作是一个独立的副本。当 Rust 端的packed array作为属性注册时,GDScript 在你修改它时会创建一个新实例,从而使修改对 Rust 代码不可见。

有一个 GitHub issue 讨论了更多细节。

因此,最好使用 Array<T> 或注册指定的 #[func] 方法,在 Rust 端执行变更操作。

自定义类型的 #[var]#[export]

如果你希望注册用户自定义类型的属性,以便它们能从 GDScript 代码(#[var])或编辑器(#[export])中访问,那么你可以分别实现 VarExport 特性。

这些trait也提供了派生宏,分别是 #[derive(Var)]#[derive(Export)]

性能

启用各种类型的 VarExport 看起来很方便,但请记住,每次引擎访问属性时,你的转换函数都会被调用,有时可能是在后台。特别是对于 #[export] 字段,编辑器 UI 的交互或从场景文件的序列化和反序列化可能会产生大量的流量。

作为一般规则,尽量使用 Godot 自身的类型,例如 ArrayDictionaryGd。这些类型是引用计数或简单的指针。