From 5aa11ddc41db0d62975a5726007c634887f84121 Mon Sep 17 00:00:00 2001 From: GwWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:32:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(architecture):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E6=A0=B8=E5=BF=83=E7=BB=84=E4=BB=B6=E4=B8=8E?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E6=A8=A1=E5=BC=8F=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Architecture 基类与 IArchitecture 接口,实现单例模式与组件注册管理 - 集成 IOC 容器支持系统、模型、工具的依赖注入与生命周期管理 - 实现命令模式基础类 AbstractCommand 与接口 ICommand,支持带返回值命令 - 提供事件系统集成,支持事件的发布与订阅机制 - 添加控制器接口 IController,整合命令发送、事件注册与模型获取能力 - 创建详细的 README 文档说明各组件使用方式与设计模式应用 - 支持命令、查询、事件的统一调度与解耦通信机制 --- .gitignore | 5 + GFramework.csproj | 9 + GFramework.sln | 16 + README.md | 4 + framework/README.md | 1108 +++++++++++++++++ framework/architecture/Architecture.cs | 268 ++++ framework/architecture/IArchitecture.cs | 110 ++ framework/architecture/README.md | 169 +++ framework/command/AbstractCommand.cs | 68 + framework/command/ICanSendCommand.cs | 9 + framework/command/ICommand.cs | 39 + framework/command/README.md | 293 +++++ framework/controller/IController.cs | 16 + framework/controller/README.md | 409 ++++++ framework/events/DefaultUnRegister.cs | 21 + framework/events/EasyEvent.cs | 31 + framework/events/EasyEventGeneric.cs | 154 +++ framework/events/EasyEvents.cs | 67 + framework/events/ICanRegisterEvent.cs | 10 + framework/events/ICanSendEvent.cs | 9 + framework/events/IEasyEvent.cs | 14 + framework/events/IUnRegister.cs | 12 + framework/events/IUnRegisterList.cs | 12 + framework/events/OrEvent.cs | 53 + framework/events/README.md | 503 ++++++++ framework/events/TypeEventSystem.cs | 23 + framework/events/UnRegisterList.cs | 39 + framework/extensions/CanGetExtensions.cs | 50 + .../extensions/CanRegisterEventExtensions.cs | 28 + framework/extensions/CanSendExtensions.cs | 76 ++ framework/extensions/OrEventExtensions.cs | 17 + framework/extensions/README.md | 503 ++++++++ framework/extensions/UnRegisterExtension.cs | 26 + .../extensions/UnRegisterListExtension.cs | 34 + framework/ioc/IocContainer.cs | 42 + framework/ioc/README.md | 446 +++++++ framework/model/AbstractModel.cs | 37 + framework/model/ICanGetModel.cs | 9 + framework/model/IModel.cs | 16 + framework/model/README.md | 140 +++ framework/property/BindableProperty.cs | 120 ++ .../property/BindablePropertyUnRegister.cs | 38 + framework/property/IBindableProperty.cs | 19 + .../property/IReadonlyBindableProperty.cs | 37 + framework/property/README.md | 297 +++++ framework/query/AbstractQuery.cs | 36 + framework/query/ICanSendQuery.cs | 9 + framework/query/IQuery.cs | 20 + framework/query/README.md | 395 ++++++ framework/rule/IBelongToArchitecture.cs | 17 + framework/rule/ICanSetArchitecture.cs | 15 + framework/rule/README.md | 329 +++++ framework/system/AbstractSystem.cs | 35 + framework/system/ICanGetSystem.cs | 9 + framework/system/ISystem.cs | 20 + framework/system/README.md | 527 ++++++++ framework/utility/ICanGetUtility.cs | 9 + framework/utility/IUtility.cs | 7 + framework/utility/README.md | 584 +++++++++ 59 files changed, 7418 insertions(+) create mode 100644 .gitignore create mode 100644 GFramework.csproj create mode 100644 GFramework.sln create mode 100644 README.md create mode 100644 framework/README.md create mode 100644 framework/architecture/Architecture.cs create mode 100644 framework/architecture/IArchitecture.cs create mode 100644 framework/architecture/README.md create mode 100644 framework/command/AbstractCommand.cs create mode 100644 framework/command/ICanSendCommand.cs create mode 100644 framework/command/ICommand.cs create mode 100644 framework/command/README.md create mode 100644 framework/controller/IController.cs create mode 100644 framework/controller/README.md create mode 100644 framework/events/DefaultUnRegister.cs create mode 100644 framework/events/EasyEvent.cs create mode 100644 framework/events/EasyEventGeneric.cs create mode 100644 framework/events/EasyEvents.cs create mode 100644 framework/events/ICanRegisterEvent.cs create mode 100644 framework/events/ICanSendEvent.cs create mode 100644 framework/events/IEasyEvent.cs create mode 100644 framework/events/IUnRegister.cs create mode 100644 framework/events/IUnRegisterList.cs create mode 100644 framework/events/OrEvent.cs create mode 100644 framework/events/README.md create mode 100644 framework/events/TypeEventSystem.cs create mode 100644 framework/events/UnRegisterList.cs create mode 100644 framework/extensions/CanGetExtensions.cs create mode 100644 framework/extensions/CanRegisterEventExtensions.cs create mode 100644 framework/extensions/CanSendExtensions.cs create mode 100644 framework/extensions/OrEventExtensions.cs create mode 100644 framework/extensions/README.md create mode 100644 framework/extensions/UnRegisterExtension.cs create mode 100644 framework/extensions/UnRegisterListExtension.cs create mode 100644 framework/ioc/IocContainer.cs create mode 100644 framework/ioc/README.md create mode 100644 framework/model/AbstractModel.cs create mode 100644 framework/model/ICanGetModel.cs create mode 100644 framework/model/IModel.cs create mode 100644 framework/model/README.md create mode 100644 framework/property/BindableProperty.cs create mode 100644 framework/property/BindablePropertyUnRegister.cs create mode 100644 framework/property/IBindableProperty.cs create mode 100644 framework/property/IReadonlyBindableProperty.cs create mode 100644 framework/property/README.md create mode 100644 framework/query/AbstractQuery.cs create mode 100644 framework/query/ICanSendQuery.cs create mode 100644 framework/query/IQuery.cs create mode 100644 framework/query/README.md create mode 100644 framework/rule/IBelongToArchitecture.cs create mode 100644 framework/rule/ICanSetArchitecture.cs create mode 100644 framework/rule/README.md create mode 100644 framework/system/AbstractSystem.cs create mode 100644 framework/system/ICanGetSystem.cs create mode 100644 framework/system/ISystem.cs create mode 100644 framework/system/README.md create mode 100644 framework/utility/ICanGetUtility.cs create mode 100644 framework/utility/IUtility.cs create mode 100644 framework/utility/README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/GFramework.csproj b/GFramework.csproj new file mode 100644 index 0000000..17b910f --- /dev/null +++ b/GFramework.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/GFramework.sln b/GFramework.sln new file mode 100644 index 0000000..1095a82 --- /dev/null +++ b/GFramework.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework", "GFramework.csproj", "{9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BEDDD6C-DF8B-4E71-9C75-F44EC669ABBD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..667ef5f --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# 项目介绍 +本项目参考(CV)自[QFramework](https://github.com/liangxiegame/QFramework) +# 为什么要有这个项目 +- 原来的项目是单文件框架,我把框架拆成多个文件,方便管理 \ No newline at end of file diff --git a/framework/README.md b/framework/README.md new file mode 100644 index 0000000..91fd2a7 --- /dev/null +++ b/framework/README.md @@ -0,0 +1,1108 @@ +# Framework 架构框架 + +> 一个基于 CQRS、MVC 和事件驱动的轻量级游戏开发架构框架,专为 Godot 引擎设计。 + +## 📖 目录 + +- [框架概述](#框架概述) +- [核心概念](#核心概念) +- [架构图](#架构图) +- [快速开始](#快速开始) +- [包说明](#包说明) +- [组件联动](#组件联动) +- [最佳实践](#最佳实践) +- [示例项目](#示例项目) + +## 框架概述 + +本框架是一个专为 Godot C# 游戏开发设计的轻量级架构,它结合了多种经典设计模式: + +- **MVC 架构模式** - 清晰的层次划分 +- **CQRS 模式** - 命令查询职责分离 +- **IoC/DI** - 依赖注入和控制反转 +- **事件驱动** - 松耦合的组件通信 +- **响应式编程** - 可绑定属性和数据流 + +### 核心特性 + +✅ **清晰的分层架构** - Model、View、Controller、System、Utility 各司其职 +✅ **类型安全** - 基于泛型的组件获取和事件系统 +✅ **松耦合** - 通过事件和接口实现组件解耦 +✅ **易于测试** - 依赖注入和纯函数设计 +✅ **可扩展** - 基于接口的规则体系 +✅ **生命周期管理** - 自动的注册和注销机制 + +## 核心概念 + +### 五层架构 + +``` +┌─────────────────────────────────────────┐ +│ View (Godot Nodes) │ UI 层:Godot 节点 +├─────────────────────────────────────────┤ +│ Controller │ 控制层:处理用户输入 +├─────────────────────────────────────────┤ +│ System │ 逻辑层:业务逻辑 +├─────────────────────────────────────────┤ +│ Model │ 数据层:游戏状态 +├─────────────────────────────────────────┤ +│ Utility │ 工具层:无状态工具 +└─────────────────────────────────────────┘ +``` + +### 横切关注点 + +``` +Command ──┐ +Query ──┼──→ 跨层操作(修改/查询数据) +Event ──┘ +``` + +## 架构图 + +### 整体架构 + +``` + ┌──────────────────┐ + │ Architecture │ ← 单例,管理所有组件 + └────────┬─────────┘ + │ + ┌────────────────────┼────────────────────┐ + │ │ │ + ┌───▼────┐ ┌───▼────┐ ┌───▼─────┐ + │ Model │ │ System │ │ Utility │ + │ 层 │ │ 层 │ │ 层 │ + └───┬────┘ └───┬────┘ └────────┘ + │ │ + │ ┌─────────────┤ + │ │ │ + ┌───▼────▼───┐ ┌───▼──────┐ + │ Controller │ │ Command/ │ + │ 层 │ │ Query │ + └─────┬──────┘ └──────────┘ + │ + ┌─────▼─────┐ + │ View │ + │ (Godot节点)│ + └───────────┘ +``` + +### 数据流向 + +``` +用户输入 → Controller → Command → System → Model → Event → Controller → View 更新 + +查询流程:Controller → Query → Model → 返回数据 +``` + +## 快速开始 + +本框架采用"约定优于配置"的设计理念,只需 4 步即可搭建完整的游戏架构。 + +### 为什么需要这个框架? + +在传统的 Godot 开发中,我们经常遇到这些问题: +- 💔 **代码耦合严重**:UI 直接访问游戏逻辑,逻辑直接操作 UI +- 🔄 **难以维护**:修改一个功能需要改动多个文件 +- 🐛 **难以测试**:业务逻辑和 UI 混在一起无法独立测试 +- 📦 **难以复用**:代码紧密耦合,无法在其他项目中复用 + +本框架通过清晰的分层解决这些问题。 + +### 1. 定义架构(Architecture) + +**作用**:Architecture 是整个游戏的"中央调度器",负责管理所有组件的生命周期。 + +```csharp +using GFramework.framework.architecture; + +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 注册 Model - 游戏数据 + this.RegisterModel(new PlayerModel()); + + // 注册 System - 业务逻辑 + this.RegisterSystem(new CombatSystem()); + + // 注册 Utility - 工具类 + this.RegisterUtility(new StorageUtility()); + } +} +``` + +**优势**: +- ✅ **单例模式**:通过 `GameArchitecture.Interface` 全局访问 +- ✅ **自动初始化**:注册时自动调用组件的 Init 方法 +- ✅ **依赖注入**:组件自动获得架构引用,无需手动传递 +- ✅ **集中管理**:所有组件注册在一处,一目了然 + +### 2. 定义 Model(数据层) + +**作用**:Model 是游戏的"数据库",只负责存储和管理游戏状态。 + +```csharp +public class PlayerModel : AbstractModel +{ + // 使用 BindableProperty 实现响应式数据 + public BindableProperty Health { get; } = new(100); + public BindableProperty Gold { get; } = new(0); + + protected override void OnInit() + { + // Model 中可以监听自己的数据变化 + Health.Register(hp => + { + if (hp <= 0) this.SendEvent(new PlayerDiedEvent()); + }); + } +} +//当然也可以不这样 +public class PlayerModel : AbstractModel +{ + public int Health { get;private set; } + public int Gold { get;private set; } + + protected override void OnInit() + { + Health = 100; + Gold = 0; + } +} +``` + +**优势**: +- ✅ **数据响应式**:BindableProperty 让数据变化自动通知 UI +- ✅ **职责单一**:只存储数据,不包含复杂业务逻辑 +- ✅ **易于测试**:可以独立测试数据逻辑 +- ✅ **数据持久化**:可以轻松序列化整个 Model 进行存档 + +**为什么不在 Model 中写业务逻辑?** +- 保持 Model 简单纯粹,业务逻辑应该在 System 中处理 +- Model 应该是"被动"的,等待其他组件修改它 + +### 3. 定义 System(业务逻辑层) + +**作用**:System 是游戏的"大脑",处理所有业务逻辑。 + +```csharp +public class CombatSystem : AbstractSystem +{ + protected override void OnInit() + { + // System 通过事件驱动,响应游戏中的各种事件 + this.RegisterEvent(OnEnemyAttack); + } + + private void OnEnemyAttack(EnemyAttackEvent e) + { + var playerModel = this.GetModel(); + + // 处理业务逻辑:计算伤害、更新数据 + playerModel.Health.Value -= e.Damage; + + // 发送事件通知其他组件 + this.SendEvent(new PlayerTookDamageEvent { Damage = e.Damage }); + } +} +``` + +**优势**: +- ✅ **事件驱动**:通过事件解耦,不同 System 之间松耦合 +- ✅ **可组合**:多个 System 协同工作,每个专注自己的领域 +- ✅ **易于扩展**:新增功能只需添加新的 System 和事件监听 +- ✅ **独立测试**:可以模拟事件来测试 System 的逻辑 + +**System 与 Model 的关系**: +- System 读取和修改 Model 的数据 +- System 不应该存储重要的游戏状态(状态应在 Model 中) +- System 可以存储临时的计算结果或缓存 + +### 4. 定义 Controller(控制层) + +**作用**:Controller 是"桥梁",连接 UI(View)和业务逻辑。 + +```csharp +public partial class PlayerController : Node, IController +{ + [Export] private Label _healthLabel; + private IUnRegisterList _unregisterList = new UnRegisterList(); + + // 实现接口,连接到架构 + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public override void _Ready() + { + var playerModel = this.GetModel(); + + // 数据绑定:Model 数据变化自动更新 UI + playerModel.Health + .RegisterWithInitValue(hp => _healthLabel.Text = $"HP: {hp}") + .AddToUnregisterList(_unregisterList); + } + + public override void _ExitTree() + { + // 重要:节点销毁时注销所有监听,避免内存泄漏 + _unregisterList.UnRegisterAll(); + } +} +``` + +**优势**: +- ✅ **自动更新 UI**:通过 BindableProperty,数据变化自动反映到界面 +- ✅ **生命周期管理**:自动注销监听,避免内存泄漏 +- ✅ **分离关注点**:UI 逻辑和业务逻辑完全分离 +- ✅ **易于修改 UI**:改变 UI 不影响业务逻辑 + +**为什么使用 RegisterWithInitValue?** +- 注册监听时立即获得当前值,避免 UI 显示空白 +- 后续数据变化会自动触发更新 + +### 完成!现在你有了一个完整的架构 + +这 4 步完成后,你就拥有了: +- 📦 **清晰的数据层**(Model) +- 🧠 **独立的业务逻辑**(System) +- 🎮 **灵活的控制层**(Controller) +- 🔧 **统一的生命周期管理**(Architecture) + +### 下一步该做什么? + +1. **添加 Command**:封装用户操作(如购买物品、使用技能) +2. **添加 Query**:封装数据查询(如查询背包物品数量) +3. **添加更多 System**:如任务系统、背包系统、商店系统 +4. **使用 Utility**:添加工具类(如存档工具、数学工具) + +## 包说明 + +| 包名 | 职责 | 文档 | +|-----|------|------| +| **architecture** | 架构核心,管理所有组件生命周期 | [📖 查看](architecture/README.md) | +| **model** | 数据模型层,存储游戏状态 | [📖 查看](model/README.md) | +| **system** | 业务逻辑层,处理游戏系统 | [📖 查看](system/README.md) | +| **controller** | 控制器层,连接视图和逻辑 | [📖 查看](controller/README.md) | +| **utility** | 工具类层,提供无状态工具 | [📖 查看](utility/README.md) | +| **command** | 命令模式,封装写操作 | [📖 查看](command/README.md) | +| **query** | 查询模式,封装读操作 | [📖 查看](query/README.md) | +| **events** | 事件系统,组件间通信 | [📖 查看](events/README.md) | +| **property** | 可绑定属性,响应式编程 | [📖 查看](property/README.md) | +| **ioc** | IoC 容器,依赖注入 | [📖 查看](ioc/README.md) | +| **rule** | 规则接口,定义组件约束 | [📖 查看](rule/README.md) | +| **extensions** | 扩展方法,简化 API 调用 | [📖 查看](extensions/README.md) | + +## 组件联动 + +### 1. 初始化流程 + +``` +Architecture.Interface + └─> Init() + ├─> RegisterModel → Model.SetArchitecture() → Model.Init() + ├─> RegisterSystem → System.SetArchitecture() → System.Init() + └─> RegisterUtility (无需初始化) +``` + +### 2. Command 执行流程 + +``` +Controller.SendCommand(command) + └─> command.SetArchitecture(architecture) // 自动注入 + └─> command.Execute() + └─> command.OnExecute() // 子类实现 + ├─> GetModel() // 获取数据 + ├─> 修改 Model 数据 + └─> SendEvent() // 发送事件 +``` + +### 3. Event 传播流程 + +``` +组件.SendEvent(event) + └─> TypeEventSystem.Send(event) + └─> 通知所有订阅者 + ├─> Controller 响应 → 更新 UI + ├─> System 响应 → 执行逻辑 + └─> Model 响应 → 更新状态 +``` + +### 4. BindableProperty 数据绑定 + +``` +Model: BindableProperty Health = new(100); +Controller: Health.RegisterWithInitValue(hp => UpdateUI(hp)) +修改值: Health.Value = 50 → 触发所有回调 → UI 自动更新 +``` + +## 最佳实践 + +掌握这些最佳实践,能让你充分发挥框架的优势,避免常见陷阱。 + +### 1. 分层职责原则 📋 + +每一层都有明确的职责边界,遵循这些原则能让代码更清晰、更易维护。 + +#### ✅ 好的实践 vs ❌ 坏的实践 + +**Model 层**: +```csharp +// ✅ 好:只存储数据 +public class PlayerModel : AbstractModel +{ + public BindableProperty Health { get; } = new(100); + public BindableProperty MaxHealth { get; } = new(100); + protected override void OnInit() { } +} + +// ❌ 坏:包含业务逻辑 +public class PlayerModel : AbstractModel +{ + public void TakeDamage(int damage) // ❌ 业务逻辑应在 System + { + Health.Value -= damage; + if (Health.Value <= 0) Die(); + } +} +``` + +**System 层**: +```csharp +// ✅ 好:处理业务逻辑 +public class CombatSystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent(OnAttack); + } + + private void OnAttack(AttackEvent e) + { + // 业务逻辑在这里 + var target = this.GetModel(); + int finalDamage = CalculateDamage(e.BaseDamage, target); + target.Health.Value -= finalDamage; + } +} + +// ❌ 坏:直接操作 UI +public class CombatSystem : AbstractSystem +{ + private Label _damageLabel; // ❌ 不应持有 UI 引用 + + private void OnAttack(AttackEvent e) + { + _damageLabel.Text = $"-{e.Damage}"; // ❌ 应通过事件通知 + } +} +``` + +**Controller 层**: +```csharp +// ✅ 好:只处理 UI 和用户输入 +public partial class AttackButton : Button, IController +{ + public override void _Ready() + { + Pressed += () => this.SendCommand(new AttackCommand()); + } +} + +// ❌ 坏:包含业务逻辑 +public partial class AttackButton : Button, IController +{ + public override void _Ready() + { + Pressed += () => + { + var player = this.GetModel(); + var enemy = this.GetModel(); + + // ❌ 这些业务逻辑应该在 System/Command 中 + int damage = player.AttackPower.Value - enemy.Defense.Value; + enemy.Health.Value -= damage; + }; + } +} +``` + +### 2. 通信方式选择指南 💬 + +不同的通信方式适用于不同场景,选对方式能让代码更优雅。 + +| 通信方式 | 使用场景 | 示例 | 优势 | +|---------|---------|------|------| +| **Command** | 用户操作、修改状态 | 购买物品、攻击敌人 | 可撤销、可记录 | +| **Query** | 查询数据、检查条件 | 查询金币数量、检查是否可购买 | 明确只读意图 | +| **Event** | 通知其他组件 | 玩家死亡、物品拾取 | 松耦合、可扩展 | +| **BindableProperty** | 数据→UI 自动同步 | 生命值变化更新血条 | 自动化、不会遗漏 | + +**实战示例:购物系统** + +```csharp +// 1. Controller:用户点击购买按钮 +public void OnBuyButtonPressed() +{ + // 先用 Query 检查条件 + bool canBuy = this.SendQuery(new CanBuyItemQuery + { + ItemId = "sword", + Price = 100 + }); + + if (canBuy) + { + // 使用 Command 执行操作 + this.SendCommand(new BuyItemCommand { ItemId = "sword" }); + } + else + { + ShowMessage("金币不足!"); + } +} + +// 2. Query:检查是否可以购买 +public class CanBuyItemQuery : AbstractQuery +{ + public string ItemId { get; set; } + public int Price { get; set; } + + protected override bool OnDo() + { + var player = this.GetModel(); + return player.Gold.Value >= Price; + } +} + +// 3. Command:执行购买 +public class BuyItemCommand : AbstractCommand +{ + public string ItemId { get; set; } + + protected override void OnExecute() + { + var player = this.GetModel(); + var shop = this.GetModel(); + + int price = shop.GetPrice(ItemId); + player.Gold.Value -= price; + + // 发送事件通知 + this.SendEvent(new ItemPurchasedEvent { ItemId = ItemId }); + } +} + +// 4. System:响应购买事件 +public class InventorySystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent(OnItemPurchased); + } + + private void OnItemPurchased(ItemPurchasedEvent e) + { + var inventory = this.GetModel(); + inventory.AddItem(e.ItemId); + } +} + +// 5. Controller:通过 BindableProperty 自动更新 UI +public partial class GoldDisplay : Label, IController +{ + public override void _Ready() + { + var player = this.GetModel(); + + // 金币变化自动更新 UI + player.Gold + .RegisterWithInitValue(gold => Text = $"金币: {gold}") + .UnRegisterWhenNodeExitTree(this); + } +} +``` + +### 3. 生命周期管理 ♻️ + +**为什么需要注销?** + +忘记注销监听器会导致: +- 💥 **内存泄漏**:对象无法被 GC 回收 +- 🐛 **逻辑错误**:已销毁的对象仍在响应事件 +- 📉 **性能下降**:无用的监听器消耗资源 + +#### 方式一:自动注销(推荐用于 Godot 节点) + +```csharp +public override void _Ready() +{ + // 节点退出场景树时自动注销 + this.RegisterEvent(OnEvent) + .UnRegisterWhenNodeExitTree(this); + + playerModel.Health + .RegisterWithInitValue(UpdateHealthBar) + .UnRegisterWhenNodeExitTree(this); +} +``` + +**优势**:一行代码搞定,不会忘记 + +#### 方式二:统一管理(推荐用于多个监听器) + +```csharp +private IUnRegisterList _unregisterList = new UnRegisterList(); + +public override void _Ready() +{ + // 所有监听器统一管理 + this.RegisterEvent(OnEvent1) + .AddToUnregisterList(_unregisterList); + + this.RegisterEvent(OnEvent2) + .AddToUnregisterList(_unregisterList); + + playerModel.Health + .RegisterWithInitValue(UpdateUI) + .AddToUnregisterList(_unregisterList); +} + +public override void _ExitTree() +{ + // 一次性注销所有 + _unregisterList.UnRegisterAll(); +} +``` + +**优势**:集中管理,清晰明了 + +#### 方式三:手动注销(需要精确控制时机) + +```csharp +private IUnRegister _healthUnregister; + +public override void _Ready() +{ + _healthUnregister = playerModel.Health.Register(UpdateUI); +} + +public void StopListening() +{ + // 在特定时机手动注销 + _healthUnregister?.UnRegister(); + _healthUnregister = null; +} +``` + +**优势**:可以在任何时候注销 + +### 4. 性能优化技巧 ⚡ + +#### 技巧 1:缓存组件引用 + +```csharp +// ❌ 低效:每帧都查询 +public override void _Process(double delta) +{ + var model = this.GetModel(); // 每帧查询 IoC 容器 + UpdateUI(model.Health.Value); +} + +// ✅ 高效:只查询一次 +private PlayerModel _playerModel; + +public override void _Ready() +{ + _playerModel = this.GetModel(); // 只查询一次 +} + +public override void _Process(double delta) +{ + UpdateUI(_playerModel.Health.Value); +} +``` + +**原因**:GetModel 需要查询 IoC 容器,虽然很快但不必要重复查询 + +#### 技巧 2:批量操作避免频繁触发 + +```csharp +// ❌ 低效:每次修改都触发事件 +foreach (var item in 100items) +{ + inventory.AddItem(item); // 触发100次 UI 更新! +} + +// ✅ 高效:批量操作后统一通知 +foreach (var item in items) +{ + inventory.Items.SetValueWithoutEvent( + inventory.Items.Value + item + ); +} +// 最后统一触发一次 +this.SendEvent(new InventoryChangedEvent()); +``` + +**原因**:每次数据变化都会触发所有监听器,批量操作会导致性能问题 + +#### 技巧 3:使用对象池 + +```csharp +// 在 Utility 中实现对象池 +public class PoolUtility : IUtility +{ + private Dictionary> _pools = new(); + + public T Get() where T : new() + { + var type = typeof(T); + if (_pools.ContainsKey(type) && _pools[type].Count > 0) + return (T)_pools[type].Dequeue(); + return new T(); + } + + public void Return(T obj) + { + var type = typeof(T); + if (!_pools.ContainsKey(type)) + _pools[type] = new Queue(); + _pools[type].Enqueue(obj); + } +} + +// 使用对象池 +var pool = this.GetUtility(); +var bullet = pool.Get(); +// 使用完毕后归还 +pool.Return(bullet); +``` + +### 5. 可测试性设计 🧪 + +框架天然支持单元测试,因为所有组件都是通过接口交互的。 + +```csharp +[TestFixture] +public class CombatSystemTests +{ + private TestArchitecture _architecture; + private PlayerModel _player; + private CombatSystem _combat; + + [SetUp] + public void Setup() + { + // 1. 创建测试架构 + _architecture = new TestArchitecture(); + + // 2. 注册需要的组件 + _player = new PlayerModel(); + _architecture.RegisterModel(_player); + + _combat = new CombatSystem(); + _architecture.RegisterSystem(_combat); + } + + [Test] + public void TestPlayerTakesDamage() + { + // 3. 设置初始状态 + _player.Health.Value = 100; + + // 4. 触发事件 + _architecture.SendEvent(new EnemyAttackEvent { Damage = 30 }); + + // 5. 验证结果 + Assert.AreEqual(70, _player.Health.Value); + } + + [TearDown] + public void TearDown() + { + _architecture = null; + } +} +``` + +**优势**: +- 不需要启动游戏就能测试逻辑 +- 可以快速验证各种边界情况 +- 易于进行回归测试 + +## 示例项目 + +通过一个完整的 RPG 战斗系统示例,展示框架各组件如何协同工作。 + +### 完整示例:RPG 战斗系统 ⚔️ + +实现功能:回合制战斗、伤害计算、战斗日志、胜负判定、UI 实时更新。 + +```csharp +// 架构定义 +public class RPGArchitecture : Architecture +{ + protected override void Init() + { + this.RegisterModel(new PlayerModel()); + this.RegisterModel(new EnemyModel()); + this.RegisterSystem(new CombatSystem()); + this.RegisterUtility(new MathUtility()); + } +} + +// 数据层 +public class PlayerModel : AbstractModel +{ + public BindableProperty Health { get; } = new(100); + public BindableProperty AttackPower { get; } = new(20); + public BindableProperty IsAlive { get; } = new(true); + + protected override void OnInit() + { + Health.Register(hp => + { + IsAlive.Value = hp > 0; + if (hp <= 0) this.SendEvent(new PlayerDiedEvent()); + }); + } +} + +// 业务逻辑层 +public class CombatSystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent(OnPlayerAttack); + } + + private void OnPlayerAttack(PlayerAttackEvent e) + { + var player = this.GetModel(); + var enemy = this.GetModel(); + var mathUtil = this.GetUtility(); + + int damage = mathUtil.CalculateDamage(player.AttackPower.Value, enemy.Defense.Value); + enemy.Health.Value -= damage; + + this.SendEvent(new DamageDealtEvent { Damage = damage }); + } +} + +// 控制层 +public partial class BattleUI : Control, IController +{ + [Export] private Label _healthLabel; + [Export] private Button _attackButton; + private IUnRegisterList _unregisterList = new UnRegisterList(); + + public IArchitecture GetArchitecture() => RPGArchitecture.Interface; + + public override void _Ready() + { + var player = this.GetModel(); + + // 数据绑定:自动更新 UI + player.Health + .RegisterWithInitValue(hp => _healthLabel.Text = $"HP: {hp}") + .AddToUnregisterList(_unregisterList); + + _attackButton.Pressed += () => this.SendCommand(new AttackCommand()); + } + + public override void _ExitTree() => _unregisterList.UnRegisterAll(); +} +``` + +**扩展示例**:添加技能系统只需新增 Model 和 System,无需修改现有代码。 + +```csharp +// 新增技能系统 +public class SkillSystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent(OnUseSkill); + } + + private void OnUseSkill(UseSkillEvent e) + { + // 技能逻辑 + } +} + +// 在架构中注册 +protected override void Init() +{ + this.RegisterSystem(new SkillSystem()); + // 现有代码无需修改 +} +``` + +**优势总结**: +- 数据驱动,易于存档 +- 事件解耦,易于扩展 +- UI 自动化,不会遗漏更新 +- 可独立测试业务逻辑 + +## 设计理念 + +框架的设计遵循 SOLID 原则和经典设计模式,让代码更易维护和扩展。 + +### 1. 单一职责原则(SRP)💼 + +**理念**:每个类只负责一件事,只有一个改变的理由。 + +**在框架中的体现**: +- **Model**:只负责存储数据 +- **System**:只负责处理业务逻辑 +- **Controller**:只负责 UI 交互 +- **Utility**:只负责提供工具方法 + +**好处**: +- 代码更容易理解和维护 +- 修改一个功能不会影响其他功能 +- 单元测试更简单 + +### 2. 开闭原则(OCP)🔓 + +**理念**:对扩展开放,对修改封闭。 + +**在框架中的实现**: +- 通过**事件系统**添加新功能,无需修改现有代码 +- 新的 System 可以监听现有事件,插入自己的逻辑 +- 符合"插件式"的架构设计 + +**示例**: +```csharp +// 现有:战斗系统 +public class CombatSystem : AbstractSystem +{ + private void OnAttack(AttackEvent e) + { + // 计算伤害 + this.SendEvent(new DamageDealtEvent { Damage = damage }); + } +} + +// 扩展:添加暴击系统,不修改 CombatSystem +public class CriticalSystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent(OnDamage); + } + + private void OnDamage(DamageDealtEvent e) + { + if (RollCritical()) + { + this.SendEvent(new CriticalHitEvent { Damage = e.Damage * 2 }); + } + } +} +``` + +### 3. 依赖倒置原则(DIP)🔄 + +**理念**:依赖抽象(接口)而非具体实现。 + +**在框架中的应用**: +- 所有组件通过接口交互 +- 通过 IoC 容器注入依赖 +- 易于替换实现和编写测试 + +**好处**: +- 降低耦合度 +- 易于进行单元测试(可以 mock) +- 可以灵活替换实现 + +### 4. 接口隔离原则(ISP)✂️ + +**理念**:使用多个专门的接口,而不是一个庞大的接口。 + +**在框架中的体现**: +```csharp +// 小而专注的接口 +public interface ICanGetModel : IBelongToArchitecture { } +public interface ICanSendCommand : IBelongToArchitecture { } +public interface ICanRegisterEvent : IBelongToArchitecture { } + +// Controller 组合需要的能力 +public interface IController : + ICanGetModel, + ICanSendCommand, + ICanRegisterEvent { } +``` + +**优势**: +- 类只需要实现它真正需要的方法 +- 接口更容易理解和使用 +- 减少不必要的依赖 + +### 5. 组合优于继承 🧩 + +**理念**:通过接口组合获得能力,而不是通过继承。 + +**传统继承的问题**: +- 继承层次深,难以理解 +- 子类与父类紧密耦合 +- 难以灵活组合能力 + +**框架的解决方案**: +```csharp +// ✅ 通过接口组合能力 +public interface IController : + ICanGetModel, // 组合:获取 Model 的能力 + ICanSendCommand, // 组合:发送 Command 的能力 + ICanRegisterEvent // 组合:注册事件的能力 +{ } + +// ❌ 避免深层继承 +public class BaseController { } +public class GameController : BaseController { } +public class BattleController : GameController { } +``` + +### 框架核心设计模式 🎨 + +| 设计模式 | 应用位置 | 解决的问题 | 带来的好处 | +|---------|---------|-----------|-----------| +| **单例模式** | Architecture | 全局唯一的架构实例 | 统一的组件管理 | +| **工厂模式** | IoC 容器 | 组件的创建和管理 | 解耦创建逻辑 | +| **观察者模式** | Event 系统 | 组件间的通信 | 松耦合通信 | +| **命令模式** | Command | 封装操作请求 | 支持撤销重做 | +| **策略模式** | System | 不同的业务逻辑 | 易于切换策略 | +| **依赖注入** | 整体架构 | 组件间的依赖 | 自动管理依赖 | +| **模板方法** | Abstract 类 | 定义算法骨架 | 统一流程规范 | + +### 为什么这样设计?🤔 + +#### 1. 降低学习成本 +- 遵循业界标准模式和原则 +- 有经验的开发者能快速上手 +- 清晰的分层易于理解 + +#### 2. 提高代码质量 +- 强制分层,避免意大利面代码 +- 职责明确,减少 bug +- 易于 Code Review + +#### 3. 便于团队协作 +- 清晰的职责划分,减少冲突 +- 统一的代码风格 +- 新人容易融入项目 + +#### 4. 易于维护扩展 +- 符合 SOLID 原则 +- 通过事件添加功能无需修改现有代码 +- 模块化设计,易于替换 + +#### 5. 支持单元测试 +- 依赖注入让 mock 变得简单 +- 接口设计便于测试 +- 业务逻辑与 UI 分离,可独立测试 + +### 架构演进建议 🚀 + +#### 小型项目(< 5000 行代码) +- 使用基础的 MVC 分层 +- 适度使用 Command 和 Query +- 事件系统用于关键通知 + +#### 中型项目(5000-20000 行) +- 完整使用框架所有特性 +- 细分 System 的职责 +- 引入更多 Utility 复用代码 + +#### 大型项目(> 20000 行) +- 考虑按功能模块拆分多个 Architecture +- 使用事件总线连接不同模块 +- 引入领域驱动设计(DDD)概念 + +### 常见反模式 ⚠️ + +#### 反模式 1:上帝类(God Class) +```csharp +// ❌ 一个类做所有事情 +public class GameManager +{ + public void Attack() { } + public void Move() { } + public void SaveGame() { } + public void LoadGame() { } + public void UpdateUI() { } + // ... 几千行代码 +} + +// ✅ 正确:分层设计 +public class CombatSystem { /* 只负责战斗 */ } +public class MovementSystem { /* 只负责移动 */ } +public class SaveSystem { /* 只负责存档 */ } +``` + +#### 反模式 2:循环依赖 +```csharp +// ❌ A 依赖 B,B 又依赖 A +public class SystemA +{ + private SystemB _systemB; +} +public class SystemB +{ + private SystemA _systemA; +} + +// ✅ 正确:通过事件解耦 +public class SystemA +{ + protected override void OnInit() + { + this.SendEvent(new EventA()); + } +} +public class SystemB +{ + protected override void OnInit() + { + this.RegisterEvent(OnEventA); + } +} +``` + +#### 反模式 3:过度设计 +```csharp +// ❌ 简单功能过度抽象 +public interface IClickable { } +public interface IHoverable { } +public interface IFocusable { } +public class Button : IClickable, IHoverable, IFocusable { } + +// ✅ 正确:根据实际需求设计 +public class Button : Control, IController +{ + // 简单直接 +} +``` + +### 设计哲学总结 📝 + +1. **简单优于复杂**:能用简单方案就不用复杂方案 +2. **显式优于隐式**:让代码意图明确 +3. **解耦优于耦合**:通过接口和事件降低耦合 +4. **组合优于继承**:灵活组合能力而非深层继承 +5. **约定优于配置**:遵循框架约定,减少配置 + +**记住**:好的架构不是一开始就完美的,而是在迭代中不断演进的。从简单开始,根据项目需求逐步采用框架特性。 + +## 相关资源 + +- Godot 官方文档: https://docs.godotengine.org/ +- CQRS 模式介绍: https://martinfowler.com/bliki/CQRS.html +- 事件驱动架构: https://martinfowler.com/articles/201701-event-driven.html + +--- + +**版本**: 1.0.0 +**适用引擎**: Godot 4.x (C#) +**许可证**: MIT \ No newline at end of file diff --git a/framework/architecture/Architecture.cs b/framework/architecture/Architecture.cs new file mode 100644 index 0000000..bdffd0d --- /dev/null +++ b/framework/architecture/Architecture.cs @@ -0,0 +1,268 @@ + +using GFramework.framework.command; +using GFramework.framework.events; +using GFramework.framework.ioc; +using GFramework.framework.model; +using GFramework.framework.query; +using GFramework.framework.system; +using GFramework.framework.utility; + + +namespace GFramework.framework.architecture; + +/// +/// 架构基类,提供系统、模型、工具等组件的注册与管理功能。 +/// 使用单例模式确保全局唯一实例,并支持命令、查询和事件机制。 +/// +/// 派生类类型,用于实现单例 +public abstract class Architecture : IArchitecture where T : Architecture, new() +{ + /// + /// 标记架构是否已初始化完成 + /// + private bool _mInited; + + /// + /// 存储尚未初始化的系统集合,在初始化阶段统一调用Init方法 + /// + private readonly HashSet _mSystems = []; + + /// + /// 存储尚未初始化的模型集合,在初始化阶段统一调用Init方法 + /// + private readonly HashSet _mModels = []; + + /// + /// 注册补丁委托,允许在架构创建后执行额外逻辑 + /// + public static Action OnRegisterPatch { get; set; } = _ => { }; + + /// + /// 静态只读字段,用于延迟初始化架构实例 + /// 使用Lazy确保线程安全的单例模式实现 + /// + /// + /// 初始化过程包括: + /// 1. 创建T类型的实例 + /// 2. 调用用户自定义的Init方法 + /// 3. 执行注册的补丁逻辑 + /// 4. 初始化所有已注册的模型和系统 + /// 5. 清理临时集合并标记初始化完成 + /// + /// T类型的架构实例 + private static readonly Lazy MArchitectureLazy = new(() => + { + var arch = new T(); + // 调用用户实现的初始化 + arch.Init(); + + // 执行注册的补丁逻辑 + OnRegisterPatch?.Invoke(arch); + + // 初始化所有已注册但尚未初始化的模型 + foreach (var model in arch._mModels) + { + model.Init(); + } + + arch._mModels.Clear(); + + // 初始化所有已注册但尚未初始化的系统 + foreach (var system in arch._mSystems) + { + system.Init(); + } + + arch._mSystems.Clear(); + + arch._mInited = true; + return arch; + }, System.Threading.LazyThreadSafetyMode.ExecutionAndPublication); + + /// + /// 获取架构实例的受保护静态属性 + /// 通过Lazy初始化确保只创建一个实例 + /// + /// T类型的架构实例 + protected static T MArchitecture => MArchitectureLazy.Value; + + /// + /// 获取架构实例的公共静态属性,以接口形式暴露 + /// 提供对外访问架构功能的标准接口 + /// + /// IArchitecture接口类型的架构实例 + public static IArchitecture Interface => MArchitectureLazy.Value; + + + /// + /// 抽象初始化方法,由子类重写以进行自定义初始化操作 + /// + protected abstract void Init(); + + /// + /// 控制反转容器,用于存储和获取各种服务(如系统、模型、工具) + /// + private readonly IocContainer _mContainer = new(); + + /// + /// 注册一个系统到架构中。 + /// 若当前未初始化,则暂存至待初始化列表;否则立即初始化该系统。 + /// + /// 要注册的系统类型 + /// 要注册的系统实例 + public void RegisterSystem(TSystem system) where TSystem : ISystem + { + system.SetArchitecture(this); + _mContainer.Register(system); + + if (!_mInited) + { + _mSystems.Add(system); + } + else + { + system.Init(); + } + } + + /// + /// 注册一个模型到架构中。 + /// 若当前未初始化,则暂存至待初始化列表;否则立即初始化该模型。 + /// + /// 要注册的模型类型 + /// 要注册的模型实例 + public void RegisterModel(TModel model) where TModel : IModel + { + model.SetArchitecture(this); + _mContainer.Register(model); + + if (!_mInited) + { + _mModels.Add(model); + } + else + { + model.Init(); + } + } + + /// + /// 注册一个工具到架构中。 + /// 工具不会被延迟初始化,直接放入IOC容器供后续使用。 + /// + /// 要注册的工具类型 + /// 要注册的工具实例 + public void RegisterUtility(TUtility utility) where TUtility : IUtility => + _mContainer.Register(utility); + + /// + /// 从IOC容器中获取指定类型的系统实例 + /// + /// 目标系统类型 + /// 对应的系统实例 + public TSystem GetSystem() where TSystem : class, ISystem => _mContainer.Get(); + + /// + /// 从IOC容器中获取指定类型的模型实例 + /// + /// 目标模型类型 + /// 对应的模型实例 + public TModel GetModel() where TModel : class, IModel => _mContainer.Get(); + + /// + /// 从IOC容器中获取指定类型的工具实例 + /// + /// 目标工具类型 + /// 对应的工具实例 + public TUtility GetUtility() where TUtility : class, IUtility => _mContainer.Get(); + + /// + /// 发送一个带返回结果的命令请求 + /// + /// 命令执行后的返回值类型 + /// 要发送的命令对象 + /// 命令执行的结果 + public TResult SendCommand(ICommand command) => ExecuteCommand(command); + + /// + /// 发送一个无返回结果的命令请求 + /// + /// 命令的具体类型 + /// 要发送的命令对象 + public void SendCommand(TCommand command) where TCommand : ICommand => ExecuteCommand(command); + + /// + /// 执行一个带返回结果的命令 + /// + /// 命令执行后的返回值类型 + /// 要执行的命令对象 + /// 命令执行的结果 + protected virtual TResult ExecuteCommand(ICommand command) + { + command.SetArchitecture(this); + return command.Execute(); + } + + /// + /// 执行一个无返回结果的命令 + /// + /// 要执行的命令对象 + protected virtual void ExecuteCommand(ICommand command) + { + command.SetArchitecture(this); + command.Execute(); + } + + /// + /// 发起一次查询请求并获得其结果 + /// + /// 查询结果的数据类型 + /// 要发起的查询对象 + /// 查询得到的结果数据 + public TResult SendQuery(IQuery query) => DoQuery(query); + + /// + /// 实际执行查询逻辑的方法 + /// + /// 查询结果的数据类型 + /// 要处理的查询对象 + /// 查询结果 + protected virtual TResult DoQuery(IQuery query) + { + query.SetArchitecture(this); + return query.Do(); + } + + /// + /// 类型化事件系统,负责事件的发布与订阅管理 + /// + private readonly TypeEventSystem _mTypeEventSystem = new(); + + /// + /// 发布一个默认构造的新事件对象 + /// + /// 事件类型 + public void SendEvent() where TEvent : new() => _mTypeEventSystem.Send(); + + /// + /// 发布一个具体的事件对象 + /// + /// 事件类型 + /// 要发布的事件实例 + public void SendEvent(TEvent e) => _mTypeEventSystem.Send(e); + + /// + /// 订阅某个特定类型的事件 + /// + /// 事件类型 + /// 当事件发生时触发的动作 + /// 可用于取消订阅的对象 + public IUnRegister RegisterEvent(Action onEvent) => _mTypeEventSystem.Register(onEvent); + + /// + /// 取消对某类型事件的监听 + /// + /// 事件类型 + /// 之前绑定的事件处理器 + public void UnRegisterEvent(Action onEvent) => _mTypeEventSystem.UnRegister(onEvent); +} \ No newline at end of file diff --git a/framework/architecture/IArchitecture.cs b/framework/architecture/IArchitecture.cs new file mode 100644 index 0000000..5bdd2d4 --- /dev/null +++ b/framework/architecture/IArchitecture.cs @@ -0,0 +1,110 @@ + + +using GFramework.framework.command; +using GFramework.framework.events; +using GFramework.framework.model; +using GFramework.framework.query; +using GFramework.framework.system; +using GFramework.framework.utility; + +namespace GFramework.framework.architecture; + +/// +/// 架构接口,定义了应用程序架构的核心功能,包括系统、模型、工具的注册和获取, +/// 以及命令、查询、事件的发送和处理机制 +/// +public interface IArchitecture +{ + /// + /// 注册系统实例到架构中 + /// + /// 系统类型,必须实现ISystem接口 + /// 要注册的系统实例 + void RegisterSystem(T system) where T : ISystem; + + /// + /// 注册模型实例到架构中 + /// + /// 模型类型,必须实现IModel接口 + /// 要注册的模型实例 + void RegisterModel(T model) where T : IModel; + + /// + /// 注册工具实例到架构中 + /// + /// 工具类型,必须实现IUtility接口 + /// 要注册的工具实例 + void RegisterUtility(T utility) where T : IUtility; + + /// + /// 从架构中获取指定类型的系统实例 + /// + /// 系统类型,必须是class且实现ISystem接口 + /// 指定类型的系统实例 + T GetSystem() where T : class, ISystem; + + /// + /// 从架构中获取指定类型的模型实例 + /// + /// 模型类型,必须是class且实现IModel接口 + /// 指定类型的模型实例 + T GetModel() where T : class, IModel; + + /// + /// 从架构中获取指定类型的工具实例 + /// + /// 工具类型,必须是class且实现IUtility接口 + /// 指定类型的工具实例 + T GetUtility() where T : class, IUtility; + + /// + /// 发送并执行指定的命令 + /// + /// 命令类型,必须实现ICommand接口 + /// 要执行的命令实例 + void SendCommand(T command) where T : ICommand; + + /// + /// 发送并执行带有返回值的命令 + /// + /// 命令执行结果的类型 + /// 要执行的命令实例 + /// 命令执行的结果 + TResult SendCommand(ICommand command); + + /// + /// 发送并执行查询操作 + /// + /// 查询结果的类型 + /// 要执行的查询实例 + /// 查询的结果 + TResult SendQuery(IQuery query); + + /// + /// 发送无参事件 + /// + /// 事件类型,必须具有无参构造函数 + void SendEvent() where T : new(); + + /// + /// 发送指定的事件实例 + /// + /// 事件类型 + /// 要发送的事件实例 + void SendEvent(T e); + + /// + /// 注册事件监听器 + /// + /// 事件类型 + /// 事件触发时的回调方法 + /// 用于取消注册的句柄 + IUnRegister RegisterEvent(Action onEvent); + + /// + /// 取消注册事件监听器 + /// + /// 事件类型 + /// 要取消注册的事件回调方法 + void UnRegisterEvent(Action onEvent); +} diff --git a/framework/architecture/README.md b/framework/architecture/README.md new file mode 100644 index 0000000..939aa8f --- /dev/null +++ b/framework/architecture/README.md @@ -0,0 +1,169 @@ +# Architecture 包使用说明 + +## 概述 + +Architecture 包是整个框架的核心,提供了基于 MVC 架构模式的应用程序架构基础。它实现了依赖注入(IoC)容器、组件生命周期管理,以及命令、查询、事件的统一调度机制。 + +## 核心类 + +### 1. [`IArchitecture`](IArchitecture.cs) + +架构接口,定义了框架的核心功能契约。 + +**主要职责:** +- 组件注册:注册 System、Model、Utility +- 组件获取:从容器中获取已注册的组件 +- 命令处理:发送并执行命令 +- 查询处理:发送并执行查询 +- 事件管理:发送、注册、注销事件 + +**核心方法:** + +```csharp +// 注册组件 +void RegisterSystem(T system) where T : ISystem; +void RegisterModel(T model) where T : IModel; +void RegisterUtility(T utility) where T : IUtility; + +// 获取组件 +T GetSystem() where T : class, ISystem; +T GetModel() where T : class, IModel; +T GetUtility() where T : class, IUtility; + +// 命令处理 +void SendCommand(T command) where T : ICommand; +TResult SendCommand(ICommand command); + +// 查询处理 +TResult SendQuery(IQuery query); + +// 事件管理 +void SendEvent() where T : new(); +void SendEvent(T e); +IUnRegister RegisterEvent(Action onEvent); +void UnRegisterEvent(Action onEvent); +``` + +### 2. [`Architecture`](Architecture.cs) + +架构基类,实现了 [`IArchitecture`](IArchitecture.cs) 接口,提供完整的架构功能实现。 + +**特性:** +- **单例模式**:使用泛型和 `Lazy` 确保全局唯一实例 +- **线程安全**:采用 `LazyThreadSafetyMode.ExecutionAndPublication` 保证多线程安全 +- **生命周期管理**:自动管理 System 和 Model 的初始化顺序 +- **IoC 容器**:内置依赖注入容器,管理所有组件实例 +- **事件系统**:集成类型化事件系统,支持类型安全的事件通信 + +**初始化流程:** +1. 创建架构实例 +2. 调用用户自定义的 `Init()` 方法 +3. 执行注册的补丁逻辑(`OnRegisterPatch`) +4. 初始化所有已注册的 Model +5. 初始化所有已注册的 System +6. 标记初始化完成 + +**使用示例:** + +```csharp +// 1. 定义你的架构 +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 注册 Model + RegisterModel(new PlayerModel()); + RegisterModel(new InventoryModel()); + + // 注册 System + RegisterSystem(new GameplaySystem()); + RegisterSystem(new SaveSystem()); + + // 注册 Utility + RegisterUtility(new StorageUtility()); + RegisterUtility(new TimeUtility()); + } +} + +// 2. 在其他地方使用架构 +public class GameController : IController +{ + private IArchitecture _architecture; + + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public void Start() + { + // 获取 Model + var playerModel = this.GetModel(); + + // 发送命令 + this.SendCommand(); + + // 发送查询 + var score = this.SendQuery(new GetScoreQuery()); + + // 注册事件 + this.RegisterEvent(OnPlayerDied); + } + + private void OnPlayerDied(PlayerDiedEvent e) + { + // 处理玩家死亡事件 + } +} +``` + +**高级特性:** + +```csharp +// 动态扩展架构(补丁系统) +Architecture.OnRegisterPatch += arch => +{ + // 在架构初始化完成前注入额外逻辑 + arch.RegisterSystem(new DebugSystem()); +}; + +// 在运行时动态注册组件(初始化后) +var newSystem = new DynamicSystem(); +GameArchitecture.Interface.RegisterSystem(newSystem); +// newSystem.Init() 会被立即调用 +``` + +## 设计模式 + +### 单例模式 +通过泛型约束和 `Lazy` 实现类型安全的单例。 + +### 依赖注入(IoC) +使用内置 IoC 容器管理组件生命周期和依赖关系。 + +### 命令模式 +通过 [`ICommand`](../command/ICommand.cs) 封装所有用户操作。 + +### 查询模式(CQRS) +通过 [`IQuery`](../query/IQuery.cs) 分离查询和命令操作。 + +### 观察者模式 +通过事件系统实现组件间的松耦合通信。 + +## 最佳实践 + +1. **保持架构类简洁**:只在 `Init()` 中注册组件,不要包含业务逻辑 +2. **合理划分职责**: + - Model:数据和状态 + - System:业务逻辑和规则 + - Utility:无状态的工具方法 +3. **使用接口访问**:通过 `Interface` 属性访问架构,便于测试 +4. **事件命名规范**:使用过去式命名事件类,如 `PlayerDiedEvent` +5. **避免循环依赖**:System 不应直接引用 System,应通过事件通信 + +## 相关包 + +- [`command`](../command/README.md) - 命令模式实现 +- [`query`](../query/README.md) - 查询模式实现 +- [`events`](../events/README.md) - 事件系统 +- [`ioc`](../ioc/README.md) - IoC 容器 +- [`model`](../model/README.md) - 数据模型 +- [`system`](../system/README.md) - 业务系统 +- [`utility`](../utility/README.md) - 工具类 \ No newline at end of file diff --git a/framework/command/AbstractCommand.cs b/framework/command/AbstractCommand.cs new file mode 100644 index 0000000..dc9f6da --- /dev/null +++ b/framework/command/AbstractCommand.cs @@ -0,0 +1,68 @@ +using GFramework.framework.architecture; +using GFramework.framework.rule; + + +namespace GFramework.framework.command; + +/// +/// 抽象命令类,实现 ICommand 接口,为具体命令提供基础架构支持 +/// +public abstract class AbstractCommand : ICommand +{ + private IArchitecture _mArchitecture; + + /// + /// 获取命令所属的架构实例 + /// + /// IArchitecture 架构接口实例 + IArchitecture IBelongToArchitecture.GetArchitecture() => _mArchitecture; + + /// + /// 设置命令所属的架构实例 + /// + /// 要设置的架构实例 + void ICanSetArchitecture.SetArchitecture(IArchitecture architecture) => _mArchitecture = architecture; + + /// + /// 执行命令,调用抽象方法 OnExecute 来实现具体逻辑 + /// + void ICommand.Execute() => OnExecute(); + + /// + /// 抽象方法,由子类实现具体的命令执行逻辑 + /// + protected abstract void OnExecute(); +} + +/// +/// 带返回值的抽象命令类,实现 ICommand{TResult} 接口,为需要返回结果的命令提供基础架构支持 +/// +/// 命令执行后返回的结果类型 +public abstract class AbstractCommand : ICommand +{ + private IArchitecture _mArchitecture; + + /// + /// 获取命令所属的架构实例 + /// + /// IArchitecture 架构接口实例 + IArchitecture IBelongToArchitecture.GetArchitecture() => _mArchitecture; + + /// + /// 设置命令所属的架构实例 + /// + /// 要设置的架构实例 + void ICanSetArchitecture.SetArchitecture(IArchitecture architecture) => _mArchitecture = architecture; + + /// + /// 执行命令,调用抽象方法 OnExecute 来实现具体逻辑并返回结果 + /// + /// TResult 类型的执行结果 + TResult ICommand.Execute() => OnExecute(); + + /// + /// 抽象方法,由子类实现具体的命令执行逻辑 + /// + /// TResult 类型的执行结果 + protected abstract TResult OnExecute(); +} diff --git a/framework/command/ICanSendCommand.cs b/framework/command/ICanSendCommand.cs new file mode 100644 index 0000000..8df3169 --- /dev/null +++ b/framework/command/ICanSendCommand.cs @@ -0,0 +1,9 @@ +using GFramework.framework.rule; + +namespace GFramework.framework.command; + +/// +/// 定义一个可以发送命令的接口,继承自IBelongToArchitecture接口。 +/// 该接口用于标识那些具备发送命令能力的架构组件。 +/// +public interface ICanSendCommand : IBelongToArchitecture; diff --git a/framework/command/ICommand.cs b/framework/command/ICommand.cs new file mode 100644 index 0000000..e5ae761 --- /dev/null +++ b/framework/command/ICommand.cs @@ -0,0 +1,39 @@ +using GFramework.framework.events; +using GFramework.framework.model; +using GFramework.framework.query; +using GFramework.framework.rule; +using GFramework.framework.system; +using GFramework.framework.utility; + +namespace GFramework.framework.command; + +/// +/// 命令接口,定义了无返回值命令的基本契约 +/// 该接口继承了多个框架能力接口,使命令可以访问架构、系统、模型、工具,并能够发送事件、命令和查询 +/// +public interface ICommand : ICanSetArchitecture, ICanGetSystem, ICanGetModel, ICanGetUtility, + ICanSendEvent, ICanSendCommand, ICanSendQuery +{ + /// + /// 执行命令的核心方法 + /// 该方法不接受参数且无返回值,具体实现由派生类完成 + /// + void Execute(); +} + +/// +/// 带返回值的命令接口,定义了有返回值命令的基本契约 +/// 该接口继承了多个框架能力接口,使命令可以访问架构、系统、模型、工具,并能够发送事件、命令和查询 +/// +/// 命令执行后返回的结果类型 +public interface ICommand : ICanSetArchitecture, ICanGetSystem, ICanGetModel, + ICanGetUtility, + ICanSendEvent, ICanSendCommand, ICanSendQuery +{ + /// + /// 执行命令的核心方法 + /// 该方法不接受参数,但会返回指定类型的结果 + /// + /// 命令执行后的结果,类型为 TResult + TResult Execute(); +} diff --git a/framework/command/README.md b/framework/command/README.md new file mode 100644 index 0000000..931763b --- /dev/null +++ b/framework/command/README.md @@ -0,0 +1,293 @@ +# Command 包使用说明 + +## 概述 + +Command 包实现了命令模式(Command Pattern),用于封装用户操作和业务逻辑。通过命令模式,可以将请求封装为对象,实现操作的参数化、队列化、日志记录、撤销等功能。 + +## 核心接口 + +### 1. [`ICommand`](ICommand.cs) + +无返回值命令接口,定义了命令的基本契约。 + +**继承的能力接口:** +- [`ICanSetArchitecture`](../rule/ICanSetArchitecture.cs) - 可设置架构 +- [`ICanGetSystem`](../system/ICanGetSystem.cs) - 可获取系统 +- [`ICanGetModel`](../model/ICanGetModel.cs) - 可获取模型 +- [`ICanGetUtility`](../utility/ICanGetUtility.cs) - 可获取工具 +- [`ICanSendEvent`](../events/ICanSendEvent.cs) - 可发送事件 +- [`ICanSendCommand`](ICanSendCommand.cs) - 可发送命令 +- [`ICanSendQuery`](../query/ICanSendQuery.cs) - 可发送查询 + +**核心方法:** +```csharp +void Execute(); // 执行命令 +``` + +### 2. [`ICommand`](ICommand.cs) + +带返回值的命令接口,用于需要返回执行结果的命令。 + +**核心方法:** +```csharp +TResult Execute(); // 执行命令并返回结果 +``` + +### 3. [`ICanSendCommand`](ICanSendCommand.cs) + +标记接口,表示实现者可以发送命令。继承自 [`IBelongToArchitecture`](../rule/IBelongToArchitecture.cs)。 + +## 核心类 + +### 1. [`AbstractCommand`](AbstractCommand.cs) + +无返回值命令的抽象基类,提供了命令的基础实现。 + +**使用示例:** + +```csharp +// 定义一个开始游戏的命令 +public class StartGameCommand : AbstractCommand +{ + protected override void OnExecute() + { + // 获取需要的模型 + var playerModel = this.GetModel(); + var gameModel = this.GetModel(); + + // 执行业务逻辑 + playerModel.Health.Value = 100; + gameModel.GameState.Value = GameState.Playing; + + // 发送事件通知其他模块 + this.SendEvent(new GameStartedEvent()); + } +} + +// 使用命令 +public class GameController : IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public void OnStartButtonClicked() + { + // 方式1:发送命令实例 + this.SendCommand(new StartGameCommand()); + + // 方式2:通过泛型发送(需要无参构造函数) + this.SendCommand(); + } +} +``` + +### 2. [`AbstractCommand`](AbstractCommand.cs) + +带返回值命令的抽象基类。 + +**使用示例:** + +```csharp +// 定义一个计算伤害的命令 +public class CalculateDamageCommand : AbstractCommand +{ + private readonly int _attackPower; + private readonly int _defense; + + public CalculateDamageCommand(int attackPower, int defense) + { + _attackPower = attackPower; + _defense = defense; + } + + protected override int OnExecute() + { + // 获取游戏配置 + var config = this.GetModel(); + + // 计算最终伤害 + var baseDamage = _attackPower - _defense; + var finalDamage = Math.Max(1, baseDamage * config.DamageMultiplier); + + return (int)finalDamage; + } +} + +// 使用带返回值的命令 +public class CombatSystem : AbstractSystem +{ + protected override void OnInit() { } + + public void Attack(Character attacker, Character defender) + { + // 发送命令并获取返回值 + var damage = this.SendCommand( + new CalculateDamageCommand(attacker.AttackPower, defender.Defense) + ); + + // 应用伤害 + defender.Health -= damage; + + // 发送伤害事件 + this.SendEvent(new DamageDealtEvent(attacker, defender, damage)); + } +} +``` + +## 命令的生命周期 + +1. **创建命令**:实例化命令对象,传入必要的参数 +2. **设置架构**:框架自动调用 `SetArchitecture()` 设置架构引用 +3. **执行命令**:调用 `Execute()` 方法,内部委托给 `OnExecute()` +4. **返回结果**:对于带返回值的命令,返回执行结果 +5. **命令销毁**:命令执行完毕后可以被垃圾回收 + +## 使用场景 + +### 1. 用户交互操作 + +```csharp +// UI 按钮点击 +public class SaveGameCommand : AbstractCommand +{ + protected override void OnExecute() + { + var saveSystem = this.GetSystem(); + var playerModel = this.GetModel(); + + saveSystem.SavePlayerData(playerModel); + this.SendEvent(new GameSavedEvent()); + } +} +``` + +### 2. 业务流程控制 + +```csharp +// 关卡切换 +public class LoadLevelCommand : AbstractCommand +{ + private readonly int _levelId; + + public LoadLevelCommand(int levelId) + { + _levelId = levelId; + } + + protected override void OnExecute() + { + var levelSystem = this.GetSystem(); + var uiSystem = this.GetSystem(); + + // 显示加载界面 + uiSystem.ShowLoadingScreen(); + + // 加载关卡 + levelSystem.LoadLevel(_levelId); + + // 发送事件 + this.SendEvent(new LevelLoadedEvent(_levelId)); + } +} +``` + +### 3. 网络请求封装 + +```csharp +// 登录命令 +public class LoginCommand : AbstractCommand +{ + private readonly string _username; + private readonly string _password; + + public LoginCommand(string username, string password) + { + _username = username; + _password = password; + } + + protected override bool OnExecute() + { + var networkSystem = this.GetSystem(); + var playerModel = this.GetModel(); + + // 发送登录请求 + var result = networkSystem.Login(_username, _password); + + if (result.Success) + { + playerModel.UserId = result.UserId; + playerModel.Username = _username; + this.SendEvent(new LoginSuccessEvent()); + return true; + } + else + { + this.SendEvent(new LoginFailedEvent(result.ErrorMessage)); + return false; + } + } +} +``` + +## 命令 vs 系统方法 + +**何时使用命令:** +- 需要参数化操作 +- 需要记录操作历史(用于撤销/重做) +- 操作需要跨多个系统协调 +- 用户触发的离散操作 + +**何时使用系统方法:** +- 持续运行的逻辑(如每帧更新) +- 系统内部的私有逻辑 +- 不需要外部调用的功能 + +## 最佳实践 + +1. **保持命令原子性**:一个命令应该完成一个完整的业务操作 +2. **命令无状态**:命令不应该保存长期状态,执行完即可丢弃 +3. **参数通过构造函数传递**:命令需要的参数应在创建时传入 +4. **避免命令嵌套**:命令内部尽量不要发送其他命令,使用事件通信 +5. **合理使用返回值**:只在确实需要返回结果时使用 `ICommand` +6. **命令命名规范**:使用动词+名词形式,如 `StartGameCommand`、`SavePlayerCommand` + +## 扩展功能 + +### 命令撤销/重做(可扩展) + +```csharp +// 可撤销命令接口 +public interface IUndoableCommand : ICommand +{ + void Undo(); +} + +// 实现可撤销命令 +public class MoveCommand : AbstractCommand, IUndoableCommand +{ + private Vector3 _oldPosition; + private Vector3 _newPosition; + + protected override void OnExecute() + { + var player = this.GetModel(); + _oldPosition = player.Position; + player.Position = _newPosition; + } + + public void Undo() + { + var player = this.GetModel(); + player.Position = _oldPosition; + } +} +``` + +## 相关包 + +- [`architecture`](../architecture/README.md) - 架构核心,负责命令的分发和执行 +- [`extensions`](../extensions/README.md) - 提供 `SendCommand()` 扩展方法 +- [`query`](../query/README.md) - 查询模式,用于数据查询 +- [`events`](../events/README.md) - 事件系统,命令执行后的通知机制 +- [`system`](../system/README.md) - 业务系统,命令的主要执行者 +- [`model`](../model/README.md) - 数据模型,命令操作的数据 \ No newline at end of file diff --git a/framework/controller/IController.cs b/framework/controller/IController.cs new file mode 100644 index 0000000..8dfebd4 --- /dev/null +++ b/framework/controller/IController.cs @@ -0,0 +1,16 @@ +using GFramework.framework.command; +using GFramework.framework.events; +using GFramework.framework.model; +using GFramework.framework.query; +using GFramework.framework.system; +using GFramework.framework.utility; + +namespace GFramework.framework.controller; + +/// +/// 控制器接口,定义了控制器需要实现的所有功能契约 +/// 该接口继承了多个框架核心接口,用于支持控制器的各种能力 +/// 包括架构归属、命令发送、系统获取、模型获取、事件注册、查询发送和工具获取等功能 +/// +public interface IController : ICanSendCommand, ICanGetSystem, ICanGetModel, + ICanRegisterEvent, ICanSendQuery, ICanGetUtility; diff --git a/framework/controller/README.md b/framework/controller/README.md new file mode 100644 index 0000000..15d2f59 --- /dev/null +++ b/framework/controller/README.md @@ -0,0 +1,409 @@ +# Controller 包使用说明 + +## 概述 + +Controller 包定义了控制器(Controller)的接口规范。控制器是 MVC 架构中的 C 层,负责处理用户交互、协调视图和模型,是连接表现层和业务层的桥梁。在本框架中,Controller 通常对应 Godot 的节点脚本。 + +## 核心接口 + +### [`IController`](IController.cs) + +控制器接口,定义了控制器需要实现的所有功能契约。 + +**继承的能力接口:** +- [`ICanSendCommand`](../command/ICanSendCommand.cs) - 可发送命令 +- [`ICanGetSystem`](../system/ICanGetSystem.cs) - 可获取系统 +- [`ICanGetModel`](../model/ICanGetModel.cs) - 可获取模型 +- [`ICanRegisterEvent`](../events/ICanRegisterEvent.cs) - 可注册事件 +- [`ICanSendQuery`](../query/ICanSendQuery.cs) - 可发送查询 +- [`ICanGetUtility`](../utility/ICanGetUtility.cs) - 可获取工具 + +**能力说明:** + +控制器拥有框架中最全面的能力集合,可以: +1. 发送命令执行业务逻辑 +2. 获取系统调用服务 +3. 获取模型读写数据 +4. 注册事件监听变化 +5. 发送查询获取信息 +6. 获取工具使用辅助功能 + +## 使用示例 + +### 基础控制器实现 + +```csharp +using Godot; +using GFramework.framework.controller; +using GFramework.framework.architecture; + +// Godot 节点控制器 +public partial class PlayerController : Node, IController +{ + private IUnRegisterList _unregisterList = new UnRegisterList(); + + // 实现架构获取 + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public override void _Ready() + { + // 获取模型 + var playerModel = this.GetModel(); + + // 监听模型变化 + playerModel.Health.RegisterWithInitValue(OnHealthChanged) + .AddToUnregisterList(_unregisterList); + + // 注册事件 + this.RegisterEvent(OnPlayerLevelUp) + .AddToUnregisterList(_unregisterList); + } + + // 处理用户输入 + public override void _Process(double delta) + { + if (Input.IsActionJustPressed("attack")) + { + // 发送命令 + this.SendCommand(new AttackCommand()); + } + + if (Input.IsActionJustPressed("use_item")) + { + // 发送查询 + var inventory = this.SendQuery(new GetInventoryQuery()); + if (inventory.HasItem("potion")) + { + this.SendCommand(new UseItemCommand("potion")); + } + } + } + + private void OnHealthChanged(int newHealth) + { + // 更新 UI 显示 + UpdateHealthBar(newHealth); + } + + private void OnPlayerLevelUp(PlayerLevelUpEvent e) + { + // 显示升级特效 + ShowLevelUpEffect(); + } + + public override void _ExitTree() + { + // 清理事件注册 + _unregisterList.UnRegisterAll(); + } + + private void UpdateHealthBar(int health) { /* UI 更新逻辑 */ } + private void ShowLevelUpEffect() { /* 特效逻辑 */ } +} +``` + +### UI 控制器示例 + +```csharp +// UI 面板控制器 +public partial class MainMenuController : Control, IController +{ + [Export] private Button _startButton; + [Export] private Button _settingsButton; + [Export] private Button _quitButton; + + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public override void _Ready() + { + // 绑定按钮事件 + _startButton.Pressed += OnStartButtonPressed; + _settingsButton.Pressed += OnSettingsButtonPressed; + _quitButton.Pressed += OnQuitButtonPressed; + + // 获取模型更新 UI + var gameModel = this.GetModel(); + UpdateUI(gameModel); + } + + private void OnStartButtonPressed() + { + // 通过命令启动游戏 + this.SendCommand(); + } + + private void OnSettingsButtonPressed() + { + // 查询当前设置 + var settings = this.SendQuery(new GetSettingsQuery()); + + // 打开设置面板 + var uiSystem = this.GetSystem(); + uiSystem.OpenSettingsPanel(settings); + } + + private void OnQuitButtonPressed() + { + // 发送退出命令 + this.SendCommand(); + } + + private void UpdateUI(GameModel model) { /* UI 更新逻辑 */ } +} +``` + +### 复杂交互控制器 + +```csharp +// 战斗控制器 +public partial class CombatController : Node, IController +{ + private IUnRegisterList _unregisterList = new UnRegisterList(); + private PlayerModel _playerModel; + private CombatSystem _combatSystem; + + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public override void _Ready() + { + // 缓存常用引用 + _playerModel = this.GetModel(); + _combatSystem = this.GetSystem(); + + // 注册多个事件 + this.RegisterEvent(OnEnemySpawned) + .AddToUnregisterList(_unregisterList); + + this.RegisterEvent(OnCombatEnded) + .AddToUnregisterList(_unregisterList); + + // 监听模型状态 + _playerModel.CombatState.Register(OnCombatStateChanged) + .AddToUnregisterList(_unregisterList); + } + + private void OnEnemySpawned(EnemySpawnedEvent e) + { + // 进入战斗状态 + this.SendCommand(new EnterCombatCommand(e.Enemy)); + } + + private void OnCombatEnded(CombatEndedEvent e) + { + if (e.Victory) + { + // 查询奖励 + var rewards = this.SendQuery(new CalculateRewardsQuery(e.Enemy)); + + // 发放奖励 + this.SendCommand(new GiveRewardsCommand(rewards)); + } + else + { + // 处理失败 + this.SendCommand(); + } + } + + private void OnCombatStateChanged(CombatState state) + { + // 根据战斗状态更新 UI + var uiSystem = this.GetSystem(); + uiSystem.UpdateCombatUI(state); + } + + public override void _ExitTree() + { + _unregisterList.UnRegisterAll(); + } +} +``` + +## 控制器职责 + +### ✅ 应该做的事 + +1. **处理用户输入** + - 键盘、鼠标、触摸输入 + - UI 按钮点击 + - 手势识别 + +2. **协调视图和模型** + - 监听模型变化更新视图 + - 将用户操作转换为命令 + +3. **管理界面逻辑** + - UI 元素的显示/隐藏 + - 动画播放控制 + - 视觉反馈 + +4. **事件监听** + - 注册关心的事件 + - 响应事件更新界面 + +### ❌ 不应该做的事 + +1. **不包含业务逻辑** + - 业务逻辑应该在 System 中 + - 控制器只负责调用和协调 + +2. **不直接修改模型** + - 应该通过 Command 修改模型 + - 避免直接访问 Model 的 setter + +3. **不处理复杂计算** + - 复杂计算应该在 Query 或 System 中 + - 控制器保持简洁 + +4. **不保存核心状态** + - 核心状态应该在 Model 中 + - 控制器可以保存临时 UI 状态 + +## 生命周期管理 + +### 事件注销 + +```csharp +public partial class MyController : Node, IController +{ + // 使用 UnRegisterList 统一管理 + private IUnRegisterList _unregisterList = new UnRegisterList(); + + public override void _Ready() + { + // 所有事件注册都添加到列表 + this.RegisterEvent(OnGameEvent) + .AddToUnregisterList(_unregisterList); + + this.GetModel().Health.Register(OnHealthChanged) + .AddToUnregisterList(_unregisterList); + } + + public override void _ExitTree() + { + // 节点销毁时统一注销所有事件 + _unregisterList.UnRegisterAll(); + } +} +``` + +### Godot 特定的生命周期 + +```csharp +public partial class GameController : Node, IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + // 节点进入场景树 + public override void _Ready() + { + // 初始化控制器 + InitializeController(); + } + + // 每帧更新 + public override void _Process(double delta) + { + // 处理实时输入 + HandleInput(); + } + + // 物理帧更新 + public override void _PhysicsProcess(double delta) + { + // 处理物理相关输入 + } + + // 节点即将退出场景树 + public override void _ExitTree() + { + // 清理资源 + CleanupController(); + } +} +``` + +## 最佳实践 + +1. **一个控制器对应一个视图** + - 每个 Godot 场景/节点有对应的控制器 + - 避免一个控制器管理多个不相关的视图 + +2. **使用依赖注入获取依赖** + - 通过 `GetModel()`、`GetSystem()` 获取依赖 + - 不要在构造函数中获取,应在 `_Ready()` 中 + +3. **保持控制器轻量** + - 复杂逻辑放在 Command、Query、System 中 + - 控制器只做协调和转发 + +4. **合理使用缓存** + - 频繁使用的 Model、System 可以缓存引用 + - 平衡性能和内存占用 + +5. **统一管理事件注销** + - 使用 `IUnRegisterList` 统一管理 + - 在 `_ExitTree()` 中统一注销 + +6. **命名规范** + - 控制器类名:`XxxController` + - 继承 Godot 节点:`Node`、`Control`、`Node2D` 等 + +## 常见模式 + +### 数据绑定模式 + +```csharp +public partial class ScoreController : Label, IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public override void _Ready() + { + // 绑定模型数据到 UI + this.GetModel() + .Score + .RegisterWithInitValue(score => Text = $"Score: {score}") + .UnRegisterWhenNodeExitTree(this); + } +} +``` + +### 状态机模式 + +```csharp +public partial class PlayerStateController : Node, IController +{ + private Dictionary _stateHandlers; + + public override void _Ready() + { + _stateHandlers = new Dictionary + { + { PlayerState.Idle, HandleIdleState }, + { PlayerState.Moving, HandleMovingState }, + { PlayerState.Attacking, HandleAttackingState } + }; + + this.GetModel() + .State + .Register(OnStateChanged) + .UnRegisterWhenNodeExitTree(this); + } + + private void OnStateChanged(PlayerState state) + { + _stateHandlers[state]?.Invoke(); + } +} +``` + +## 相关包 + +- [`architecture`](../architecture/README.md) - 提供架构访问能力 +- [`command`](../command/README.md) - 控制器发送命令执行业务逻辑 +- [`query`](../query/README.md) - 控制器发送查询获取数据 +- [`events`](../events/README.md) - 控制器注册事件监听变化 +- [`model`](../model/README.md) - 控制器读取模型数据 +- [`system`](../system/README.md) - 控制器调用系统服务 +- [`extensions`](../extensions/README.md) - 提供便捷的扩展方法 \ No newline at end of file diff --git a/framework/events/DefaultUnRegister.cs b/framework/events/DefaultUnRegister.cs new file mode 100644 index 0000000..f6b4f58 --- /dev/null +++ b/framework/events/DefaultUnRegister.cs @@ -0,0 +1,21 @@ +namespace GFramework.framework.events; + + +/// +/// 默认注销器类,用于执行注销操作 +/// +/// 注销时要执行的回调函数 +public class DefaultUnRegister(Action onUnRegister): IUnRegister +{ + private Action _mOnUnRegister = onUnRegister; + + /// + /// 执行注销操作,调用注册的回调函数并清理引用 + /// + public void UnRegister() + { + // 调用注销回调函数并清理引用 + _mOnUnRegister.Invoke(); + _mOnUnRegister = null; + } +} diff --git a/framework/events/EasyEvent.cs b/framework/events/EasyEvent.cs new file mode 100644 index 0000000..cf53642 --- /dev/null +++ b/framework/events/EasyEvent.cs @@ -0,0 +1,31 @@ +namespace GFramework.framework.events; + +/// +/// 简单事件类,用于注册、注销和触发无参事件回调 +/// +public class EasyEvent +{ + private Action _mOnEvent = () => { }; + + /// + /// 注册事件回调函数 + /// + /// 要注册的事件回调函数 + /// 用于注销事件的 unregister 对象 + public IUnRegister Register(Action onEvent) + { + _mOnEvent += onEvent; + return new DefaultUnRegister(() => { UnRegister(onEvent); }); + } + + /// + /// 注销已注册的事件回调函数 + /// + /// 要注销的事件回调函数 + public void UnRegister(Action onEvent) => _mOnEvent -= onEvent; + + /// + /// 触发所有已注册的事件回调函数 + /// + public void Trigger() => _mOnEvent?.Invoke(); +} diff --git a/framework/events/EasyEventGeneric.cs b/framework/events/EasyEventGeneric.cs new file mode 100644 index 0000000..d75ab97 --- /dev/null +++ b/framework/events/EasyEventGeneric.cs @@ -0,0 +1,154 @@ +namespace GFramework.framework.events; + +/// +/// 泛型事件类,支持一个泛型参数 T 的事件注册、注销与触发。 +/// 实现了 IEasyEvent 接口以提供统一的事件操作接口。 +/// +/// 事件回调函数的第一个参数类型。 +public class EasyEvent : IEasyEvent +{ + /// + /// 存储已注册的事件处理委托。 + /// 默认为空操作(no-op)委托,避免 null 检查。 + /// + private Action _mOnEvent = e => { }; + + /// + /// 注册一个事件监听器,并返回可用于取消注册的对象。 + /// + /// 要注册的事件处理方法。 + /// IUnRegister 对象,用于稍后注销该事件监听器。 + public IUnRegister Register(Action onEvent) + { + _mOnEvent += onEvent; + return new DefaultUnRegister(() => { UnRegister(onEvent); }); + } + + /// + /// 取消指定的事件监听器。 + /// + /// 需要被注销的事件处理方法。 + public void UnRegister(Action onEvent) => _mOnEvent -= onEvent; + + /// + /// 触发所有已注册的事件处理程序,并传递参数 t。 + /// + /// 传递给事件处理程序的参数。 + public void Trigger(T t) => _mOnEvent?.Invoke(t); + + /// + /// 显式实现 IEasyEvent 接口中的 Register 方法。 + /// 允许使用无参 Action 来订阅当前带参事件。 + /// + /// 无参事件处理方法。 + /// IUnRegister 对象,用于稍后注销该事件监听器。 + IUnRegister IEasyEvent.Register(Action onEvent) + { + return Register(Action); + void Action(T _) => onEvent(); + } +} + +/// +/// 支持两个泛型参数 T 和 TK 的事件类。 +/// 提供事件注册、注销和触发功能。 +/// +/// 第一个参数类型。 +/// 第二个参数类型。 +public class EasyEvent : IEasyEvent +{ + /// + /// 存储已注册的双参数事件处理委托。 + /// 默认为空操作(no-op)委托。 + /// + private Action _mOnEvent = (_, _) => { }; + + /// + /// 注册一个接受两个参数的事件监听器,并返回可用于取消注册的对象。 + /// + /// 要注册的事件处理方法。 + /// IUnRegister 对象,用于稍后注销该事件监听器。 + public IUnRegister Register(Action onEvent) + { + _mOnEvent += onEvent; + return new DefaultUnRegister(() => { UnRegister(onEvent); }); + } + + /// + /// 取消指定的双参数事件监听器。 + /// + /// 需要被注销的事件处理方法。 + public void UnRegister(Action onEvent) => _mOnEvent -= onEvent; + + /// + /// 触发所有已注册的事件处理程序,并传递参数 t 和 k。 + /// + /// 第一个参数。 + /// 第二个参数。 + public void Trigger(T t, TK k) => _mOnEvent?.Invoke(t, k); + + /// + /// 显式实现 IEasyEvent 接口中的 Register 方法。 + /// 允许使用无参 Action 来订阅当前带参事件。 + /// + /// 无参事件处理方法。 + /// IUnRegister 对象,用于稍后注销该事件监听器。 + IUnRegister IEasyEvent.Register(Action onEvent) + { + return Register(Action); + void Action(T _, TK __) => onEvent(); + } +} + +/// +/// 支持三个泛型参数 T、TK 和 TS 的事件类。 +/// 提供事件注册、注销和触发功能。 +/// +/// 第一个参数类型。 +/// 第二个参数类型。 +/// 第三个参数类型。 +public class EasyEvent : IEasyEvent +{ + /// + /// 存储已注册的三参数事件处理委托。 + /// 默认为空操作(no-op)委托。 + /// + private Action _mOnEvent = (_, _, _) => { }; + + /// + /// 注册一个接受三个参数的事件监听器,并返回可用于取消注册的对象。 + /// + /// 要注册的事件处理方法。 + /// IUnRegister 对象,用于稍后注销该事件监听器。 + public IUnRegister Register(Action onEvent) + { + _mOnEvent += onEvent; + return new DefaultUnRegister(() => { UnRegister(onEvent); }); + } + + /// + /// 取消指定的三参数事件监听器。 + /// + /// 需要被注销的事件处理方法。 + public void UnRegister(Action onEvent) => _mOnEvent -= onEvent; + + /// + /// 触发所有已注册的事件处理程序,并传递参数 t、k 和 s。 + /// + /// 第一个参数。 + /// 第二个参数。 + /// 第三个参数。 + public void Trigger(T t, TK k, TS s) => _mOnEvent?.Invoke(t, k, s); + + /// + /// 显式实现 IEasyEvent 接口中的 Register 方法。 + /// 允许使用无参 Action 来订阅当前带参事件。 + /// + /// 无参事件处理方法。 + /// IUnRegister 对象,用于稍后注销该事件监听器。 + IUnRegister IEasyEvent.Register(Action onEvent) + { + return Register(Action); + void Action(T _, TK __, TS ___) => onEvent(); + } +} diff --git a/framework/events/EasyEvents.cs b/framework/events/EasyEvents.cs new file mode 100644 index 0000000..6fd74a8 --- /dev/null +++ b/framework/events/EasyEvents.cs @@ -0,0 +1,67 @@ +namespace GFramework.framework.events; + +/// +/// EasyEvents事件管理器类,用于全局事件的注册、获取和管理 +/// 提供了类型安全的事件系统,支持泛型事件的自动创建和检索 +/// +public class EasyEvents +{ + /// + /// 全局单例事件管理器实例 + /// + private static readonly EasyEvents MGlobalEvents = new(); + + /// + /// 获取指定类型的全局事件实例 + /// + /// 事件类型,必须实现IEasyEvent接口 + /// 指定类型的事件实例,如果未注册则返回默认值 + public static T Get() where T : IEasyEvent => MGlobalEvents.GetEvent(); + + /// + /// 注册指定类型的全局事件 + /// + /// 事件类型,必须实现IEasyEvent接口且具有无参构造函数 + public static void Register() where T : IEasyEvent, new() => MGlobalEvents.AddEvent(); + + /// + /// 存储事件类型与事件实例映射关系的字典 + /// + private readonly Dictionary _mTypeEvents = new(); + + /// + /// 添加指定类型的事件到事件字典中 + /// + /// 事件类型,必须实现IEasyEvent接口且具有无参构造函数 + public void AddEvent() where T : IEasyEvent, new() => _mTypeEvents.Add(typeof(T), new T()); + + /// + /// 获取指定类型的事件实例 + /// + /// 事件类型,必须实现IEasyEvent接口 + /// 指定类型的事件实例,如果不存在则返回默认值 + public T GetEvent() where T : IEasyEvent + { + return _mTypeEvents.TryGetValue(typeof(T), out var e) ? (T)e : default; + } + + /// + /// 获取指定类型的事件实例,如果不存在则创建并添加到事件字典中 + /// + /// 事件类型,必须实现IEasyEvent接口且具有无参构造函数 + /// 指定类型的事件实例 + public T GetOrAddEvent() where T : IEasyEvent, new() + { + var eType = typeof(T); + // 尝试从字典中获取事件实例 + if (_mTypeEvents.TryGetValue(eType, out var e)) + { + return (T)e; + } + + // 如果不存在则创建新实例并添加到字典中 + var t = new T(); + _mTypeEvents.Add(eType, t); + return t; + } +} diff --git a/framework/events/ICanRegisterEvent.cs b/framework/events/ICanRegisterEvent.cs new file mode 100644 index 0000000..aa40ee5 --- /dev/null +++ b/framework/events/ICanRegisterEvent.cs @@ -0,0 +1,10 @@ +using GFramework.framework.rule; + +namespace GFramework.framework.events; + +/// +/// 定义一个可以注册事件的接口,继承自IBelongToArchitecture接口。 +/// 该接口用于标识那些能够注册事件的对象,通常在框架的事件系统中使用。 +/// 实现此接口的类型表明它属于某个架构组件,并具备事件注册的能力。 +/// +public interface ICanRegisterEvent:IBelongToArchitecture; diff --git a/framework/events/ICanSendEvent.cs b/framework/events/ICanSendEvent.cs new file mode 100644 index 0000000..d2744e8 --- /dev/null +++ b/framework/events/ICanSendEvent.cs @@ -0,0 +1,9 @@ +using GFramework.framework.rule; + +namespace GFramework.framework.events; + +/// +/// 定义一个可以发送事件的接口,继承自IBelongToArchitecture接口。 +/// 该接口用于标识那些具备发送事件能力的类型,通常作为架构中事件发送功能的基础接口。 +/// +public interface ICanSendEvent : IBelongToArchitecture; diff --git a/framework/events/IEasyEvent.cs b/framework/events/IEasyEvent.cs new file mode 100644 index 0000000..34e7a78 --- /dev/null +++ b/framework/events/IEasyEvent.cs @@ -0,0 +1,14 @@ +namespace GFramework.framework.events; + +/// +/// 事件接口,定义了事件注册的基本功能 +/// +public interface IEasyEvent +{ + /// + /// 注册事件处理函数 + /// + /// 事件触发时要执行的回调函数 + /// 用于取消注册的句柄对象 + IUnRegister Register(Action onEvent); +} \ No newline at end of file diff --git a/framework/events/IUnRegister.cs b/framework/events/IUnRegister.cs new file mode 100644 index 0000000..13575b9 --- /dev/null +++ b/framework/events/IUnRegister.cs @@ -0,0 +1,12 @@ +namespace GFramework.framework.events; + +/// +/// 提供注销功能的接口 +/// +public interface IUnRegister +{ + /// + /// 执行注销操作 + /// + void UnRegister(); +} diff --git a/framework/events/IUnRegisterList.cs b/framework/events/IUnRegisterList.cs new file mode 100644 index 0000000..3f15959 --- /dev/null +++ b/framework/events/IUnRegisterList.cs @@ -0,0 +1,12 @@ +namespace GFramework.framework.events; + +/// +/// 提供统一注销功能的接口,用于管理需要注销的对象列表 +/// +public interface IUnRegisterList +{ + /// + /// 获取需要注销的对象列表 + /// + List UnregisterList { get; } +} diff --git a/framework/events/OrEvent.cs b/framework/events/OrEvent.cs new file mode 100644 index 0000000..07a587f --- /dev/null +++ b/framework/events/OrEvent.cs @@ -0,0 +1,53 @@ +using GFramework.framework.extensions; + +namespace GFramework.framework.events; + +/// +/// OrEvent类用于实现事件的或逻辑组合,当任意一个注册的事件触发时,都会触发OrEvent本身 +/// +public class OrEvent : IUnRegisterList +{ + /// + /// 将指定的事件与当前OrEvent进行或逻辑组合 + /// + /// 要组合的事件对象 + /// 返回当前OrEvent实例,支持链式调用 + public OrEvent Or(IEasyEvent easyEvent) + { + easyEvent.Register(Trigger).AddToUnregisterList(this); + return this; + } + + private Action _mOnEvent = () => { }; + + /// + /// 注册事件处理函数 + /// + /// 要注册的事件处理函数 + /// 返回一个可取消注册的对象 + public IUnRegister Register(Action onEvent) + { + _mOnEvent += onEvent; + return new DefaultUnRegister(() => { UnRegister(onEvent); }); + } + + /// + /// 取消注册指定的事件处理函数 + /// + /// 要取消注册的事件处理函数 + public void UnRegister(Action onEvent) + { + _mOnEvent -= onEvent; + this.UnRegisterAll(); + } + + /// + /// 触发所有已注册的事件处理函数 + /// + private void Trigger() => _mOnEvent?.Invoke(); + + /// + /// 获取取消注册列表 + /// + public List UnregisterList { get; } = new List(); +} diff --git a/framework/events/README.md b/framework/events/README.md new file mode 100644 index 0000000..2b03473 --- /dev/null +++ b/framework/events/README.md @@ -0,0 +1,503 @@ +# Events 包使用说明 + +## 概述 + +Events 包提供了一套完整的事件系统,实现了观察者模式(Observer Pattern)。通过事件系统,可以实现组件间的松耦合通信,支持无参和带参事件、事件注册/注销、以及灵活的事件组合。 + +## 核心接口 + +### 1. [`IEasyEvent`](IEasyEvent.cs) + +基础事件接口,定义了事件注册的基本功能。 + +**核心方法:** +```csharp +IUnRegister Register(Action onEvent); // 注册事件处理函数 +``` + +### 2. [`IUnRegister`](IUnRegister.cs) + +注销接口,用于取消事件注册。 + +**核心方法:** +```csharp +void UnRegister(); // 执行注销操作 +``` + +### 3. [`IUnRegisterList`](IUnRegisterList.cs) + +注销列表接口,用于批量管理注销对象。 + +**属性:** +```csharp +List UnregisterList { get; } // 获取注销列表 +``` + +### 4. 能力接口 + +- [`ICanRegisterEvent`](ICanRegisterEvent.cs) - 标记可以注册事件的组件 +- [`ICanSendEvent`](ICanSendEvent.cs) - 标记可以发送事件的组件 + +## 核心类 + +### 1. [`EasyEvent`](EasyEvent.cs) + +无参事件类,支持注册、注销和触发无参事件。 + +**使用示例:** + +```csharp +// 创建事件 +var onClicked = new EasyEvent(); + +// 注册监听 +var unregister = onClicked.Register(() => +{ + GD.Print("Button clicked!"); +}); + +// 触发事件 +onClicked.Trigger(); + +// 取消注册 +unregister.UnRegister(); +``` + +### 2. [`EasyEvent`](EasyEventGeneric.cs) + +单参数泛型事件类,支持一个参数的事件。 + +**使用示例:** + +```csharp +// 创建带参数的事件 +var onScoreChanged = new EasyEvent(); + +// 注册监听 +onScoreChanged.Register(newScore => +{ + GD.Print($"Score changed to: {newScore}"); +}); + +// 触发事件并传递参数 +onScoreChanged.Trigger(100); +``` + +### 3. [`EasyEvent`](EasyEventGeneric.cs) + +双参数泛型事件类。 + +**使用示例:** + +```csharp +// 伤害事件:攻击者、伤害值 +var onDamageDealt = new EasyEvent(); + +onDamageDealt.Register((attacker, damage) => +{ + GD.Print($"{attacker} dealt {damage} damage!"); +}); + +onDamageDealt.Trigger("Player", 50); +``` + +### 4. [`EasyEvent`](EasyEventGeneric.cs) + +三参数泛型事件类。 + +**使用示例:** + +```csharp +// 位置变化事件:对象、旧位置、新位置 +var onPositionChanged = new EasyEvent(); + +onPositionChanged.Register((obj, oldPos, newPos) => +{ + GD.Print($"{obj} moved from {oldPos} to {newPos}"); +}); + +onPositionChanged.Trigger("Player", Vector3.Zero, new Vector3(10, 0, 0)); +``` + +### 5. [`EasyEvents`](EasyEvents.cs) + +全局事件管理器,提供类型安全的事件注册和获取。 + +**使用示例:** + +```csharp +// 注册全局事件类型 +EasyEvents.Register(); + +// 获取事件实例 +var gameStartEvent = EasyEvents.Get(); + +// 注册监听 +gameStartEvent.Register(() => +{ + GD.Print("Game started!"); +}); + +// 触发事件 +gameStartEvent.Trigger(); +``` + +### 6. [`TypeEventSystem`](TypeEventSystem.cs) + +类型化事件系统,支持基于类型的事件发送和注册。 + +**使用示例:** + +```csharp +// 使用全局事件系统 +var eventSystem = TypeEventSystem.Global; + +// 注册类型化事件 +eventSystem.Register(e => +{ + GD.Print($"Player died at position: {e.Position}"); +}); + +// 发送事件(自动创建实例) +eventSystem.Send(); + +// 发送事件(传递实例) +eventSystem.Send(new PlayerDiedEvent +{ + Position = new Vector3(10, 0, 5) +}); +``` + +### 7. [`DefaultUnRegister`](DefaultUnRegister.cs) + +默认注销器实现,封装注销回调。 + +**使用示例:** + +```csharp +Action onUnregister = () => GD.Print("Unregistered"); +var unregister = new DefaultUnRegister(onUnregister); + +// 执行注销 +unregister.UnRegister(); +``` + +### 8. [`OrEvent`](OrEvent.cs) + +事件或运算组合器,当任意一个事件触发时触发。 + +**使用示例:** + +```csharp +var onAnyInput = new OrEvent() + .Or(onKeyPressed) + .Or(onMouseClicked) + .Or(onTouchDetected); + +// 当上述任意事件触发时,执行回调 +onAnyInput.Register(() => +{ + GD.Print("Input detected!"); +}); +``` + +## 在架构中使用事件 + +### 定义事件类 + +```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() + { + // 监听生命值变化 + Health.Register(newHealth => + { + if (newHealth <= 0) + { + // 发送玩家死亡事件 + this.SendEvent(new PlayerDiedEvent + { + Position = Position, + Cause = "Health depleted" + }); + } + }); + } +} +``` + +### 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 + }); + } +} +``` + +### Controller 中注册事件 + +```csharp +public partial class GameController : Node, IController +{ + private IUnRegisterList _unregisterList = new UnRegisterList(); + + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public override void _Ready() + { + // 注册多个事件 + this.RegisterEvent(OnGameStarted) + .AddToUnregisterList(_unregisterList); + + this.RegisterEvent(OnPlayerDied) + .AddToUnregisterList(_unregisterList); + + this.RegisterEvent(OnLevelCompleted) + .AddToUnregisterList(_unregisterList); + } + + private void OnGameStarted(GameStartedEvent e) + { + GD.Print("Game started!"); + } + + private void OnPlayerDied(PlayerDiedEvent e) + { + GD.Print($"Player died at {e.Position}: {e.Cause}"); + ShowGameOverScreen(); + } + + private void OnLevelCompleted(LevelCompletedEvent e) + { + GD.Print($"Level {e.LevelId} completed! Score: {e.Score}"); + ShowVictoryScreen(e); + } + + public override void _ExitTree() + { + _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.Now + }); + }); + } +} +``` + +### 4. 临时事件监听 + +```csharp +public class TutorialController : Node, IController +{ + public override void _Ready() + { + // 只监听一次 + 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 +public class MyController : Node, IController +{ + // 统一管理所有注销对象 + private IUnRegisterList _unregisterList = new UnRegisterList(); + + public override void _Ready() + { + // 所有注册都添加到列表 + this.RegisterEvent(OnEvent1) + .AddToUnregisterList(_unregisterList); + + this.RegisterEvent(OnEvent2) + .AddToUnregisterList(_unregisterList); + } + + public override void _ExitTree() + { + // 一次性注销所有 + _unregisterList.UnRegisterAll(); + } +} +``` + +### 使用 Godot 节点生命周期 + +```csharp +public override void _Ready() +{ + // 当节点退出场景树时自动注销 + this.RegisterEvent(OnGameEvent) + .UnRegisterWhenNodeExitTree(this); +} +``` + +## 最佳实践 + +1. **事件命名规范** + - 使用过去式:`PlayerDiedEvent`、`LevelCompletedEvent` + - 使用 `Event` 后缀:便于识别 + - 使用结构体:减少内存分配 + +2. **事件数据设计** + - 只包含必要信息 + - 使用值类型(struct)提高性能 + - 避免传递可变引用 + +3. **避免事件循环** + - 事件处理器中谨慎发送新事件 + - 使用命令打破循环依赖 + +4. **合理使用事件** + - 用于通知状态变化 + - 用于跨模块通信 + - 不用于返回数据(使用 Query) + +5. **注销管理** + - 始终注销事件监听 + - 使用 `IUnRegisterList` 批量管理 + - 利用 Godot 节点生命周期 + +6. **性能考虑** + - 避免频繁触发的事件(如每帧) + - 事件处理器保持轻量 + - 使用结构体事件减少 GC + +## 事件 vs 其他通信方式 + +| 方式 | 适用场景 | 优点 | 缺点 | +|------|---------|------|------| +| **Event** | 状态变化通知、跨模块通信 | 松耦合、一对多 | 难以追踪调用链 | +| **Command** | 执行操作、修改状态 | 封装逻辑、可撤销 | 单向通信 | +| **Query** | 查询数据 | 职责清晰、有返回值 | 同步调用 | +| **BindableProperty** | UI 数据绑定 | 自动更新、响应式 | 仅限单一属性 | + +## 相关包 + +- [`architecture`](../architecture/README.md) - 提供全局事件系统 +- [`extensions`](../extensions/README.md) - 提供事件扩展方法 +- [`property`](../property/README.md) - 可绑定属性基于事件实现 +- [`controller`](../controller/README.md) - 控制器监听事件 +- [`model`](../model/README.md) - 模型发送事件 +- [`system`](../system/README.md) - 系统发送和监听事件 \ No newline at end of file diff --git a/framework/events/TypeEventSystem.cs b/framework/events/TypeEventSystem.cs new file mode 100644 index 0000000..4be8834 --- /dev/null +++ b/framework/events/TypeEventSystem.cs @@ -0,0 +1,23 @@ +namespace GFramework.framework.events; + +/// +/// TypeEventSystem +/// +public class TypeEventSystem +{ + private readonly EasyEvents _mEvents = new(); + + public static readonly TypeEventSystem Global = new(); + + public void Send() where T : new() => _mEvents.GetEvent>()?.Trigger(new T()); + + public void Send(T e) => _mEvents.GetEvent>()?.Trigger(e); + + public IUnRegister Register(Action onEvent) => _mEvents.GetOrAddEvent>().Register(onEvent); + + public void UnRegister(Action onEvent) + { + var e = _mEvents.GetEvent>(); + e?.UnRegister(onEvent); + } +} \ No newline at end of file diff --git a/framework/events/UnRegisterList.cs b/framework/events/UnRegisterList.cs new file mode 100644 index 0000000..855db26 --- /dev/null +++ b/framework/events/UnRegisterList.cs @@ -0,0 +1,39 @@ +namespace GFramework.framework.events; + + +/// +/// 取消注册列表类,用于管理多个需要取消注册的对象 +/// +public class UnRegisterList : IUnRegisterList +{ + private readonly List _unRegisterList = []; + + /// + /// 向取消注册列表中添加一个新的可取消注册对象 + /// + /// 需要添加到列表中的可取消注册对象 + public void Add(IUnRegister unRegister) + { + _unRegisterList.Add(unRegister); + } + + /// + /// 对列表中的所有对象执行取消注册操作,并清空列表 + /// + public void UnRegisterAll() + { + // 遍历所有注册项并执行取消注册 + foreach (var t in _unRegisterList) + { + t.UnRegister(); + } + + // 清空列表 + _unRegisterList.Clear(); + } + + /// + /// 获取取消注册列表的只读属性 + /// + public List UnregisterList { get; } +} diff --git a/framework/extensions/CanGetExtensions.cs b/framework/extensions/CanGetExtensions.cs new file mode 100644 index 0000000..03bace6 --- /dev/null +++ b/framework/extensions/CanGetExtensions.cs @@ -0,0 +1,50 @@ +using GFramework.framework.model; +using GFramework.framework.system; +using GFramework.framework.utility; + +namespace GFramework.framework.extensions; + +/// +/// 提供获取模型对象扩展方法的静态类 +/// +public static class CanGetModelExtension +{ + /// + /// 从架构中获取指定类型的模型实例 + /// + /// 要获取的模型类型,必须实现IModel接口 + /// 实现ICanGetModel接口的对象实例 + /// 指定类型的模型实例 + public static T GetModel(this ICanGetModel self) where T : class, IModel => + self.GetArchitecture().GetModel(); +} + +/// +/// 提供获取系统对象扩展方法的静态类 +/// +public static class CanGetSystemExtension +{ + /// + /// 从架构中获取指定类型的系统实例 + /// + /// 要获取的系统类型,必须实现ISystem接口 + /// 实现ICanGetSystem接口的对象实例 + /// 指定类型的系统实例 + public static T GetSystem(this ICanGetSystem self) where T : class, ISystem => + self.GetArchitecture().GetSystem(); +} + +/// +/// 提供获取工具对象扩展方法的静态类 +/// +public static class CanGetUtilityExtension +{ + /// + /// 从架构中获取指定类型的工具实例 + /// + /// 要获取的工具类型,必须实现IUtility接口 + /// 实现ICanGetUtility接口的对象实例 + /// 指定类型的工具实例 + public static T GetUtility(this ICanGetUtility self) where T : class, IUtility => + self.GetArchitecture().GetUtility(); +} \ No newline at end of file diff --git a/framework/extensions/CanRegisterEventExtensions.cs b/framework/extensions/CanRegisterEventExtensions.cs new file mode 100644 index 0000000..3e73a79 --- /dev/null +++ b/framework/extensions/CanRegisterEventExtensions.cs @@ -0,0 +1,28 @@ +using GFramework.framework.events; + +namespace GFramework.framework.extensions; + +/// +/// 事件注册扩展类,提供ICanRegisterEvent接口的扩展方法用于注册和注销事件 +/// +public static class CanRegisterEventExtensions +{ + /// + /// 注册指定类型的事件处理函数 + /// + /// 事件数据类型 + /// 实现ICanRegisterEvent接口的对象实例 + /// 事件处理回调函数 + /// 返回事件注销器接口,可用于后续注销事件 + public static IUnRegister RegisterEvent(this ICanRegisterEvent self, Action onEvent) => + self.GetArchitecture().RegisterEvent(onEvent); + + /// + /// 注销指定类型的事件处理函数 + /// + /// 事件数据类型 + /// 实现ICanRegisterEvent接口的对象实例 + /// 要注销的事件处理回调函数 + public static void UnRegisterEvent(this ICanRegisterEvent self, Action onEvent) => + self.GetArchitecture().UnRegisterEvent(onEvent); +} diff --git a/framework/extensions/CanSendExtensions.cs b/framework/extensions/CanSendExtensions.cs new file mode 100644 index 0000000..a5ff398 --- /dev/null +++ b/framework/extensions/CanSendExtensions.cs @@ -0,0 +1,76 @@ +using GFramework.framework.command; +using GFramework.framework.events; +using GFramework.framework.query; + +namespace GFramework.framework.extensions; + +/// +/// 提供发送命令功能的扩展类 +/// +public static class CanSendCommandExtension +{ + /// + /// 发送指定类型的命令,该命令类型必须实现ICommand接口且具有无参构造函数 + /// + /// 命令类型,必须实现ICommand接口且具有无参构造函数 + /// 实现ICanSendCommand接口的对象实例 + public static void SendCommand(this ICanSendCommand self) where T : ICommand, new() => + self.GetArchitecture().SendCommand(new T()); + + /// + /// 发送指定的命令实例 + /// + /// 命令类型,必须实现ICommand接口 + /// 实现ICanSendCommand接口的对象实例 + /// 要发送的命令实例 + public static void SendCommand(this ICanSendCommand self, T command) where T : ICommand => + self.GetArchitecture().SendCommand(command); + + /// + /// 发送带有返回值的命令并获取执行结果 + /// + /// 命令执行结果的类型 + /// 实现ICanSendCommand接口的对象实例 + /// 要发送的命令实例,必须实现ICommand<TResult>接口 + /// 命令执行后的返回结果 + public static TResult SendCommand(this ICanSendCommand self, ICommand command) => + self.GetArchitecture().SendCommand(command); +} + +/// +/// 提供发送事件功能的扩展类 +/// +public static class CanSendEventExtension +{ + /// + /// 发送指定类型的事件,该事件类型必须具有无参构造函数 + /// + /// 事件类型,必须具有无参构造函数 + /// 实现ICanSendEvent接口的对象实例 + public static void SendEvent(this ICanSendEvent self) where T : new() => + self.GetArchitecture().SendEvent(); + + /// + /// 发送指定的事件实例 + /// + /// 事件类型 + /// 实现ICanSendEvent接口的对象实例 + /// 要发送的事件实例 + public static void SendEvent(this ICanSendEvent self, T e) => self.GetArchitecture().SendEvent(e); +} + +/// +/// 提供发送查询功能的扩展类 +/// +public static class CanSendQueryExtension +{ + /// + /// 发送查询请求并获取查询结果 + /// + /// 查询结果的类型 + /// 实现ICanSendQuery接口的对象实例 + /// 要发送的查询实例,必须实现IQuery<TResult>接口 + /// 查询操作的返回结果 + public static TResult SendQuery(this ICanSendQuery self, IQuery query) => + self.GetArchitecture().SendQuery(query); +} diff --git a/framework/extensions/OrEventExtensions.cs b/framework/extensions/OrEventExtensions.cs new file mode 100644 index 0000000..72d3c9c --- /dev/null +++ b/framework/extensions/OrEventExtensions.cs @@ -0,0 +1,17 @@ +using GFramework.framework.events; + +namespace GFramework.framework.extensions; + +/// +/// 提供Or事件扩展方法的静态类 +/// +public static class OrEventExtensions +{ + /// + /// 创建一个OrEvent实例,将当前事件与指定事件进行逻辑或运算组合 + /// + /// 当前的IEasyEvent事件实例 + /// 要与当前事件进行或运算的另一个IEasyEvent事件实例 + /// 返回一个新的OrEvent实例,表示两个事件的或运算结果 + public static OrEvent Or(this IEasyEvent self, IEasyEvent e) => new OrEvent().Or(self).Or(e); +} \ No newline at end of file diff --git a/framework/extensions/README.md b/framework/extensions/README.md new file mode 100644 index 0000000..dad7ab7 --- /dev/null +++ b/framework/extensions/README.md @@ -0,0 +1,503 @@ +# Extensions 包使用说明 + +## 概述 + +Extensions 包提供了一系列扩展方法,简化了框架各个接口的使用。通过扩展方法,可以用更简洁的语法访问框架功能,提高代码可读性和开发效率。 + +## 扩展方法类别 + +### 1. 获取组件扩展 ([`CanGetExtensions.cs`](CanGetExtensions.cs)) + +为 [`ICanGetModel`](../model/ICanGetModel.cs)、[`ICanGetSystem`](../system/ICanGetSystem.cs)、[`ICanGetUtility`](../utility/ICanGetUtility.cs) 提供扩展方法。 + +#### CanGetModelExtension + +```csharp +public static T GetModel(this ICanGetModel self) where T : class, IModel +``` + +**使用示例:** + +```csharp +// 在 Controller、Command、Query 中使用 +public class PlayerController : IController +{ + public void UpdateUI() + { + // 直接通过 this 调用 + var playerModel = this.GetModel(); + var inventoryModel = this.GetModel(); + } +} +``` + +#### CanGetSystemExtension + +```csharp +public static T GetSystem(this ICanGetSystem self) where T : class, ISystem +``` + +**使用示例:** + +```csharp +public class SaveCommand : AbstractCommand +{ + protected override void OnExecute() + { + // 获取系统 + var saveSystem = this.GetSystem(); + var networkSystem = this.GetSystem(); + + saveSystem.SaveGame(); + } +} +``` + +#### CanGetUtilityExtension + +```csharp +public static T GetUtility(this ICanGetUtility self) where T : class, IUtility +``` + +**使用示例:** + +```csharp +public class GameModel : AbstractModel +{ + protected override void OnInit() + { + // 获取工具 + var timeUtility = this.GetUtility(); + var storageUtility = this.GetUtility(); + } +} +``` + +### 2. 发送命令扩展 ([`CanSendExtensions.cs`](CanSendExtensions.cs)) + +为 [`ICanSendCommand`](../command/ICanSendCommand.cs) 提供扩展方法。 + +#### CanSendCommandExtension + +```csharp +// 发送无参命令(通过类型) +public static void SendCommand(this ICanSendCommand self) + where T : ICommand, new() + +// 发送命令实例 +public static void SendCommand(this ICanSendCommand self, T command) + where T : ICommand + +// 发送带返回值的命令 +public static TResult SendCommand(this ICanSendCommand self, + ICommand command) +``` + +**使用示例:** + +```csharp +public class GameController : IController +{ + public void OnStartButtonClicked() + { + // 方式1:通过类型发送(需要无参构造函数) + this.SendCommand(); + + // 方式2:发送命令实例 + this.SendCommand(new LoadLevelCommand(levelId: 1)); + + // 方式3:发送带返回值的命令 + var score = this.SendCommand(new CalculateScoreCommand()); + } +} +``` + +### 3. 发送事件扩展 ([`CanSendExtensions.cs`](CanSendExtensions.cs)) + +为 [`ICanSendEvent`](../events/ICanSendEvent.cs) 提供扩展方法。 + +#### CanSendEventExtension + +```csharp +// 发送无参事件 +public static void SendEvent(this ICanSendEvent self) where T : new() + +// 发送事件实例 +public static void SendEvent(this ICanSendEvent self, T e) +``` + +**使用示例:** + +```csharp +public class PlayerModel : AbstractModel +{ + public void TakeDamage(int damage) + { + Health -= damage; + + if (Health <= 0) + { + // 方式1:发送无参事件 + this.SendEvent(); + + // 方式2:发送带数据的事件 + this.SendEvent(new PlayerDiedEvent + { + Position = Position, + Cause = "Damage" + }); + } + } +} +``` + +### 4. 发送查询扩展 ([`CanSendExtensions.cs`](CanSendExtensions.cs)) + +为 [`ICanSendQuery`](../query/ICanSendQuery.cs) 提供扩展方法。 + +#### CanSendQueryExtension + +```csharp +public static TResult SendQuery(this ICanSendQuery self, + IQuery query) +``` + +**使用示例:** + +```csharp +public class InventoryController : IController +{ + public void ShowInventory() + { + // 发送查询获取数据 + var items = this.SendQuery(new GetInventoryItemsQuery()); + var gold = this.SendQuery(new GetPlayerGoldQuery()); + + UpdateInventoryUI(items, gold); + } +} +``` + +### 5. 注册事件扩展 ([`CanRegisterEventExtensions.cs`](CanRegisterEventExtensions.cs)) + +为 [`ICanRegisterEvent`](../events/ICanRegisterEvent.cs) 提供扩展方法。 + +#### CanRegisterEventExtensions + +```csharp +// 注册事件 +public static IUnRegister RegisterEvent(this ICanRegisterEvent self, + Action onEvent) + +// 注销事件 +public static void UnRegisterEvent(this ICanRegisterEvent self, + Action onEvent) +``` + +**使用示例:** + +```csharp +public class GameController : Node, IController +{ + private IUnRegisterList _unregisterList = new UnRegisterList(); + + public override void _Ready() + { + // 注册事件监听 + this.RegisterEvent(OnGameStarted) + .AddToUnregisterList(_unregisterList); + + this.RegisterEvent(OnPlayerLevelUp) + .AddToUnregisterList(_unregisterList); + } + + private void OnGameStarted(GameStartedEvent e) { } + private void OnPlayerLevelUp(PlayerLevelUpEvent e) { } +} +``` + +### 6. OrEvent 扩展 ([`OrEventExtensions.cs`](OrEventExtensions.cs)) + +为 [`IEasyEvent`](../events/IEasyEvent.cs) 提供事件组合功能。 + +#### OrEventExtensions + +```csharp +public static OrEvent Or(this IEasyEvent self, IEasyEvent e) +``` + +**使用示例:** + +```csharp +// 组合多个事件:当任意一个触发时执行 +var onAnyInput = onKeyPressed.Or(onMouseClicked).Or(onTouchDetected); + +onAnyInput.Register(() => +{ + GD.Print("Any input detected!"); +}); + +// 链式组合 +var onAnyDamage = onPhysicalDamage + .Or(onMagicDamage) + .Or(onPoisonDamage); +``` + +### 7. UnRegister 扩展 ([`UnRegisterExtension.cs`](UnRegisterExtension.cs)) + +为 [`IUnRegister`](../events/IUnRegister.cs) 提供 Godot 生命周期绑定。 + +#### UnRegisterExtension + +```csharp +public static IUnRegister UnRegisterWhenNodeExitTree(this IUnRegister unRegister, + Node node) +``` + +**使用示例:** + +```csharp +#if GODOT +public class PlayerController : Node, IController +{ + public override void _Ready() + { + // 当节点退出场景树时自动注销 + this.RegisterEvent(OnGameEvent) + .UnRegisterWhenNodeExitTree(this); + + this.GetModel() + .Health + .Register(OnHealthChanged) + .UnRegisterWhenNodeExitTree(this); + } + + // 不需要手动在 _ExitTree 中注销 +} +#endif +``` + +### 8. UnRegisterList 扩展 ([`UnRegisterListExtension.cs`](UnRegisterListExtension.cs)) + +为 [`IUnRegister`](../events/IUnRegister.cs) 和 [`IUnRegisterList`](../events/IUnRegisterList.cs) 提供批量管理功能。 + +#### UnRegisterListExtension + +```csharp +// 添加到注销列表 +public static void AddToUnregisterList(this IUnRegister self, + IUnRegisterList unRegisterList) + +// 批量注销 +public static void UnRegisterAll(this IUnRegisterList self) +``` + +**使用示例:** + +```csharp +public class ComplexController : Node, IController +{ + private IUnRegisterList _unregisterList = new UnRegisterList(); + + public override void _Ready() + { + // 所有注册都添加到列表中 + this.RegisterEvent(OnEvent1) + .AddToUnregisterList(_unregisterList); + + this.RegisterEvent(OnEvent2) + .AddToUnregisterList(_unregisterList); + + this.GetModel().Property1.Register(OnProperty1Changed) + .AddToUnregisterList(_unregisterList); + + this.GetModel().Property2.Register(OnProperty2Changed) + .AddToUnregisterList(_unregisterList); + } + + public override void _ExitTree() + { + // 一次性注销所有 + _unregisterList.UnRegisterAll(); + } +} +``` + +## 完整使用示例 + +### Controller 示例 + +```csharp +public partial class GameplayController : Node, IController +{ + private IUnRegisterList _unregisterList = new UnRegisterList(); + + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public override void _Ready() + { + // 使用扩展方法获取 Model + var playerModel = this.GetModel(); + var gameModel = this.GetModel(); + + // 使用扩展方法注册事件 + this.RegisterEvent(OnGameStarted) + .AddToUnregisterList(_unregisterList); + + // 监听可绑定属性 + playerModel.Health.Register(OnHealthChanged) + .AddToUnregisterList(_unregisterList); + + // 或者使用 Godot 特定的自动注销 + gameModel.Score.Register(OnScoreChanged) + .UnRegisterWhenNodeExitTree(this); + } + + public override void _Process(double delta) + { + if (Input.IsActionJustPressed("attack")) + { + // 使用扩展方法发送命令 + this.SendCommand(new AttackCommand(targetId: 1)); + } + + if (Input.IsActionJustPressed("use_item")) + { + // 使用扩展方法发送查询 + var hasPotion = this.SendQuery(new HasItemQuery("health_potion")); + if (hasPotion) + { + this.SendCommand(); + } + } + } + + private void OnGameStarted(GameStartedEvent e) + { + GD.Print("Game started!"); + } + + private void OnHealthChanged(int health) + { + UpdateHealthBar(health); + } + + private void OnScoreChanged(int score) + { + UpdateScoreDisplay(score); + } + + public override void _ExitTree() + { + _unregisterList.UnRegisterAll(); + } +} +``` + +### Command 示例 + +```csharp +public class ComplexGameCommand : AbstractCommand +{ + protected override void OnExecute() + { + // 获取多个组件 + var playerModel = this.GetModel(); + var gameSystem = this.GetSystem(); + var timeUtility = this.GetUtility(); + + // 执行业务逻辑 + var currentTime = timeUtility.GetCurrentTime(); + gameSystem.ProcessGameLogic(playerModel, currentTime); + + // 发送事件通知 + this.SendEvent(new GameStateChangedEvent()); + + // 可以发送其他命令(谨慎使用) + this.SendCommand(); + } +} +``` + +### System 示例 + +```csharp +public class AchievementSystem : AbstractSystem +{ + protected override void OnInit() + { + // 注册事件监听 + this.RegisterEvent(OnEnemyKilled); + this.RegisterEvent(OnLevelCompleted); + } + + private void OnEnemyKilled(EnemyKilledEvent e) + { + // 获取模型 + var playerModel = this.GetModel(); + playerModel.EnemyKillCount++; + + // 检查成就 + if (playerModel.EnemyKillCount >= 100) + { + // 发送成就解锁事件 + this.SendEvent(new AchievementUnlockedEvent + { + AchievementId = "kill_100_enemies" + }); + } + } + + private void OnLevelCompleted(LevelCompletedEvent e) + { + // 发送查询 + var completionTime = this.SendQuery(new GetLevelTimeQuery(e.LevelId)); + + if (completionTime < 60) + { + this.SendEvent(new AchievementUnlockedEvent + { + AchievementId = "speed_runner" + }); + } + } +} +``` + +## 扩展方法的优势 + +1. **简洁的语法**:不需要显式调用 `GetArchitecture()` +2. **类型安全**:编译时检查类型 +3. **可读性高**:代码意图更清晰 +4. **智能提示**:IDE 可以提供完整的自动补全 +5. **链式调用**:支持流式编程风格 + +## 注意事项 + +1. **确保引用命名空间**: + ```csharp + using GFramework.framework.extensions; + ``` + +2. **理解扩展方法本质**: + - 扩展方法是静态方法的语法糖 + - 不会改变原始类型的结构 + - 仅在编译时解析 + +3. **Godot 特定功能**: + - `UnRegisterWhenNodeExitTree` 仅在 Godot 环境下可用 + - 使用 `#if GODOT` 编译指令控制 + +4. **性能考虑**: + - 扩展方法本身无性能开销 + - 实际调用的是底层方法 + +## 相关包 + +- [`architecture`](../architecture/README.md) - 扩展方法最终调用架构方法 +- [`command`](../command/README.md) - 命令发送扩展 +- [`query`](../query/README.md) - 查询发送扩展 +- [`events`](../events/README.md) - 事件注册和 Or 组合扩展 +- [`model`](../model/README.md) - 模型获取扩展 +- [`system`](../system/README.md) - 系统获取扩展 +- [`utility`](../utility/README.md) - 工具获取扩展 \ No newline at end of file diff --git a/framework/extensions/UnRegisterExtension.cs b/framework/extensions/UnRegisterExtension.cs new file mode 100644 index 0000000..2646b7d --- /dev/null +++ b/framework/extensions/UnRegisterExtension.cs @@ -0,0 +1,26 @@ +#if GODOT +using Godot; +using GFramework.framework.events; + +namespace GFramework.framework.extensions; + +/// +/// 提供取消注册扩展方法的静态类 +/// +public static class UnRegisterExtension +{ + /// + /// 当节点退出场景树时自动取消注册监听器 + /// + /// 需要在节点退出时被取消注册的监听器接口实例 + /// Godot节点对象,当该节点退出场景树时触发取消注册操作 + /// 返回传入的原始IUnRegister实例,支持链式调用 + public static IUnRegister UnRegisterWhenNodeExitTree(this IUnRegister unRegister, Node node) + { + // 监听节点的TreeExiting事件,在节点即将退出场景树时执行取消注册操作 + node.TreeExiting += unRegister.UnRegister; + return unRegister; + } +} + +#endif \ No newline at end of file diff --git a/framework/extensions/UnRegisterListExtension.cs b/framework/extensions/UnRegisterListExtension.cs new file mode 100644 index 0000000..9eb3dd9 --- /dev/null +++ b/framework/extensions/UnRegisterListExtension.cs @@ -0,0 +1,34 @@ +using GFramework.framework.events; + +namespace GFramework.framework.extensions; + + +/// +/// 扩展方法类,为IUnRegister和IUnRegisterList接口提供便捷的注册和注销功能 +/// +public static class UnRegisterListExtension +{ + /// + /// 将指定的可注销对象添加到注销列表中 + /// + /// 要添加的可注销对象 + /// 目标注销列表 + public static void AddToUnregisterList(this IUnRegister self, IUnRegisterList unRegisterList) => + unRegisterList.UnregisterList.Add(self); + + /// + /// 注销列表中的所有对象并清空列表 + /// + /// 包含注销列表的对象 + public static void UnRegisterAll(this IUnRegisterList self) + { + // 遍历注销列表中的所有对象并执行注销操作 + foreach (var unRegister in self.UnregisterList) + { + unRegister.UnRegister(); + } + + // 清空注销列表 + self.UnregisterList.Clear(); + } +} diff --git a/framework/ioc/IocContainer.cs b/framework/ioc/IocContainer.cs new file mode 100644 index 0000000..aee059e --- /dev/null +++ b/framework/ioc/IocContainer.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +namespace GFramework.framework.ioc; + +/// +/// IOC容器类,用于管理对象的注册和获取 +/// +public class IocContainer +{ + private readonly Dictionary _mInstances = new(); + + /// + /// 注册一个实例到IOC容器中 + /// + /// 实例的类型 + /// 要注册的实例对象 + public void Register(T instance) + { + var key = typeof(T); + + _mInstances[key] = instance; + } + + /// + /// 从IOC容器中获取指定类型的实例 + /// + /// 要获取的实例类型 + /// 返回指定类型的实例,如果未找到则返回null + public T Get() where T : class + { + var key = typeof(T); + + // 尝试从字典中获取实例 + if (_mInstances.TryGetValue(key, out var retInstance)) + { + return retInstance as T; + } + + return null; + } +} diff --git a/framework/ioc/README.md b/framework/ioc/README.md new file mode 100644 index 0000000..b70c6fc --- /dev/null +++ b/framework/ioc/README.md @@ -0,0 +1,446 @@ +# IoC 包使用说明 + +## 概述 + +IoC(Inversion of Control,控制反转)包提供了一个轻量级的依赖注入容器,用于管理框架中各种组件的注册和获取。通过 IoC 容器,可以实现组件间的解耦,便于测试和维护。 + +## 核心类 + +### [`IocContainer`](IocContainer.cs) + +IoC 容器类,负责管理对象的注册和获取。 + +**主要功能:** +- 注册实例到容器 +- 从容器中获取实例 +- 类型安全的依赖管理 + +## 核心方法 + +### 1. Register + +注册一个实例到容器中。 + +```csharp +public void Register(T instance) +``` + +**参数:** +- `instance`: 要注册的实例对象 + +**使用示例:** + +```csharp +var container = new IocContainer(); + +// 注册各种类型的实例 +container.Register(new PlayerModel()); +container.Register(new GameSystem()); +container.Register(new StorageUtility()); +``` + +### 2. Get + +从容器中获取指定类型的实例。 + +```csharp +public T Get() where T : class +``` + +**返回值:** +- 返回指定类型的实例,如果未找到则返回 `null` + +**使用示例:** + +```csharp +// 获取已注册的实例 +var playerModel = container.Get(); +var gameSystem = container.Get(); + +// 如果类型未注册,返回 null +var unknownService = container.Get(); // null +``` + +## 在框架中的使用 + +### Architecture 中的应用 + +IoC 容器是 [`Architecture`](../architecture/Architecture.cs) 类的核心组件,用于管理所有的 System、Model 和 Utility。 + +```csharp +public abstract class Architecture : IArchitecture where T : Architecture, new() +{ + // 内置 IoC 容器 + private readonly IocContainer _mContainer = new(); + + // 注册系统 + public void RegisterSystem(TSystem system) where TSystem : ISystem + { + system.SetArchitecture(this); + _mContainer.Register(system); // 注册到容器 + // ... + } + + // 获取系统 + public TSystem GetSystem() where TSystem : class, ISystem + => _mContainer.Get(); // 从容器获取 + + // Model 和 Utility 同理 +} +``` + +### 注册组件到容器 + +```csharp +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 这些方法内部都使用 IoC 容器 + + // 注册 Model(存储游戏数据) + RegisterModel(new PlayerModel()); + RegisterModel(new InventoryModel()); + + // 注册 System(业务逻辑) + RegisterSystem(new GameplaySystem()); + RegisterSystem(new SaveSystem()); + + // 注册 Utility(工具类) + RegisterUtility(new TimeUtility()); + RegisterUtility(new StorageUtility()); + } +} +``` + +### 从容器获取组件 + +```csharp +// 通过扩展方法间接使用 IoC 容器 +public class PlayerController : IController +{ + public void Start() + { + // GetModel 内部调用 Architecture.GetModel + // Architecture.GetModel 内部调用 IocContainer.Get + var playerModel = this.GetModel(); + + var gameplaySystem = this.GetSystem(); + var timeUtility = this.GetUtility(); + } +} +``` + +## 工作原理 + +### 内部实现 + +```csharp +public class IocContainer +{ + // 使用字典存储类型到实例的映射 + private readonly Dictionary _mInstances = new(); + + public void Register(T instance) + { + var key = typeof(T); + _mInstances[key] = instance; // 注册或覆盖 + } + + public T Get() where T : class + { + var key = typeof(T); + if (_mInstances.TryGetValue(key, out var retInstance)) + { + return retInstance as T; // 类型转换 + } + return null; + } +} +``` + +### 注册流程 + +``` +用户代码 + ↓ +RegisterSystem(system) + ↓ +IocContainer.Register(system) + ↓ +Dictionary[typeof(T)] = system +``` + +### 获取流程 + +``` +用户代码 + ↓ +this.GetSystem() + ↓ +Architecture.GetSystem() + ↓ +IocContainer.Get() + ↓ +Dictionary.TryGetValue(typeof(T)) + ↓ +返回实例或 null +``` + +## 使用示例 + +### 基础使用 + +```csharp +// 1. 创建容器 +var container = new IocContainer(); + +// 2. 注册服务 +var playerService = new PlayerService(); +container.Register(playerService); + +// 3. 获取服务 +var service = container.Get(); +service.DoSomething(); +``` + +### 接口和实现分离 + +```csharp +// 定义接口 +public interface IDataService +{ + void SaveData(string data); + string LoadData(); +} + +// 实现类 +public class LocalDataService : IDataService +{ + public void SaveData(string data) { /* 本地存储 */ } + public string LoadData() { /* 本地加载 */ return ""; } +} + +public class CloudDataService : IDataService +{ + public void SaveData(string data) { /* 云端存储 */ } + public string LoadData() { /* 云端加载 */ return ""; } +} + +// 注册(可以根据配置选择不同实现) +var container = new IocContainer(); + +#if CLOUD_SAVE +container.Register(new CloudDataService()); +#else +container.Register(new LocalDataService()); +#endif + +// 使用(不需要关心具体实现) +var dataService = container.Get(); +dataService.SaveData("game data"); +``` + +### 覆盖注册 + +```csharp +var container = new IocContainer(); + +// 首次注册 +container.Register(new DefaultConfig()); + +// 后续注册会覆盖 +container.Register(new CustomConfig()); + +// 获取到的是最后注册的实例 +var config = container.Get(); // CustomConfig +``` + +## 设计特点 + +### 1. 简单轻量 + +- 只有两个核心方法:`Register` 和 `Get` +- 基于字典实现,性能高效 +- 无复杂的依赖解析逻辑 + +### 2. 手动注册 + +- 需要显式注册每个组件 +- 不支持自动依赖注入 +- 完全可控的组件生命周期 + +### 3. 单例模式 + +- 每个类型只能注册一个实例 +- 适合管理全局单例服务 +- 后续注册会覆盖前面的实例 + +### 4. 类型安全 + +- 基于泛型,编译时类型检查 +- 避免字符串键导致的错误 +- IDE 友好,支持自动补全 + +## 与其他 IoC 容器的区别 + +### 本框架的 IocContainer + +```csharp +// 简单直接 +var container = new IocContainer(); +container.Register(new MyService()); +var service = container.Get(); +``` + +**特点:** +- ✅ 简单易用 +- ✅ 性能高 +- ❌ 不支持构造函数注入 +- ❌ 不支持自动解析依赖 +- ❌ 不支持生命周期管理(Transient/Scoped/Singleton) + +### 完整的 IoC 框架(如 Autofac、Zenject) + +```csharp +// 复杂但功能强大 +var builder = new ContainerBuilder(); +builder.RegisterType().As().SingleInstance(); +builder.RegisterType().WithParameter("config", config); +var container = builder.Build(); + +// 自动解析依赖 +var controller = container.Resolve(); +``` + +**特点:** +- ✅ 自动依赖注入 +- ✅ 生命周期管理 +- ✅ 复杂场景支持 +- ❌ 学习成本高 +- ❌ 性能开销大 + +## 最佳实践 + +### 1. 在架构初始化时注册 + +```csharp +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 按顺序注册组件 + // 1. 工具类(无依赖) + RegisterUtility(new TimeUtility()); + RegisterUtility(new StorageUtility()); + + // 2. 模型(可能依赖工具) + RegisterModel(new PlayerModel()); + RegisterModel(new GameModel()); + + // 3. 系统(可能依赖模型和工具) + RegisterSystem(new GameplaySystem()); + RegisterSystem(new SaveSystem()); + } +} +``` + +### 2. 使用接口类型注册 + +```csharp +// ❌ 不推荐:直接使用实现类 +RegisterSystem(new ConcreteSystem()); +var system = GetSystem(); + +// ✅ 推荐:使用接口 +RegisterSystem(new ConcreteSystem()); +var system = GetSystem(); +``` + +### 3. 避免运行时频繁注册 + +```csharp +// ❌ 不好:游戏运行时频繁注册 +void Update() +{ + RegisterService(new TempService()); // 每帧创建 +} + +// ✅ 好:在初始化时一次性注册 +protected override void Init() +{ + RegisterService(new PersistentService()); +} +``` + +### 4. 检查 null 返回值 + +```csharp +// 获取可能不存在的服务 +var service = container.Get(); +if (service != null) +{ + service.DoSomething(); +} +else +{ + GD.Print("Service not registered!"); +} +``` + +## 注意事项 + +1. **类型唯一性** + - 每个类型只能注册一个实例 + - 重复注册会覆盖之前的实例 + +2. **手动管理依赖顺序** + - 组件的依赖关系需要手动保证 + - 先注册被依赖的组件 + +3. **无生命周期管理** + - 实例一旦注册就一直存在 + - 需要手动管理实例的生命周期 + +4. **线程安全** + - 当前实现非线程安全 + - 避免多线程同时访问 + +## 扩展可能性 + +如果需要更复杂的功能,可以扩展 `IocContainer`: + +```csharp +// 支持工厂模式 +public class AdvancedIocContainer : IocContainer +{ + private Dictionary> _factories = new(); + + public void RegisterFactory(Func factory) where T : class + { + _factories[typeof(T)] = () => factory(); + } + + public new T Get() where T : class + { + // 先尝试获取实例 + var instance = base.Get(); + if (instance != null) return instance; + + // 如果没有实例,尝试使用工厂创建 + if (_factories.TryGetValue(typeof(T), out var factory)) + { + return factory() as T; + } + + return null; + } +} +``` + +## 相关包 + +- [`architecture`](../architecture/README.md) - 使用 IoC 容器管理所有组件 +- [`model`](../model/README.md) - Model 通过 IoC 容器注册和获取 +- [`system`](../system/README.md) - System 通过 IoC 容器注册和获取 +- [`utility`](../utility/README.md) - Utility 通过 IoC 容器注册和获取 \ No newline at end of file diff --git a/framework/model/AbstractModel.cs b/framework/model/AbstractModel.cs new file mode 100644 index 0000000..5049692 --- /dev/null +++ b/framework/model/AbstractModel.cs @@ -0,0 +1,37 @@ +using GFramework.framework.architecture; +using GFramework.framework.model; + +namespace GFramework.framework.model; + +/// +/// 抽象模型基类,实现IModel接口,提供模型的基本架构支持 +/// +public abstract class AbstractModel : IModel +{ + /// + /// 模型所属的架构实例 + /// + protected IArchitecture Architecture; + + /// + /// 获取模型所属的架构实例 + /// + /// 返回当前模型关联的架构对象 + public IArchitecture GetArchitecture() => Architecture; + + /// + /// 设置模型所属的架构实例 + /// + /// 要关联到此模型的架构实例 + public void SetArchitecture(IArchitecture architecture) => Architecture = architecture; + + /// + /// 初始化模型,调用抽象方法OnInit执行具体初始化逻辑 + /// + void IModel.Init() => OnInit(); + + /// + /// 抽象初始化方法,由子类实现具体的初始化逻辑 + /// + protected abstract void OnInit(); +} diff --git a/framework/model/ICanGetModel.cs b/framework/model/ICanGetModel.cs new file mode 100644 index 0000000..a549b77 --- /dev/null +++ b/framework/model/ICanGetModel.cs @@ -0,0 +1,9 @@ +using GFramework.framework.rule; + +namespace GFramework.framework.model; + +/// +/// 定义一个接口,表示可以获取模型的架构组件。 +/// 该接口继承自IBelongToArchitecture,表明实现此接口的类型属于特定架构。 +/// +public interface ICanGetModel : IBelongToArchitecture; diff --git a/framework/model/IModel.cs b/framework/model/IModel.cs new file mode 100644 index 0000000..a7a6478 --- /dev/null +++ b/framework/model/IModel.cs @@ -0,0 +1,16 @@ +using GFramework.framework.events; +using GFramework.framework.rule; +using GFramework.framework.utility; + +namespace GFramework.framework.model; + +/// +/// 模型接口,定义了模型的基本行为和功能 +/// +public interface IModel : ICanSetArchitecture, ICanGetUtility, ICanSendEvent +{ + /// + /// 初始化模型 + /// + void Init(); +} diff --git a/framework/model/README.md b/framework/model/README.md new file mode 100644 index 0000000..82461ae --- /dev/null +++ b/framework/model/README.md @@ -0,0 +1,140 @@ +# Model 包使用说明 + +## 概述 + +Model 包定义了数据模型层的接口和基类。Model 是 MVC 架构中的 M 层,负责管理应用程序的数据和状态。Model 层应该只包含数据和简单的数据逻辑,不包含复杂的业务逻辑。 + +## 核心接口 + +### [`IModel`](IModel.cs) + +模型接口,定义了模型的基本行为和功能。 + +**继承的能力接口:** +- [`ICanSetArchitecture`](../rule/ICanSetArchitecture.cs) - 可设置架构引用 +- [`ICanGetUtility`](../utility/ICanGetUtility.cs) - 可获取工具类 +- [`ICanSendEvent`](../events/ICanSendEvent.cs) - 可发送事件 + +**核心方法:** +```csharp +void Init(); // 初始化模型 +``` + +### [`ICanGetModel`](ICanGetModel.cs) + +标记接口,表示实现者可以获取模型。继承自 [`IBelongToArchitecture`](../rule/IBelongToArchitecture.cs)。 + +## 核心类 + +### [`AbstractModel`](AbstractModel.cs) + +抽象模型基类,提供模型的基础实现。 + +**使用示例:** + +```csharp +public class PlayerModel : AbstractModel +{ + // 使用 BindableProperty 定义可监听的属性 + public BindableProperty Name { get; } = new("Player"); + public BindableProperty Level { get; } = new(1); + public BindableProperty Health { get; } = new(100); + public BindableProperty MaxHealth { get; } = new(100); + + protected override void OnInit() + { + // 监听生命值变化 + Health.Register(newHealth => + { + if (newHealth <= 0) + { + this.SendEvent(new PlayerDiedEvent()); + } + }); + } +} +``` + +## Model 的职责 + +### ✅ 应该做的事 + +1. **存储数据和状态** +2. **提供数据访问接口** +3. **监听自身属性变化并做出响应** +4. **发送数据变化事件** + +### ❌ 不应该做的事 + +1. **不包含复杂业务逻辑** - 业务逻辑应该在 System 中 +2. **不直接依赖其他 Model** - 通过 Command 协调 +3. **不包含 UI 逻辑** - UI 逻辑应该在 Controller 中 + +## 常见 Model 示例 + +### 玩家数据模型 + +```csharp +public class PlayerModel : AbstractModel +{ + public BindableProperty PlayerId { get; } = new(""); + public BindableProperty PlayerName { get; } = new("Player"); + public BindableProperty Level { get; } = new(1); + public BindableProperty Health { get; } = new(100); + public BindableProperty MaxHealth { get; } = new(100); + public BindableProperty Gold { get; } = new(0); + + public Vector3 Position { get; set; } + + protected override void OnInit() + { + Health.Register(hp => + { + if (hp <= 0) + { + this.SendEvent(new PlayerDiedEvent()); + } + }); + } +} +``` + +### 游戏状态模型 + +```csharp +public class GameModel : AbstractModel +{ + public BindableProperty State { get; } = new(GameState.Menu); + public BindableProperty Score { get; } = new(0); + public BindableProperty HighScore { get; } = new(0); + public BindableProperty CurrentLevel { get; } = new(1); + + protected override void OnInit() + { + Score.Register(newScore => + { + if (newScore > HighScore.Value) + { + HighScore.Value = newScore; + this.SendEvent(new NewHighScoreEvent { Score = newScore }); + } + }); + } +} +``` + +## 最佳实践 + +1. **使用 BindableProperty 存储需要监听的数据** +2. **Model 之间不要直接调用,通过 Command/System 协调** +3. **复杂计算和业务逻辑放在 System 中** +4. **使用事件通知数据的重要变化** +5. **保持 Model 简单纯粹,只做数据管理** + +## 相关包 + +- [`architecture`](../architecture/README.md) - 提供 Model 的注册和获取 +- [`property`](../property/README.md) - BindableProperty 用于定义可监听属性 +- [`events`](../events/README.md) - Model 发送事件通知变化 +- [`utility`](../utility/README.md) - Model 可以使用工具类 +- [`extensions`](../extensions/README.md) - 提供 GetModel 等扩展方法 \ No newline at end of file diff --git a/framework/property/BindableProperty.cs b/framework/property/BindableProperty.cs new file mode 100644 index 0000000..5eaf36a --- /dev/null +++ b/framework/property/BindableProperty.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using GFramework.framework.events; + +namespace GFramework.framework.property; + + +/// +/// 可绑定属性类,用于实现数据绑定功能 +/// +/// 属性值的类型 +/// 属性的默认值 +public class BindableProperty(T defaultValue = default) : IBindableProperty +{ + protected T MValue = defaultValue; + + /// + /// 获取或设置属性值比较器,默认使用Equals方法进行比较 + /// + public static Func Comparer { get; set; } = (a, b) => a.Equals(b); + + /// + /// 设置自定义比较器 + /// + /// 用于比较两个值是否相等的函数 + /// 当前可绑定属性实例 + public BindableProperty WithComparer(Func comparer) + { + Comparer = comparer; + return this; + } + + /// + /// 获取或设置属性值,当值发生变化时会触发注册的回调事件 + /// + public T Value + { + get => GetValue(); + set + { + // 使用 default(T) 替代 null 比较,避免 SonarQube 警告 + if (EqualityComparer.Default.Equals(value, default) && + EqualityComparer.Default.Equals(MValue, default)) + return; + + // 若新值与旧值相等则不执行后续操作 + if (!EqualityComparer.Default.Equals(value, default) && Comparer(value, MValue)) + return; + + SetValue(value); + _mOnValueChanged?.Invoke(value); + } + } + + /// + /// 设置属性值的虚方法,可在子类中重写 + /// + /// 新的属性值 + protected virtual void SetValue(T newValue) => MValue = newValue; + + /// + /// 获取属性值的虚方法,可在子类中重写 + /// + /// 当前属性值 + protected virtual T GetValue() => MValue; + + /// + /// 直接设置属性值而不触发事件 + /// + /// 新的属性值 + public void SetValueWithoutEvent(T newValue) => MValue = newValue; + + private Action _mOnValueChanged = (_) => { }; + + /// + /// 注册属性值变化事件回调 + /// + /// 属性值变化时的回调函数 + /// 可用于取消注册的接口 + public IUnRegister Register(Action onValueChanged) + { + _mOnValueChanged += onValueChanged; + return new BindablePropertyUnRegister(this, onValueChanged); + } + + /// + /// 注册属性值变化事件回调,并立即调用回调函数传递当前值 + /// + /// 属性值变化时的回调函数 + /// 可用于取消注册的接口 + public IUnRegister RegisterWithInitValue(Action action) + { + action(MValue); + return Register(action); + } + + /// + /// 取消注册属性值变化事件回调 + /// + /// 要取消注册的回调函数 + public void UnRegister(Action onValueChanged) => _mOnValueChanged -= onValueChanged; + + /// + /// 实现IEasyEvent接口的注册方法,将无参事件转换为有参事件处理 + /// + /// 无参事件回调 + /// 可用于取消注册的接口 + IUnRegister IEasyEvent.Register(Action onEvent) + { + return Register(Action); + void Action(T _) => onEvent(); + } + + /// + /// 返回属性值的字符串表示形式 + /// + /// 属性值的字符串表示 + public override string ToString() => Value.ToString(); +} + diff --git a/framework/property/BindablePropertyUnRegister.cs b/framework/property/BindablePropertyUnRegister.cs new file mode 100644 index 0000000..a9418f5 --- /dev/null +++ b/framework/property/BindablePropertyUnRegister.cs @@ -0,0 +1,38 @@ +using System; +using GFramework.framework.events; + +namespace GFramework.framework.property; + + +/// +/// 可绑定属性注销器类,用于取消注册可绑定属性的值变化监听 +/// +/// 可绑定属性的值类型 +/// 需要注销的可绑定属性实例 +/// 需要注销的值变化回调函数 +public class BindablePropertyUnRegister(BindableProperty bindableProperty, Action onValueChanged) + : IUnRegister +{ + /// + /// 获取或设置可绑定属性实例 + /// + public BindableProperty BindableProperty { get; set; } = bindableProperty; + + /// + /// 获取或设置值变化时的回调函数 + /// + public Action OnValueChanged { get; set; } = onValueChanged; + + /// + /// 执行注销操作,取消注册值变化监听并清理引用 + /// + public void UnRegister() + { + // 调用可绑定属性的注销方法,传入值变化回调函数 + BindableProperty.UnRegister(OnValueChanged); + // 清理属性引用 + BindableProperty = null; + // 清理回调函数引用 + OnValueChanged = null; + } +} diff --git a/framework/property/IBindableProperty.cs b/framework/property/IBindableProperty.cs new file mode 100644 index 0000000..c677184 --- /dev/null +++ b/framework/property/IBindableProperty.cs @@ -0,0 +1,19 @@ +namespace GFramework.framework.property; + +/// +/// 可绑定属性接口,继承自只读可绑定属性接口,提供可读写的属性绑定功能 +/// +/// 属性值的类型 +public interface IBindableProperty : IReadonlyBindableProperty +{ + /// + /// 获取或设置属性的值 + /// + new T Value { get; set; } + + /// + /// 设置属性值但不触发事件通知 + /// + /// 要设置的新值 + void SetValueWithoutEvent(T newValue); +} diff --git a/framework/property/IReadonlyBindableProperty.cs b/framework/property/IReadonlyBindableProperty.cs new file mode 100644 index 0000000..9274e65 --- /dev/null +++ b/framework/property/IReadonlyBindableProperty.cs @@ -0,0 +1,37 @@ +using System; +using GFramework.framework.events; + +namespace GFramework.framework.property; + + +/// +/// 只读可绑定属性接口,提供属性值的读取和变更监听功能 +/// +/// 属性值的类型 +public interface IReadonlyBindableProperty : IEasyEvent +{ + /// + /// 获取属性的当前值 + /// + T Value { get; } + + /// + /// 注册属性值变更回调,并立即执行一次初始值的回调 + /// + /// 属性值变更时执行的回调函数,参数为新的属性值 + /// 用于取消注册的句柄对象 + IUnRegister RegisterWithInitValue(Action action); + + /// + /// 取消注册属性值变更回调 + /// + /// 要取消注册的回调函数 + void UnRegister(Action onValueChanged); + + /// + /// 注册属性值变更回调 + /// + /// 属性值变更时执行的回调函数,参数为新的属性值 + /// 用于取消注册的句柄对象 + IUnRegister Register(Action onValueChanged); +} diff --git a/framework/property/README.md b/framework/property/README.md new file mode 100644 index 0000000..1f3db01 --- /dev/null +++ b/framework/property/README.md @@ -0,0 +1,297 @@ +# Property 包使用说明 + +## 概述 + +Property 包提供了可绑定属性(BindableProperty)的实现,支持属性值的监听和响应式编程。这是实现 MVVM 模式和数据绑定的核心组件。 + +## 核心接口 + +### [`IReadonlyBindableProperty`](IReadonlyBindableProperty.cs) + +只读可绑定属性接口,提供属性值的读取和变更监听功能。 + +**核心成员:** +```csharp +T Value { get; } // 获取属性值 +IUnRegister Register(Action onValueChanged); // 注册监听 +IUnRegister RegisterWithInitValue(Action action); // 注册并立即回调当前值 +void UnRegister(Action onValueChanged); // 取消监听 +``` + +### [`IBindableProperty`](IBindableProperty.cs) + +可绑定属性接口,继承自只读接口,增加了修改能力。 + +**核心成员:** +```csharp +new T Value { get; set; } // 可读写的属性值 +void SetValueWithoutEvent(T newValue); // 设置值但不触发事件 +``` + +## 核心类 + +### [`BindableProperty`](BindableProperty.cs) + +可绑定属性的完整实现。 + +**使用示例:** + +```csharp +// 创建可绑定属性 +var health = new BindableProperty(100); + +// 监听值变化 +var unregister = health.Register(newValue => +{ + GD.Print($"Health changed to: {newValue}"); +}); + +// 修改值(会触发监听器) +health.Value = 50; // 输出: Health changed to: 50 + +// 取消监听 +unregister.UnRegister(); + +// 设置值但不触发事件 +health.SetValueWithoutEvent(75); +``` + +**高级功能:** + +```csharp +// 1. 注册并立即获得当前值 +health.RegisterWithInitValue(value => +{ + GD.Print($"Current health: {value}"); // 立即输出当前值 + // 后续值变化时也会调用 +}); + +// 2. 自定义比较器 +var position = new BindableProperty(Vector3.Zero) + .WithComparer((a, b) => a.DistanceTo(b) < 0.01f); // 距离小于0.01认为相等 + +// 3. 链式调用 +health.Value = 100; +``` + +### [`BindablePropertyUnRegister`](BindablePropertyUnRegister.cs) + +可绑定属性的注销器,负责清理监听。 + +## 在 Model 中使用 + +### 定义可绑定属性 + +```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 IReadonlyBindableProperty ReadonlyHealth => Health; + + protected override void OnInit() + { + // 内部监听属性变化 + Health.Register(hp => + { + if (hp <= 0) + { + this.SendEvent(new PlayerDiedEvent()); + } + }); + } +} +``` + +## 在 Controller 中监听 + +### UI 数据绑定 + +```csharp +public partial class PlayerUI : Control, IController +{ + [Export] private Label _healthLabel; + [Export] private Label _nameLabel; + + private IUnRegisterList _unregisterList = new UnRegisterList(); + + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public override void _Ready() + { + var playerModel = this.GetModel(); + + // 绑定生命值到UI(立即显示当前值) + playerModel.Health + .RegisterWithInitValue(health => + { + _healthLabel.Text = $"HP: {health}/{playerModel.MaxHealth.Value}"; + }) + .AddToUnregisterList(_unregisterList); + + // 绑定名称 + playerModel.Name + .RegisterWithInitValue(name => + { + _nameLabel.Text = name; + }) + .AddToUnregisterList(_unregisterList); + } + + public override void _ExitTree() + { + _unregisterList.UnRegisterAll(); + } +} +``` + +## 常见使用模式 + +### 1. 双向绑定 + +```csharp +// Model +public class SettingsModel : AbstractModel +{ + public BindableProperty MasterVolume { get; } = new(1.0f); + protected override void OnInit() { } +} + +// UI Controller +public partial class VolumeSlider : HSlider, IController +{ + private BindableProperty _volumeProperty; + + public override void _Ready() + { + _volumeProperty = this.GetModel().MasterVolume; + + // Model -> UI + _volumeProperty.RegisterWithInitValue(vol => Value = vol) + .UnRegisterWhenNodeExitTree(this); + + // UI -> Model + ValueChanged += newValue => _volumeProperty.Value = (float)newValue; + } +} +``` + +### 2. 计算属性 + +```csharp +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(); // 初始计算 + } +} +``` + +### 3. 属性验证 + +```csharp +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. 条件监听 + +```csharp +public 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. 避免频繁触发 + +```csharp +// 使用 SetValueWithoutEvent 批量修改 +public void LoadPlayerData(SaveData data) +{ + // 临时关闭事件 + Health.SetValueWithoutEvent(data.Health); + Mana.SetValueWithoutEvent(data.Mana); + Gold.SetValueWithoutEvent(data.Gold); + + // 最后统一触发一次更新事件 + this.SendEvent(new PlayerDataLoadedEvent()); +} +``` + +### 2. 自定义比较器 + +```csharp +// 避免浮点数精度问题导致的频繁触发 +var position = new BindableProperty() + .WithComparer((a, b) => a.DistanceTo(b) < 0.001f); +``` + +## 最佳实践 + +1. **在 Model 中定义属性** - BindableProperty 主要用于 Model 层 +2. **使用只读接口暴露** - 防止外部随意修改 +3. **及时注销监听** - 使用 UnRegisterList 或 UnRegisterWhenNodeExitTree +4. **使用 RegisterWithInitValue** - UI 绑定时立即获取初始值 +5. **避免循环依赖** - 属性监听器中修改其他属性要小心 + +## 相关包 + +- [`model`](../model/README.md) - Model 中大量使用 BindableProperty +- [`events`](../events/README.md) - BindableProperty 基于事件系统实现 +- [`controller`](../controller/README.md) - Controller 监听属性变化更新 UI +- [`extensions`](../extensions/README.md) - 提供便捷的注销扩展方法 \ No newline at end of file diff --git a/framework/query/AbstractQuery.cs b/framework/query/AbstractQuery.cs new file mode 100644 index 0000000..ab73f7b --- /dev/null +++ b/framework/query/AbstractQuery.cs @@ -0,0 +1,36 @@ +using GFramework.framework.architecture; + +namespace GFramework.framework.query; + +/// +/// 抽象查询类,提供查询操作的基础实现 +/// +/// 查询结果的类型 +public abstract class AbstractQuery : IQuery +{ + /// + /// 执行查询操作 + /// + /// 查询结果 + public T Do() => OnDo(); + + /// + /// 抽象方法,由子类实现具体的查询逻辑 + /// + /// 查询结果 + protected abstract T OnDo(); + + private IArchitecture _mArchitecture; + + /// + /// 获取架构实例 + /// + /// 架构实例 + public IArchitecture GetArchitecture() => _mArchitecture; + + /// + /// 设置架构实例 + /// + /// 要设置的架构实例 + public void SetArchitecture(IArchitecture architecture) => _mArchitecture = architecture; +} diff --git a/framework/query/ICanSendQuery.cs b/framework/query/ICanSendQuery.cs new file mode 100644 index 0000000..1664b9e --- /dev/null +++ b/framework/query/ICanSendQuery.cs @@ -0,0 +1,9 @@ +using GFramework.framework.rule; + +namespace GFramework.framework.query; + +/// +/// 定义一个可以发送查询的接口契约 +/// 该接口继承自IBelongToArchitecture,表示实现此接口的类型属于某个架构组件 +/// +public interface ICanSendQuery : IBelongToArchitecture; diff --git a/framework/query/IQuery.cs b/framework/query/IQuery.cs new file mode 100644 index 0000000..28f8d74 --- /dev/null +++ b/framework/query/IQuery.cs @@ -0,0 +1,20 @@ +using GFramework.framework.model; +using GFramework.framework.rule; +using GFramework.framework.system; + +namespace GFramework.framework.query; + + +/// +/// 查询接口,定义了执行查询操作的契约 +/// +/// 查询结果的类型 +public interface IQuery : ICanSetArchitecture, ICanGetModel, ICanGetSystem, + ICanSendQuery +{ + /// + /// 执行查询操作并返回结果 + /// + /// 查询的结果,类型为 TResult + TResult Do(); +} diff --git a/framework/query/README.md b/framework/query/README.md new file mode 100644 index 0000000..9f8d849 --- /dev/null +++ b/framework/query/README.md @@ -0,0 +1,395 @@ +# Query 包使用说明 + +## 概述 + +Query 包实现了 CQRS(命令查询职责分离)模式中的查询部分。Query 用于封装数据查询逻辑,与 Command 不同的是,Query 有返回值且不应该修改系统状态。 + +## 核心接口 + +### [`ICanSendQuery`](ICanSendQuery.cs) + +标记接口,表示该类型可以发送查询。 + +**继承关系:** +```csharp +public interface ICanSendQuery : IBelongToArchitecture +``` + +### [`IQuery`](IQuery.cs) + +查询接口,定义了查询的基本契约。 + +**核心成员:** +```csharp +TResult Do(); // 执行查询并返回结果 +``` + +**继承的能力:** +- `ICanSetArchitecture` - 可设置架构 +- `ICanGetModel` - 可获取 Model +- `ICanGetSystem` - 可获取 System +- `ICanSendQuery` - 可发送其他 Query + +## 核心类 + +### [`AbstractQuery`](AbstractQuery.cs) + +抽象查询基类,提供了查询的基础实现。 + +**使用方式:** +```csharp +public abstract class AbstractQuery : IQuery +{ + public T Do() => OnDo(); + protected abstract T OnDo(); // 子类实现查询逻辑 +} +``` + +## 基本使用 + +### 1. 定义查询 + +```csharp +// 查询玩家金币数量 +public class GetPlayerGoldQuery : AbstractQuery +{ + protected override int OnDo() + { + return this.GetModel().Gold.Value; + } +} + +// 查询玩家是否死亡 +public class IsPlayerDeadQuery : AbstractQuery +{ + protected override bool OnDo() + { + return this.GetModel().Health.Value <= 0; + } +} + +// 查询背包中指定物品的数量 +public class GetItemCountQuery : AbstractQuery +{ + public string ItemId { get; set; } + + protected override int OnDo() + { + var inventory = this.GetModel(); + return inventory.GetItemCount(ItemId); + } +} +``` + +### 2. 发送查询(在 Controller 中) + +```csharp +public partial class ShopUI : Control, IController +{ + [Export] private Button _buyButton; + [Export] private int _itemPrice = 100; + + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public override void _Ready() + { + _buyButton.Pressed += OnBuyButtonPressed; + } + + private void OnBuyButtonPressed() + { + // 查询玩家金币 + int playerGold = this.SendQuery(new GetPlayerGoldQuery()); + + if (playerGold >= _itemPrice) + { + // 发送购买命令 + this.SendCommand(new BuyItemCommand { ItemId = "sword_01" }); + } + else + { + GD.Print("金币不足!"); + } + } +} +``` + +### 3. 在 System 中使用 + +```csharp +public class CombatSystem : AbstractSystem +{ + protected override void OnInit() + { + // 注册事件监听 + this.RegisterEvent(OnEnemyAttack); + } + + private void OnEnemyAttack(EnemyAttackEvent e) + { + // 查询玩家是否已经死亡 + bool isDead = this.SendQuery(new IsPlayerDeadQuery()); + + if (!isDead) + { + // 执行伤害逻辑 + this.SendCommand(new TakeDamageCommand { Damage = e.Damage }); + } + } +} +``` + +## 高级用法 + +### 1. 带参数的复杂查询 + +```csharp +// 查询指定范围内的敌人列表 +public class GetEnemiesInRangeQuery : AbstractQuery> +{ + public Vector3 Center { get; set; } + public float Radius { get; set; } + + protected override List OnDo() + { + var enemySystem = this.GetSystem(); + return enemySystem.GetEnemiesInRange(Center, Radius); + } +} + +// 使用 +var enemies = this.SendQuery(new GetEnemiesInRangeQuery +{ + Center = playerPosition, + Radius = 10.0f +}); +``` + +### 2. 组合查询 + +```csharp +// 查询玩家是否可以使用技能 +public class CanUseSkillQuery : AbstractQuery +{ + public string SkillId { get; set; } + + protected override bool OnDo() + { + var playerModel = this.GetModel(); + var skillModel = this.GetModel(); + + // 查询技能消耗 + var skillCost = this.SendQuery(new GetSkillCostQuery { SkillId = SkillId }); + + // 检查是否满足条件 + return playerModel.Mana.Value >= skillCost.ManaCost + && !this.SendQuery(new IsSkillOnCooldownQuery { SkillId = SkillId }); + } +} + +public class GetSkillCostQuery : AbstractQuery +{ + public string SkillId { get; set; } + + protected override SkillCost OnDo() + { + return this.GetModel().GetSkillCost(SkillId); + } +} + +public class IsSkillOnCooldownQuery : AbstractQuery +{ + public string SkillId { get; set; } + + protected override bool OnDo() + { + return this.GetModel().IsOnCooldown(SkillId); + } +} +``` + +### 3. 聚合数据查询 + +```csharp +// 查询玩家战斗力 +public class GetPlayerPowerQuery : AbstractQuery +{ + protected override int OnDo() + { + var playerModel = this.GetModel(); + var equipmentModel = this.GetModel(); + + int basePower = playerModel.Level.Value * 10; + int equipmentPower = equipmentModel.GetTotalPower(); + int buffPower = this.SendQuery(new GetBuffPowerQuery()); + + return basePower + equipmentPower + buffPower; + } +} + +// 查询玩家详细信息(用于UI显示) +public class GetPlayerInfoQuery : AbstractQuery +{ + protected override PlayerInfo OnDo() + { + var playerModel = this.GetModel(); + + return new PlayerInfo + { + Name = playerModel.Name.Value, + Level = playerModel.Level.Value, + Health = playerModel.Health.Value, + MaxHealth = playerModel.MaxHealth.Value, + Gold = this.SendQuery(new GetPlayerGoldQuery()), + Power = this.SendQuery(new GetPlayerPowerQuery()) + }; + } +} +``` + +### 4. 跨 System 查询 + +```csharp +// 在 AI System 中查询玩家状态 +public class EnemyAISystem : AbstractSystem +{ + protected override void OnInit() { } + + public void UpdateEnemyBehavior(Enemy enemy) + { + // 查询玩家位置 + var playerPos = this.SendQuery(new GetPlayerPositionQuery()); + + // 查询玩家是否在攻击范围内 + bool inRange = this.SendQuery(new IsPlayerInRangeQuery + { + Position = enemy.Position, + Range = enemy.AttackRange + }); + + if (inRange) + { + // 查询是否可以攻击 + bool canAttack = this.SendQuery(new CanEnemyAttackQuery + { + EnemyId = enemy.Id + }); + + if (canAttack) + { + this.SendCommand(new EnemyAttackCommand { EnemyId = enemy.Id }); + } + } + } +} +``` + +## Command vs Query + +### Command(命令) +- **用途**:修改系统状态 +- **返回值**:无返回值(void) +- **示例**:购买物品、造成伤害、升级角色 + +### Query(查询) +- **用途**:读取数据,不修改状态 +- **返回值**:有返回值 +- **示例**:获取金币数量、检查技能冷却、查询玩家位置 + +```csharp +// ❌ 错误:在 Query 中修改状态 +public class BadQuery : AbstractQuery +{ + protected override int OnDo() + { + var model = this.GetModel(); + model.Gold.Value += 100; // 不应该在 Query 中修改数据! + return model.Gold.Value; + } +} + +// ✅ 正确:Query 只读取数据 +public class GoodQuery : AbstractQuery +{ + protected override int OnDo() + { + return this.GetModel().Gold.Value; + } +} + +// ✅ 修改数据应该使用 Command +public class AddGoldCommand : AbstractCommand +{ + public int Amount { get; set; } + + protected override void OnExecute() + { + var model = this.GetModel(); + model.Gold.Value += Amount; + } +} +``` + +## 最佳实践 + +1. **查询只读取,不修改** - 保持 Query 的纯粹性 +2. **小而专注** - 每个 Query 只负责一个具体的查询任务 +3. **可组合** - 复杂查询可以通过组合简单查询实现 +4. **避免过度查询** - 如果需要频繁查询,考虑使用 BindableProperty +5. **命名清晰** - Query 名称应该清楚表达查询意图(Get、Is、Can、Has等前缀) + +## 性能优化 + +### 1. 缓存查询结果 + +```csharp +// 在 Model 中缓存复杂计算 +public class PlayerModel : AbstractModel +{ + private int? _cachedPower; + + public int GetPower() + { + if (_cachedPower == null) + { + _cachedPower = CalculatePower(); + } + return _cachedPower.Value; + } + + private int CalculatePower() + { + // 复杂计算... + return 100; + } + + public void InvalidatePowerCache() + { + _cachedPower = null; + } +} +``` + +### 2. 批量查询 + +```csharp +// 一次查询多个数据,而不是多次单独查询 +public class GetMultipleItemCountsQuery : AbstractQuery> +{ + public List ItemIds { get; set; } + + protected override Dictionary OnDo() + { + var inventory = this.GetModel(); + return ItemIds.ToDictionary(id => id, id => inventory.GetItemCount(id)); + } +} +``` + +## 相关包 + +- [`command`](../command/README.md) - CQRS 的命令部分 +- [`model`](../model/README.md) - Query 主要从 Model 获取数据 +- [`system`](../system/README.md) - System 中可以发送 Query +- [`controller`](../controller/README.md) - Controller 中可以发送 Query +- [`extensions`](../extensions/README.md) - 提供 SendQuery 扩展方法 \ No newline at end of file diff --git a/framework/rule/IBelongToArchitecture.cs b/framework/rule/IBelongToArchitecture.cs new file mode 100644 index 0000000..274ef55 --- /dev/null +++ b/framework/rule/IBelongToArchitecture.cs @@ -0,0 +1,17 @@ +using GFramework.framework.architecture; + +namespace GFramework.framework.rule; + + +/// +/// 定义一个接口,用于标识某个对象属于特定的架构体系。 +/// 实现此接口的对象可以通过GetArchitecture方法获取其所属的架构实例。 +/// +public interface IBelongToArchitecture +{ + /// + /// 获取当前对象所属的架构实例。 + /// + /// 返回实现IArchitecture接口的架构实例 + IArchitecture GetArchitecture(); +} diff --git a/framework/rule/ICanSetArchitecture.cs b/framework/rule/ICanSetArchitecture.cs new file mode 100644 index 0000000..dcc2250 --- /dev/null +++ b/framework/rule/ICanSetArchitecture.cs @@ -0,0 +1,15 @@ +using GFramework.framework.architecture; + +namespace GFramework.framework.rule; + +/// +/// 定义一个接口,用于设置架构实例 +/// +public interface ICanSetArchitecture +{ + /// + /// 设置架构实例 + /// + /// 要设置的架构实例 + void SetArchitecture(IArchitecture architecture); +} diff --git a/framework/rule/README.md b/framework/rule/README.md new file mode 100644 index 0000000..6704ce0 --- /dev/null +++ b/framework/rule/README.md @@ -0,0 +1,329 @@ +# Rule 包使用说明 + +## 概述 + +Rule 包定义了框架的核心规则接口,这些接口规定了框架各个组件之间的关系和约束。所有框架组件都需要遵循这些规则接口。 + +## 核心接口 + +### [`IBelongToArchitecture`](IBelongToArchitecture.cs) + +标记接口,表示某个对象属于特定的架构体系。 + +**接口定义:** +```csharp +public interface IBelongToArchitecture +{ + IArchitecture GetArchitecture(); +} +``` + +**实现此接口的类型:** +- Controller +- System +- Model +- Command +- Query +- Event 处理器 + +**作用:** +所有实现此接口的类型都能够获取其所属的架构实例,从而访问架构提供的各种能力。 + +### [`ICanSetArchitecture`](ICanSetArchitecture.cs) + +定义可以设置架构实例的能力。 + +**接口定义:** +```csharp +public interface ICanSetArchitecture +{ + void SetArchitecture(IArchitecture architecture); +} +``` + +**实现此接口的类型:** +- Command +- Query + +**作用:** +在 Command 和 Query 执行前,框架会自动调用 `SetArchitecture` 方法注入架构实例,使其能够访问 Model、System 等组件。 + +## 接口关系图 + +``` +IBelongToArchitecture (属于架构) + ↓ 被继承 + ├── ICanGetModel (可获取 Model) + ├── ICanGetSystem (可获取 System) + ├── ICanGetUtility (可获取 Utility) + ├── ICanSendCommand (可发送 Command) + ├── ICanSendEvent (可发送 Event) + ├── ICanSendQuery (可发送 Query) + └── ICanRegisterEvent (可注册 Event) + +ICanSetArchitecture (可设置架构) + ↓ 被继承 + ├── ICommand (命令接口) + └── IQuery (查询接口) +``` + +## 使用场景 + +### 1. Controller 实现 IBelongToArchitecture + +```csharp +// Controller 通过实现 IBelongToArchitecture 获得架构访问能力 +public partial class PlayerController : Node, IController +{ + // 实现 IBelongToArchitecture 接口 + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public override void _Ready() + { + // 因为实现了 IBelongToArchitecture,所以可以使用扩展方法 + var playerModel = this.GetModel(); + this.SendCommand(new InitPlayerCommand()); + this.RegisterEvent(OnPlayerDied); + } + + private void OnPlayerDied(PlayerDiedEvent e) + { + GD.Print("Player died!"); + } +} +``` + +### 2. Command 实现 ICanSetArchitecture + +```csharp +// Command 实现 ICanSetArchitecture,框架会自动注入架构 +public class BuyItemCommand : AbstractCommand +{ + public string ItemId { get; set; } + + protected override void OnExecute() + { + // 框架已经通过 SetArchitecture 注入了架构实例 + // 所以这里可以直接使用 this.GetModel + var playerModel = this.GetModel(); + var shopModel = this.GetModel(); + + int price = shopModel.GetItemPrice(ItemId); + if (playerModel.Gold.Value >= price) + { + playerModel.Gold.Value -= price; + this.SendCommand(new AddItemCommand { ItemId = ItemId }); + } + } +} +``` + +### 3. 自定义组件遵循规则 + +```csharp +// 自定义管理器遵循框架规则 +public class SaveManager : IBelongToArchitecture +{ + private IArchitecture _architecture; + + public SaveManager(IArchitecture architecture) + { + _architecture = architecture; + } + + public IArchitecture GetArchitecture() => _architecture; + + public void SaveGame() + { + // 因为实现了 IBelongToArchitecture,可以使用扩展方法 + var playerModel = this.GetModel(); + var saveData = new SaveData + { + PlayerName = playerModel.Name.Value, + Level = playerModel.Level.Value, + Gold = playerModel.Gold.Value + }; + + // 保存逻辑... + } +} +``` + +## 规则约束 + +### 1. 组件注册规则 + +```csharp +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // ✅ 正确:Model/System/Utility 自动获得架构引用 + this.RegisterModel(new PlayerModel()); + this.RegisterSystem(new CombatSystem()); + this.RegisterUtility(new StorageUtility()); + } +} + +// Model 继承 AbstractModel,AbstractModel 实现了 IBelongToArchitecture +public class PlayerModel : AbstractModel +{ + // 无需手动实现 GetArchitecture,基类已实现 + protected override void OnInit() + { + // 可以直接使用架构能力 + this.SendEvent(new PlayerModelInitializedEvent()); + } +} +``` + +### 2. Command/Query 自动注入规则 + +```csharp +// Controller 中发送 Command +public class ShopUI : Control, IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + private void OnBuyButtonPressed() + { + var command = new BuyItemCommand { ItemId = "sword_01" }; + + // SendCommand 内部会自动调用 command.SetArchitecture(this.GetArchitecture()) + this.SendCommand(command); + } +} + +// Command 执行时已经有了架构引用 +public class BuyItemCommand : AbstractCommand +{ + public string ItemId { get; set; } + + protected override void OnExecute() + { + // 此时 GetArchitecture() 已经可用 + var model = this.GetModel(); + } +} +``` + +## 设计模式 + +### 1. 依赖注入模式 + +Rule 接口体现了依赖注入(DI)的思想: + +```csharp +// 接口定义了"需要什么" +public interface IBelongToArchitecture +{ + IArchitecture GetArchitecture(); +} + +// 框架负责"提供什么" +public static class CanSendExtensions +{ + public static void SendCommand(this ICanSendCommand self, T command) + where T : ICommand + { + // 自动注入架构依赖 + command.SetArchitecture(self.GetArchitecture()); + command.Execute(); + } +} +``` + +### 2. 接口隔离原则(ISP) + +Rule 接口遵循接口隔离原则,每个接口职责单一: + +```csharp +// ❌ 不好的设计:一个大接口包含所有能力 +public interface IBigInterface +{ + IArchitecture GetArchitecture(); + void SetArchitecture(IArchitecture architecture); + T GetModel() where T : class, IModel; + T GetSystem() where T : class, ISystem; + void SendCommand(T command) where T : ICommand; + // ... 更多方法 +} + +// ✅ 好的设计:小接口组合 +public interface IBelongToArchitecture { ... } // 只负责获取架构 +public interface ICanSetArchitecture { ... } // 只负责设置架构 +public interface ICanGetModel { ... } // 只负责获取 Model +public interface ICanSendCommand { ... } // 只负责发送 Command +``` + +### 3. 组合优于继承 + +通过接口组合实现不同能力: + +```csharp +// Controller 需要获取 Model 和发送 Command +public interface IController : ICanGetModel, ICanGetSystem, ICanSendCommand, + ICanSendQuery, ICanRegisterEvent +{ +} + +// Command 需要设置架构和获取 Model/System +public interface ICommand : ICanSetArchitecture, ICanGetModel, ICanGetSystem, + ICanSendEvent, ICanSendQuery +{ +} + +// System 只需要获取其他组件 +public interface ISystem : ICanGetModel, ICanGetUtility, ICanGetSystem, + ICanRegisterEvent, ICanSendEvent, ICanSendQuery +{ +} +``` + +## 扩展规则 + +### 自定义规则接口 + +```csharp +// 定义新的规则接口 +public interface ICanAccessDatabase : IBelongToArchitecture +{ +} + +// 提供扩展方法 +public static class CanAccessDatabaseExtensions +{ + public static DatabaseUtility GetDatabase(this ICanAccessDatabase self) + { + return self.GetArchitecture().GetUtility(); + } +} + +// 让特定组件实现这个接口 +public class DatabaseCommand : AbstractCommand, ICanAccessDatabase +{ + protected override void OnExecute() + { + var db = this.GetDatabase(); + // 使用数据库... + } +} +``` + +## 最佳实践 + +1. **遵循既有规则** - 优先使用框架提供的规则接口 +2. **不要绕过规则** - 不要直接存储架构引用,使用接口获取 +3. **接口组合** - 通过实现多个小接口获得所需能力 +4. **扩展规则** - 需要新能力时,定义新的规则接口 +5. **保持一致** - 自定义组件也应遵循相同的规则体系 + +## 相关包 + +- [`architecture`](../architecture/README.md) - 定义 IArchitecture 接口 +- [`command`](../command/README.md) - Command 实现 ICanSetArchitecture +- [`query`](../query/README.md) - Query 实现 ICanSetArchitecture +- [`controller`](../controller/README.md) - Controller 实现 IBelongToArchitecture +- [`system`](../system/README.md) - System 实现 IBelongToArchitecture +- [`model`](../model/README.md) - Model 实现 IBelongToArchitecture +- [`extensions`](../extensions/README.md) - 基于规则接口提供扩展方法 \ No newline at end of file diff --git a/framework/system/AbstractSystem.cs b/framework/system/AbstractSystem.cs new file mode 100644 index 0000000..8ad9245 --- /dev/null +++ b/framework/system/AbstractSystem.cs @@ -0,0 +1,35 @@ +using GFramework.framework.architecture; +using GFramework.framework.rule; + +namespace GFramework.framework.system; + +/// +/// 抽象系统基类,实现系统接口的基本功能 +/// 提供架构关联和初始化机制 +/// +public abstract class AbstractSystem : ISystem +{ + private IArchitecture _mArchitecture; + + /// + /// 获取当前系统所属的架构实例 + /// + /// 返回系统关联的架构对象 + IArchitecture IBelongToArchitecture.GetArchitecture() => _mArchitecture; + + /// + /// 设置系统所属的架构实例 + /// + /// 要关联的架构对象 + void ICanSetArchitecture.SetArchitecture(IArchitecture architecture) => _mArchitecture = architecture; + + /// + /// 系统初始化方法,调用抽象初始化方法 + /// + void ISystem.Init() => OnInit(); + + /// + /// 抽象初始化方法,由子类实现具体的初始化逻辑 + /// + protected abstract void OnInit(); +} diff --git a/framework/system/ICanGetSystem.cs b/framework/system/ICanGetSystem.cs new file mode 100644 index 0000000..f9557d4 --- /dev/null +++ b/framework/system/ICanGetSystem.cs @@ -0,0 +1,9 @@ +using GFramework.framework.rule; + +namespace GFramework.framework.system; + +/// +/// 定义一个接口,表示可以获取系统的对象。 +/// 该接口继承自IBelongToArchitecture接口,用于标识那些属于系统架构并能够获取系统实例的类型。 +/// +public interface ICanGetSystem : IBelongToArchitecture; diff --git a/framework/system/ISystem.cs b/framework/system/ISystem.cs new file mode 100644 index 0000000..7dcbabc --- /dev/null +++ b/framework/system/ISystem.cs @@ -0,0 +1,20 @@ +using GFramework.framework.events; +using GFramework.framework.model; +using GFramework.framework.rule; +using GFramework.framework.utility; + +namespace GFramework.framework.system; + +/// +/// 系统接口,定义了系统的基本行为和功能 +/// 该接口继承了多个框架相关的接口,提供了系统初始化能力 +/// +public interface ISystem : ICanSetArchitecture, ICanGetModel, ICanGetUtility, + ICanRegisterEvent, ICanSendEvent, ICanGetSystem +{ + /// + /// 初始化系统 + /// 在系统被创建后调用,用于执行系统的初始化逻辑 + /// + void Init(); +} diff --git a/framework/system/README.md b/framework/system/README.md new file mode 100644 index 0000000..9b82309 --- /dev/null +++ b/framework/system/README.md @@ -0,0 +1,527 @@ +# System 包使用说明 + +## 概述 + +System 包定义了业务逻辑层(Business Logic Layer)。System 负责处理游戏的核心业务逻辑,协调 Model 之间的交互,响应事件并执行复杂的业务流程。 + +## 核心接口 + +### [`ICanGetSystem`](ICanGetSystem.cs) + +标记接口,表示该类型可以获取其他 System。 + +**继承关系:** +```csharp +public interface ICanGetSystem : IBelongToArchitecture +``` + +### [`ISystem`](ISystem.cs) + +System 接口,定义了系统的基本行为。 + +**核心成员:** +```csharp +void Init(); // 系统初始化方法 +``` + +**继承的能力:** +- `ICanSetArchitecture` - 可设置架构 +- `ICanGetModel` - 可获取 Model +- `ICanGetUtility` - 可获取 Utility +- `ICanGetSystem` - 可获取其他 System +- `ICanRegisterEvent` - 可注册事件 +- `ICanSendEvent` - 可发送事件 + +## 核心类 + +### [`AbstractSystem`](AbstractSystem.cs) + +抽象 System 基类,提供了 System 的基础实现。 + +**使用方式:** +```csharp +public abstract class AbstractSystem : ISystem +{ + void ISystem.Init() => OnInit(); + protected abstract void OnInit(); // 子类实现初始化逻辑 +} +``` + +## 基本使用 + +### 1. 定义 System + +```csharp +// 战斗系统 +public class CombatSystem : AbstractSystem +{ + protected override void OnInit() + { + // 注册事件监听 + this.RegisterEvent(OnEnemyAttack); + this.RegisterEvent(OnPlayerAttack); + } + + private void OnEnemyAttack(EnemyAttackEvent e) + { + var playerModel = this.GetModel(); + + // 计算伤害 + int damage = CalculateDamage(e.AttackPower, playerModel.Defense.Value); + + // 应用伤害 + playerModel.Health.Value -= damage; + + // 发送伤害事件 + this.SendEvent(new PlayerTookDamageEvent { Damage = damage }); + } + + private void OnPlayerAttack(PlayerAttackEvent e) + { + var playerModel = this.GetModel(); + var enemyModel = this.GetModel(); + + int damage = CalculateDamage(playerModel.AttackPower.Value, e.Enemy.Defense); + e.Enemy.Health -= damage; + + this.SendEvent(new EnemyTookDamageEvent + { + EnemyId = e.Enemy.Id, + Damage = damage + }); + } + + private int CalculateDamage(int attackPower, int defense) + { + return Math.Max(1, attackPower - defense / 2); + } +} +``` + +### 2. 注册 System + +```csharp +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 注册 Model + this.RegisterModel(new PlayerModel()); + this.RegisterModel(new EnemyModel()); + + // 注册 System(系统注册后会自动调用 Init) + this.RegisterSystem(new CombatSystem()); + this.RegisterSystem(new InventorySystem()); + this.RegisterSystem(new QuestSystem()); + } +} +``` + +### 3. 在其他组件中获取 System + +```csharp +// 在 Controller 中 +public partial class GameController : Node, IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public override void _Ready() + { + // 获取 System + var combatSystem = this.GetSystem(); + var questSystem = this.GetSystem(); + } +} + +// 在 Command 中 +public class StartBattleCommand : AbstractCommand +{ + protected override void OnExecute() + { + var combatSystem = this.GetSystem(); + // 使用 System... + } +} +``` + +## 常见使用模式 + +### 1. 事件驱动的 System + +```csharp +public class InventorySystem : AbstractSystem +{ + protected override void OnInit() + { + // 监听物品相关事件 + this.RegisterEvent(OnItemAdded); + this.RegisterEvent(OnItemRemoved); + this.RegisterEvent(OnItemUsed); + } + + private void OnItemAdded(ItemAddedEvent e) + { + var inventoryModel = this.GetModel(); + + // 添加物品 + inventoryModel.AddItem(e.ItemId, e.Count); + + // 检查成就 + CheckAchievements(e.ItemId); + + // 发送通知 + this.SendEvent(new ShowNotificationEvent + { + Message = $"获得物品: {e.ItemId} x{e.Count}" + }); + } + + private void OnItemUsed(ItemUsedEvent e) + { + var inventoryModel = this.GetModel(); + var playerModel = this.GetModel(); + + if (inventoryModel.HasItem(e.ItemId)) + { + // 应用物品效果 + ApplyItemEffect(e.ItemId, playerModel); + + // 移除物品 + inventoryModel.RemoveItem(e.ItemId, 1); + + this.SendEvent(new ItemEffectAppliedEvent { ItemId = e.ItemId }); + } + } + + private void ApplyItemEffect(string itemId, PlayerModel player) + { + // 物品效果逻辑... + if (itemId == "health_potion") + { + player.Health.Value = Math.Min( + player.Health.Value + 50, + player.MaxHealth.Value + ); + } + } + + private void CheckAchievements(string itemId) + { + // 成就检查逻辑... + } +} +``` + +### 2. 定时更新的 System + +```csharp +public class BuffSystem : AbstractSystem +{ + private List _activeBuffs = new(); + + protected override void OnInit() + { + this.RegisterEvent(OnBuffApplied); + this.RegisterEvent(OnUpdate); + } + + private void OnBuffApplied(BuffAppliedEvent e) + { + _activeBuffs.Add(new BuffData + { + BuffId = e.BuffId, + Duration = e.Duration, + RemainingTime = e.Duration + }); + + ApplyBuffEffect(e.BuffId, true); + } + + private void OnUpdate(GameUpdateEvent e) + { + // 更新所有 Buff + for (int i = _activeBuffs.Count - 1; i >= 0; i--) + { + var buff = _activeBuffs[i]; + buff.RemainingTime -= e.DeltaTime; + + if (buff.RemainingTime <= 0) + { + // Buff 过期 + ApplyBuffEffect(buff.BuffId, false); + _activeBuffs.RemoveAt(i); + + this.SendEvent(new BuffExpiredEvent { BuffId = buff.BuffId }); + } + } + } + + private void ApplyBuffEffect(string buffId, bool apply) + { + var playerModel = this.GetModel(); + // 应用或移除 Buff 效果... + } +} +``` + +### 3. 跨 System 协作 + +```csharp +public class QuestSystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent(OnEnemyKilled); + this.RegisterEvent(OnItemCollected); + } + + private void OnEnemyKilled(EnemyKilledEvent e) + { + var questModel = this.GetModel(); + var activeQuests = questModel.GetActiveQuests(); + + foreach (var quest in activeQuests) + { + if (quest.Type == QuestType.KillEnemy && quest.TargetId == e.EnemyType) + { + quest.Progress++; + + if (quest.Progress >= quest.RequiredAmount) + { + // 任务完成 + CompleteQuest(quest.Id); + } + } + } + } + + private void CompleteQuest(string questId) + { + var questModel = this.GetModel(); + var quest = questModel.GetQuest(questId); + + // 标记任务完成 + questModel.CompleteQuest(questId); + + // 发放奖励(通过其他 System) + this.SendEvent(new QuestCompletedEvent + { + QuestId = questId, + Rewards = quest.Rewards + }); + } +} + +public class RewardSystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent(OnQuestCompleted); + } + + private void OnQuestCompleted(QuestCompletedEvent e) + { + var playerModel = this.GetModel(); + + // 发放奖励 + foreach (var reward in e.Rewards) + { + switch (reward.Type) + { + case RewardType.Gold: + playerModel.Gold.Value += reward.Amount; + break; + case RewardType.Experience: + playerModel.Experience.Value += reward.Amount; + break; + case RewardType.Item: + this.SendEvent(new ItemAddedEvent + { + ItemId = reward.ItemId, + Count = reward.Amount + }); + break; + } + } + + this.SendEvent(new RewardsGrantedEvent { Rewards = e.Rewards }); + } +} +``` + +### 4. 管理复杂状态机 + +```csharp +public class GameStateSystem : AbstractSystem +{ + private GameState _currentState = GameState.MainMenu; + + protected override void OnInit() + { + this.RegisterEvent(OnStateChangeRequest); + } + + private void OnStateChangeRequest(GameStateChangeRequestEvent e) + { + if (CanTransition(_currentState, e.TargetState)) + { + ExitState(_currentState); + _currentState = e.TargetState; + EnterState(_currentState); + + this.SendEvent(new GameStateChangedEvent + { + PreviousState = _currentState, + NewState = e.TargetState + }); + } + } + + private bool CanTransition(GameState from, GameState to) + { + // 状态转换规则 + return (from, to) switch + { + (GameState.MainMenu, GameState.Playing) => true, + (GameState.Playing, GameState.Paused) => true, + (GameState.Paused, GameState.Playing) => true, + (GameState.Playing, GameState.GameOver) => true, + _ => false + }; + } + + private void EnterState(GameState state) + { + switch (state) + { + case GameState.Playing: + // 开始游戏 + this.SendCommand(new StartGameCommand()); + break; + case GameState.Paused: + // 暂停游戏 + this.SendEvent(new GamePausedEvent()); + break; + case GameState.GameOver: + // 游戏结束 + this.SendCommand(new GameOverCommand()); + break; + } + } + + private void ExitState(GameState state) + { + // 清理当前状态 + } +} +``` + +## System vs Model + +### Model(数据层) +- **职责**:存储数据和状态 +- **特点**:被动,等待修改 +- **示例**:PlayerModel、InventoryModel + +### System(逻辑层) +- **职责**:处理业务逻辑,协调 Model +- **特点**:主动,响应事件 +- **示例**:CombatSystem、QuestSystem + +```csharp +// ✅ 正确的职责划分 + +// Model: 存储数据 +public class PlayerModel : AbstractModel +{ + public BindableProperty Health { get; } = new(100); + public BindableProperty Mana { get; } = new(50); + + protected override void OnInit() { } +} + +// System: 处理逻辑 +public class CombatSystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent(OnAttack); + } + + private void OnAttack(AttackEvent e) + { + var playerModel = this.GetModel(); + + // System 负责计算和决策 + int damage = CalculateDamage(e); + playerModel.Health.Value -= damage; + + if (playerModel.Health.Value <= 0) + { + this.SendEvent(new PlayerDiedEvent()); + } + } +} +``` + +## 最佳实践 + +1. **单一职责** - 每个 System 专注于一个业务领域 +2. **事件驱动** - 通过事件与其他组件通信 +3. **无状态或少状态** - 优先将状态存储在 Model 中 +4. **可组合** - System 之间通过事件松耦合协作 +5. **初始化注册** - 在 `OnInit` 中注册所有事件监听 + +## 性能优化 + +### 1. 避免频繁的 GetModel/GetSystem + +```csharp +// ❌ 不好:每次都获取 +private void OnUpdate(GameUpdateEvent e) +{ + var model = this.GetModel(); // 频繁调用 + // ... +} + +// ✅ 好:缓存引用 +private PlayerModel _playerModel; + +protected override void OnInit() +{ + _playerModel = this.GetModel(); // 只获取一次 +} + +private void OnUpdate(GameUpdateEvent e) +{ + // 直接使用缓存的引用 + _playerModel.Health.Value += 1; +} +``` + +### 2. 批量处理 + +```csharp +public class ParticleSystem : AbstractSystem +{ + private List _particles = new(); + + private void OnUpdate(GameUpdateEvent e) + { + // 批量更新,而不是每个粒子发一个事件 + for (int i = _particles.Count - 1; i >= 0; i--) + { + UpdateParticle(_particles[i], e.DeltaTime); + } + } +} +``` + +## 相关包 + +- [`model`](../model/README.md) - System 操作 Model 的数据 +- [`events`](../events/README.md) - System 通过事件通信 +- [`command`](../command/README.md) - System 中可以发送 Command +- [`query`](../query/README.md) - System 中可以发送 Query +- [`utility`](../utility/README.md) - System 可以使用 Utility +- [`architecture`](../architecture/README.md) - 在架构中注册 System \ No newline at end of file diff --git a/framework/utility/ICanGetUtility.cs b/framework/utility/ICanGetUtility.cs new file mode 100644 index 0000000..2a54708 --- /dev/null +++ b/framework/utility/ICanGetUtility.cs @@ -0,0 +1,9 @@ +using GFramework.framework.rule; + +namespace GFramework.framework.utility; + +/// +/// 定义一个接口,表示可以获取工具类的对象 +/// 该接口继承自IBelongToArchitecture,表明实现此接口的类型属于某个架构组件 +/// +public interface ICanGetUtility : IBelongToArchitecture; diff --git a/framework/utility/IUtility.cs b/framework/utility/IUtility.cs new file mode 100644 index 0000000..a644a31 --- /dev/null +++ b/framework/utility/IUtility.cs @@ -0,0 +1,7 @@ +namespace GFramework.framework.utility; + +/// +/// IUtility接口定义了通用工具类的基本契约 +/// 该接口作为所有工具类实现的基础接口,提供统一的工具方法规范 +/// +public interface IUtility; diff --git a/framework/utility/README.md b/framework/utility/README.md new file mode 100644 index 0000000..76ed3e9 --- /dev/null +++ b/framework/utility/README.md @@ -0,0 +1,584 @@ +# Utility 包使用说明 + +## 概述 + +Utility 包定义了工具类层。Utility 提供无状态的辅助功能,如数学计算、文件操作、序列化等通用工具方法。与 System 不同,Utility 不依赖架构状态,是纯粹的工具函数集合。 + +## 核心接口 + +### [`ICanGetUtility`](ICanGetUtility.cs) + +标记接口,表示该类型可以获取 Utility。 + +**继承关系:** +```csharp +public interface ICanGetUtility : IBelongToArchitecture +``` + +### [`IUtility`](IUtility.cs) + +Utility 标记接口,所有工具类都应实现此接口。 + +**接口定义:** +```csharp +public interface IUtility +{ + // 标记接口,无方法定义 +} +``` + +## 基本使用 + +### 1. 定义 Utility + +```csharp +// 存储工具类 +public class StorageUtility : IUtility +{ + private const string SavePath = "user://save_data.json"; + + public void Save(T data) + { + string json = Json.Stringify(data); + using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Write); + file.StoreString(json); + } + + public T Load() where T : new() + { + if (!FileAccess.FileExists(SavePath)) + return new T(); + + using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Read); + string json = file.GetAsText(); + return Json.Parse(json); + } + + public void Delete() + { + if (FileAccess.FileExists(SavePath)) + { + DirAccess.RemoveAbsolute(SavePath); + } + } +} + +// 数学工具类 +public class MathUtility : IUtility +{ + public float Lerp(float a, float b, float t) + { + return a + (b - a) * Mathf.Clamp(t, 0f, 1f); + } + + public Vector3 BezierCurve(Vector3 p0, Vector3 p1, Vector3 p2, float t) + { + float u = 1 - t; + return u * u * p0 + 2 * u * t * p1 + t * t * p2; + } + + public bool IsInRange(Vector3 point, Vector3 center, float radius) + { + return point.DistanceTo(center) <= radius; + } + + public int RollDice(int sides) + { + return GD.RandRange(1, sides); + } +} + +// 时间工具类 +public class TimeUtility : IUtility +{ + public string FormatTime(float seconds) + { + int minutes = (int)(seconds / 60); + int secs = (int)(seconds % 60); + return $"{minutes:D2}:{secs:D2}"; + } + + public long GetCurrentTimestamp() + { + return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + + public bool IsExpired(long timestamp, int durationSeconds) + { + return GetCurrentTimestamp() > timestamp + durationSeconds; + } +} +``` + +### 2. 注册 Utility + +```csharp +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 注册 Utility(不需要初始化) + this.RegisterUtility(new StorageUtility()); + this.RegisterUtility(new MathUtility()); + this.RegisterUtility(new TimeUtility()); + } +} +``` + +### 3. 使用 Utility + +```csharp +// 在 System 中使用 +public class SaveSystem : AbstractSystem +{ + protected override void OnInit() + { + this.RegisterEvent(OnSaveGame); + this.RegisterEvent(OnLoadGame); + } + + private void OnSaveGame(SaveGameEvent e) + { + var storage = this.GetUtility(); + var playerModel = this.GetModel(); + + var saveData = new SaveData + { + PlayerName = playerModel.Name.Value, + Level = playerModel.Level.Value, + Gold = playerModel.Gold.Value, + Timestamp = this.GetUtility().GetCurrentTimestamp() + }; + + storage.Save(saveData); + this.SendEvent(new GameSavedEvent()); + } + + private void OnLoadGame(LoadGameEvent e) + { + var storage = this.GetUtility(); + var saveData = storage.Load(); + + if (saveData != null) + { + var playerModel = this.GetModel(); + playerModel.Name.Value = saveData.PlayerName; + playerModel.Level.Value = saveData.Level; + playerModel.Gold.Value = saveData.Gold; + + this.SendEvent(new GameLoadedEvent()); + } + } +} + +// 在 Command 中使用 +public class MovePlayerCommand : AbstractCommand +{ + public Vector3 TargetPosition { get; set; } + public float Speed { get; set; } + + protected override void OnExecute() + { + var playerModel = this.GetModel(); + var mathUtil = this.GetUtility(); + + // 使用工具类计算 + Vector3 currentPos = playerModel.Position.Value; + Vector3 direction = (TargetPosition - currentPos).Normalized(); + Vector3 newPos = currentPos + direction * Speed; + + playerModel.Position.Value = newPos; + } +} +``` + +## 常见 Utility 类型 + +### 1. 序列化/反序列化工具 + +```csharp +public class JsonUtility : IUtility +{ + public string Serialize(T obj) + { + return Json.Stringify(obj); + } + + public T Deserialize(string json) where T : new() + { + return Json.Parse(json); + } + + public bool TryDeserialize(string json, out T result) where T : new() + { + try + { + result = Json.Parse(json); + return true; + } + catch + { + result = default; + return false; + } + } +} +``` + +### 2. 随机数工具 + +```csharp +public class RandomUtility : IUtility +{ + private Random _random = new Random(); + + public int Range(int min, int max) + { + return _random.Next(min, max + 1); + } + + public float Range(float min, float max) + { + return min + (float)_random.NextDouble() * (max - min); + } + + public T Choose(params T[] items) + { + return items[Range(0, items.Length - 1)]; + } + + public List Shuffle(List list) + { + var shuffled = new List(list); + for (int i = shuffled.Count - 1; i > 0; i--) + { + int j = Range(0, i); + (shuffled[i], shuffled[j]) = (shuffled[j], shuffled[i]); + } + return shuffled; + } + + public bool Probability(float chance) + { + return _random.NextDouble() < chance; + } +} +``` + +### 3. 字符串工具 + +```csharp +public class StringUtility : IUtility +{ + public string Truncate(string text, int maxLength, string suffix = "...") + { + if (text.Length <= maxLength) + return text; + return text.Substring(0, maxLength - suffix.Length) + suffix; + } + + public string FormatNumber(int number) + { + if (number >= 1000000) + return $"{number / 1000000.0:F1}M"; + if (number >= 1000) + return $"{number / 1000.0:F1}K"; + return number.ToString(); + } + + public string ToTitleCase(string text) + { + return System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(text.ToLower()); + } + + public bool IsValidEmail(string email) + { + return Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$"); + } +} +``` + +### 4. 加密工具 + +```csharp +public class EncryptionUtility : IUtility +{ + private const string EncryptionKey = "YourSecretKey123"; + + public string Encrypt(string plainText) + { + byte[] data = System.Text.Encoding.UTF8.GetBytes(plainText); + byte[] encrypted = EncryptBytes(data); + return Convert.ToBase64String(encrypted); + } + + public string Decrypt(string encryptedText) + { + byte[] data = Convert.FromBase64String(encryptedText); + byte[] decrypted = DecryptBytes(data); + return System.Text.Encoding.UTF8.GetString(decrypted); + } + + private byte[] EncryptBytes(byte[] data) + { + // 简单的 XOR 加密示例(实际项目应使用更安全的算法) + byte[] key = System.Text.Encoding.UTF8.GetBytes(EncryptionKey); + byte[] result = new byte[data.Length]; + for (int i = 0; i < data.Length; i++) + { + result[i] = (byte)(data[i] ^ key[i % key.Length]); + } + return result; + } + + private byte[] DecryptBytes(byte[] data) + { + return EncryptBytes(data); // XOR 解密与加密相同 + } +} +``` + +### 5. 对象池工具 + +```csharp +public class ObjectPoolUtility : IUtility +{ + private Dictionary> _pools = new(); + + public T Get() where T : new() + { + Type type = typeof(T); + if (_pools.ContainsKey(type) && _pools[type].Count > 0) + { + return (T)_pools[type].Dequeue(); + } + return new T(); + } + + public void Return(T obj) + { + Type type = typeof(T); + if (!_pools.ContainsKey(type)) + { + _pools[type] = new Queue(); + } + _pools[type].Enqueue(obj); + } + + public void Clear() + { + Type type = typeof(T); + if (_pools.ContainsKey(type)) + { + _pools[type].Clear(); + } + } + + public void ClearAll() + { + _pools.Clear(); + } +} +``` + +### 6. 日志工具 + +```csharp +public class LogUtility : IUtility +{ + public enum LogLevel + { + Debug, + Info, + Warning, + Error + } + + public void Log(string message, LogLevel level = LogLevel.Info) + { + string prefix = level switch + { + LogLevel.Debug => "[DEBUG]", + LogLevel.Info => "[INFO]", + LogLevel.Warning => "[WARN]", + LogLevel.Error => "[ERROR]", + _ => "" + }; + + string timestamp = DateTime.Now.ToString("HH:mm:ss"); + GD.Print($"{timestamp} {prefix} {message}"); + } + + public void Debug(string message) => Log(message, LogLevel.Debug); + public void Info(string message) => Log(message, LogLevel.Info); + public void Warning(string message) => Log(message, LogLevel.Warning); + public void Error(string message) => Log(message, LogLevel.Error); +} +``` + +## Utility vs System + +### Utility(工具层) +- **无状态** - 不存储业务数据 +- **纯函数** - 相同输入产生相同输出 +- **独立性** - 不依赖架构状态 +- **可复用** - 可在多个项目中使用 + +### System(逻辑层) +- **有状态** - 可能存储临时状态 +- **业务逻辑** - 处理特定业务流程 +- **架构依赖** - 需要访问 Model +- **项目特定** - 针对特定项目设计 + +```csharp +// ✅ Utility: 无状态的工具方法 +public class MathUtility : IUtility +{ + public float CalculateDamage(float attackPower, float defense) + { + return Math.Max(1, attackPower - defense * 0.5f); + } +} + +// ✅ System: 有状态的业务逻辑 +public class CombatSystem : AbstractSystem +{ + private List _activeCombats = new(); + + protected override void OnInit() + { + this.RegisterEvent(OnAttack); + } + + private void OnAttack(AttackEvent e) + { + var mathUtil = this.GetUtility(); + var playerModel = this.GetModel(); + + // 使用 Utility 计算,但在 System 中处理业务逻辑 + float damage = mathUtil.CalculateDamage(e.AttackPower, playerModel.Defense.Value); + playerModel.Health.Value -= (int)damage; + + _activeCombats.Add(new CombatInstance { Damage = damage }); + } +} +``` + +## 最佳实践 + +1. **保持无状态** - Utility 不应存储业务状态 +2. **纯函数优先** - 相同输入应产生相同输出 +3. **单一职责** - 每个 Utility 专注于一类功能 +4. **避免依赖** - 不依赖 Model、System 等架构组件 +5. **可测试** - 易于单元测试的纯函数 + +## 错误示例 + +```csharp +// ❌ 错误:Utility 中存储状态 +public class BadUtility : IUtility +{ + private int _counter = 0; // 不应该有状态 + + public int GetNextId() + { + return ++_counter; // 依赖内部状态 + } +} + +// ❌ 错误:Utility 中访问 Model +public class BadUtility2 : IUtility +{ + public void DoSomething() + { + // Utility 不应该访问架构组件 + var model = this.GetModel(); // 编译错误! + } +} + +// ✅ 正确:如果需要状态,应该使用 System +public class IdGeneratorSystem : AbstractSystem +{ + private int _counter = 0; + + protected override void OnInit() { } + + public int GetNextId() + { + return ++_counter; + } +} +``` + +## 性能优化 + +### 1. 缓存计算结果 + +```csharp +public class PathfindingUtility : IUtility +{ + private Dictionary<(Vector3, Vector3), List> _pathCache = new(); + + public List FindPath(Vector3 start, Vector3 end, bool useCache = true) + { + var key = (start, end); + + if (useCache && _pathCache.ContainsKey(key)) + { + return _pathCache[key]; + } + + var path = CalculatePath(start, end); + + if (useCache) + { + _pathCache[key] = path; + } + + return path; + } + + private List CalculatePath(Vector3 start, Vector3 end) + { + // A* 算法等复杂计算... + return new List(); + } + + public void ClearCache() + { + _pathCache.Clear(); + } +} +``` + +### 2. 对象复用 + +```csharp +public class CollectionUtility : IUtility +{ + private List _tempList = new(); + + public List GetPointsInRadius(Vector3 center, float radius, List points) + { + _tempList.Clear(); + + foreach (var point in points) + { + if (point.DistanceTo(center) <= radius) + { + _tempList.Add(point); + } + } + + return new List(_tempList); // 返回副本 + } +} +``` + +## 相关包 + +- [`system`](../system/README.md) - System 中使用 Utility +- [`command`](../command/README.md) - Command 中可以使用 Utility +- [`architecture`](../architecture/README.md) - 在架构中注册 Utility +- [`ioc`](../ioc/README.md) - Utility 通过 IoC 容器管理 +- [`extensions`](../extensions/README.md) - 提供 GetUtility 扩展方法 \ No newline at end of file