用户单例

理解 单例模式 对于正确使用此系统非常重要。

争议

“单例模式”通常被视为一种反模式,因为它违反了多种整洁、模块化代码的良好实践。然而,它也是一个可以用来解决某些设计问题的工具。因此,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 中。