# QfStudio.Godette.ReactiveUI **Repository Path**: lightyears/QfStudio.Godette.ReactiveUI ## Basic Information - **Project Name**: QfStudio.Godette.ReactiveUI - **Description**: Godot Engine ReactiveUI 集成 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-07-27 - **Last Updated**: 2026-07-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: Godot4, MVVM, Reactiveui ## README # QfStudio.Godette.ReactiveUI > ReactiveUI 的 Godot 引擎集成 [ReactiveUI](https://www.reactiveui.net/) 是一个面向 .NET 的跨平台 MVVM(Model-View-ViewModel)框架,以组合式风格构建。它利用响应式扩展(Reactive Extensions)将 UI 元素绑定到 ViewModel 的属性和命令,让视图和业务逻辑各司其职。 `QfStudio.Godette.ReactiveUI` 提供了让 ReactiveUI 在 Godot 引擎中运行所需的一套平台服务,包括调度器、视图激活、属性变更通知和命令绑定。如果你在 Avalonia 或 WPF 上用过 ReactiveUI,那么 `this.Bind` / `this.BindCommand` / `WhenActivated` 这些用法完全一样,只是底层对接的是 Godot 的节点和信号。实现细节见 [Developer.md](Docs/Developer.md)。 当前版本的 QfStudio.Godette.ReactiveUI 兼容 ReactiveUI v23,暂不支持今年 7 月 26 日刚发布的 ReactiveUI v24。本库尚未做零分配(zero-allocation)优化,预计未来一年内随 ReactiveUI v24 的升级一并完成减少内存分配的工作。 ## 安装 ``` dotnet add package QfStudio.Godette.ReactiveUI --prerelease ``` 还有两个推荐安装的可选包,能大幅改善开发体验: [GodotSharp.SourceGenerators](https://github.com/Cat-Lips/GodotSharp.SourceGenerators) 提供 `[SceneTree]` 特性,用于类型安全的场景加载和强类型节点访问,无需手写 `GetNode` 调用。 ``` dotnet add package GodotSharp.SourceGenerators ```
使用与不使用 [SceneTree] 的对比 将该特性标注在 `.tscn` 的根脚本上,即可获得: - `TscnFilePath` 静态属性,用于类型安全的场景加载。 - `unique_name_in_owner` 节点的强类型字段,无需手写 `GetNode`。 ```csharp // 使用 [SceneTree] — 节点可直接作为属性访问 [SceneTree(root: "_root")] public partial class MyScene : Control { public override void _Ready() { BackButton.Pressed += () => GetTree().ChangeSceneToFile(HomeScene.TscnFilePath); NameEdit.Text = "hello"; } } ``` ```csharp // 不使用 [SceneTree] — 需要通过字符串路径调用 GetNode public partial class MyScene : Control { public override void _Ready() { GetNode
[ReactiveUI.SourceGenerators](https://github.com/reactiveui/ReactiveUI.SourceGenerators) 提供 `[Reactive]` 特性,为 partial 属性自动生成 `RaiseAndSetIfChanged` 样板代码。 ``` dotnet add package ReactiveUI.SourceGenerators ```
使用与不使用 [Reactive] 的对比 ```csharp // 使用 [Reactive] public partial class MyViewModel : ReactiveObject { [Reactive] public partial string Name { get; set; } = ""; } ``` ```csharp // 不使用 [Reactive] — 手动编写 backing field + RaiseAndSetIfChanged public class MyViewModel : ReactiveObject { private string _name = ""; public string Name { get => _name; set => this.RaiseAndSetIfChanged(ref _name, value); } } ```
### Autoload 配置 创建一个引导类来初始化 ReactiveUI 服务,然后在 Godot 中将其注册为 Autoload: ```csharp using System.Threading; using QfStudio.Godette.ReactiveUI; using ReactiveUI; using ReactiveUI.Builder; using Splat; public partial class RxAppBootstrapper : Godot.Node { private readonly GodotFrameScheduler _processFrameScheduler = new(); private readonly GodotFrameScheduler _physicsFrameScheduler = new(); public RxAppBootstrapper() { var scheduler = GodotMainThreadScheduler.Create(SynchronizationContext.Current!); GodotSchedulers.MainThreadScheduler = scheduler; GodotSchedulers.ProcessFrameScheduler = _processFrameScheduler; GodotSchedulers.PhysicsFrameScheduler = _physicsFrameScheduler; var viewLocator = new GodotViewLocator(); viewLocator.RegisterViewsFromAssemblyViaReflection(typeof(RxAppBootstrapper).Assembly, verbose: false); RxAppBuilder.CreateReactiveUIBuilder() .WithMainThreadScheduler(scheduler) .WithRegistration(locator => { locator.RegisterConstant(new GodotActivationFetcher(), typeof(IActivationForViewFetcher)); locator.RegisterConstant(new GodotPropertyBinder(), typeof(ICreatesObservableForProperty)); locator.RegisterConstant(new GodotPollBasedPropertyBinder(), typeof(ICreatesObservableForProperty)); locator.RegisterConstant(new GodotCommandBinder(), typeof(ICreatesCommandBinding)); locator.RegisterConstant(viewLocator, typeof(GodotViewLocator)); }) .WithConverter(new FloatToDoubleConverter()) .WithConverter(new DoubleToFloatConverter()) .WithCoreServices() .BuildApp(); } public override void _Process(double delta) { _processFrameScheduler.NotifyProcess(delta); } public override void _PhysicsProcess(double delta) { _physicsFrameScheduler.NotifyProcess(delta); } } ``` 在 Godot 编辑器中,进入 **项目 > 项目设置 > Autoload**,将该脚本添加为 Autoload,建议命名为 `RxAppBootstrapper`。 `RxAppBuilder.BuildApp()` 会把 `.WithMainThreadScheduler(...)` 注册的调度器同步到 ReactiveUI 的 `RxSchedulers.MainThreadScheduler`,所以下文示例里的 `ObserveOn(RxSchedulers.MainThreadScheduler)` 指的就是这里设的 `GodotMainThreadScheduler`。`GodotSchedulers` 是同一组实例在 Godot 端的别名,供帧运算符和其他 Godot 专用 API 使用。 如果少了上面代码里的 `FloatToDoubleConverter` / `DoubleToFloatConverter`,在绑定暴露 `double` 属性的 Godot 控件(比如 `Range.Value`、`ColorPicker.Color`)和 ViewModel 的 `float` 属性时,绑定时会抛出 `ConverterNotFoundException`。另外本库还提供了 `EnumToStringConverter`、`StringToEnumConverter` 以及 `Variant` 与基本类型间的转换器——按需在上面的构建器里用 `.WithConverter(...)` 注册即可。 ## 用法 ### 核心概念 **激活语义**:视图激活(`true`)的条件是它的 Godot `Node` 在场景树**且** `IsNodeReady()` 返回 `true`。`GodotActivationFetcher` 通过三条路径发出 `true`: - `Ready` 信号(首次进入场景树,所有子节点已初始化完毕); - `TreeEntered` + `IsNodeReady()`(节点已就绪后重新进入); - 订阅时立刻检查 `IsInsideTree() && IsNodeReady()`。 `TreeExited` 时发出 `false`,语义和 Avalonia 的 `AttachedToVisualTree` / `DetachedFromVisualTree` 一样。 注意:C# 中的 `_Ready` 虚方法在 `Ready` 信号触发**之前**执行,因此在 `_Ready` 中赋值的 `ViewModel` 在 `WhenActivated` 触发时已就绪。 ### `usings` 以下示例均假定已引入这些命名空间: ```csharp using QfStudio.Godette.ReactiveUI; using ReactiveUI; using System.Reactive.Disposables; // 用于贯穿全文的 DisposeWith(d) ``` `[GodotViewFor]` 生成的 `ViewModel` 属性由本库内置的源生成器发布到 `QfStudio.Godette.ReactiveUI` 命名空间——无需额外编写 `using` 或添加包引用。 ### 基本设置 ViewModel 需要实现 `IActivatableViewModel`。视图使用 `[GodotViewFor]` 源生成器特性实现 `IViewFor`。在构造函数的 `WhenActivated` 回调中进行绑定: ```csharp // ViewModel public partial class MyViewModel : ReactiveObject, IActivatableViewModel { public ViewModelActivator Activator { get; } = new(); [Reactive] public partial string Name { get; set; } = ""; } // View(.tscn 根脚本) [GodotViewFor] public partial class MyScene : Control { public MyScene() { this.WhenActivated(d => { this.Bind(ViewModel, vm => vm.Name, v => v.NameEdit.Text) .DisposeWith(d); }); } public override void _Ready() { ViewModel = new MyViewModel(); } } ```
为什么要在 _Ready 中赋值 ViewModel 在 Godot 中,**没有内置的 UI/路由框架**来自动创建视图并注入 `ViewModel`。Avalonia + ReactiveUI 通过 `RoutingState` 和平台的 `IViewLocator`(由 DataTemplates / Splat 解析)在导航时自动构建视图并设置 `ViewModel`。Godot 则不同,每个场景的根脚本都必须自己实例化 ViewModel。推荐在 `_Ready` 中完成,原因如下: - Godot 保证 `_Ready` 在所有子节点初始化完后才调用,因此 `[SceneTree]` 生成的节点属性(如 `NameEdit`)在此处不为 null; - Godot 的 C# 虚方法 `_Ready` 在 `Ready` 信号触发**之前**执行,而 `WhenActivated` 通过 `GodotActivationFetcher` 订阅的是 `Ready` 信号,因此 `_Ready` 中的 `ViewModel = new MyViewModel();` 在 `WhenActivated` 回调之前就已完成。 如果你自行处理路由(参见下文 [路由](#路由)),`RoutedViewController` 在解析视图后会设置 `view.ViewModel = viewModel`,因此路由视图无需在 `_Ready` 中赋值——仅顶层/根场景需要。
### 数据绑定 使用 `Bind` 进行双向绑定,使用 `OneWayBind` 进行单向绑定: ```csharp this.WhenActivated(d => { // 双向:LineEdit.Text <-> ViewModel.Name this.Bind(ViewModel, vm => vm.Name, v => v.NameEdit.Text) .DisposeWith(d); // 单向 + 转换器 this.OneWayBind(ViewModel, vm => vm.Score, v => v.ScoreLabel.Text, score => $"{score:F1}") .DisposeWith(d); // 派生值 this.WhenAnyValue(x => x.ViewModel!.Name, x => x.ViewModel!.Notes) .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(tuple => { /* 更新 UI */ }) .DisposeWith(d); }); ``` ### 命令绑定 将 `ReactiveCommand` 绑定到 `BaseButton`(在 `Pressed` 时触发)或 `LineEdit`(在 `TextSubmitted` 时触发)。`CanExecute` 会自动禁用控件: ```csharp this.WhenActivated(d => { // 按钮按下时执行命令 this.BindCommand(ViewModel, vm => vm.SaveCommand, v => v.SaveButton) .DisposeWith(d); // LineEdit 提交时执行命令,将当前文本作为参数传递 this.BindCommand(ViewModel, vm => vm.SearchCommand, v => v.SearchEdit, vm => vm.QueryString) .DisposeWith(d); // 条件命令 this.Bind(ViewModel, vm => vm.IsEnabled, v => v.CheckButton.ButtonPressed) .DisposeWith(d); this.BindCommand(ViewModel, vm => vm.DoWorkCommand, v => v.WorkButton) .DisposeWith(d); }); ```
工作原理 两个绑定器协同工作,投递属性变更通知: **`GodotPropertyBinder`——基于信号** 订阅 Godot 内置信号,变更即时到达,无帧延迟: | 控件类型 | 属性 | Godot 信号 | |---|---|---| | `Range` | `Value` | `ValueChanged` | | `LineEdit` | `Text` | `TextChanged` | | `TextEdit` | `Text` | `TextChanged` | | `BaseButton` | `ButtonPressed` | `Toggled` | | `TabContainer` | `CurrentTab` | `TabChanged` | | `TabBar` | `CurrentTab` | `TabChanged` | | `OptionButton` | `Selected` | `ItemSelected` | | `ColorPicker` | `Color` | `ColorChanged` | | `ColorPickerButton` | `Color` | `ColorChanged` | **`GodotPollBasedPropertyBinder`——逐帧轮询** 没有专用信号的 `GodotObject` 属性,绑定器通过 `Observable.PollEveryUpdate` 每帧读取值,变化时发出通知。由于采用轮询机制,最多存在一帧的延迟。
### 激活生命周期 视图激活(进入场景树并就绪)时 `WhenActivated` 触发。所有通过 `DisposeWith(d)` 注册的订阅在停用时自动清理: ```csharp public MyScene() { this.WhenActivated(d => { // 视图停用时自动释放订阅 this.WhenAnyValue(x => x.ViewModel!.Name) .Subscribe(name => GD.Print($"Name: {name}")) .DisposeWith(d); Disposable.Create(() => GD.Print("deactivated")) .DisposeWith(d); }); } ``` ### 信号 -> Observable 本库为大部分通过信号通知变更的 Godot 控件提供了对应的 `ObserveXxx()` 扩展方法,覆盖 `BaseButton`、`Range`、`LineEdit`、`TextEdit`、`ItemList`、`OptionButton`、`TabBar`、`TabContainer`、`ColorPicker`、`ColorPickerButton`、`Tree`、`PopupMenu`、`FileDialog` 以及 `SceneTree`: ```csharp this.WhenActivated(d => { ToggleButton.ObserveToggled() .Subscribe(on => ViewModel!.IsToggled = on) .DisposeWith(d); LineEdit.ObserveTextChanged() .Subscribe(text => ViewModel!.InputText = text) .DisposeWith(d); GetTree().ObserveProcessFrame() .Subscribe(_ => ViewModel!.FrameCount++) .DisposeWith(d); }); ``` 对于任意 `GodotObject`,通过内置的扩展方法把自定义信号转成 `IObservable`: ```csharp this.WhenActivated(d => { // 提供了 0...7 个类型化参数的重载; // N 参数重载发出 ValueTuple,0 参数重载发出 Unit // 0 参数信号 -> IObservable MyNode.ObserveSignal("my_signal") .Subscribe(_ => { /* 触发时无载荷 */ }) .DisposeWith(d); // 1 参数信号 -> IObservable> MyNode.ObserveSignal("my_signal") .Subscribe(args => { /* args.Item1 */ }) .DisposeWith(d); // 3 参数信号 -> IObservable> MyNode.ObserveSignal("my_signal") .Subscribe(args => { var (i, s, b) = args; /* ... */ }) .DisposeWith(d); }); ``` ### 集合绑定 将 `ObservableCollection` 同步到 Godot 容器。`ItemsBinder` 将 ViewModel 映射为子节点;`ItemListBinder`、`OptionButtonBinder`、`TabBarBinder` 和 `PopupMenuBinder` 分别绑定到对应的控件。 ```csharp // 节点容器:ObservableCollection -> VBoxContainer 子节点 var itemsBinder = new ItemsBinder( new GodotViewLocator()); // 若已注册,也可用 Splat.Locator.Current.GetService()! this.WhenActivated(d => { itemsBinder.Connect(ItemsContainer, ViewModel!.Items) .DisposeWith(d); }); // ItemList:每个条目的文本/图标 var itemListBinder = new ItemListBinder(textSelector: vm => vm.Name); this.WhenActivated(d => { itemListBinder.Connect(itemListControl, items) .DisposeWith(d); itemListBinder.ObserveSelection() .Subscribe(vm => { /* 处理选中 */ }) .DisposeWith(d); }); // OptionButton / TabBar var optionBinder = new OptionButtonBinder(textSelector: vm => vm.Name); this.WhenActivated(d => { optionBinder.Connect(optionButton, items) .DisposeWith(d); optionBinder.ObserveSelection() .Subscribe(vm => { /* 处理选中 */ }) .DisposeWith(d); }); // PopupMenu 带命令绑定 -- 每个条目执行各自的 ICommand var menuBinder = new PopupMenuBinder( textSelector: vm => vm.Label, iconSelector: vm => vm.Icon, commandSelector: vm => vm.ActionCommand, commandParameterSelector: vm => vm.Parameter); this.WhenActivated(d => { menuBinder.Connect(popupMenu, menuItems) .DisposeWith(d); menuBinder.ObserveSelection() .Subscribe(vm => { /* 处理选中 */ }) .DisposeWith(d); }); ``` `Connect(...)` 返回一个 `IDisposable`,用于断开绑定器与容器和集合之间的连接。请务必释放它(通常在 `WhenActivated` 内通过 `DisposeWith(...)` 完成),这样才能在停用时正确清理。 索引绑定器(`ItemListBinder`、`OptionButtonBinder`、`TabBarBinder`、`PopupMenuBinder`)接受 `Expression>` / `Expression>` 选择器。如果 `TViewModel` 实现了 `INotifyPropertyChanged`(如继承 `ReactiveObject`),绑定器会通过 ReactiveUI 的 `WhenAnyValue` 订阅变更,在 ViewModel 的 `[Reactive]` 属性变化时自动同步控件的文本或图标。未实现 `INotifyPropertyChanged` 的 POCO ViewModel 仅在添加或替换时写入初始值,后续属性变化不会传播。
PopupMenu 命令绑定 `PopupMenuBinder` 支持通过两个额外的构造函数参数绑定 `ICommand`: - `commandSelector` —— `Expression>`,为每个菜单项选择对应的命令。 - `commandParameterSelector` —— 可选的 `Expression>`,为每个菜单项选择传递给 `CanExecute` 和 `Execute` 的参数。 当提供 `commandSelector` 时,绑定器会: 1. 追踪每个命令的 `CanExecuteChanged` 事件,自动调用 `Container.SetItemDisabled` 以反映 `CanExecute` 状态。 2. 订阅 `Container.ObserveIdPressed()`,在菜单项被点击时执行对应的命令及其参数。 ```csharp // ViewModel -- 每个菜单项携带各自的命令 public partial class MenuItemViewModel : ReactiveObject { [Reactive] public partial string Label { get; set; } = ""; [Reactive] public partial Texture2D? Icon { get; set; } public ICommand? ActionCommand { get; set; } public object? Parameter { get; set; } } // 命令绑定 var menuBinder = new PopupMenuBinder( textSelector: vm => vm.Label, iconSelector: vm => vm.Icon, commandSelector: vm => vm.ActionCommand, commandParameterSelector: vm => vm.Parameter); ```
为什么使用 Binder 而非 ItemsControl 在 Avalonia/WPF 中,集合同步是模板系统自带的:绑定 `ItemsControl.ItemsSource`,框架的 `ItemContainerGenerator` 依次为每个条目创建容器、应用 `DataTemplate`、连接 `DataContext`。Godot 没有 XAML/模板引擎,也没有 `ItemsSource`——其 `VBoxContainer`、`ItemList`、`OptionButton`、`Tree` 等是异构控件,增删 API 各不相同(`AddChild`、`AddItem`、`AddItem`+`set_metadata`、`CreateItem`……),绑定层无法接入一个统一的"条目生成器"。 `*Binder` 类型正是为此设计的。每个绑定器封装了一类 Godot 控件特有的增删替换逻辑,对外暴露统一的 `Connect(container, collection)` 接口。这样视图代码保持了声明式风格(与 ReactiveUI 中其他地方使用的 `WhenActivated` + `DisposeWith(d)` 一致),同时对 Godot 原生 API 仅是一层轻量适配——没有影子视觉树、没有中间"项宿主"节点、没有大开销的模板展开。代价是需要根据控件选择对应的绑定器(节点容器用 `ItemsBinder`,`ItemList` 用 `ItemListBinder`……),而非一个万能的 `ItemsControl`。 第二个原因是架构上的:Avalonia 风格的 `ItemsControl` 需要继承 Godot 控件(`Godot.Node`),但 Godot 将每个派生自 `Godot.Node` 的 C# 类视为关联至项目源码目录中唯一路径的脚本资源,且**完全不支持**泛型 `Godot.Node` 类型(见 [Developer.md § Godot 限制](../Docs/Developer.md#limitations-for-godot))。因此一个可复用的泛型集合宿主既无法放在第三方程序集中,也无法按条目类型化。绑定器规避了这两个限制——它是一个普通的泛型 C# 类,通过 `Connect(container, ...)` 驱动一个*已有的* Godot 控件,这正是它能随本库一起发布而泛型 `ItemsControl` 无法做到的原因。
### Interaction(交互对话框) 将 ViewModel 的 `Interaction` 绑定到视图层的处理器(如对话框): ```csharp // ViewModel public Interaction ConfirmDelete { get; } = new(); DeleteCommand = ReactiveCommand.CreateFromTask(async () => { var confirmed = await ConfirmDelete.Handle("确认删除?"); ResultText = confirmed ? "已确认" : "已取消"; }); // View this.WhenActivated(d => { this.BindInteraction(ViewModel, vm => vm.ConfirmDelete, async context => { ConfirmDialog.DialogText = context.Input; ConfirmDialog.PopupCentered(); var tcs = new TaskCompletionSource(); ConfirmDialog.Confirmed += () => tcs.TrySetResult(true); ConfirmDialog.Canceled += () => tcs.TrySetResult(false); context.SetOutput(await tcs.Task); }).DisposeWith(d); }); ``` ### 验证 [ReactiveUI.Validation](https://github.com/reactiveui/ReactiveUI.Validation) 是一个独立包——需先安装: ``` dotnet add package ReactiveUI.Validation ``` 然后在 ViewModel 上定义验证规则,在 View 上绑定错误消息: ```csharp // 引入:ReactiveUI, ReactiveUI.SourceGenerators, // ReactiveUI.Validation.Abstractions, ReactiveUI.Validation.Contexts, // ReactiveUI.Validation.Extensions // ViewModel — 实现 IActivatableViewModel 和 IValidatableViewModel public partial class MyViewModel : ReactiveObject, IActivatableViewModel, IValidatableViewModel { public ViewModelActivator Activator { get; } = new(); public IValidationContext ValidationContext { get; } = new ValidationContext(); [Reactive] public partial string Email { get; set; } = ""; public MyViewModel() { this.ValidationRule(vm => vm.Email, email => !string.IsNullOrWhiteSpace(email) && email.Contains('@'), "邮箱必须包含 '@'。"); } } // View this.WhenActivated(d => { this.Bind(ViewModel, vm => vm.Email, v => v.EmailEdit.Text) .DisposeWith(d); this.BindValidation(ViewModel, vm => vm.Email, v => v.ErrorLabel.Text) .DisposeWith(d); }); ``` ### 视图定位(`GodotViewLocator`) `GodotViewLocator` 连接了 ReactiveUI 的视图解析和 Godot 的 `PackedScene` 系统。在 Avalonia 里,`IViewLocator` 通常通过 XAML `DataTemplates` 接入——绑定时平台检查 ViewModel 类型,然后实例化 XAML 里对应的 `Control`。Godot 没有和 `DataTemplate` 类似的视图解析机制,场景都是 `GD.Load(path).Instantiate()` 加载的。`GodotViewLocator` 手动做了这个映射:把 ViewModel 类型注册到一个 `.tscn` 路径上,`ResolveView` 就会加载并实例化该场景作为 `IViewFor`。 将 `GodotViewLocator` 注册到 Splat 容器中(如 Autoload 所示)是**可选的**。你也可以在需要时按需创建——例如 `var locator = new GodotViewLocator(); locator.RegisterView(...);`——然后直接传递给 `RoutedViewController` / `ItemsBinder`。Splat 注册仅为方便,使通过 `Locator.Current` 解析的库组件能找到同一个实例。 向 `GodotViewLocator` 注册视图有三种方式: ```csharp var locator = new GodotViewLocator(); // 1. 显式注册视图 + ViewModel 类型 locator.RegisterView("res://Views/MyView.tscn"); // 若安装了 GodotSharp.SourceGenerators,也可: locator.RegisterView(MyView.TscnFilePath); // 2. 仅指定 ViewModel 类型(视图类型从 IViewFor 实现推断) locator.RegisterView("res://Views/MyView.tscn"); // 3. 通过反射扫描整个程序集——选取所有实现了 // IViewFor 且暴露了静态 TscnFilePath 属性的具体类型 locator.RegisterViewsFromAssemblyViaReflection(typeof(MyView).Assembly); ``` 方式 3 依赖 **[GodotSharp.SourceGenerators](https://github.com/Cat-Lips/GodotSharp.SourceGenerators)**(`[SceneTree]`)生成的 `TscnFilePath` 静态属性。该包完全是可选的——方式 1 和 2 直接传递 `"res://..."` 路径字符串即可,无需源生成器。不使用该包时,手动为每对 ViewModel/View 调用 `RegisterView(...)` 即可;注册完成后,路由、`ItemsBinder` 等视图解析功能照常工作。 `RegisterView(path)` 仅存储 ViewModel 类型和 `.tscn` 路径。解析时 `GodotViewLocator` 执行 `GD.Load(path).Instantiate>()`——实际返回的视图类由 `.tscn` 的根脚本决定(必须实现 `IViewFor`)。`RegisterView(...)` 中的 `` 类型参数仅用于编译期检查,不影响解析结果。 `ResolveView` 通常由 ReactiveUI(或下面示例中的 `RoutedViewController`)调用,而非用户代码——只需保持注册信息的最新即可。 ### 路由 ReactiveUI 的 `RoutingState` 配合 `GodotViewLocator` 实现页面导航。本库提供了视图定位器和注册 API;导航时切换子节点仍需一个适配器——下面这个 `RoutedViewController` **不包含**在 NuGet 包中,是可以从 [IntegrationTests/Views/Routing/RoutedViewController.cs](QfStudio.Godette.IntegrationTests/Views/Routing/RoutedViewController.cs) 复制使用的示例代码: ```csharp // 初始化(在 _Ready 或构造函数中) var locator = new GodotViewLocator(); locator.RegisterView(PageAView.TscnFilePath); locator.RegisterView(PageBView.TscnFilePath); var shell = new ShellViewModel(); // 实现 IScreen,包含 RoutingState var router = new RoutedViewController(shell.Router, locator); // 示例适配器,见上方说明 router.Connect(ContentContainer); // 导航 shell.Router.Navigate.Execute(new PageAViewModel(shell)); shell.Router.NavigateBack.Execute().Subscribe(); ``` ### 帧运算符 基于 `SceneTree.ProcessFrame` 的帧感知响应式运算符: ```csharp this.WhenActivated(d => { // 每帧触发(传入 RxSchedulers.PhysicsFrameScheduler 可改为物理帧) Observable.EveryUpdate() .Subscribe(_ => { /* 每帧执行 */ }) .DisposeWith(d); // 延迟 N 帧 Observable.AfterFrame(0) .DelayFrame(30) .Subscribe(_ => { /* 30 帧后触发 */ }) .DisposeWith(d); // N 帧后触发一次,此后每 M 帧触发一次 Observable.IntervalFrame(60) .Subscribe(_ => { /* 每 60 帧 */ }) .DisposeWith(d); // 延迟 N 帧后发出单个值 Observable.ReturnFrame("ready", 30) .Subscribe(msg => { /* 30 帧后触发 */ }) .DisposeWith(d); // 防抖:静默 30 帧后发出 input.DebounceFrame(30) .Subscribe(value => { /* ... */ }) .DisposeWith(d); // 节流:每个 60 帧窗口内仅发出首个值 input.ThrottleFirstFrame(60) .Subscribe(value => { /* ... */ }) .DisposeWith(d); // 分批:收集值,每 30 帧发出一个列表 input.ChunkFrame(30) .Subscribe(batch => { /* IList */ }) .DisposeWith(d); // 每帧轮询属性,值变化时发出 Observable.PollEveryUpdate(this, v => v.FreeIcon.Position) .Subscribe(pos => { /* ... */ }) .DisposeWith(d); }); ``` ## 替代方案 如果本库不适合你,可以看看: - [**R3**](https://github.com/Cysharp/R3)——UniRx 作者开发的零分配 Rx.NET 重新实现。如果你更倾向于使用 `ReactiveProperty` 而非完整的 MVVM,或希望帧运算符作为核心功能,R3 是个不错的选择。它可以与 ReactiveUI 并行使用(例如 ReactiveUI 负责 UI 层,R3 负责业务逻辑层)。 --- ## 许可证 MIT 许可证 ## 开发 参阅 [Developer.md](Docs/Developer.md)。 ## AI 使用声明 本项目使用了 AI 辅助编码,AI 的作用仅限于提供代码建议和处理琐碎任务。 所有代码均已尽最大努力进行人工审阅把关。 无不妥代码入库。