用户单例
理解 单例模式 对于正确使用此系统非常重要。
“单例模式”通常被视为一种反模式,因为它违反了多种整洁、模块化代码的良好实践。然而,它也是一个可以用来解决某些设计问题的工具。因此,Godot 内部使用了单例模式,并且 godot-rust 用户也可以使用。
关于批评的更多内容,请参考 这里。
Godot 中的自定义引擎单例:
- 是
Object类型 - 始终在编辑器中运行(implied
#[class(tool)]) - 始终可供 GDScript 和 GDExtension 访问
- 必须在
InitStage::Scene步骤中进行注册和注销
Godot 在其 API 中提供了 许多 内置单例。您可以在 这里 找到完整列表。
目录
注册自定义单例
你可以使用 #[class(singleton)] 将指定类注册为单例。
#![allow(unused)] fn main() { #[derive(GodotClass)] #[class(init, singleton)] struct MySingleton { // 对于 `#[class(singleton)]`,默认基类是 Object,而非 RefCounted。 base: Base<Object>, } // Can be accessed like any other singleton from the main thread. let val = MySingleton::singleton().bind().foo(); }
现在你的单例已经可用(在你重新编译并重新加载后),你应该能够从 GDScript 中像这样访问它:
extends Node
func _ready() -> void:
MySingleton.foo()
使用带有 on_main_loop_frame 的单例
自 Godot 4.5+ 起,可以使用 on_main_loop_frame 来调用用户单例:
fn global_delta() -> f64 {
let ticks = ProjectSettings::singleton()
.get("physics/common/physics_ticks_per_second")
.to::<i64>();
1.0 / (ticks as f64)
}
#[derive(GodotClass)]
#[class(init, singleton)]
struct MySingleton {
#[init(val = global_delta())]
delta: f64,
#[init(val = true)]
paused: bool,
#[init(val = Instant::now())]
time: Instant,
base: Base<Object>,
}
struct MyExtension;
#[gdextension]
unsafe impl ExtensionLibrary for MyExtension {
fn on_main_loop_frame() {
if Engine::singleton().is_editor_hint() {
return;
}
MySingleton::singleton().bind_mut().frame();
}
}
impl MySingleton {
pub fn frame(&mut self) {
if self.paused {
return;
}
let elapsed = self.time.elapsed().as_secs_f64();
self.elapsed += elapsed;
if self.elapsed >= self.delta {
let time_scale = Engine::singleton().get_time_scale();
self.run_simulation(self.elapsed * time_scale);
self.elapsed = 0.0;
}
self.time = Instant::now();
}
}
不使用过程宏注册自定义单例
可以通过 godot::classes::Engine 注册自定义单例。此外,实现 UserSingleton 可以通过 singleton() 访问已注册的单例实例。
用户单例应以其类名注册 —— 否则某些 Godot 组件(例如 4.4 之前的 GDScript)可能无法正确处理它们,并且在使用 T::singleton() 时编辑器可能崩溃。
引擎中每个单例类应只有一个实例,且该实例在库加载期间有效。因此,用户单例仅限于手动内存管理的类(即不继承自 RefCounted 的类)。
#[derive(GodotClass)]
#[class(init, base = Object)]
struct MySingleton {}
// 提供允许使用 MySingleton::singleton() 的 blanket 实现。
// 确保 `MySingleton` 是一个有效的单例
// (即非引用计数的 GodotClass)。
impl UserSingleton for MySingleton {}
struct MyExtension;
#[gdextension]
unsafe impl ExtensionLibrary for MyExtension {
fn on_stage_init(stage: InitStage) {
// 单例应在 MainLoop 启动前注册;
// 否则 GDScriptParser 将无法识别它。
if stage == InitStage::Scene {
let obj = MySingleton::new_alloc();
Engine::singleton()
.register_singleton(
&MySingleton::class_id().to_string_name(),
&obj
);
}
}
fn on_stage_deinit(stage: InitStage) {
if stage == InitStage::Scene {
let obj = MySingleton::singleton();
Engine::singleton()
.unregister_singleton(
&MySingleton::class_id().to_string_name()
);
obj.free();
}
}
}
单例和 SceneTree
单例不能安全地访问场景树。在任何给定时刻,它们可能存在而没有活动的场景树。
虽然技术上可以通过一些取巧的方法访问场景树,但 强烈建议 为此目的使用自定义的 EditorPlugin。
创建一个 EditorPlugin 允许注册一个“自动加载的单例”,这个单例是一个 Node(或其派生类型),并且当游戏启动时,Godot 会自动将其加载到 SceneTree 中。