- 更新 Game 数据与存储页面、Godot UI 页面中的 reader-facing 说明,移除内部证据口吻、外部项目指代和生硬导流 - 更新 CQRS 抽象层与 SourceGenerators.Common README 的标签表述,避免暴露源文件路径列表和实现级打包术语 - 补充 documentation-full-coverage-governance 的 RP-050 恢复点、验证结果与 origin/main stop-condition 计量
9.2 KiB
title, description
| title | description |
|---|---|
| Godot UI 系统 | 说明 GFramework.Godot 当前如何把 Game UI 契约接到 PackedScene、页面行为和层级 UI 接入路径上。 |
Godot UI 系统
GFramework.Godot.UI 当前负责的是把 GFramework.Game 的 UI 路由契约接到 Control / CanvasLayer /
PackedScene 上,而不是再提供一套 Godot 专用路由器类型。
当前真正参与这条链路的核心类型是:
IGodotUiRegistry/GodotUiRegistryGodotUiFactoryCanvasItemUiPageBehaviorBase<T>UiPageBehaviorFactoryPage/Overlay/Modal/Toast/Topmost五类 layer behavior- 项目侧实现的
IUiRoot - 项目侧继承
UiRouterBase的路由器
当前公开入口
IGodotUiRegistry
Godot 侧 UI 资源表,底层是 IAssetRegistry<PackedScene>。它只负责:
uiKey -> PackedScene映射- 让
GodotUiFactory可以按 key 实例化 UI 页面
框架当前不会自动扫描 .tscn、不会自动根据类型名补全注册表。
GodotUiFactory
GodotUiFactory.Create(string uiKey) 的当前行为比场景工厂更严格:
- 从
IGodotUiRegistry取出PackedScene - 调用
Instantiate() - 节点必须实现
IUiPageBehaviorProvider - 返回
provider.GetPage()
如果实例化得到的节点没有实现 IUiPageBehaviorProvider,当前实现会直接抛 InvalidCastException。因此页面节点必须显式提供
GetPage(),或者通过 [AutoUiPage] 生成对应样板。
CanvasItemUiPageBehaviorBase<T>
Godot runtime 的页面行为包装基类。它把 IUiPageBehavior 的这些语义接到 CanvasItem 上:
KeyLayerHandleIsAliveIsVisibleInteractionProfileOnEnter/OnExitOnPause/OnResumeOnShow/OnHideTryHandleUiAction(UiInputAction action)
如果 owner 同时实现了 IUiPage、IUiInteractionProfileProvider、IUiActionHandler,这些契约都会被页面行为继续利用。
UiPageBehaviorFactory
当前 layer 到 behavior 的映射来自运行时代码本身:
UiLayer.Page->PageLayerUiPageBehavior<T>UiLayer.Overlay->OverlayLayerUiPageBehavior<T>UiLayer.Modal->ModalLayerUiPageBehavior<T>UiLayer.Toast->ToastLayerUiPageBehavior<T>UiLayer.Topmost->TopmostLayerUiPageBehavior<T>
几个容易混淆的默认语义如下:
Page- 不可重入,阻断输入
Overlay- 可重入,非模态,不阻断输入;暂停时不会停掉节点处理
Modal- 可重入,模态,阻断输入
Toast- 可重入,非模态,不阻断输入
Topmost- 不可重入,模态,阻断输入
最小接入路径
1. 继续在项目层保留自己的路由器
仓库当前不存在 GodotUiRouter 类型。实际做法仍然是项目侧继承 GFramework.Game.UI.UiRouterBase。
最小形态通常就是:
using LoggingTransitionHandler = GFramework.Game.UI.Handler.LoggingTransitionHandler;
namespace GameProject.UI;
[Log]
public partial class UiRouter : UiRouterBase
{
protected override void RegisterHandlers()
{
_log.Debug("Registering default transition handlers");
RegisterHandler(new LoggingTransitionHandler());
}
}
Godot runtime 自身并不接管这层路由器定义。
2. 注册 IGodotUiRegistry 与 IUiFactory
最小 wiring 需要显式注册 UI 资源表和工厂:
using GFramework.Game.Abstractions.UI;
using GFramework.Godot.UI;
using Godot;
public sealed class GameUiRegistry : GodotUiRegistry
{
public GameUiRegistry()
{
Register(nameof(UiKey.MainMenu), GD.Load<PackedScene>("res://ui/main_menu.tscn"));
Register(nameof(UiKey.PauseMenu), GD.Load<PackedScene>("res://ui/pause_menu.tscn"));
Register(nameof(UiKey.OptionsMenu), GD.Load<PackedScene>("res://ui/options_menu.tscn"));
}
}
architecture.RegisterUtility<IGodotUiRegistry>(new GameUiRegistry());
architecture.RegisterUtility<IUiFactory>(new GodotUiFactory());
architecture.RegisterSystem(new UiRouter());
3. 提供 IUiRoot
UiRouterBase 只负责页面栈、layer UI、输入仲裁和暂停语义;真正把页面挂到 Godot 容器的是项目自己的 IUiRoot。
项目侧常见的 UiRoot 接法如下:
- 继承
CanvasLayer - 为每个
UiLayer创建一个Control容器 - 在
_Ready()时调用_uiRouter.BindRoot(this) - 在
AddUiPage/RemoveUiPage中处理CanvasItem挂载与释放
最小形态可以写成:
public sealed class UiRoot : CanvasLayer, IUiRoot
{
[GetSystem] private IUiRouter _uiRouter = null!;
public override void _Ready()
{
__InjectContextBindings_Generated();
_uiRouter.BindRoot(this);
}
public void AddUiPage(IUiPageBehavior child)
{
AddUiPage(child, UiLayer.Page);
}
public void AddUiPage(IUiPageBehavior child, UiLayer layer, int orderInLayer = 0)
{
if (child.View is not CanvasItem item)
throw new InvalidOperationException("UIPage View must be a Godot Node");
AddChild(item);
item.ZIndex = (int)layer * 100 + orderInLayer;
}
public void RemoveUiPage(IUiPageBehavior child)
{
if (child.View is Node node && node.GetParent() == this)
RemoveChild(node);
}
}
4. 让页面节点提供 GetPage()
因为 GodotUiFactory 不会自动回退到默认 behavior,页面节点必须显式提供 GetPage()。
方式 A:手写 IUiPageBehaviorProvider
public partial class PauseMenu : Control, IUiPage, IUiPageBehaviorProvider
{
private IUiPageBehavior? _page;
public IUiPageBehavior GetPage()
{
return _page ??= UiPageBehaviorFactory.Create(this, nameof(UiKey.PauseMenu), UiLayer.Modal);
}
public void OnEnter(IUiPageEnterParam? param)
{
}
public void OnExit()
{
}
public void OnPause()
{
}
public void OnResume()
{
}
public void OnShow()
{
}
public void OnHide()
{
}
}
方式 B:用 [AutoUiPage] 让生成器补样板
更常见的接法,是让生成器产出 UiKeyStr 和 GetPage():
using GFramework.Game.Abstractions.UI;
using GFramework.Godot.SourceGenerators.Abstractions.UI;
using Godot;
[AutoUiPage(nameof(UiKey.MainMenu), nameof(UiLayer.Page))]
public partial class MainMenu : Control, IUiPageBehaviorProvider, IUiPage
{
public void OnEnter(IUiPageEnterParam? param)
{
}
public void OnExit()
{
}
public void OnPause()
{
}
public void OnResume()
{
}
public void OnShow()
{
}
public void OnHide()
{
}
}
当前生成器补出的核心样板与源码一致:
public IUiPageBehavior GetPage()
{
return __autoUiPageBehavior_Generated ??=
UiPageBehaviorFactory.Create(this, UiKeyStr, UiLayer.Page);
}
要注意两点:
[AutoUiPage]不会替你自动补: IUiPageBehaviorProvider- UI 层级是生成器输入的一部分;
Page/Modal/Overlay语义不是后面再猜出来的
5. 按 layer 选择正确入口
Godot runtime 只是落地 UiRouterBase 的语义,因此入口仍然和 GFramework.Game 一致:
页面栈:
await uiRouter.ReplaceAsync(nameof(UiKey.MainMenu));
await uiRouter.PushAsync(nameof(UiKey.Settings));
await uiRouter.PopAsync();
层级 UI:
var handle = uiRouter.Show(nameof(UiKey.PauseMenu), UiLayer.Modal);
uiRouter.Hide(handle, UiLayer.Modal);
当前实现里,Show(..., UiLayer.Page) 会直接抛异常;Page 层必须走 PushAsync / ReplaceAsync。
输入与暂停语义
如果页面只实现 IUiPage,它只有基础生命周期。
如果还需要更强的输入仲裁或暂停语义,可以继续实现:
IUiInteractionProfileProviderIUiActionHandler
当前这条链路是成立的:
- 页面行为从 owner 读取
UiInteractionProfile - router 根据 profile 判断动作捕获、世界输入阻断和暂停策略
- 如果页面实现了
IUiActionHandler,TryHandleUiAction(...)会继续下沉到页面
这也是为什么 PauseMenu 一类 modal 页面可以声明:
- 捕获
Cancel - 阻断 World pointer / action input
- 在可见时持有暂停
- 即使在暂停状态也继续处理节点逻辑
当前边界
没有 GodotUiRouter
仓库当前没有这个类型;真实入口仍然是项目侧的 UiRouterBase 派生类。
UI 工厂不会自动补 behavior
和 GodotSceneFactory 不同,GodotUiFactory 当前不会按节点类型自动创建 behavior。节点不实现
IUiPageBehaviorProvider 时会直接失败。
Page 层不是 Show(...) 的适用对象
UiLayer.Page 代表页面栈语义,而不是普通 layer UI。当前实现明确要求:
Page用PushAsync/ReplaceAsyncOverlay/Modal/Toast/Topmost用Show/Hide
root 仍然由项目控制
IUiRoot 决定:
- 每个 layer 是否拆独立容器
- 层内排序怎么算
- 页面移除时如何释放节点
Godot runtime 不会替项目自动生成统一 UI 根节点。