mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
Merge pull request #266 from GeWuYou/docs/sdk-update-documentation
docs(core): 收口 Core 事件属性与日志专题页
This commit is contained in:
commit
4d306498b9
@ -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<T>.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/`,避免默认启动入口再次膨胀
|
||||
|
||||
@ -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<T>`、何时切到 `Store<TState>`”的当前结构,
|
||||
并补充 `BindableProperty<T>.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/` 粒度归档旧阶段细节
|
||||
|
||||
@ -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<TEvent>()`
|
||||
- `SendEvent(eventData)`
|
||||
- `RegisterEvent(Action<TEvent>)`
|
||||
- `UnRegisterEvent(Action<TEvent>)`
|
||||
|
||||
示例:
|
||||
|
||||
```csharp
|
||||
void UnRegister(); // 执行注销操作
|
||||
```
|
||||
using GFramework.Core.Extensions;
|
||||
using GFramework.Core.System;
|
||||
|
||||
### IUnRegisterList
|
||||
public sealed record PlayerDiedEvent(int PlayerId);
|
||||
|
||||
注销列表接口,用于批量管理注销对象。
|
||||
|
||||
**属性:**
|
||||
|
||||
```csharp
|
||||
IList<IUnRegister> UnregisterList { get; } // 获取注销列表
|
||||
```
|
||||
|
||||
### IEventBus
|
||||
|
||||
事件总线接口,提供基于类型的事件发送和注册。
|
||||
|
||||
**核心方法:**
|
||||
|
||||
```csharp
|
||||
IUnRegister Register<T>(Action<T> onEvent); // 注册类型化事件
|
||||
void Send<T>(T e); // 发送事件实例
|
||||
void Send<T>() where T : new(); // 发送事件(自动创建实例)
|
||||
void UnRegister<T>(Action<T> 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<PlayerDiedEvent>(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`<T>`
|
||||
如果你在架构外单独使用,也可以直接构造 `EventBus`。
|
||||
|
||||
单参数泛型事件类,支持一个参数的事件。
|
||||
## EventBus 与 EnhancedEventBus
|
||||
|
||||
**核心方法:**
|
||||
默认实现是 `EventBus`,提供类型化发送与订阅:
|
||||
|
||||
```csharp
|
||||
IUnRegister Register(Action<T> onEvent); // 注册事件监听器
|
||||
void Trigger(T eventData); // 触发事件并传递参数
|
||||
```
|
||||
using GFramework.Core.Events;
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```csharp
|
||||
// 创建带参数的事件
|
||||
var onScoreChanged = new Event<int>();
|
||||
|
||||
// 注册监听
|
||||
onScoreChanged.Register(newScore =>
|
||||
{
|
||||
Console.WriteLine($"Score changed to: {newScore}");
|
||||
});
|
||||
|
||||
// 触发事件并传递参数
|
||||
onScoreChanged.Trigger(100);
|
||||
```
|
||||
|
||||
### Event<T, TK>
|
||||
|
||||
双参数泛型事件类。
|
||||
|
||||
**核心方法:**
|
||||
|
||||
```csharp
|
||||
IUnRegister Register(Action<T, TK> onEvent); // 注册事件监听器
|
||||
void Trigger(T param1, TK param2); // 触发事件并传递两个参数
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```csharp
|
||||
// 伤害事件:攻击者、伤害值
|
||||
var onDamageDealt = new Event<string, int>();
|
||||
|
||||
onDamageDealt.Register((attacker, damage) =>
|
||||
{
|
||||
Console.WriteLine($"{attacker} dealt {damage} damage!");
|
||||
});
|
||||
|
||||
onDamageDealt.Trigger("Player", 50);
|
||||
```
|
||||
|
||||
### EasyEvents
|
||||
|
||||
全局事件管理器,提供类型安全的事件注册和获取。
|
||||
|
||||
**核心方法:**
|
||||
|
||||
```csharp
|
||||
static void Register<T>() where T : IEvent, new(); // 注册事件类型
|
||||
static T Get<T>() where T : IEvent, new(); // 获取事件实例
|
||||
static T GetOrAddEvent<T>() where T : IEvent, new(); // 获取或创建事件实例
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```csharp
|
||||
// 注册全局事件类型
|
||||
EasyEvents.Register<GameStartEvent>();
|
||||
|
||||
// 获取事件实例
|
||||
var gameStartEvent = EasyEvents.Get<GameStartEvent>();
|
||||
|
||||
// 注册监听
|
||||
gameStartEvent.Register(() =>
|
||||
{
|
||||
Console.WriteLine("Game started!");
|
||||
});
|
||||
|
||||
// 触发事件
|
||||
gameStartEvent.Trigger();
|
||||
```
|
||||
|
||||
### EventBus
|
||||
|
||||
类型化事件系统,支持基于类型的事件发送和注册。这是架构中默认的事件总线实现。
|
||||
|
||||
**核心方法:**
|
||||
|
||||
```csharp
|
||||
IUnRegister Register<T>(Action<T> onEvent); // 注册类型化事件
|
||||
void Send<T>(T e); // 发送事件实例
|
||||
void Send<T>() where T : new(); // 发送事件(自动创建实例)
|
||||
void UnRegister<T>(Action<T> onEvent); // 注销事件监听器
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```csharp
|
||||
// 使用全局事件系统
|
||||
var eventBus = new EventBus();
|
||||
|
||||
// 注册类型化事件
|
||||
eventBus.Register<PlayerDiedEvent>(e =>
|
||||
eventBus.Register<PlayerJoinedEvent>(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<PlayerDiedEvent>();
|
||||
|
||||
// 注销事件监听器
|
||||
eventBus.UnRegister<PlayerDiedEvent>(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<InputCommand>(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<string> Achievements;
|
||||
}
|
||||
```
|
||||
|
||||
### Model 中发送事件
|
||||
|
||||
```csharp
|
||||
public class PlayerModel : AbstractModel
|
||||
{
|
||||
public BindableProperty<int> 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<T>(handler, priority)`:按优先级订阅
|
||||
- `RegisterWithContext<T>(...)`:拿到 `EventContext<T>`
|
||||
- `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<T>`
|
||||
- `Event<T1, T2>`
|
||||
- `OrEvent`
|
||||
- `EventListenerScope<TEvent>`
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
// 注册多个事件
|
||||
this.RegisterEvent<GameStartedEvent>(OnGameStarted)
|
||||
.AddToUnregisterList(_unregisterList);
|
||||
这类类型更适合局部组合和 UI/工具层内聚逻辑,不适合作为全局消息总线的替代品。
|
||||
|
||||
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied)
|
||||
.AddToUnregisterList(_unregisterList);
|
||||
## 与 Store / CQRS 的边界
|
||||
|
||||
this.RegisterEvent<LevelCompletedEvent>(OnLevelCompleted)
|
||||
.AddToUnregisterList(_unregisterList);
|
||||
}
|
||||
- 轻量运行时广播:`EventBus`
|
||||
- 聚合状态演进:`Store<TState>`,必要时用 `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<DamageDealtEvent>(e =>
|
||||
{
|
||||
if (e.Damage >= 50)
|
||||
{
|
||||
ShowCriticalHitEffect();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 事件转发
|
||||
|
||||
```csharp
|
||||
public class EventBridge : AbstractSystem
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 将内部事件转发为公共事件
|
||||
this.RegisterEvent<InternalPlayerDiedEvent>(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<FirstEnemyKilledEvent>(e =>
|
||||
{
|
||||
ShowTutorialComplete();
|
||||
unregister?.UnRegister(); // 立即注销
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 条件事件
|
||||
|
||||
```csharp
|
||||
public class AchievementSystem : AbstractSystem
|
||||
{
|
||||
private int _killCount = 0;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
this.RegisterEvent<EnemyKilledEvent>(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<Event1>(OnEvent1)
|
||||
.AddToUnregisterList(_unregisterList);
|
||||
|
||||
this.RegisterEvent<Event2>(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
|
||||
一个简单判断规则是:如果你关心“谁来处理、是否有返回值、是否要挂 pipeline”,用 CQRS;如果你只是广播
|
||||
“这件事发生了”,事件系统更直接。
|
||||
|
||||
@ -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
|
||||
这时更合理的做法是保留 `ILogger` 调用面不变,只替换 provider / factory / formatter / appender 组合。
|
||||
|
||||
@ -1,477 +1,97 @@
|
||||
# Property 包使用说明
|
||||
# Property
|
||||
|
||||
## 概述
|
||||
`GFramework.Core.Property` 负责字段级响应式值。它最适合“一个字段变化就足以驱动视图或局部业务逻辑”的场景;
|
||||
如果你的状态已经是聚合状态树、需要 reducer / middleware / history,再切到
|
||||
[state-management](./state-management.md)。
|
||||
|
||||
Property 包提供了可绑定属性(BindableProperty)的实现,支持属性值的监听和响应式编程。这是实现数据绑定和响应式编程的核心组件。
|
||||
## 安装方式
|
||||
|
||||
BindableProperty 是 GFramework 中 Model 层数据管理的基础,通过事件机制实现属性变化的通知。
|
||||
|
||||
> 对于简单字段和局部 UI 绑定,`BindableProperty<T>` 仍然是首选方案。
|
||||
> 如果你需要统一管理复杂状态树、通过 action / reducer 演进状态,或复用局部状态选择器,
|
||||
> 请同时参考 [`state-management`](./state-management)。
|
||||
|
||||
## 核心接口
|
||||
|
||||
### IReadonlyBindableProperty`<T>`
|
||||
|
||||
只读可绑定属性接口,提供属性值的读取和变更监听功能。
|
||||
|
||||
**核心成员:**
|
||||
|
||||
```csharp
|
||||
// 获取属性值
|
||||
T Value { get; }
|
||||
|
||||
// 注册监听(不立即触发回调)
|
||||
IUnRegister Register(Action<T> onValueChanged);
|
||||
|
||||
// 注册监听并立即触发回调传递当前值
|
||||
IUnRegister RegisterWithInitValue(Action<T> action);
|
||||
|
||||
// 取消监听
|
||||
void UnRegister(Action<T> onValueChanged);
|
||||
```bash
|
||||
dotnet add package GeWuYou.GFramework.Core
|
||||
dotnet add package GeWuYou.GFramework.Core.Abstractions
|
||||
```
|
||||
|
||||
### IBindableProperty`<T>`
|
||||
## 最常用类型
|
||||
|
||||
可绑定属性接口,继承自只读接口,增加了修改能力。
|
||||
当前最常见的公开类型是:
|
||||
|
||||
**核心成员:**
|
||||
- `IReadonlyBindableProperty<T>`
|
||||
- `IBindableProperty<T>`
|
||||
- `BindableProperty<T>`
|
||||
|
||||
一般做法是:内部持有 `BindableProperty<T>`,对外只暴露 `IReadonlyBindableProperty<T>`。
|
||||
|
||||
## 最小示例
|
||||
|
||||
```csharp
|
||||
// 可读写的属性值
|
||||
new T Value { get; set; }
|
||||
using GFramework.Core.Property;
|
||||
using GFramework.Core.Abstractions.Property;
|
||||
using GFramework.Core.Model;
|
||||
|
||||
// 设置值但不触发事件
|
||||
void SetValueWithoutEvent(T newValue);
|
||||
```
|
||||
|
||||
## 核心类
|
||||
|
||||
### BindableProperty`<T>`
|
||||
|
||||
可绑定属性的完整实现。
|
||||
|
||||
**核心方法:**
|
||||
|
||||
```csharp
|
||||
// 构造函数
|
||||
BindableProperty(T defaultValue = default!);
|
||||
|
||||
// 属性值
|
||||
T Value { get; set; }
|
||||
|
||||
// 注册监听
|
||||
IUnRegister Register(Action<T> onValueChanged);
|
||||
IUnRegister RegisterWithInitValue(Action<T> action);
|
||||
|
||||
// 取消监听
|
||||
void UnRegister(Action<T> onValueChanged);
|
||||
|
||||
// 设置值但不触发事件
|
||||
void SetValueWithoutEvent(T newValue);
|
||||
|
||||
// 设置自定义比较器
|
||||
BindableProperty<T> WithComparer(Func<T, T, bool> comparer);
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```csharp
|
||||
// 创建可绑定属性
|
||||
var health = new BindableProperty<int>(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<int>.Comparer = (a, b) => Math.Abs(a - b) < 1;
|
||||
|
||||
// 3. 使用实例方法设置比较器
|
||||
var position = new BindableProperty<Vector3>(Vector3.Zero)
|
||||
.WithComparer((a, b) => a.DistanceTo(b) < 0.01f); // 距离小于0.01认为相等
|
||||
|
||||
// 4. 字符串比较器示例
|
||||
var name = new BindableProperty<string>("Player")
|
||||
.WithComparer((a, b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase));
|
||||
```
|
||||
|
||||
### BindablePropertyUnRegister`<T>`
|
||||
|
||||
可绑定属性的注销器,负责清理监听。
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```csharp
|
||||
var unregister = health.Register(OnHealthChanged);
|
||||
// 当需要取消监听时
|
||||
unregister.UnRegister();
|
||||
```
|
||||
|
||||
## BindableProperty 工作原理
|
||||
|
||||
BindableProperty 基于事件系统实现属性变化通知:
|
||||
|
||||
1. **值设置**:当设置 `Value` 属性时,首先进行值比较
|
||||
2. **变化检测**:使用 `EqualityComparer<T>.Default` 或自定义比较器检测值变化
|
||||
3. **事件触发**:如果值发生变化,调用所有注册的回调函数
|
||||
4. **内存管理**:通过 `IUnRegister` 机制管理监听器的生命周期
|
||||
|
||||
## 在 Model 中使用
|
||||
|
||||
### 什么时候继续使用 BindableProperty
|
||||
|
||||
以下场景仍然优先推荐 `BindableProperty<T>`:
|
||||
|
||||
- 单个字段变化就能驱动视图更新
|
||||
- 状态范围局限在单个 Model 内
|
||||
- 不需要统一的 action / reducer 写入入口
|
||||
- 不需要从聚合状态树中复用局部选择逻辑
|
||||
|
||||
如果你的状态已经演化为“多个字段必须一起更新”或“多个模块共享同一聚合状态”,
|
||||
可以在 Model 内部组合 `Store<TState>`,而不是把所有字段都继续拆成独立属性。
|
||||
|
||||
### 与 Store / StateMachine 的边界
|
||||
|
||||
- `BindableProperty<T>`:字段级响应式值
|
||||
- `Store<TState>`:聚合状态容器,负责统一归约状态变化
|
||||
- `StateMachine`:流程状态切换,不负责数据状态归约
|
||||
|
||||
一个复杂 Model 可以同时持有 Store 和 BindableProperty:
|
||||
|
||||
```csharp
|
||||
public class PlayerStateModel : AbstractModel
|
||||
{
|
||||
public Store<PlayerState> Store { get; } = new(new PlayerState(100, "Player"));
|
||||
public BindableProperty<bool> IsDirty { get; } = new(false);
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
Store.RegisterReducer<DamageAction>((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<string> Name { get; } = new("Player");
|
||||
public BindableProperty<int> Level { get; } = new(1);
|
||||
public BindableProperty<int> Health { get; } = new(100);
|
||||
public BindableProperty<int> MaxHealth { get; } = new(100);
|
||||
public BindableProperty<Vector3> Position { get; } = new(Vector3.Zero);
|
||||
|
||||
// 只读属性(外部只能读取和监听)
|
||||
|
||||
public IReadonlyBindableProperty<int> 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<PlayerModel>();
|
||||
|
||||
// 绑定生命值到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<float> MasterVolume { get; } = new(1.0f);
|
||||
protected override void OnInit() { }
|
||||
}
|
||||
## 一个需要注意的兼容点
|
||||
|
||||
// UI Controller
|
||||
[ContextAware]
|
||||
public partial class VolumeSlider : HSlider, IController
|
||||
{
|
||||
private BindableProperty<float> _volumeProperty;
|
||||
`BindableProperty<T>.Comparer` 是按闭合泛型 `T` 共享的静态比较器,`WithComparer(...)` 本质上会改写这一共享
|
||||
比较器。也就是说,多个 `BindableProperty<int>` 实例会观察到同一比较规则;只有当你确定整个 `T` 族都要共享同一
|
||||
判等语义时,再去改它。
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_volumeProperty = this.GetModel<SettingsModel>().MasterVolume;
|
||||
## 什么时候继续用 Property
|
||||
|
||||
// Model -> UI
|
||||
_volumeProperty.RegisterWithInitValue(vol => Value = vol)
|
||||
.UnRegisterWhenNodeExitTree(this);
|
||||
下面这些场景仍然优先使用 `BindableProperty<T>`:
|
||||
|
||||
// UI -> Model
|
||||
ValueChanged += newValue => _volumeProperty.Value = (float)newValue;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 单个字段变化就能驱动 UI
|
||||
- 状态范围局限在单个 Model 或单个页面
|
||||
- 不需要统一的 action / reducer 写入口
|
||||
- 不需要撤销/重做、历史快照或中间件
|
||||
|
||||
### 2. 计算属性
|
||||
## 什么时候该切到 Store
|
||||
|
||||
```c#
|
||||
public class PlayerModel : AbstractModel
|
||||
{
|
||||
public BindableProperty<int> Health { get; } = new(100);
|
||||
public BindableProperty<int> MaxHealth { get; } = new(100);
|
||||
public BindableProperty<float> 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<TState>`:
|
||||
|
||||
### 3. 属性验证
|
||||
- 多个字段必须作为一个原子状态一起演进
|
||||
- 多个模块共享同一聚合状态
|
||||
- 需要 reducer / middleware / 历史回放
|
||||
- 需要从整棵状态树中复用局部选择逻辑
|
||||
|
||||
```c#
|
||||
public class PlayerModel : AbstractModel
|
||||
{
|
||||
private BindableProperty<int> _health = new(100);
|
||||
|
||||
public BindableProperty<int> Health
|
||||
{
|
||||
get => _health;
|
||||
set
|
||||
{
|
||||
// 限制范围
|
||||
var clampedValue = Math.Clamp(value.Value, 0, MaxHealth.Value);
|
||||
_health.Value = clampedValue;
|
||||
}
|
||||
}
|
||||
|
||||
public BindableProperty<int> 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<PlayerModel>();
|
||||
|
||||
// 只在生命值低于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<Vector3>()
|
||||
.WithComparer((a, b) => a.DistanceTo(b) < 0.001f);
|
||||
```
|
||||
|
||||
## 实现原理
|
||||
|
||||
### 值变化检测
|
||||
|
||||
```c#
|
||||
// 使用 EqualityComparer<T>.Default 进行比较
|
||||
if (!EqualityComparer<T>.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<TState>`,再渐进迁移现有 UI 或 Controller 的读取方式。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user