From a5a35ce6ed8d0c9ff8e6a087ead1e29be633c99b Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:07:38 +0800 Subject: [PATCH] =?UTF-8?q?docs(core):=20=E6=94=B6=E5=8F=A3=20Core=20?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=B1=9E=E6=80=A7=E4=B8=8E=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E4=B8=93=E9=A2=98=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 Core events、property 与 logging 专题页,改回当前公开入口、边界与迁移建议 - 记录 state-management 与 coroutine 的复核结论,明确本轮无需继续机械改写 - 推进 documentation-governance-and-refresh 到 RP-005,并更新下一恢复点到 game 与 source-generators 栏目 --- ...ntation-governance-and-refresh-tracking.md | 29 +- ...umentation-governance-and-refresh-trace.md | 28 +- docs/zh-CN/core/events.md | 636 +++--------------- docs/zh-CN/core/logging.md | 398 ++--------- docs/zh-CN/core/property.md | 504 ++------------ 5 files changed, 244 insertions(+), 1351 deletions(-) diff --git a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md index 4dd0f89c..aa5c3d3a 100644 --- a/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md +++ b/ai-plan/public/documentation-governance-and-refresh/todos/documentation-governance-and-refresh-tracking.md @@ -7,21 +7,19 @@ ## 当前恢复点 -- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-004` +- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-005` - 当前阶段:`Phase 3` - 当前焦点: - - 已完成 `docs/zh-CN/core/architecture.md`、`context.md`、`lifecycle.md`、`command.md`、`query.md` 与 - `cqrs.md` 的专题页重写 - - `core` 关键专题页已改回当前 `Architecture`、`ArchitectureContext`、旧 Command/Query 兼容层与新 CQRS - runtime 的真实入口语义 - - 下一轮需要继续推进 `docs/zh-CN/core/*` 余下专题页,以及 `docs/zh-CN/game/*`、 - `docs/zh-CN/source-generators/*` 的专题页核对 + - 已完成 `docs/zh-CN/core/events.md`、`property.md` 与 `logging.md` 的专题页重写 + - 已按源码与测试复核 `docs/zh-CN/core/state-management.md`、`coroutine.md`,当前内容与实现基本一致,无需再做 + 机械改写 + - 下一轮需要把重心转到 `docs/zh-CN/game/*` 与 `docs/zh-CN/source-generators/*` 的专题页核对 ## 当前状态摘要 - 文档治理规则已收口到仓库规范,README、站点入口与采用链路不再依赖旧文档自证 -- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态 -- 当前主题仍是 active topic,因为 `core` 其余专题页及 `game`、`source-generators` 栏目下仍可能包含与实现漂移的旧内容 +- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态,本轮计划中的 `core` 剩余高风险页面已完成收口 +- 当前主题仍是 active topic,因为 `game` 与 `source-generators` 栏目下仍可能包含与实现漂移的旧内容 ## 当前活跃事实 @@ -36,11 +34,15 @@ - `documentation-governance-and-refresh` active trace 已把重复的 `### 下一步` 标题改成带恢复点标识的唯一标题,消除 `MD024/no-duplicate-heading` 告警 - `gframework-pr-review` 脚本已修复“空 `APPROVED` review 覆盖非空 CodeRabbit review body”的解析路径,当前分支可重新提取 Nitpick comments +- `docs/zh-CN/core/events.md`、`property.md` 与 `logging.md` 已改成“当前角色、最常用入口、边界和迁移建议”的结构, + 不再复刻旧版大而全 API 列表 +- `docs/zh-CN/core/property.md` 已明确记录 `BindableProperty.Comparer` 的闭合泛型级共享语义,避免文档继续误导读者把 + `WithComparer(...)` 当成实例级配置 +- `docs/zh-CN/core/state-management.md` 与 `coroutine.md` 已按当前 runtime / 测试重新核对,当前内容可继续保留 ## 当前风险 -- 旧专题页示例失真风险:`docs/zh-CN/core/*`、`game/*` 与 `source-generators/*` 中仍可能保留看似合理但与 - 真实实现不一致的示例 +- 旧专题页示例失真风险:`docs/zh-CN/game/*` 与 `source-generators/*` 中仍可能保留看似合理但与真实实现不一致的示例 - 缓解措施:继续按源码、测试、`*.csproj` 与 `ai-libs/` 下已验证参考实现核对,不把旧文档当事实来源 - 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择 - 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring @@ -64,7 +66,6 @@ ## 下一步 -1. 继续核对 `docs/zh-CN/core/*` 余下专题页,优先处理 `events`、`property`、`state-management`、`coroutine` - 与 `logging` -2. 再推进 `docs/zh-CN/game/*` 与 `docs/zh-CN/source-generators/*` 的专题页重写,优先处理仍引用旧安装方式或旧 API 的页面 +1. 继续核对 `docs/zh-CN/game/*`,优先处理仍引用旧安装方式、旧状态系统或旧 UI / Scene 接法的页面 +2. 再推进 `docs/zh-CN/source-generators/*`,重点核对生成器 wiring、包关系与最小接入示例 3. 若 active trace 再积累新的已完成阶段,按恢复点粒度迁入 `archive/traces/`,避免默认启动入口再次膨胀 diff --git a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md index c8f4130d..a044fca7 100644 --- a/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md +++ b/ai-plan/public/documentation-governance-and-refresh/traces/documentation-governance-and-refresh-trace.md @@ -102,6 +102,28 @@ 2. 若 active trace 继续累计多个已完成恢复点,按 `archive/traces/` 粒度归档旧阶段细节 3. 保持 PR review 跟进时优先验证最新未解决线程、非空 CodeRabbit review body 与 MegaLinter 明确告警 -1. 继续处理 `docs/zh-CN/core/events.md`、`property.md`、`state-management.md`、`coroutine.md`、`logging.md` -2. 保持同样的证据顺序:源码、`*.csproj`、模块 README、`ai-libs/` 参考实现 -3. 完成下一批专题页重写后再次执行 `cd docs && bun run build` +### 阶段:Core 剩余高风险专题页核对(RP-005) + +- 依据 `documentation-governance-and-refresh` active tracking 的恢复点,继续核对 + `docs/zh-CN/core/events.md`、`property.md`、`state-management.md`、`coroutine.md`、`logging.md` +- 对照 `GFramework.Core/Events/*`、`Property/*`、`Logging/*`、`StateManagement/*`、`Coroutine/*` 以及对应测试后确认: + - `events.md`、`property.md` 与 `logging.md` 仍带有旧版“大而全 API 列表”写法,与当前公开入口和推荐边界不匹配 + - `state-management.md` 与 `coroutine.md` 已和当前 runtime / 测试语义基本对齐,本轮无需为了统一文风做额外重写 +- 重写 `events.md`,使其回到“上下文入口、`EventBus` / `EnhancedEventBus`、优先级传播、局部事件对象、与 Store / CQRS + 的边界”的当前结构 +- 重写 `property.md`,使其回到“字段级响应式值、何时继续使用 `BindableProperty`、何时切到 `Store`”的当前结构, + 并补充 `BindableProperty.Comparer` 按闭合泛型共享的兼容注意点 +- 重写 `logging.md`,使其回到“`LoggerFactoryResolver` 默认行为、`ArchitectureConfiguration` 日志 provider 配置、 + `IStructuredLogger` / `LogContext`、provider 替换边界”的当前结构 +- 执行 `cd docs && bun run build` 通过,说明本轮 `core` 专题页收口没有破坏文档站构建 + +### 当前结论(RP-005) + +- 本轮计划中的 `core` 剩余高风险页面已完成核对;`state-management` 与 `coroutine` 经复核后可继续保留 +- `core` 栏目下一步不再需要围绕这五页反复停留,后续重心应转到 `docs/zh-CN/game/*` 与 `docs/zh-CN/source-generators/*` + +### 下一步(RP-005) + +1. 继续核对 `docs/zh-CN/game/*`,优先处理仍引用旧安装方式、旧状态系统或旧 UI / Scene 接法的页面 +2. 再推进 `docs/zh-CN/source-generators/*`,重点核对生成器 wiring、包关系与最小接入示例 +3. 若 active trace 继续累计多个已完成恢复点,按 `archive/traces/` 粒度归档旧阶段细节 diff --git a/docs/zh-CN/core/events.md b/docs/zh-CN/core/events.md index 8edac1ec..3988741d 100644 --- a/docs/zh-CN/core/events.md +++ b/docs/zh-CN/core/events.md @@ -1,602 +1,130 @@ -# Events 包使用说明 +# Events -## 概述 +`GFramework.Core.Events` 是架构内的轻量广播层。它适合表达“某件事已经发生”的运行时信号、模块间松耦合通知, +以及为旧模块保留 `EventBus` 语义;如果你需要请求/响应、pipeline behavior 或 handler registry,优先使用 +[cqrs](./cqrs.md)。 -Events 包提供了一套完整的事件系统,实现了观察者模式(Observer Pattern)。通过事件系统,可以实现组件间的松耦合通信,支持无参和带参事件、事件注册/注销、以及灵活的事件组合。 +## 安装方式 -事件系统是 GFramework 架构中组件间通信的核心机制,与命令模式和查询模式共同构成了完整的 CQRS 架构。 - -## 核心接口 - -### IEvent - -基础事件接口,定义了事件注册的基本功能。 - -**核心方法:** - -```csharp -IUnRegister Register(Action onEvent); // 注册事件处理函数 +```bash +dotnet add package GeWuYou.GFramework.Core +dotnet add package GeWuYou.GFramework.Core.Abstractions ``` -### IUnRegister +事件实现位于 `GFramework.Core`,抽象接口位于 `GFramework.Core.Abstractions`。 -注销接口,用于取消事件注册。 +## 最常用入口 -**核心方法:** +如果你已经在 `ArchitectureContext` 或任何 `IContextAware` 对象里,最常见的入口仍然是: + +- `SendEvent()` +- `SendEvent(eventData)` +- `RegisterEvent(Action)` +- `UnRegisterEvent(Action)` + +示例: ```csharp -void UnRegister(); // 执行注销操作 -``` +using GFramework.Core.Extensions; +using GFramework.Core.System; -### IUnRegisterList +public sealed record PlayerDiedEvent(int PlayerId); -注销列表接口,用于批量管理注销对象。 - -**属性:** - -```csharp -IList UnregisterList { get; } // 获取注销列表 -``` - -### IEventBus - -事件总线接口,提供基于类型的事件发送和注册。 - -**核心方法:** - -```csharp -IUnRegister Register(Action onEvent); // 注册类型化事件 -void Send(T e); // 发送事件实例 -void Send() where T : new(); // 发送事件(自动创建实例) -void UnRegister(Action onEvent); // 注销事件监听器 -``` - -## 核心类 - -### EasyEvent - -无参事件类,支持注册、注销和触发无参事件。 - -**核心方法:** - -```csharp -IUnRegister Register(Action onEvent); // 注册事件监听器 -void Trigger(); // 触发事件 -``` - -**使用示例:** - -```csharp -// 创建事件 -var onClicked = new EasyEvent(); - -// 注册监听 -var unregister = onClicked.Register(() => +public sealed class CombatSystem : AbstractSystem { - Console.WriteLine("Button clicked!"); -}); + protected override void OnInit() + { + this.RegisterEvent(OnPlayerDied); + } -// 触发事件 -onClicked.Trigger(); + private void OnPlayerDied(PlayerDiedEvent @event) + { + Logger.Info("Player died: {0}", @event.PlayerId); + } -// 取消注册 -unregister.UnRegister(); + public void KillPlayer(int playerId) + { + this.SendEvent(new PlayerDiedEvent(playerId)); + } +} ``` -### Event`` +如果你在架构外单独使用,也可以直接构造 `EventBus`。 -单参数泛型事件类,支持一个参数的事件。 +## EventBus 与 EnhancedEventBus -**核心方法:** +默认实现是 `EventBus`,提供类型化发送与订阅: ```csharp -IUnRegister Register(Action onEvent); // 注册事件监听器 -void Trigger(T eventData); // 触发事件并传递参数 -``` +using GFramework.Core.Events; -**使用示例:** - -```csharp -// 创建带参数的事件 -var onScoreChanged = new Event(); - -// 注册监听 -onScoreChanged.Register(newScore => -{ - Console.WriteLine($"Score changed to: {newScore}"); -}); - -// 触发事件并传递参数 -onScoreChanged.Trigger(100); -``` - -### Event - -双参数泛型事件类。 - -**核心方法:** - -```csharp -IUnRegister Register(Action onEvent); // 注册事件监听器 -void Trigger(T param1, TK param2); // 触发事件并传递两个参数 -``` - -**使用示例:** - -```csharp -// 伤害事件:攻击者、伤害值 -var onDamageDealt = new Event(); - -onDamageDealt.Register((attacker, damage) => -{ - Console.WriteLine($"{attacker} dealt {damage} damage!"); -}); - -onDamageDealt.Trigger("Player", 50); -``` - -### EasyEvents - -全局事件管理器,提供类型安全的事件注册和获取。 - -**核心方法:** - -```csharp -static void Register() where T : IEvent, new(); // 注册事件类型 -static T Get() where T : IEvent, new(); // 获取事件实例 -static T GetOrAddEvent() where T : IEvent, new(); // 获取或创建事件实例 -``` - -**使用示例:** - -```csharp -// 注册全局事件类型 -EasyEvents.Register(); - -// 获取事件实例 -var gameStartEvent = EasyEvents.Get(); - -// 注册监听 -gameStartEvent.Register(() => -{ - Console.WriteLine("Game started!"); -}); - -// 触发事件 -gameStartEvent.Trigger(); -``` - -### EventBus - -类型化事件系统,支持基于类型的事件发送和注册。这是架构中默认的事件总线实现。 - -**核心方法:** - -```csharp -IUnRegister Register(Action onEvent); // 注册类型化事件 -void Send(T e); // 发送事件实例 -void Send() where T : new(); // 发送事件(自动创建实例) -void UnRegister(Action onEvent); // 注销事件监听器 -``` - -**使用示例:** - -```csharp -// 使用全局事件系统 var eventBus = new EventBus(); -// 注册类型化事件 -eventBus.Register(e => +eventBus.Register(e => { - Console.WriteLine($"Player died at position: {e.Position}"); + Console.WriteLine(e.Name); }); -// 发送事件(传递实例) -eventBus.Send(new PlayerDiedEvent -{ - Position = new Vector3(10, 0, 5) -}); - -// 发送事件(自动创建实例) -eventBus.Send(); - -// 注销事件监听器 -eventBus.UnRegister(OnPlayerDied); +eventBus.Send(new PlayerJoinedEvent("Alice")); ``` -### DefaultUnRegister +如果你还需要统计、过滤或弱引用订阅,可以改用 `EnhancedEventBus`。它在 `EventBus` 基础上额外提供: -默认注销器实现,封装注销回调。 +- `Statistics` +- `SendFilterable(...)` / `RegisterFilterable(...)` +- `SendWeak(...)` / `RegisterWeak(...)` -**使用示例:** +这类能力更适合工具层、编辑器层或长生命周期对象,不必默认扩散到每个业务事件。 + +## 优先级、传播与上下文事件 + +当事件处理顺序或“是否继续传播”本身就是语义的一部分时,使用优先级入口: ```csharp -Action onUnregister = () => Console.WriteLine("Unregistered"); -var unregister = new DefaultUnRegister(onUnregister); +using GFramework.Core.Abstractions.Events; +using GFramework.Core.Events; -// 执行注销 -unregister.UnRegister(); -``` +public sealed record InputCommand(string Name); -### OrEvent +var eventBus = new EventBus(); -事件或运算组合器,当任意一个事件触发时触发。 - -**核心方法:** - -```csharp -OrEvent Or(IEvent @event); // 添加要组合的事件 -``` - -**使用示例:** - -```csharp -var onAnyInput = new OrEvent() - .Or(onKeyPressed) - .Or(onMouseClicked) - .Or(onTouchDetected); - -// 当上述任意事件触发时,执行回调 -onAnyInput.Register(() => +eventBus.RegisterWithContext(ctx => { - Console.WriteLine("Input detected!"); -}); -``` - -### UnRegisterList - -批量管理注销对象的列表。 - -**核心方法:** - -```csharp -void Add(IUnRegister unRegister); // 添加注销器到列表 -void UnRegisterAll(); // 批量注销所有事件 -``` - -**使用示例:** - -```csharp -var unregisterList = new UnRegisterList(); - -// 添加到列表 -someEvent.Register(OnEvent).AddToUnregisterList(unregisterList); - -// 批量注销 -unregisterList.UnRegisterAll(); -``` - -### ArchitectureEvents - -定义了架构生命周期相关的事件。 - -**包含事件:** - -- `ArchitectureLifecycleReadyEvent` - 架构生命周期准备就绪 -- `ArchitectureDestroyingEvent` - 架构销毁中 -- `ArchitectureDestroyedEvent` - 架构已销毁 -- `ArchitectureFailedInitializationEvent` - 架构初始化失败 - -## 在架构中使用事件 - -### 定义事件类 - -```csharp -// 简单事件 -public struct GameStartedEvent { } - -// 带数据的事件 -public struct PlayerDiedEvent -{ - public Vector3 Position; - public string Cause; -} - -// 复杂事件 -public struct LevelCompletedEvent -{ - public int LevelId; - public float CompletionTime; - public int Score; - public List Achievements; -} -``` - -### Model 中发送事件 - -```csharp -public class PlayerModel : AbstractModel -{ - public BindableProperty Health { get; } = new(100); - - protected override void OnInit() + if (ctx.Data.Name == "Pause") { - // 监听生命值变化 - Health.Register(newHealth => - { - if (newHealth <= 0) - { - // 发送玩家死亡事件 - this.SendEvent(new PlayerDiedEvent - { - Position = Position, - Cause = "Health depleted" - }); - } - }); + Console.WriteLine("Pause handled"); + ctx.MarkAsHandled(); } -} +}, priority: 10); + +eventBus.Send(new InputCommand("Pause"), EventPropagation.UntilHandled); ``` -### System 中发送事件 +当前公开语义是: -```csharp -public class CombatSystem : AbstractSystem -{ - protected override void OnInit() { } - - public void DealDamage(Character attacker, Character target, int damage) - { - target.Health -= damage; - - // 发送伤害事件 - this.SendEvent(new DamageDealtEvent - { - Attacker = attacker.Name, - Target = target.Name, - Damage = damage - }); - } -} -``` +- `Register(handler, priority)`:按优先级订阅 +- `RegisterWithContext(...)`:拿到 `EventContext` +- `EventPropagation.All`:广播给全部监听器 +- `EventPropagation.UntilHandled`:直到上下文事件被标记为 handled +- `EventPropagation.Highest`:只执行最高优先级层 -### Controller 中注册事件 +## 局部事件对象 -```csharp -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Rule; +如果事件只在一个对象或一个小模块内部流动,不必一定挂到 `EventBus`。当前仍可直接使用: -[ContextAware] -public partial class GameController : IController -{ - private IUnRegisterList _unregisterList = new UnRegisterList(); +- `EasyEvent` +- `Event` +- `Event` +- `OrEvent` +- `EventListenerScope` - public void Initialize() - { - // 注册多个事件 - this.RegisterEvent(OnGameStarted) - .AddToUnregisterList(_unregisterList); +这类类型更适合局部组合和 UI/工具层内聚逻辑,不适合作为全局消息总线的替代品。 - this.RegisterEvent(OnPlayerDied) - .AddToUnregisterList(_unregisterList); +## 与 Store / CQRS 的边界 - this.RegisterEvent(OnLevelCompleted) - .AddToUnregisterList(_unregisterList); - } +- 轻量运行时广播:`EventBus` +- 聚合状态演进:`Store`,必要时用 `BridgeToEventBus(...)` 兼容旧事件消费者 +- 新业务请求模型:`GFramework.Cqrs` - private void OnGameStarted(GameStartedEvent e) - { - Console.WriteLine("Game started!"); - } - - private void OnPlayerDied(PlayerDiedEvent e) - { - Console.WriteLine($"Player died at {e.Position}: {e.Cause}"); - ShowGameOverScreen(); - } - - private void OnLevelCompleted(LevelCompletedEvent e) - { - Console.WriteLine($"Level {e.LevelId} completed! Score: {e.Score}"); - ShowVictoryScreen(e); - } - - public void Cleanup() - { - _unregisterList.UnRegisterAll(); - } -} -``` - -## 高级用法 - -### 1. 事件链式组合 - -```csharp -// 使用 Or 组合多个事件 -var onAnyDamage = new OrEvent() - .Or(onPhysicalDamage) - .Or(onMagicDamage) - .Or(onPoisonDamage); - -onAnyDamage.Register(() => -{ - PlayDamageSound(); -}); -``` - -### 2. 事件过滤 - -```csharp -// 只处理高伤害事件 -this.RegisterEvent(e => -{ - if (e.Damage >= 50) - { - ShowCriticalHitEffect(); - } -}); -``` - -### 3. 事件转发 - -```csharp -public class EventBridge : AbstractSystem -{ - protected override void OnInit() - { - // 将内部事件转发为公共事件 - this.RegisterEvent(e => - { - this.SendEvent(new PublicPlayerDiedEvent - { - PlayerId = e.Id, - Timestamp = DateTime.UtcNow - }); - }); - } -} -``` - -### 4. 临时事件监听 - -```csharp -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[ContextAware] -public partial class TutorialController : IController -{ - public void Initialize() - { - // 只监听一次 - IUnRegister unregister = null; - unregister = this.RegisterEvent(e => - { - ShowTutorialComplete(); - unregister?.UnRegister(); // 立即注销 - }); - } -} -``` - -### 5. 条件事件 - -```csharp -public class AchievementSystem : AbstractSystem -{ - private int _killCount = 0; - - protected override void OnInit() - { - this.RegisterEvent(e => - { - _killCount++; - - // 条件满足时发送成就事件 - if (_killCount >= 100) - { - this.SendEvent(new AchievementUnlockedEvent - { - AchievementId = "kill_100_enemies" - }); - } - }); - } -} -``` - -## 生命周期管理 - -### 使用 UnRegisterList - -```csharp -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[ContextAware] -public partial class MyController : IController -{ - // 统一管理所有注销对象 - private IUnRegisterList _unregisterList = new UnRegisterList(); - - public void Initialize() - { - // 所有注册都添加到列表 - this.RegisterEvent(OnEvent1) - .AddToUnregisterList(_unregisterList); - - this.RegisterEvent(OnEvent2) - .AddToUnregisterList(_unregisterList); - } - - public void Cleanup() - { - // 一次性注销所有 - _unregisterList.UnRegisterAll(); - } -} -``` - -## 最佳实践 - -1. **事件命名规范** - - 使用过去式:`PlayerDiedEvent`、`LevelCompletedEvent` - - 使用 `Event` 后缀:便于识别 - - 使用结构体:减少内存分配 - -2. **事件数据设计** - - 只包含必要信息 - - 使用值类型(struct)提高性能 - - 避免传递可变引用 - -3. **避免事件循环** - - 事件处理器中谨慎发送新事件 - - 使用命令打破循环依赖 - -4. **合理使用事件** - - 用于通知状态变化 - - 用于跨模块通信 - - 不用于返回数据(使用 Query) - -5. **注销管理** - - 始终注销事件监听 - - 使用 `IUnRegisterList` 批量管理 - - 在适当的生命周期点调用 `Cleanup()` - -6. **性能考虑** - - 避免频繁触发的事件(如每帧) - - 事件处理器保持轻量 - - 使用结构体事件减少 GC - -7. **事件设计原则** - - 高内聚:事件应该代表一个完整的业务概念 - - 低耦合:事件发送者不需要知道接收者 - - 可测试:事件应该易于模拟和测试 - -## 事件 vs 其他通信方式 - -| 方式 | 适用场景 | 优点 | 缺点 | -|----------------------|--------------|-----------|---------| -| **Event** | 状态变化通知、跨模块通信 | 松耦合、一对多 | 难以追踪调用链 | -| **Command** | 执行操作、修改状态 | 封装逻辑、可撤销 | 单向通信 | -| **Query** | 查询数据 | 职责清晰、有返回值 | 同步调用 | -| **BindableProperty** | UI 数据绑定 | 自动更新、响应式 | 仅限单一属性 | - -## 事件系统架构 - -事件系统在 GFramework 中的架构位置: - -``` -Architecture (架构核心) -├── EventBus (事件总线) -├── CommandBus (命令总线) -├── QueryBus (查询总线) -└── IocContainer (IoC容器) - -Components (组件) -├── Model (发送事件) -├── System (发送/接收事件) -└── Controller (接收事件) -``` - -## 相关包 - -- [`architecture`](./architecture.md) - 提供全局事件系统 -- [`extensions`](./extensions.md) - 提供事件扩展方法 -- [`property`](./property.md) - 可绑定属性基于事件实现 -- **Controller** - 控制器监听事件(接口定义在 Core.Abstractions 中) -- [`model`](./model.md) - 模型发送事件 -- [`system`](./system.md) - 系统发送和监听事件 -- [`command`](./command.md) - 与事件配合实现 CQRS -- [`query`](./query.md) - 与事件配合实现 CQRS \ No newline at end of file +一个简单判断规则是:如果你关心“谁来处理、是否有返回值、是否要挂 pipeline”,用 CQRS;如果你只是广播 +“这件事发生了”,事件系统更直接。 diff --git a/docs/zh-CN/core/logging.md b/docs/zh-CN/core/logging.md index faff9bfd..914a155d 100644 --- a/docs/zh-CN/core/logging.md +++ b/docs/zh-CN/core/logging.md @@ -1,364 +1,86 @@ -# Logging 包使用说明 +# Logging -## 概述 +`GFramework.Core.Logging` 是 Core runtime 的默认日志实现。只加载抽象层时,`LoggerFactoryResolver` 会退回 +silent provider;加载 `GFramework.Core` 或在 `ArchitectureConfiguration` 里显式提供 provider 后,日志才会 +真正输出。 -Logging 包提供了灵活的日志系统,支持多级别日志记录。默认日志级别为 `Info`,确保框架的关键操作都能被记录下来。 - -## 核心接口 - -### ILogger - -日志记录器接口,定义了日志记录的基本功能。 - -**核心方法:** +## 最小用法 ```csharp -// 日志级别检查 -bool IsTraceEnabled(); -bool IsDebugEnabled(); -bool IsInfoEnabled(); -bool IsWarnEnabled(); -bool IsErrorEnabled(); -bool IsFatalEnabled(); +using GFramework.Core.Abstractions.Logging; -// 记录日志 -void Trace(string msg); -void Trace(string format, object arg); -void Trace(string format, object arg1, object arg2); -void Trace(string format, params object[] arguments); -void Trace(string msg, Exception t); +var logger = LoggerFactoryResolver.Provider.CreateLogger("Bootstrap"); -void Debug(string msg); -void Debug(string format, object arg); -void Debug(string format, object arg1, object arg2); -void Debug(string format, params object[] arguments); -void Debug(string msg, Exception t); - -void Info(string msg); -void Info(string format, object arg); -void Info(string format, object arg1, object arg2); -void Info(string format, params object[] arguments); -void Info(string msg, Exception t); - -void Warn(string msg); -void Warn(string format, object arg); -void Warn(string format, object arg1, object arg2); -void Warn(string format, params object[] arguments); -void Warn(string msg, Exception t); - -void Error(string msg); -void Error(string format, object arg); -void Error(string format, object arg1, object arg2); -void Error(string format, params object[] arguments); -void Error(string msg, Exception t); - -void Fatal(string msg); -void Fatal(string format, object arg); -void Fatal(string format, object arg1, object arg2); -void Fatal(string format, params object[] arguments); -void Fatal(string msg, Exception t); - -// 获取日志器名称 -string Name(); +logger.Info("Application started"); +logger.Warn("Config file missing"); ``` -### ILoggerFactory +默认 `ArchitectureConfiguration` 会把 provider 配成 `ConsoleLoggerFactoryProvider`,最小级别是 `Info`。如果你 +直接走标准 `Architecture` 启动路径,这条配置会自动生效。 -日志工厂接口,用于创建日志记录器实例。 - -**核心方法:** +## 在 Architecture 中调整日志级别 ```csharp -ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info); -``` +using GFramework.Core.Architectures; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Abstractions.Properties; +using GFramework.Core.Logging; -### ILoggerFactoryProvider - -日志工厂提供程序接口,用于获取日志工厂。 - -**核心方法:** - -```csharp -ILoggerFactory GetLoggerFactory(); -ILogger CreateLogger(string name); -``` - -### LogLevel - -日志级别枚举。 - -```csharp -public enum LogLevel +var configuration = new ArchitectureConfiguration { - Trace = 0, // 最详细的跟踪信息 - Debug = 1, // 调试信息 - Info = 2, // 一般信息(默认级别) - Warning = 3, // 警告信息 - Error = 4, // 错误信息 - Fatal = 5 // 致命错误 -} -``` - -## 核心类 - -### AbstractLogger - -抽象日志基类,封装了日志级别判断、格式化与异常处理逻辑。平台日志器只需实现 `Write` 方法即可。 - -**使用示例:** - -```csharp -public class CustomLogger : AbstractLogger -{ - public CustomLogger(string? name = null, LogLevel minLevel = LogLevel.Info) - : base(name, minLevel) + LoggerProperties = new LoggerProperties { - } - - protected override void Write(LogLevel level, string message, Exception? exception) - { - // 自定义日志输出逻辑 - var logMessage = $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] [{level}] {message}"; - if (exception != null) - logMessage += $"\n{exception}"; - - Console.WriteLine(logMessage); - } -} -``` - -### ConsoleLogger - -控制台日志记录器实现,支持彩色输出。 - -**使用示例:** - -```csharp -// 创建控制台日志记录器 -var logger = new ConsoleLogger("MyLogger", LogLevel.Debug); - -// 记录不同级别的日志 -logger.Info("应用程序启动"); -logger.Debug("调试信息"); -logger.Warn("警告信息"); -logger.Error("错误信息"); -logger.Fatal("致命错误"); -``` - -**输出格式:** - -``` -[2025-01-09 01:40:00.000] INFO [MyLogger] 应用程序启动 -[2025-01-09 01:40:01.000] DEBUG [MyLogger] 调试信息 -[2025-01-09 01:40:02.000] WARN [MyLogger] 警告信息 -``` - -**日志级别颜色:** - -- **Trace**: 深灰色 -- **Debug**: 青色 -- **Info**: 白色 -- **Warning**: 黄色 -- **Error**: 红色 -- **Fatal**: 洋红色 - -**构造函数参数:** - -- `name`:日志器名称,默认为 "ROOT" -- `minLevel`:最低日志级别,默认为 LogLevel.Info -- `writer`:TextWriter 输出流,默认为 Console.Out -- `useColors`:是否使用颜色,默认为 true(仅在输出到控制台时生效) - -### ConsoleLoggerFactory - -控制台日志工厂,用于创建控制台日志记录器实例。 - -**使用示例:** - -```csharp -var factory = new ConsoleLoggerFactory(); -var logger = factory.GetLogger("MyModule", LogLevel.Debug); -logger.Info("日志记录器创建成功"); -``` - -### ConsoleLoggerFactoryProvider - -控制台日志工厂提供程序实现。 - -**使用示例:** - -```csharp -var provider = new ConsoleLoggerFactoryProvider(); -provider.MinLevel = LogLevel.Debug; // 设置最低日志级别 -var logger = provider.CreateLogger("MyApp"); -logger.Info("应用程序启动"); -``` - -### LoggerFactoryResolver - -日志工厂提供程序解析器,用于管理和提供日志工厂提供程序实例。 - -**使用示例:** - -```csharp -// 设置日志工厂提供程序 -LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider(); - -// 设置最小日志级别 -LoggerFactoryResolver.MinLevel = LogLevel.Debug; - -// 获取日志记录器 -var logger = LoggerFactoryResolver.Provider.CreateLogger("MyApp"); -logger.Info("应用程序启动"); -``` - -## 在架构中使用日志 - -### 1. 在 Architecture 中使用 - -```csharp -public class GameArchitecture : Architecture -{ - protected override void Init() - { - var logger = LoggerFactoryResolver.Provider.CreateLogger("GameArchitecture"); - logger.Info("游戏架构初始化开始"); - - RegisterModel(new PlayerModel()); - RegisterSystem(new GameSystem()); - - logger.Info("游戏架构初始化完成"); - } -} -``` - -### 2. 在 System 中使用 - -```csharp -public class CombatSystem : AbstractSystem -{ - protected override void OnInit() - { - var logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem"); - logger.Info("战斗系统初始化完成"); - } - - protected override void OnDestroy() - { - var logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem"); - logger.Info("战斗系统已销毁"); - } -} -``` - -### 3. 在 Model 中使用 - -```csharp -public class PlayerModel : AbstractModel -{ - protected override void OnInit() - { - var logger = LoggerFactoryResolver.Provider.CreateLogger("PlayerModel"); - logger.Info("玩家模型初始化完成"); - } -} -``` - -### 4. 自定义日志级别 - -``` -public class DebugLogger : AbstractLogger -{ - public DebugLogger() : base("Debug", LogLevel.Debug) - { - } - - protected override void Write(LogLevel level, string message, Exception? exception) - { - // 只输出调试及更高级别的日志 - if (level >= LogLevel.Debug) + LoggerFactoryProvider = new ConsoleLoggerFactoryProvider { - Console.WriteLine($"[{level}] {message}"); - if (exception != null) - Console.WriteLine(exception); + MinLevel = LogLevel.Debug } } +}; +``` + +如果你只是想减少噪音或临时打开 `Debug`,通常只调 `MinLevel` 就够了。 + +## 结构化日志与上下文 + +默认 Core logger 实现支持 `IStructuredLogger` 和 `LogContext`。当你需要把 `requestId`、`sceneName` 之类的 +上下文随异步流透传时,优先用上下文属性,而不是把所有信息拼进字符串。 + +```csharp +using GFramework.Core.Abstractions.Logging; + +var logger = LoggerFactoryResolver.Provider.CreateLogger("Matchmaking"); + +using (LogContext.Push("RequestId", requestId)) +{ + if (logger is IStructuredLogger structured) + { + structured.Log( + LogLevel.Info, + "Player matched", + ("PlayerId", playerId), + ("RoomId", roomId)); + } } ``` -## 日志级别说明 +## 当前仓库内置的常用实现 -| 级别 | 说明 | 使用场景 | -|-------------|----------|-------------------| -| **Trace** | 最详细的跟踪信息 | 调试复杂的执行流程,记录函数调用等 | -| **Debug** | 调试信息 | 开发阶段,记录变量值、流程分支等 | -| **Info** | 一般信息 | 记录重要的业务流程和系统状态 | -| **Warning** | 警告信息 | 可能的问题但不中断程序执行 | -| **Error** | 错误信息 | 影响功能但不致命的问题 | -| **Fatal** | 致命错误 | 导致程序无法继续运行的严重错误 | +- `ConsoleLoggerFactoryProvider` +- `ConsoleLoggerFactory` +- `CompositeLogger` +- `LoggingConfigurationLoader` -## 最佳实践 +如果你需要文件输出、rolling file、async appender 或 JSON formatter,可以先用 +`LoggingConfigurationLoader` 读取 `LoggingConfiguration`,再把自定义 `ILoggerFactoryProvider` 挂到 +`ArchitectureConfiguration.LoggerProperties.LoggerFactoryProvider` 或 `LoggerFactoryResolver.Provider`。 -1. **使用合适的日志级别**: - - 使用 `Info` 记录重要业务流程 - - 使用 `Debug` 记录调试信息 - - 使用 `Warning` 记录异常情况 - - 使用 `Error` 记录错误但不影响程序运行 - - 使用 `Fatal` 记录严重错误 +## 什么时候该换 provider -2. **提供上下文信息**: - ```csharp - logger.Info($"用户登录成功: UserId={userId}, UserName={userName}"); - ``` +下面这些场景通常不该只靠改 `MinLevel`: -3. **异常日志记录**: - ```csharp - try - { - // 业务逻辑 - } - catch (Exception ex) - { - logger.Error("数据库操作失败", ex); - } - ``` +- 需要文件输出、rolling file 或 async appender +- 需要按 namespace / level 做过滤 +- 需要 JSON 格式日志 +- 需要组合多个 appender -4. **分类使用日志**: - ```csharp - var dbLogger = LoggerFactoryResolver.Provider.CreateLogger("Database"); - var netLogger = LoggerFactoryResolver.Provider.CreateLogger("Network"); - - dbLogger.Info("查询用户数据"); - netLogger.Debug("发送HTTP请求"); - ``` - -5. **在框架组件中合理使用日志**: - ```csharp - // 在系统初始化时记录 - var logger = LoggerFactoryResolver.Provider.CreateLogger("System"); - logger.Info("系统初始化完成"); - ``` - -## 注意事项 - -1. **日志级别检查**: - - 每个日志方法都会自动检查日志级别 - - 如果当前级别低于最小级别,不会输出日志 - -2. **格式化参数**: - - 支持字符串格式化参数 - - 支持异常信息传递 - -3. **ConsoleLogger 的额外参数**: - - ConsoleLogger 现在支持自定义TextWriter输出流 - - 支持禁用颜色输出的功能(useColors参数) - -## 相关包 - -- [architecture](./architecture.md) - 架构核心,使用日志系统记录生命周期事件 -- [property](./property.md) - 可绑定属性基于事件系统实现 -- [extensions](./extensions.md) - 提供便捷的扩展方法 - ---- - -**许可证**: Apache 2.0 \ No newline at end of file +这时更合理的做法是保留 `ILogger` 调用面不变,只替换 provider / factory / formatter / appender 组合。 diff --git a/docs/zh-CN/core/property.md b/docs/zh-CN/core/property.md index caf17976..ab7d6451 100644 --- a/docs/zh-CN/core/property.md +++ b/docs/zh-CN/core/property.md @@ -1,477 +1,97 @@ -# Property 包使用说明 +# Property -## 概述 +`GFramework.Core.Property` 负责字段级响应式值。它最适合“一个字段变化就足以驱动视图或局部业务逻辑”的场景; +如果你的状态已经是聚合状态树、需要 reducer / middleware / history,再切到 +[state-management](./state-management.md)。 -Property 包提供了可绑定属性(BindableProperty)的实现,支持属性值的监听和响应式编程。这是实现数据绑定和响应式编程的核心组件。 +## 安装方式 -BindableProperty 是 GFramework 中 Model 层数据管理的基础,通过事件机制实现属性变化的通知。 - -> 对于简单字段和局部 UI 绑定,`BindableProperty` 仍然是首选方案。 -> 如果你需要统一管理复杂状态树、通过 action / reducer 演进状态,或复用局部状态选择器, -> 请同时参考 [`state-management`](./state-management)。 - -## 核心接口 - -### IReadonlyBindableProperty`` - -只读可绑定属性接口,提供属性值的读取和变更监听功能。 - -**核心成员:** - -```csharp -// 获取属性值 -T Value { get; } - -// 注册监听(不立即触发回调) -IUnRegister Register(Action onValueChanged); - -// 注册监听并立即触发回调传递当前值 -IUnRegister RegisterWithInitValue(Action action); - -// 取消监听 -void UnRegister(Action onValueChanged); +```bash +dotnet add package GeWuYou.GFramework.Core +dotnet add package GeWuYou.GFramework.Core.Abstractions ``` -### IBindableProperty`` +## 最常用类型 -可绑定属性接口,继承自只读接口,增加了修改能力。 +当前最常见的公开类型是: -**核心成员:** +- `IReadonlyBindableProperty` +- `IBindableProperty` +- `BindableProperty` + +一般做法是:内部持有 `BindableProperty`,对外只暴露 `IReadonlyBindableProperty`。 + +## 最小示例 ```csharp -// 可读写的属性值 -new T Value { get; set; } +using GFramework.Core.Property; +using GFramework.Core.Abstractions.Property; +using GFramework.Core.Model; -// 设置值但不触发事件 -void SetValueWithoutEvent(T newValue); -``` - -## 核心类 - -### BindableProperty`` - -可绑定属性的完整实现。 - -**核心方法:** - -```csharp -// 构造函数 -BindableProperty(T defaultValue = default!); - -// 属性值 -T Value { get; set; } - -// 注册监听 -IUnRegister Register(Action onValueChanged); -IUnRegister RegisterWithInitValue(Action action); - -// 取消监听 -void UnRegister(Action onValueChanged); - -// 设置值但不触发事件 -void SetValueWithoutEvent(T newValue); - -// 设置自定义比较器 -BindableProperty WithComparer(Func comparer); -``` - -**使用示例:** - -```csharp -// 创建可绑定属性 -var health = new BindableProperty(100); - -// 监听值变化(不会立即触发) -var unregister = health.Register(newValue => +public sealed class PlayerModel : AbstractModel { - Console.WriteLine($"Health changed to: {newValue}"); -}); - -// 设置值(会触发监听器) -health.Value = 50; // 输出: Health changed to: 50 - -// 取消监听 -unregister.UnRegister(); - -// 设置值但不触发事件 -health.SetValueWithoutEvent(75); -``` - -**高级功能:** - -```csharp -// 1. 注册并立即获得当前值 -health.RegisterWithInitValue(value => -{ - Console.WriteLine($"Current health: {value}"); // 立即输出当前值 - // 后续值变化时也会调用 -}); - -// 2. 自定义比较器(静态方法) -BindableProperty.Comparer = (a, b) => Math.Abs(a - b) < 1; - -// 3. 使用实例方法设置比较器 -var position = new BindableProperty(Vector3.Zero) - .WithComparer((a, b) => a.DistanceTo(b) < 0.01f); // 距离小于0.01认为相等 - -// 4. 字符串比较器示例 -var name = new BindableProperty("Player") - .WithComparer((a, b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase)); -``` - -### BindablePropertyUnRegister`` - -可绑定属性的注销器,负责清理监听。 - -**使用示例:** - -```csharp -var unregister = health.Register(OnHealthChanged); -// 当需要取消监听时 -unregister.UnRegister(); -``` - -## BindableProperty 工作原理 - -BindableProperty 基于事件系统实现属性变化通知: - -1. **值设置**:当设置 `Value` 属性时,首先进行值比较 -2. **变化检测**:使用 `EqualityComparer.Default` 或自定义比较器检测值变化 -3. **事件触发**:如果值发生变化,调用所有注册的回调函数 -4. **内存管理**:通过 `IUnRegister` 机制管理监听器的生命周期 - -## 在 Model 中使用 - -### 什么时候继续使用 BindableProperty - -以下场景仍然优先推荐 `BindableProperty`: - -- 单个字段变化就能驱动视图更新 -- 状态范围局限在单个 Model 内 -- 不需要统一的 action / reducer 写入入口 -- 不需要从聚合状态树中复用局部选择逻辑 - -如果你的状态已经演化为“多个字段必须一起更新”或“多个模块共享同一聚合状态”, -可以在 Model 内部组合 `Store`,而不是把所有字段都继续拆成独立属性。 - -### 与 Store / StateMachine 的边界 - -- `BindableProperty`:字段级响应式值 -- `Store`:聚合状态容器,负责统一归约状态变化 -- `StateMachine`:流程状态切换,不负责数据状态归约 - -一个复杂 Model 可以同时持有 Store 和 BindableProperty: - -```csharp -public class PlayerStateModel : AbstractModel -{ - public Store Store { get; } = new(new PlayerState(100, "Player")); - public BindableProperty IsDirty { get; } = new(false); - - protected override void OnInit() - { - Store.RegisterReducer((state, action) => - state with { Health = Math.Max(0, state.Health - action.Amount) }); - } -} - -public sealed record PlayerState(int Health, string Name); -public sealed record DamageAction(int Amount); -``` - -### 定义可绑定属性 - -```csharp -public class PlayerModel : AbstractModel -{ - // 可读写属性 - public BindableProperty Name { get; } = new("Player"); - public BindableProperty Level { get; } = new(1); public BindableProperty Health { get; } = new(100); - public BindableProperty MaxHealth { get; } = new(100); - public BindableProperty Position { get; } = new(Vector3.Zero); - - // 只读属性(外部只能读取和监听) + public IReadonlyBindableProperty ReadonlyHealth => Health; - - protected override void OnInit() + + public void Damage(int amount) { - // 内部监听属性变化 - Health.Register(hp => - { - if (hp <= 0) - { - this.SendEvent(new PlayerDiedEvent()); - } - else if (hp < MaxHealth.Value * 0.3f) - { - this.SendEvent(new LowHealthWarningEvent()); - } - }); - - // 监听等级变化 - Level.Register(newLevel => - { - this.SendEvent(new PlayerLevelUpEvent { NewLevel = newLevel }); - }); - } - - // 业务方法 - public void TakeDamage(int damage) - { - Health.Value = Math.Max(0, Health.Value - damage); - } - - public void Heal(int amount) - { - Health.Value = Math.Min(MaxHealth.Value, Health.Value + amount); - } - - public float GetHealthPercentage() - { - return (float)Health.Value / MaxHealth.Value; + Health.Value = Math.Max(0, Health.Value - amount); } } ``` -## 在 Controller 中监听 - -### UI 数据绑定 +监听方式: ```csharp -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[ContextAware] -public partial class PlayerUI : Control, IController +var unRegister = playerModel.ReadonlyHealth.RegisterWithInitValue(health => { - [Export] private Label _healthLabel; - [Export] private Label _nameLabel; - [Export] private ProgressBar _healthBar; - - private IUnRegisterList _unregisterList = new UnRegisterList(); - - public override void _Ready() - { - var playerModel = this.GetModel(); - - // 绑定生命值到UI(立即显示当前值) - playerModel.Health - .RegisterWithInitValue(health => - { - _healthLabel.Text = $"HP: {health}/{playerModel.MaxHealth.Value}"; - _healthBar.Value = (float)health / playerModel.MaxHealth.Value * 100; - }) - .AddToUnregisterList(_unregisterList); - - // 绑定最大生命值 - playerModel.MaxHealth - .RegisterWithInitValue(maxHealth => - { - _healthBar.MaxValue = maxHealth; - }) - .AddToUnregisterList(_unregisterList); - - // 绑定名称 - playerModel.Name - .RegisterWithInitValue(name => - { - _nameLabel.Text = name; - }) - .AddToUnregisterList(_unregisterList); - - // 绑定位置(仅用于调试显示) - playerModel.Position - .RegisterWithInitValue(pos => - { - // 仅在调试模式下显示 - #if DEBUG - Console.WriteLine($"Player position: {pos}"); - #endif - }) - .AddToUnregisterList(_unregisterList); - } - - public override void _ExitTree() - { - _unregisterList.UnRegisterAll(); - } -} + Console.WriteLine($"Current HP: {health}"); +}); ``` -## 常见使用模式 +## 当前公开语义 -### 1. 双向绑定 +- `Value` + - 读写当前值;只有值被判定为“真的变化”时才会触发回调 +- `Register(...)` + - 订阅后续变化,不会立即回放当前值 +- `RegisterWithInitValue(...)` + - 先回放当前值,再继续订阅 +- `SetValueWithoutEvent(...)` + - 更新值但不触发通知 +- `UnRegister(...)` + - 显式移除某个处理器 +- `WithComparer(...)` + - 改写值变化判定逻辑 -```c# -// Model -public class SettingsModel : AbstractModel -{ - public BindableProperty MasterVolume { get; } = new(1.0f); - protected override void OnInit() { } -} +## 一个需要注意的兼容点 -// UI Controller -[ContextAware] -public partial class VolumeSlider : HSlider, IController -{ - private BindableProperty _volumeProperty; +`BindableProperty.Comparer` 是按闭合泛型 `T` 共享的静态比较器,`WithComparer(...)` 本质上会改写这一共享 +比较器。也就是说,多个 `BindableProperty` 实例会观察到同一比较规则;只有当你确定整个 `T` 族都要共享同一 +判等语义时,再去改它。 - public override void _Ready() - { - _volumeProperty = this.GetModel().MasterVolume; +## 什么时候继续用 Property - // Model -> UI - _volumeProperty.RegisterWithInitValue(vol => Value = vol) - .UnRegisterWhenNodeExitTree(this); +下面这些场景仍然优先使用 `BindableProperty`: - // UI -> Model - ValueChanged += newValue => _volumeProperty.Value = (float)newValue; - } -} -``` +- 单个字段变化就能驱动 UI +- 状态范围局限在单个 Model 或单个页面 +- 不需要统一的 action / reducer 写入口 +- 不需要撤销/重做、历史快照或中间件 -### 2. 计算属性 +## 什么时候该切到 Store -```c# -public class PlayerModel : AbstractModel -{ - public BindableProperty Health { get; } = new(100); - public BindableProperty MaxHealth { get; } = new(100); - public BindableProperty HealthPercent { get; } = new(1.0f); - - protected override void OnInit() - { - // 自动计算百分比 - Action updatePercent = () => - { - HealthPercent.Value = (float)Health.Value / MaxHealth.Value; - }; - - Health.Register(_ => updatePercent()); - MaxHealth.Register(_ => updatePercent()); - - updatePercent(); // 初始计算 - } -} -``` +如果状态已经演化为下面这些形态,更适合用 `Store`: -### 3. 属性验证 +- 多个字段必须作为一个原子状态一起演进 +- 多个模块共享同一聚合状态 +- 需要 reducer / middleware / 历史回放 +- 需要从整棵状态树中复用局部选择逻辑 -```c# -public class PlayerModel : AbstractModel -{ - private BindableProperty _health = new(100); - - public BindableProperty Health - { - get => _health; - set - { - // 限制范围 - var clampedValue = Math.Clamp(value.Value, 0, MaxHealth.Value); - _health.Value = clampedValue; - } - } - - public BindableProperty MaxHealth { get; } = new(100); - - protected override void OnInit() { } -} -``` +迁移时不必一次性抛弃旧绑定风格。当前已经提供: -### 4. 条件监听 +- `store.Select(...)` +- `store.ToBindableProperty(...)` -```c# -using GFramework.Core.Abstractions.Controller; -using GFramework.Core.SourceGenerators.Abstractions.Rule; - -[ContextAware] -public partial class CombatController : Node, IController -{ - public override void _Ready() - { - var playerModel = this.GetModel(); - - // 只在生命值低于30%时显示警告 - playerModel.Health.Register(hp => - { - if (hp < playerModel.MaxHealth.Value * 0.3f) - { - ShowLowHealthWarning(); - } - else - { - HideLowHealthWarning(); - } - }).UnRegisterWhenNodeExitTree(this); - } -} -``` - -## 性能优化 - -### 1. 避免频繁触发 - -```c# -// 使用 SetValueWithoutEvent 批量修改 -public void LoadPlayerData(SaveData data) -{ - // 临时关闭事件 - Health.SetValueWithoutEvent(data.Health); - Mana.SetValueWithoutEvent(data.Mana); - Gold.SetValueWithoutEvent(data.Gold); - - // 最后统一触发一次更新事件 - this.SendEvent(new PlayerDataLoadedEvent()); -} -``` - -### 2. 自定义比较器 - -```c# -// 避免浮点数精度问题导致的频繁触发 -var position = new BindableProperty() - .WithComparer((a, b) => a.DistanceTo(b) < 0.001f); -``` - -## 实现原理 - -### 值变化检测 - -```c# -// 使用 EqualityComparer.Default 进行比较 -if (!EqualityComparer.Default.Equals(value, MValue)) -{ - MValue = value; - _mOnValueChanged?.Invoke(value); -} -``` - -### 事件触发机制 - -```c# -// 当值变化时触发所有注册的回调 -_mOnValueChanged?.Invoke(value); -``` - -## 最佳实践 - -1. **在 Model 中定义属性** - BindableProperty 主要用于 Model 层 -2. **使用只读接口暴露** - 防止外部随意修改 -3. **及时注销监听** - 使用 UnRegisterList 或 UnRegisterWhenNodeExitTree -4. **使用 RegisterWithInitValue** - UI 绑定时立即获取初始值 -5. **避免循环依赖** - 属性监听器中修改其他属性要小心 -6. **使用自定义比较器** - 对于浮点数等需要精度控制的属性 -7. **复杂聚合状态使用 Store** - 当多个字段必须统一演进时,使用 Store 管理聚合状态更清晰 - -## 相关包 - -- [`model`](./model.md) - Model 中大量使用 BindableProperty -- [`events`](./events.md) - BindableProperty 基于事件系统实现 -- [`state-management`](./state-management) - 复杂状态树的集中式管理方案 -- [`extensions`](./extensions.md) - 提供便捷的注销扩展方法 - ---- - -**许可证**: Apache 2.0 +这意味着你可以先把写路径统一到 `Store`,再渐进迁移现有 UI 或 Controller 的读取方式。