From bbb91d597ad8a4f4f197e632e48b5c0218804783 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:53:55 +0800 Subject: [PATCH 1/8] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E5=8D=8F?= =?UTF-8?q?=E7=A8=8B=E7=B3=BB=E7=BB=9F=E3=80=81CQRS=E4=B8=8EMediator?= =?UTF-8?q?=E3=80=81=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E3=80=81=E8=B5=84=E6=BA=90=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增协程系统文档,介绍协程调度器、等待指令、基本用法和最佳实践 - 新增CQRS与Mediator文档,涵盖命令查询职责分离、处理器实现和管道行为 - 新增生命周期管理文档,说明同步异步初始化和销毁机制 - 新增资源管理系统文档,介绍资源加载、缓存和引用计数管理功能 --- docs/zh-CN/core/coroutine.md | 506 ++++++++++++++++++++++++ docs/zh-CN/core/cqrs.md | 613 +++++++++++++++++++++++++++++ docs/zh-CN/core/lifecycle.md | 461 ++++++++++++++++++++++ docs/zh-CN/core/resource.md | 507 ++++++++++++++++++++++++ docs/zh-CN/core/state-machine.md | 576 +++++++++++++++++++++++++++ docs/zh-CN/game/scene.md | 652 +++++++++++++++++++++++++++++++ docs/zh-CN/game/ui.md | 496 +++++++++++++++++++++++ 7 files changed, 3811 insertions(+) create mode 100644 docs/zh-CN/core/coroutine.md create mode 100644 docs/zh-CN/core/cqrs.md create mode 100644 docs/zh-CN/core/lifecycle.md create mode 100644 docs/zh-CN/core/resource.md create mode 100644 docs/zh-CN/core/state-machine.md create mode 100644 docs/zh-CN/game/scene.md create mode 100644 docs/zh-CN/game/ui.md diff --git a/docs/zh-CN/core/coroutine.md b/docs/zh-CN/core/coroutine.md new file mode 100644 index 0000000..6fa8ab7 --- /dev/null +++ b/docs/zh-CN/core/coroutine.md @@ -0,0 +1,506 @@ +--- +title: 协程系统 +description: 协程系统提供了轻量级的异步操作管理机制,支持时间延迟、事件等待、任务等待等多种场景。 +--- + +# 协程系统 + +## 概述 + +协程系统是 GFramework 中用于管理异步操作的核心机制。通过协程,你可以编写看起来像同步代码的异步逻辑,避免回调地狱,使代码更加清晰易读。 + +协程系统基于 C# 的迭代器(IEnumerator)实现,提供了丰富的等待指令(YieldInstruction),可以轻松处理时间延迟、事件等待、任务等待等各种异步场景。 + +**主要特性**: + +- 轻量级协程调度器 +- 丰富的等待指令(30+ 种) +- 支持协程嵌套和组合 +- 协程标签和批量管理 +- 与事件系统、命令系统、CQRS 深度集成 +- 异常处理和错误恢复 + +## 核心概念 + +### 协程调度器 + +`CoroutineScheduler` 是协程系统的核心,负责管理和执行所有协程: + +```csharp +using GFramework.Core.coroutine; + +// 创建调度器(通常由架构自动管理) +var scheduler = new CoroutineScheduler(timeSource); + +// 运行协程 +var handle = scheduler.Run(MyCoroutine()); + +// 每帧更新 +scheduler.Update(); +``` + +### 协程句柄 + +`CoroutineHandle` 用于标识和控制协程: + +```csharp +// 运行协程并获取句柄 +var handle = scheduler.Run(MyCoroutine()); + +// 检查协程是否存活 +if (scheduler.IsCoroutineAlive(handle)) +{ + // 停止协程 + scheduler.Stop(handle); +} +``` + +### 等待指令 + +等待指令(YieldInstruction)定义了协程的等待行为: + +```csharp +public interface IYieldInstruction +{ + bool IsDone { get; } + void Update(double deltaTime); +} +``` + +## 基本用法 + +### 创建简单协程 + +```csharp +using GFramework.Core.Abstractions.coroutine; +using GFramework.Core.coroutine.instructions; + +public IEnumerator SimpleCoroutine() +{ + Console.WriteLine("开始"); + + // 等待 2 秒 + yield return new Delay(2.0); + + Console.WriteLine("2 秒后"); + + // 等待 1 帧 + yield return new WaitOneFrame(); + + Console.WriteLine("下一帧"); +} +``` + +### 使用协程辅助方法 + +```csharp +using GFramework.Core.coroutine; + +public IEnumerator HelperCoroutine() +{ + // 等待指定秒数 + yield return CoroutineHelper.WaitForSeconds(1.5); + + // 等待一帧 + yield return CoroutineHelper.WaitForOneFrame(); + + // 等待多帧 + yield return CoroutineHelper.WaitForFrames(10); + + // 等待条件满足 + yield return CoroutineHelper.WaitUntil(() => isReady); + + // 等待条件不满足 + yield return CoroutineHelper.WaitWhile(() => isLoading); +} +``` + +### 在架构组件中使用 + +```csharp +using GFramework.Core.model; +using GFramework.Core.extensions; + +public class PlayerModel : AbstractModel +{ + protected override void OnInit() + { + // 启动协程 + this.StartCoroutine(RegenerateHealth()); + } + + private IEnumerator RegenerateHealth() + { + while (true) + { + // 每秒恢复 1 点生命值 + yield return CoroutineHelper.WaitForSeconds(1.0); + Health = Math.Min(Health + 1, MaxHealth); + } + } +} +``` + +## 高级用法 + +### 等待事件 + +```csharp +using GFramework.Core.coroutine.instructions; + +public IEnumerator WaitForEventExample() +{ + Console.WriteLine("等待玩家死亡事件..."); + + // 等待事件触发 + var waitEvent = new WaitForEvent(eventBus); + yield return waitEvent; + + // 获取事件数据 + var eventData = waitEvent.EventData; + Console.WriteLine($"玩家 {eventData.PlayerId} 死亡"); +} +``` + +### 等待事件(带超时) + +```csharp +public IEnumerator WaitForEventWithTimeout() +{ + var waitEvent = new WaitForEventWithTimeout( + eventBus, + timeout: 5.0 + ); + + yield return waitEvent; + + if (waitEvent.IsTimeout) + { + Console.WriteLine("等待超时"); + } + else + { + Console.WriteLine($"玩家加入: {waitEvent.EventData.PlayerName}"); + } +} +``` + +### 等待 Task + +```csharp +public IEnumerator WaitForTaskExample() +{ + // 创建异步任务 + var task = LoadDataAsync(); + + // 在协程中等待 Task 完成 + var waitTask = new WaitForTask(task); + yield return waitTask; + + // 检查异常 + if (waitTask.Exception != null) + { + Console.WriteLine($"任务失败: {waitTask.Exception.Message}"); + } + else + { + Console.WriteLine("任务完成"); + } +} + +private async Task LoadDataAsync() +{ + await Task.Delay(1000); + // 加载数据... +} +``` + +### 等待多个协程 + +```csharp +public IEnumerator WaitForMultipleCoroutines() +{ + var coroutine1 = LoadTexture(); + var coroutine2 = LoadAudio(); + var coroutine3 = LoadModel(); + + // 等待所有协程完成 + yield return new WaitForAllCoroutines( + scheduler, + coroutine1, + coroutine2, + coroutine3 + ); + + Console.WriteLine("所有资源加载完成"); +} +``` + +### 协程嵌套 + +```csharp +public IEnumerator ParentCoroutine() +{ + Console.WriteLine("父协程开始"); + + // 等待子协程完成 + yield return new WaitForCoroutine(scheduler, ChildCoroutine()); + + Console.WriteLine("子协程完成"); +} + +private IEnumerator ChildCoroutine() +{ + yield return CoroutineHelper.WaitForSeconds(1.0); + Console.WriteLine("子协程执行"); +} +``` + +### 带进度的等待 + +```csharp +public IEnumerator LoadingWithProgress() +{ + Console.WriteLine("开始加载..."); + + yield return CoroutineHelper.WaitForProgress( + duration: 3.0, + onProgress: progress => + { + Console.WriteLine($"加载进度: {progress * 100:F0}%"); + } + ); + + Console.WriteLine("加载完成"); +} +``` + +### 协程标签管理 + +```csharp +// 使用标签运行协程 +var handle1 = scheduler.Run(Coroutine1(), tag: "gameplay"); +var handle2 = scheduler.Run(Coroutine2(), tag: "gameplay"); +var handle3 = scheduler.Run(Coroutine3(), tag: "ui"); + +// 停止所有带特定标签的协程 +scheduler.StopAllWithTag("gameplay"); + +// 获取标签下的所有协程 +var gameplayCoroutines = scheduler.GetCoroutinesByTag("gameplay"); +``` + +### 延迟调用和重复调用 + +```csharp +// 延迟 2 秒后执行 +scheduler.Run(CoroutineHelper.DelayedCall(2.0, () => +{ + Console.WriteLine("延迟执行"); +})); + +// 每隔 1 秒执行一次,共执行 5 次 +scheduler.Run(CoroutineHelper.RepeatCall(1.0, 5, () => +{ + Console.WriteLine("重复执行"); +})); + +// 无限重复,直到条件不满足 +scheduler.Run(CoroutineHelper.RepeatCallWhile(1.0, () => isRunning, () => +{ + Console.WriteLine("条件重复"); +})); +``` + +### 与命令系统集成 + +```csharp +using GFramework.Core.coroutine.extensions; + +public IEnumerator ExecuteCommandInCoroutine() +{ + // 在协程中执行命令 + var command = new LoadSceneCommand(); + yield return command.ExecuteAsCoroutine(this); + + Console.WriteLine("场景加载完成"); +} +``` + +### 与 CQRS 集成 + +```csharp +public IEnumerator QueryInCoroutine() +{ + // 在协程中执行查询 + var query = new GetPlayerDataQuery { PlayerId = 1 }; + var waitQuery = query.SendAsCoroutine(this); + + yield return waitQuery; + + var playerData = waitQuery.Result; + Console.WriteLine($"玩家名称: {playerData.Name}"); +} +``` + +## 最佳实践 + +1. **使用扩展方法启动协程**:通过架构组件的扩展方法启动协程更简洁 + ```csharp + ✓ this.StartCoroutine(MyCoroutine()); + ✗ scheduler.Run(MyCoroutine()); + ``` + +2. **合理使用协程标签**:为相关协程添加标签,便于批量管理 + ```csharp + this.StartCoroutine(BattleCoroutine(), tag: "battle"); + this.StartCoroutine(EffectCoroutine(), tag: "battle"); + + // 战斗结束时停止所有战斗相关协程 + this.StopCoroutinesWithTag("battle"); + ``` + +3. **避免在协程中执行耗时操作**:协程在主线程执行,不要阻塞 + ```csharp + ✗ public IEnumerator BadCoroutine() + { + Thread.Sleep(1000); // 阻塞主线程 + yield return null; + } + + ✓ public IEnumerator GoodCoroutine() + { + yield return CoroutineHelper.WaitForSeconds(1.0); // 非阻塞 + } + ``` + +4. **正确处理协程异常**:使用 try-catch 捕获异常 + ```csharp + public IEnumerator SafeCoroutine() + { + var waitTask = new WaitForTask(riskyTask); + yield return waitTask; + + if (waitTask.Exception != null) + { + // 处理异常 + Logger.Error($"任务失败: {waitTask.Exception.Message}"); + } + } + ``` + +5. **及时停止不需要的协程**:避免资源泄漏 + ```csharp + private CoroutineHandle? _healthRegenHandle; + + public void StartHealthRegen() + { + _healthRegenHandle = this.StartCoroutine(RegenerateHealth()); + } + + public void StopHealthRegen() + { + if (_healthRegenHandle.HasValue) + { + this.StopCoroutine(_healthRegenHandle.Value); + _healthRegenHandle = null; + } + } + ``` + +6. **使用 WaitForEvent 时记得释放资源**:避免内存泄漏 + ```csharp + public IEnumerator WaitEventExample() + { + using var waitEvent = new WaitForEvent(eventBus); + yield return waitEvent; + // using 确保资源被释放 + } + ``` + +## 常见问题 + +### 问题:协程什么时候执行? + +**解答**: +协程在调度器的 `Update()` 方法中执行。在 GFramework 中,架构会自动在每帧调用调度器的更新方法。 + +### 问题:协程是多线程的吗? + +**解答**: +不是。协程在主线程中执行,是单线程的。它们通过分帧执行来实现异步效果,不会阻塞主线程。 + +### 问题:如何在协程中等待异步方法? + +**解答**: +使用 `WaitForTask` 等待 Task 完成: + +```csharp +public IEnumerator WaitAsyncMethod() +{ + var task = SomeAsyncMethod(); + yield return new WaitForTask(task); +} +``` + +### 问题:协程可以返回值吗? + +**解答**: +协程本身不能直接返回值,但可以通过闭包或类成员变量传递结果: + +```csharp +private int _result; + +public IEnumerator CoroutineWithResult() +{ + yield return CoroutineHelper.WaitForSeconds(1.0); + _result = 42; +} + +// 使用 +this.StartCoroutine(CoroutineWithResult()); +// 稍后访问 _result +``` + +### 问题:如何停止所有协程? + +**解答**: +使用调度器的 `StopAll()` 方法: + +```csharp +// 停止所有协程 +scheduler.StopAll(); + +// 或通过扩展方法 +this.StopAllCoroutines(); +``` + +### 问题:协程中的异常会怎样? + +**解答**: +协程中未捕获的异常会触发 `OnCoroutineException` 事件,并停止该协程: + +```csharp +scheduler.OnCoroutineException += (handle, exception) => +{ + Logger.Error($"协程异常: {exception.Message}"); +}; +``` + +### 问题:WaitForSeconds 和 Delay 有什么区别? + +**解答**: +它们是相同的,`WaitForSeconds` 是辅助方法,内部创建 `Delay` 实例: + +```csharp +// 两者等价 +yield return CoroutineHelper.WaitForSeconds(1.0); +yield return new Delay(1.0); +``` + +## 相关文档 + +- [事件系统](/zh-CN/core/events) - 协程与事件系统集成 +- [命令系统](/zh-CN/core/command) - 在协程中执行命令 +- [CQRS](/zh-CN/core/cqrs) - 在协程中执行查询和命令 +- [协程系统教程](/zh-CN/tutorials/coroutine-tutorial) - 分步教程 diff --git a/docs/zh-CN/core/cqrs.md b/docs/zh-CN/core/cqrs.md new file mode 100644 index 0000000..7045536 --- /dev/null +++ b/docs/zh-CN/core/cqrs.md @@ -0,0 +1,613 @@ +--- +title: CQRS 与 Mediator +description: CQRS 模式通过 Mediator 实现命令查询职责分离,提供清晰的业务逻辑组织方式。 +--- + +# CQRS 与 Mediator + +## 概述 + +CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种架构模式,将数据的读取(Query)和修改(Command)操作分离。GFramework +通过集成 Mediator 库实现了 CQRS 模式,提供了类型安全、解耦的业务逻辑处理方式。 + +通过 CQRS,你可以将复杂的业务逻辑拆分为独立的命令和查询处理器,每个处理器只负责单一职责,使代码更易于测试和维护。 + +**主要特性**: + +- 命令查询职责分离 +- 基于 Mediator 模式的解耦设计 +- 支持管道行为(Behaviors) +- 异步处理支持 +- 与架构系统深度集成 +- 支持流式处理 + +## 核心概念 + +### Command(命令) + +命令表示修改系统状态的操作,如创建、更新、删除: + +```csharp +using GFramework.Core.cqrs.command; +using GFramework.Core.Abstractions.cqrs.command; + +// 定义命令输入 +public class CreatePlayerInput : ICommandInput +{ + public string Name { get; set; } + public int Level { get; set; } +} + +// 定义命令 +public class CreatePlayerCommand : CommandBase +{ + public CreatePlayerCommand(CreatePlayerInput input) : base(input) { } +} +``` + +### Query(查询) + +查询表示读取系统状态的操作,不修改数据: + +```csharp +using GFramework.Core.cqrs.query; +using GFramework.Core.Abstractions.cqrs.query; + +// 定义查询输入 +public class GetPlayerInput : IQueryInput +{ + public int PlayerId { get; set; } +} + +// 定义查询 +public class GetPlayerQuery : QueryBase +{ + public GetPlayerQuery(GetPlayerInput input) : base(input) { } +} +``` + +### Handler(处理器) + +处理器负责执行命令或查询的具体逻辑: + +```csharp +using GFramework.Core.cqrs.command; +using Mediator; + +// 命令处理器 +public class CreatePlayerCommandHandler : AbstractCommandHandler +{ + public override async ValueTask Handle( + CreatePlayerCommand command, + CancellationToken cancellationToken) + { + var input = command.Input; + var playerModel = this.GetModel(); + + // 创建玩家 + var playerId = playerModel.CreatePlayer(input.Name, input.Level); + + return playerId; + } +} +``` + +### Mediator(中介者) + +Mediator 负责将命令/查询路由到对应的处理器: + +```csharp +// 通过 Mediator 发送命令 +var command = new CreatePlayerCommand(new CreatePlayerInput +{ + Name = "Player1", + Level = 1 +}); + +var playerId = await mediator.Send(command); +``` + +## 基本用法 + +### 定义和发送命令 + +```csharp +// 1. 定义命令输入 +public class SaveGameInput : ICommandInput +{ + public int SlotId { get; set; } + public GameData Data { get; set; } +} + +// 2. 定义命令 +public class SaveGameCommand : CommandBase +{ + public SaveGameCommand(SaveGameInput input) : base(input) { } +} + +// 3. 实现命令处理器 +public class SaveGameCommandHandler : AbstractCommandHandler +{ + public override async ValueTask Handle( + SaveGameCommand command, + CancellationToken cancellationToken) + { + var input = command.Input; + var saveSystem = this.GetSystem(); + + // 保存游戏 + await saveSystem.SaveAsync(input.SlotId, input.Data); + + // 发送事件 + this.SendEvent(new GameSavedEvent { SlotId = input.SlotId }); + + return Unit.Value; + } +} + +// 4. 发送命令 +public async Task SaveGame() +{ + var mediator = this.GetService(); + + var command = new SaveGameCommand(new SaveGameInput + { + SlotId = 1, + Data = currentGameData + }); + + await mediator.Send(command); +} +``` + +### 定义和发送查询 + +```csharp +// 1. 定义查询输入 +public class GetHighScoresInput : IQueryInput +{ + public int Count { get; set; } = 10; +} + +// 2. 定义查询 +public class GetHighScoresQuery : QueryBase> +{ + public GetHighScoresQuery(GetHighScoresInput input) : base(input) { } +} + +// 3. 实现查询处理器 +public class GetHighScoresQueryHandler : AbstractQueryHandler> +{ + public override async ValueTask> Handle( + GetHighScoresQuery query, + CancellationToken cancellationToken) + { + var input = query.Input; + var scoreModel = this.GetModel(); + + // 查询高分榜 + var scores = await scoreModel.GetTopScoresAsync(input.Count); + + return scores; + } +} + +// 4. 发送查询 +public async Task> GetHighScores() +{ + var mediator = this.GetService(); + + var query = new GetHighScoresQuery(new GetHighScoresInput + { + Count = 10 + }); + + var scores = await mediator.Send(query); + return scores; +} +``` + +### 注册处理器 + +在架构中注册 Mediator 和处理器: + +```csharp +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 注册 Mediator 行为 + RegisterMediatorBehavior(); + RegisterMediatorBehavior(); + + // 处理器会自动通过依赖注入注册 + } +} +``` + +## 高级用法 + +### Request(请求) + +Request 是更通用的消息类型,可以用于任何场景: + +```csharp +using GFramework.Core.cqrs.request; +using GFramework.Core.Abstractions.cqrs.request; + +// 定义请求输入 +public class ValidatePlayerInput : IRequestInput +{ + public string PlayerName { get; set; } +} + +// 定义请求 +public class ValidatePlayerRequest : RequestBase +{ + public ValidatePlayerRequest(ValidatePlayerInput input) : base(input) { } +} + +// 实现请求处理器 +public class ValidatePlayerRequestHandler : AbstractRequestHandler +{ + public override async ValueTask Handle( + ValidatePlayerRequest request, + CancellationToken cancellationToken) + { + var input = request.Input; + var playerModel = this.GetModel(); + + // 验证玩家名称 + var isValid = await playerModel.IsNameValidAsync(input.PlayerName); + + return isValid; + } +} +``` + +### Notification(通知) + +Notification 用于一对多的消息广播: + +```csharp +using GFramework.Core.cqrs.notification; +using GFramework.Core.Abstractions.cqrs.notification; + +// 定义通知输入 +public class PlayerLevelUpInput : INotificationInput +{ + public int PlayerId { get; set; } + public int NewLevel { get; set; } +} + +// 定义通知 +public class PlayerLevelUpNotification : NotificationBase +{ + public PlayerLevelUpNotification(PlayerLevelUpInput input) : base(input) { } +} + +// 实现通知处理器 1 +public class AchievementNotificationHandler : AbstractNotificationHandler +{ + public override async ValueTask Handle( + PlayerLevelUpNotification notification, + CancellationToken cancellationToken) + { + var input = notification.Input; + // 检查成就 + CheckLevelAchievements(input.PlayerId, input.NewLevel); + await Task.CompletedTask; + } +} + +// 实现通知处理器 2 +public class RewardNotificationHandler : AbstractNotificationHandler +{ + public override async ValueTask Handle( + PlayerLevelUpNotification notification, + CancellationToken cancellationToken) + { + var input = notification.Input; + // 发放奖励 + GiveRewards(input.PlayerId, input.NewLevel); + await Task.CompletedTask; + } +} + +// 发布通知(所有处理器都会收到) +var notification = new PlayerLevelUpNotification(new PlayerLevelUpInput +{ + PlayerId = 1, + NewLevel = 10 +}); + +await mediator.Publish(notification); +``` + +### Pipeline Behaviors(管道行为) + +Behaviors 可以在处理器执行前后添加横切关注点: + +```csharp +using Mediator; + +// 日志行为 +public class LoggingBehavior : IPipelineBehavior + where TMessage : IMessage +{ + public async ValueTask Handle( + TMessage message, + CancellationToken cancellationToken, + MessageHandlerDelegate next) + { + var messageName = message.GetType().Name; + Console.WriteLine($"[开始] {messageName}"); + + var response = await next(message, cancellationToken); + + Console.WriteLine($"[完成] {messageName}"); + + return response; + } +} + +// 性能监控行为 +public class PerformanceBehavior : IPipelineBehavior + where TMessage : IMessage +{ + public async ValueTask Handle( + TMessage message, + CancellationToken cancellationToken, + MessageHandlerDelegate next) + { + var stopwatch = Stopwatch.StartNew(); + + var response = await next(message, cancellationToken); + + stopwatch.Stop(); + var elapsed = stopwatch.ElapsedMilliseconds; + + if (elapsed > 100) + { + Console.WriteLine($"警告: {message.GetType().Name} 耗时 {elapsed}ms"); + } + + return response; + } +} + +// 注册行为 +RegisterMediatorBehavior>(); +RegisterMediatorBehavior>(); +``` + +### 验证行为 + +```csharp +public class ValidationBehavior : IPipelineBehavior + where TMessage : IMessage +{ + public async ValueTask Handle( + TMessage message, + CancellationToken cancellationToken, + MessageHandlerDelegate next) + { + // 验证输入 + if (message is IValidatable validatable) + { + var errors = validatable.Validate(); + if (errors.Any()) + { + throw new ValidationException(errors); + } + } + + return await next(message, cancellationToken); + } +} +``` + +### 流式处理 + +处理大量数据时使用流式处理: + +```csharp +// 流式查询 +public class GetAllPlayersStreamQuery : QueryBase> +{ + public GetAllPlayersStreamQuery() : base(new EmptyInput()) { } +} + +// 流式查询处理器 +public class GetAllPlayersStreamQueryHandler : AbstractStreamQueryHandler +{ + public override async IAsyncEnumerable Handle( + GetAllPlayersStreamQuery query, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var playerModel = this.GetModel(); + + await foreach (var player in playerModel.GetAllPlayersAsync(cancellationToken)) + { + yield return player; + } + } +} + +// 使用流式查询 +var query = new GetAllPlayersStreamQuery(); +var stream = await mediator.CreateStream(query); + +await foreach (var player in stream) +{ + Console.WriteLine($"玩家: {player.Name}"); +} +``` + +## 最佳实践 + +1. **命令和查询分离**:严格区分修改和读取操作 + ```csharp + ✓ CreatePlayerCommand, GetPlayerQuery // 职责清晰 + ✗ PlayerCommand // 职责不明确 + ``` + +2. **使用有意义的命名**:命令用动词,查询用 Get + ```csharp + ✓ CreatePlayerCommand, UpdateScoreCommand, GetHighScoresQuery + ✗ PlayerCommand, ScoreCommand, ScoresQuery + ``` + +3. **输入验证**:在处理器中验证输入 + ```csharp + public override async ValueTask Handle(...) + { + if (string.IsNullOrEmpty(command.Input.Name)) + throw new ArgumentException("Name is required"); + + // 处理逻辑 + } + ``` + +4. **使用 Behaviors 处理横切关注点**:日志、性能、验证等 + ```csharp + RegisterMediatorBehavior>(); + RegisterMediatorBehavior>(); + ``` + +5. **保持处理器简单**:一个处理器只做一件事 + ```csharp + ✓ 处理器只负责业务逻辑,通过架构组件访问数据 + ✗ 处理器中包含复杂的数据访问和业务逻辑 + ``` + +6. **使用 CancellationToken**:支持操作取消 + ```csharp + public override async ValueTask Handle(..., CancellationToken cancellationToken) + { + await someAsyncOperation(cancellationToken); + } + ``` + +## 常见问题 + +### 问题:Command 和 Query 有什么区别? + +**解答**: + +- **Command**:修改系统状态,可能有副作用,通常返回 void 或简单结果 +- **Query**:只读取数据,无副作用,返回查询结果 + +```csharp +// Command: 修改状态 +CreatePlayerCommand -> 创建玩家 +UpdateScoreCommand -> 更新分数 + +// Query: 读取数据 +GetPlayerQuery -> 获取玩家信息 +GetHighScoresQuery -> 获取高分榜 +``` + +### 问题:什么时候使用 Request? + +**解答**: +Request 是更通用的消息类型,当操作既不是纯命令也不是纯查询时使用: + +```csharp +// 验证操作:读取数据并返回结果,但不修改状态 +ValidatePlayerRequest + +// 计算操作:基于输入计算结果 +CalculateDamageRequest +``` + +### 问题:Notification 和 Event 有什么区别? + +**解答**: + +- **Notification**:通过 Mediator 发送,处理器在同一请求上下文中执行 +- **Event**:通过 EventBus 发送,监听器异步执行 + +```csharp +// Notification: 同步处理 +await mediator.Publish(notification); // 等待所有处理器完成 + +// Event: 异步处理 +this.SendEvent(event); // 立即返回,监听器异步执行 +``` + +### 问题:如何处理命令失败? + +**解答**: +使用异常或返回 Result 类型: + +```csharp +// 方式 1: 抛出异常 +public override async ValueTask Handle(...) +{ + if (!IsValid()) + throw new InvalidOperationException("Invalid operation"); + + return Unit.Value; +} + +// 方式 2: 返回 Result +public override async ValueTask Handle(...) +{ + if (!IsValid()) + return Result.Failure("Invalid operation"); + + return Result.Success(); +} +``` + +### 问题:处理器可以调用其他处理器吗? + +**解答**: +可以,通过 Mediator 发送新的命令或查询: + +```csharp +public override async ValueTask Handle(...) +{ + var mediator = this.GetService(); + + // 调用其他命令 + await mediator.Send(new AnotherCommand(...)); + + return Unit.Value; +} +``` + +### 问题:如何测试处理器? + +**解答**: +处理器是独立的类,易于单元测试: + +```csharp +[Test] +public async Task CreatePlayer_ShouldReturnPlayerId() +{ + // Arrange + var handler = new CreatePlayerCommandHandler(); + handler.SetContext(mockContext); + + var command = new CreatePlayerCommand(new CreatePlayerInput + { + Name = "Test", + Level = 1 + }); + + // Act + var playerId = await handler.Handle(command, CancellationToken.None); + + // Assert + Assert.That(playerId, Is.GreaterThan(0)); +} +``` + +## 相关文档 + +- [命令系统](/zh-CN/core/command) - 传统命令模式 +- [查询系统](/zh-CN/core/query) - 传统查询模式 +- [事件系统](/zh-CN/core/events) - 事件驱动架构 +- [协程系统](/zh-CN/core/coroutine) - 在协程中使用 CQRS diff --git a/docs/zh-CN/core/lifecycle.md b/docs/zh-CN/core/lifecycle.md new file mode 100644 index 0000000..26afbfd --- /dev/null +++ b/docs/zh-CN/core/lifecycle.md @@ -0,0 +1,461 @@ +--- +title: 生命周期管理 +description: 生命周期管理提供了标准化的组件初始化和销毁机制,确保资源的正确管理和释放。 +--- + +# 生命周期管理 + +## 概述 + +生命周期管理是 GFramework 中用于管理组件初始化和销毁的核心机制。通过实现标准的生命周期接口,组件可以在适当的时机执行初始化逻辑和资源清理,确保系统的稳定性和资源的有效管理。 + +GFramework 提供了同步和异步两套生命周期接口,适用于不同的使用场景。架构会自动管理所有注册组件的生命周期,开发者只需实现相应的接口即可。 + +**主要特性**: + +- 标准化的初始化和销毁流程 +- 支持同步和异步操作 +- 自动生命周期管理 +- 按注册顺序初始化,按逆序销毁 +- 与架构系统深度集成 + +## 核心概念 + +### 生命周期接口层次 + +GFramework 提供了一套完整的生命周期接口: + +```csharp +// 同步接口 +public interface IInitializable +{ + void Initialize(); +} + +public interface IDestroyable +{ + void Destroy(); +} + +public interface ILifecycle : IInitializable, IDestroyable +{ +} + +// 异步接口 +public interface IAsyncInitializable +{ + Task InitializeAsync(); +} + +public interface IAsyncDestroyable +{ + ValueTask DestroyAsync(); +} + +public interface IAsyncLifecycle : IAsyncInitializable, IAsyncDestroyable +{ +} +``` + +### 初始化阶段 + +组件在注册到架构后会自动进行初始化: + +```csharp +public class PlayerModel : AbstractModel +{ + protected override void OnInit() + { + // 初始化逻辑 + Console.WriteLine("PlayerModel 初始化"); + } +} +``` + +### 销毁阶段 + +当架构销毁时,所有实现了 `IDestroyable` 的组件会按注册的逆序被销毁: + +```csharp +public class GameSystem : AbstractSystem +{ + public void Destroy() + { + // 清理资源 + Console.WriteLine("GameSystem 销毁"); + } +} +``` + +## 基本用法 + +### 实现同步生命周期 + +最常见的方式是继承框架提供的抽象基类: + +```csharp +using GFramework.Core.model; + +public class InventoryModel : AbstractModel +{ + private List _items = new(); + + protected override void OnInit() + { + // 初始化库存 + _items = new List(); + Console.WriteLine("库存系统已初始化"); + } +} +``` + +### 实现销毁逻辑 + +对于需要清理资源的组件,实现 `IDestroyable` 接口: + +```csharp +using GFramework.Core.Abstractions.system; +using GFramework.Core.Abstractions.lifecycle; + +public class AudioSystem : ISystem, IDestroyable +{ + private AudioEngine _engine; + + public void Initialize() + { + _engine = new AudioEngine(); + _engine.Start(); + } + + public void Destroy() + { + // 清理音频资源 + _engine?.Stop(); + _engine?.Dispose(); + _engine = null; + } +} +``` + +### 在架构中注册 + +组件注册后,架构会自动管理其生命周期: + +```csharp +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 注册顺序:Model -> System -> Utility + RegisterModel(new PlayerModel()); // 1. 初始化 + RegisterModel(new InventoryModel()); // 2. 初始化 + RegisterSystem(new AudioSystem()); // 3. 初始化 + + // 销毁顺序会自动反转: + // AudioSystem -> InventoryModel -> PlayerModel + } +} +``` + +## 高级用法 + +### 异步初始化 + +对于需要异步操作的组件(如加载配置、连接数据库),使用异步生命周期: + +```csharp +using GFramework.Core.Abstractions.lifecycle; +using GFramework.Core.Abstractions.system; + +public class ConfigurationSystem : ISystem, IAsyncInitializable +{ + private Configuration _config; + + public async Task InitializeAsync() + { + // 异步加载配置文件 + _config = await LoadConfigurationAsync(); + Console.WriteLine("配置已加载"); + } + + private async Task LoadConfigurationAsync() + { + await Task.Delay(100); // 模拟异步操作 + return new Configuration(); + } +} +``` + +### 异步销毁 + +对于需要异步清理的资源(如关闭网络连接、保存数据): + +```csharp +using GFramework.Core.Abstractions.lifecycle; + +public class NetworkSystem : ISystem, IAsyncDestroyable +{ + private NetworkClient _client; + + public void Initialize() + { + _client = new NetworkClient(); + } + + public async ValueTask DestroyAsync() + { + // 异步关闭连接 + if (_client != null) + { + await _client.DisconnectAsync(); + await _client.DisposeAsync(); + } + Console.WriteLine("网络连接已关闭"); + } +} +``` + +### 完整异步生命周期 + +同时实现异步初始化和销毁: + +```csharp +public class DatabaseSystem : ISystem, IAsyncLifecycle +{ + private DatabaseConnection _connection; + + public async Task InitializeAsync() + { + // 异步连接数据库 + _connection = new DatabaseConnection(); + await _connection.ConnectAsync("connection-string"); + Console.WriteLine("数据库已连接"); + } + + public async ValueTask DestroyAsync() + { + // 异步关闭数据库连接 + if (_connection != null) + { + await _connection.CloseAsync(); + await _connection.DisposeAsync(); + } + Console.WriteLine("数据库连接已关闭"); + } +} +``` + +### 生命周期钩子 + +监听架构的生命周期阶段: + +```csharp +using GFramework.Core.Abstractions.enums; + +public class AnalyticsSystem : AbstractSystem +{ + protected override void OnInit() + { + Console.WriteLine("分析系统初始化"); + } + + public override void OnArchitecturePhase(ArchitecturePhase phase) + { + switch (phase) + { + case ArchitecturePhase.Initializing: + Console.WriteLine("架构正在初始化"); + break; + case ArchitecturePhase.Ready: + Console.WriteLine("架构已就绪"); + StartTracking(); + break; + case ArchitecturePhase.Destroying: + Console.WriteLine("架构正在销毁"); + StopTracking(); + break; + } + } + + private void StartTracking() { } + private void StopTracking() { } +} +``` + +## 最佳实践 + +1. **优先使用抽象基类**:继承 `AbstractModel`、`AbstractSystem` 等基类,它们已经实现了生命周期接口 + ```csharp + ✓ public class MyModel : AbstractModel { } + ✗ public class MyModel : IModel, IInitializable { } + ``` + +2. **初始化顺序很重要**:按依赖关系注册组件,被依赖的组件先注册 + ```csharp + protected override void Init() + { + RegisterModel(new ConfigModel()); // 先注册配置 + RegisterModel(new PlayerModel()); // 再注册依赖配置的模型 + RegisterSystem(new GameplaySystem()); // 最后注册系统 + } + ``` + +3. **销毁时释放资源**:实现 `Destroy()` 方法清理非托管资源 + ```csharp + public void Destroy() + { + // 释放事件订阅 + _eventBus.Unsubscribe(OnGameEvent); + + // 释放非托管资源 + _nativeHandle?.Dispose(); + + // 清空引用 + _cache?.Clear(); + } + ``` + +4. **异步操作使用异步接口**:避免在同步方法中阻塞异步操作 + ```csharp + ✓ public async Task InitializeAsync() { await LoadDataAsync(); } + ✗ public void Initialize() { LoadDataAsync().Wait(); } // 可能死锁 + ``` + +5. **避免在初始化中访问其他组件**:初始化顺序可能导致组件尚未就绪 + ```csharp + ✗ protected override void OnInit() + { + var system = this.GetSystem(); // 可能尚未初始化 + } + + ✓ public override void OnArchitecturePhase(ArchitecturePhase phase) + { + if (phase == ArchitecturePhase.Ready) + { + var system = this.GetSystem(); // 安全 + } + } + ``` + +6. **使用 OnArchitecturePhase 处理跨组件依赖**:在 Ready 阶段访问其他组件 + ```csharp + public override void OnArchitecturePhase(ArchitecturePhase phase) + { + if (phase == ArchitecturePhase.Ready) + { + // 此时所有组件都已初始化完成 + var config = this.GetModel(); + ApplyConfiguration(config); + } + } + ``` + +## 常见问题 + +### 问题:什么时候使用同步 vs 异步生命周期? + +**解答**: + +- **同步**:简单的初始化逻辑,如创建对象、设置默认值 +- **异步**:需要 I/O 操作的场景,如加载文件、网络请求、数据库连接 + +```csharp +// 同步:简单初始化 +public class ScoreModel : AbstractModel +{ + protected override void OnInit() + { + Score = 0; // 简单赋值 + } +} + +// 异步:需要 I/O +public class SaveSystem : ISystem, IAsyncInitializable +{ + public async Task InitializeAsync() + { + await LoadSaveDataAsync(); // 文件 I/O + } +} +``` + +### 问题:组件的初始化和销毁顺序是什么? + +**解答**: + +- **初始化顺序**:按注册顺序(先注册先初始化) +- **销毁顺序**:按注册的逆序(后注册先销毁) + +```csharp +protected override void Init() +{ + RegisterModel(new A()); // 1. 初始化,3. 销毁 + RegisterModel(new B()); // 2. 初始化,2. 销毁 + RegisterSystem(new C()); // 3. 初始化,1. 销毁 +} +``` + +### 问题:如何在初始化时访问其他组件? + +**解答**: +不要在 `OnInit()` 中访问其他组件,使用 `OnArchitecturePhase()` 在 Ready 阶段访问: + +```csharp +public class DependentSystem : AbstractSystem +{ + protected override void OnInit() + { + // ✗ 不要在这里访问其他组件 + } + + public override void OnArchitecturePhase(ArchitecturePhase phase) + { + if (phase == ArchitecturePhase.Ready) + { + // ✓ 在这里安全访问其他组件 + var config = this.GetModel(); + } + } +} +``` + +### 问题:Destroy() 方法一定会被调用吗? + +**解答**: +只有在正常销毁架构时才会调用。如果应用程序崩溃或被强制终止,`Destroy()` 可能不会被调用。因此: + +- 不要依赖 `Destroy()` 保存关键数据 +- 使用自动保存机制保护重要数据 +- 非托管资源应该实现 `IDisposable` 模式 + +### 问题:可以在 Destroy() 中访问其他组件吗? + +**解答**: +不推荐。销毁时其他组件可能已经被销毁。如果必须访问,确保检查组件是否仍然可用: + +```csharp +public void Destroy() +{ + // ✗ 不安全 + var system = this.GetSystem(); + system.DoSomething(); + + // ✓ 安全 + try + { + var system = this.GetSystem(); + system?.DoSomething(); + } + catch + { + // 组件可能已销毁 + } +} +``` + +## 相关文档 + +- [架构组件](/zh-CN/core/architecture) - 架构基础和组件注册 +- [Model 层](/zh-CN/core/model) - 数据模型的生命周期 +- [System 层](/zh-CN/core/system) - 业务系统的生命周期 +- [异步初始化](/zh-CN/core/async-initialization) - 异步架构初始化详解 diff --git a/docs/zh-CN/core/resource.md b/docs/zh-CN/core/resource.md new file mode 100644 index 0000000..b73cab2 --- /dev/null +++ b/docs/zh-CN/core/resource.md @@ -0,0 +1,507 @@ +--- +title: 资源管理系统 +description: 资源管理系统提供了统一的资源加载、缓存和卸载机制,支持引用计数和多种释放策略。 +--- + +# 资源管理系统 + +## 概述 + +资源管理系统是 GFramework 中用于管理游戏资源(如纹理、音频、模型等)的核心组件。它提供了统一的资源加载接口,自动缓存机制,以及灵活的资源释放策略,帮助你高效管理游戏资源的生命周期。 + +通过资源管理器,你可以避免重复加载相同资源,使用引用计数自动管理资源生命周期,并根据需求选择合适的释放策略。 + +**主要特性**: + +- 统一的资源加载接口(同步/异步) +- 自动资源缓存和去重 +- 引用计数管理 +- 可插拔的资源加载器 +- 灵活的释放策略(手动/自动) +- 线程安全操作 + +## 核心概念 + +### 资源管理器 + +`ResourceManager` 是资源管理的核心类,负责加载、缓存和卸载资源: + +```csharp +using GFramework.Core.Abstractions.resource; + +// 获取资源管理器(通常通过架构获取) +var resourceManager = this.GetUtility(); + +// 加载资源 +var texture = resourceManager.Load("textures/player.png"); +``` + +### 资源句柄 + +`IResourceHandle` 用于管理资源的引用计数,确保资源在使用期间不被释放: + +```csharp +// 获取资源句柄(自动增加引用计数) +using var handle = resourceManager.GetHandle("textures/player.png"); + +// 使用资源 +var texture = handle.Resource; + +// 离开作用域时自动减少引用计数 +``` + +### 资源加载器 + +`IResourceLoader` 定义了如何加载特定类型的资源: + +```csharp +public interface IResourceLoader where T : class +{ + T Load(string path); + Task LoadAsync(string path); + void Unload(T resource); +} +``` + +### 释放策略 + +`IResourceReleaseStrategy` 决定何时释放资源: + +- **手动释放**(`ManualReleaseStrategy`):引用计数为 0 时不自动释放,需要手动调用 `Unload` +- **自动释放**(`AutoReleaseStrategy`):引用计数为 0 时自动释放资源 + +## 基本用法 + +### 注册资源加载器 + +首先需要为每种资源类型注册加载器: + +```csharp +using GFramework.Core.Abstractions.resource; + +// 实现纹理加载器 +public class TextureLoader : IResourceLoader +{ + public Texture Load(string path) + { + // 同步加载纹理 + return LoadTextureFromFile(path); + } + + public async Task LoadAsync(string path) + { + // 异步加载纹理 + return await LoadTextureFromFileAsync(path); + } + + public void Unload(Texture resource) + { + // 释放纹理资源 + resource?.Dispose(); + } +} + +// 在架构中注册加载器 +public class GameArchitecture : Architecture +{ + protected override void Init() + { + var resourceManager = new ResourceManager(); + resourceManager.RegisterLoader(new TextureLoader()); + RegisterUtility(resourceManager); + } +} +``` + +### 同步加载资源 + +```csharp +// 加载资源 +var texture = resourceManager.Load("textures/player.png"); + +if (texture != null) +{ + // 使用纹理 + sprite.Texture = texture; +} +``` + +### 异步加载资源 + +```csharp +// 异步加载资源 +var texture = await resourceManager.LoadAsync("textures/player.png"); + +if (texture != null) +{ + sprite.Texture = texture; +} +``` + +### 使用资源句柄 + +```csharp +public class PlayerController +{ + private IResourceHandle? _textureHandle; + + public void LoadTexture() + { + var resourceManager = this.GetUtility(); + + // 获取句柄(增加引用计数) + _textureHandle = resourceManager.GetHandle("textures/player.png"); + + if (_textureHandle?.Resource != null) + { + sprite.Texture = _textureHandle.Resource; + } + } + + public void UnloadTexture() + { + // 释放句柄(减少引用计数) + _textureHandle?.Dispose(); + _textureHandle = null; + } +} +``` + +## 高级用法 + +### 预加载资源 + +在游戏启动或场景切换时预加载资源: + +```csharp +public async Task PreloadGameAssets() +{ + var resourceManager = this.GetUtility(); + + // 预加载多个资源 + await Task.WhenAll( + resourceManager.PreloadAsync("textures/player.png"), + resourceManager.PreloadAsync("textures/enemy.png"), + resourceManager.PreloadAsync("audio/bgm.mp3") + ); + + Console.WriteLine("资源预加载完成"); +} +``` + +### 使用自动释放策略 + +```csharp +using GFramework.Core.resource; + +// 设置自动释放策略 +var resourceManager = this.GetUtility(); +resourceManager.SetReleaseStrategy(new AutoReleaseStrategy()); + +// 使用资源句柄 +using (var handle = resourceManager.GetHandle("textures/temp.png")) +{ + // 使用资源 + var texture = handle.Resource; +} +// 离开作用域后,引用计数为 0,资源自动释放 +``` + +### 批量卸载资源 + +```csharp +// 卸载特定资源 +resourceManager.Unload("textures/old_texture.png"); + +// 卸载所有资源 +resourceManager.UnloadAll(); +``` + +### 查询资源状态 + +```csharp +// 检查资源是否已加载 +if (resourceManager.IsLoaded("textures/player.png")) +{ + Console.WriteLine("资源已在缓存中"); +} + +// 获取已加载资源数量 +Console.WriteLine($"已加载 {resourceManager.LoadedResourceCount} 个资源"); + +// 获取所有已加载资源的路径 +foreach (var path in resourceManager.GetLoadedResourcePaths()) +{ + Console.WriteLine($"已加载: {path}"); +} +``` + +### 自定义释放策略 + +```csharp +using GFramework.Core.Abstractions.resource; + +// 实现基于时间的释放策略 +public class TimeBasedReleaseStrategy : IResourceReleaseStrategy +{ + private readonly Dictionary _lastAccessTime = new(); + private readonly TimeSpan _timeout = TimeSpan.FromMinutes(5); + + public bool ShouldRelease(string path, int refCount) + { + // 引用计数为 0 且超过 5 分钟未访问 + if (refCount > 0) + return false; + + if (!_lastAccessTime.TryGetValue(path, out var lastAccess)) + return false; + + return DateTime.Now - lastAccess > _timeout; + } + + public void UpdateAccessTime(string path) + { + _lastAccessTime[path] = DateTime.Now; + } +} + +// 使用自定义策略 +resourceManager.SetReleaseStrategy(new TimeBasedReleaseStrategy()); +``` + +### 资源池模式 + +结合对象池实现资源复用: + +```csharp +public class BulletPool +{ + private readonly IResourceManager _resourceManager; + private readonly Queue _pool = new(); + private IResourceHandle? _textureHandle; + + public BulletPool(IResourceManager resourceManager) + { + _resourceManager = resourceManager; + // 加载并持有纹理句柄 + _textureHandle = _resourceManager.GetHandle("textures/bullet.png"); + } + + public Bullet Get() + { + if (_pool.Count > 0) + { + return _pool.Dequeue(); + } + + // 创建新子弹,使用缓存的纹理 + var bullet = new Bullet(); + bullet.Texture = _textureHandle?.Resource; + return bullet; + } + + public void Return(Bullet bullet) + { + bullet.Reset(); + _pool.Enqueue(bullet); + } + + public void Dispose() + { + // 释放纹理句柄 + _textureHandle?.Dispose(); + _textureHandle = null; + } +} +``` + +### 资源依赖管理 + +```csharp +public class MaterialLoader : IResourceLoader +{ + private readonly IResourceManager _resourceManager; + + public MaterialLoader(IResourceManager resourceManager) + { + _resourceManager = resourceManager; + } + + public Material Load(string path) + { + var material = new Material(); + + // 加载材质依赖的纹理 + material.DiffuseTexture = _resourceManager.Load($"{path}/diffuse.png"); + material.NormalTexture = _resourceManager.Load($"{path}/normal.png"); + + return material; + } + + public async Task LoadAsync(string path) + { + var material = new Material(); + + // 并行加载依赖资源 + var tasks = new[] + { + _resourceManager.LoadAsync($"{path}/diffuse.png"), + _resourceManager.LoadAsync($"{path}/normal.png") + }; + + var results = await Task.WhenAll(tasks); + material.DiffuseTexture = results[0]; + material.NormalTexture = results[1]; + + return material; + } + + public void Unload(Material resource) + { + // 材质卸载时,纹理由资源管理器自动管理 + resource?.Dispose(); + } +} +``` + +## 最佳实践 + +1. **使用资源句柄管理生命周期**:优先使用句柄而不是直接加载 + ```csharp + ✓ using var handle = resourceManager.GetHandle(path); + ✗ var texture = resourceManager.Load(path); // 需要手动管理 + ``` + +2. **选择合适的释放策略**:根据游戏需求选择策略 + - 手动释放:适合长期使用的资源(如 UI 纹理) + - 自动释放:适合临时资源(如特效纹理) + +3. **预加载关键资源**:避免游戏中途加载导致卡顿 + ```csharp + // 在场景加载时预加载 + await PreloadSceneAssets(); + ``` + +4. **避免重复加载**:使用 `IsLoaded` 检查缓存 + ```csharp + if (!resourceManager.IsLoaded(path)) + { + await resourceManager.LoadAsync(path); + } + ``` + +5. **及时释放不用的资源**:避免内存泄漏 + ```csharp + // 场景切换时卸载旧场景资源 + foreach (var path in oldSceneResources) + { + resourceManager.Unload(path); + } + ``` + +6. **使用 using 语句管理句柄**:确保引用计数正确 + ```csharp + ✓ using (var handle = resourceManager.GetHandle(path)) + { + // 使用资源 + } // 自动释放 + + ✗ var handle = resourceManager.GetHandle(path); + // 忘记调用 Dispose() + ``` + +## 常见问题 + +### 问题:资源加载失败怎么办? + +**解答**: +`Load` 和 `LoadAsync` 方法在失败时返回 `null`,应该检查返回值: + +```csharp +var texture = resourceManager.Load(path); +if (texture == null) +{ + Logger.Error($"Failed to load texture: {path}"); + // 使用默认纹理 + texture = defaultTexture; +} +``` + +### 问题:如何避免重复加载相同资源? + +**解答**: +资源管理器自动缓存已加载的资源,多次加载相同路径只会返回缓存的实例: + +```csharp +var texture1 = resourceManager.Load("player.png"); +var texture2 = resourceManager.Load("player.png"); +// texture1 和 texture2 是同一个实例 +``` + +### 问题:什么时候使用手动释放 vs 自动释放? + +**解答**: + +- **手动释放**:适合长期使用的资源,如 UI、角色模型 +- **自动释放**:适合临时资源,如特效、临时纹理 + +```csharp +// 手动释放:UI 资源长期使用 +resourceManager.SetReleaseStrategy(new ManualReleaseStrategy()); + +// 自动释放:特效资源用完即释放 +resourceManager.SetReleaseStrategy(new AutoReleaseStrategy()); +``` + +### 问题:资源句柄的引用计数如何工作? + +**解答**: + +- `GetHandle` 增加引用计数 +- `Dispose` 减少引用计数 +- 引用计数为 0 时,根据释放策略决定是否卸载 + +```csharp +// 引用计数: 0 +var handle1 = resourceManager.GetHandle(path); // 引用计数: 1 +var handle2 = resourceManager.GetHandle(path); // 引用计数: 2 + +handle1.Dispose(); // 引用计数: 1 +handle2.Dispose(); // 引用计数: 0(可能被释放) +``` + +### 问题:如何实现资源热重载? + +**解答**: +卸载旧资源后重新加载: + +```csharp +public void ReloadResource(string path) +{ + // 卸载旧资源 + resourceManager.Unload(path); + + // 重新加载 + var newResource = resourceManager.Load(path); +} +``` + +### 问题:资源管理器是线程安全的吗? + +**解答**: +是的,所有公共方法都是线程安全的,可以在多线程环境中使用: + +```csharp +// 在多个线程中并行加载 +Parallel.For(0, 10, i => +{ + var texture = resourceManager.Load($"texture_{i}.png"); +}); +``` + +## 相关文档 + +- [对象池系统](/zh-CN/core/pool) - 结合对象池复用资源 +- [协程系统](/zh-CN/core/coroutine) - 异步加载资源 +- [Godot 资源仓储](/zh-CN/godot/resource) - Godot 引擎的资源管理 +- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 详细教程 diff --git a/docs/zh-CN/core/state-machine.md b/docs/zh-CN/core/state-machine.md new file mode 100644 index 0000000..86d88a1 --- /dev/null +++ b/docs/zh-CN/core/state-machine.md @@ -0,0 +1,576 @@ +--- +title: 状态机系统 +description: 状态机系统提供了灵活的状态管理机制,支持状态转换、历史记录和异步操作。 +--- + +# 状态机系统 + +## 概述 + +状态机系统是 GFramework 中用于管理游戏状态的核心组件。通过状态机,你可以清晰地定义游戏的各种状态(如菜单、游戏中、暂停、游戏结束等),以及状态之间的转换规则,使游戏逻辑更加结构化和易于维护。 + +状态机系统支持同步和异步状态操作,提供状态历史记录,并与架构系统深度集成,让你可以在状态中访问所有架构组件。 + +**主要特性**: + +- 类型安全的状态管理 +- 支持同步和异步状态 +- 状态转换验证 +- 状态历史记录和回退 +- 与架构系统集成 +- 线程安全操作 + +## 核心概念 + +### 状态接口 + +`IState` 定义了状态的基本行为: + +```csharp +public interface IState +{ + void OnEnter(IState? from); // 进入状态 + void OnExit(IState? to); // 退出状态 + bool CanTransitionTo(IState target); // 转换验证 +} +``` + +### 状态机 + +`IStateMachine` 管理状态的注册和切换: + +```csharp +public interface IStateMachine +{ + IState? Current { get; } // 当前状态 + IStateMachine Register(IState state); // 注册状态 + Task ChangeToAsync() where T : IState; // 切换状态 +} +``` + +### 状态机系统 + +`IStateMachineSystem` 结合了状态机和系统的能力: + +```csharp +public interface IStateMachineSystem : ISystem, IStateMachine +{ + // 继承 ISystem 和 IStateMachine 的所有功能 +} +``` + +## 基本用法 + +### 定义状态 + +继承 `ContextAwareStateBase` 创建状态: + +```csharp +using GFramework.Core.state; + +// 菜单状态 +public class MenuState : ContextAwareStateBase +{ + public override void OnEnter(IState? from) + { + Console.WriteLine("进入菜单"); + // 显示菜单 UI + } + + public override void OnExit(IState? to) + { + Console.WriteLine("退出菜单"); + // 隐藏菜单 UI + } +} + +// 游戏状态 +public class GameplayState : ContextAwareStateBase +{ + public override void OnEnter(IState? from) + { + Console.WriteLine("开始游戏"); + // 初始化游戏场景 + } + + public override void OnExit(IState? to) + { + Console.WriteLine("结束游戏"); + // 清理游戏场景 + } +} +``` + +### 注册和使用状态机 + +```csharp +using GFramework.Core.state; + +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 创建状态机系统 + var stateMachine = new StateMachineSystem(); + + // 注册状态 + stateMachine + .Register(new MenuState()) + .Register(new GameplayState()) + .Register(new PauseState()); + + // 注册到架构 + RegisterSystem(stateMachine); + } +} +``` + +### 切换状态 + +```csharp +public class GameController : IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public async Task StartGame() + { + var stateMachine = this.GetSystem(); + + // 切换到游戏状态 + var success = await stateMachine.ChangeToAsync(); + + if (success) + { + Console.WriteLine("成功进入游戏状态"); + } + } +} +``` + +## 高级用法 + +### 状态转换验证 + +控制状态之间的转换规则: + +```csharp +public class GameplayState : ContextAwareStateBase +{ + public override bool CanTransitionTo(IState target) + { + // 只能从游戏状态转换到暂停或游戏结束状态 + return target is PauseState or GameOverState; + } + + public override void OnEnter(IState? from) + { + Console.WriteLine($"从 {from?.GetType().Name ?? "初始"} 进入游戏"); + } +} + +public class PauseState : ContextAwareStateBase +{ + public override bool CanTransitionTo(IState target) + { + // 暂停状态只能返回游戏状态 + return target is GameplayState; + } +} +``` + +### 异步状态 + +处理需要异步操作的状态: + +```csharp +using GFramework.Core.Abstractions.state; + +public class LoadingState : AsyncContextAwareStateBase +{ + public override async Task OnEnterAsync(IState? from) + { + Console.WriteLine("开始加载..."); + + // 异步加载资源 + await LoadResourcesAsync(); + + Console.WriteLine("加载完成"); + + // 自动切换到下一个状态 + var stateMachine = this.GetSystem(); + await stateMachine.ChangeToAsync(); + } + + private async Task LoadResourcesAsync() + { + // 模拟异步加载 + await Task.Delay(2000); + } + + public override async Task OnExitAsync(IState? to) + { + Console.WriteLine("退出加载状态"); + await Task.CompletedTask; + } +} +``` + +### 状态历史和回退 + +```csharp +public class GameController : IController +{ + public async Task NavigateBack() + { + var stateMachine = this.GetSystem(); + + // 回退到上一个状态 + var success = await stateMachine.GoBackAsync(); + + if (success) + { + Console.WriteLine("已返回上一个状态"); + } + } + + public void ShowHistory() + { + var stateMachine = this.GetSystem(); + + // 获取状态历史 + var history = stateMachine.GetStateHistory(); + + Console.WriteLine("状态历史:"); + foreach (var state in history) + { + Console.WriteLine($"- {state.GetType().Name}"); + } + } +} +``` + +### 在状态中访问架构组件 + +```csharp +public class GameplayState : ContextAwareStateBase +{ + public override void OnEnter(IState? from) + { + // 访问 Model + var playerModel = this.GetModel(); + playerModel.Reset(); + + // 访问 System + var audioSystem = this.GetSystem(); + audioSystem.PlayBGM("gameplay"); + + // 发送事件 + this.SendEvent(new GameStartedEvent()); + } + + public override void OnExit(IState? to) + { + // 停止音乐 + var audioSystem = this.GetSystem(); + audioSystem.StopBGM(); + + // 发送事件 + this.SendEvent(new GameEndedEvent()); + } +} +``` + +### 状态数据传递 + +```csharp +// 定义带数据的状态 +public class GameplayState : ContextAwareStateBase +{ + public int Level { get; set; } + public string Difficulty { get; set; } = "Normal"; + + public override void OnEnter(IState? from) + { + Console.WriteLine($"开始关卡 {Level},难度: {Difficulty}"); + } +} + +// 切换状态并设置数据 +public async Task StartLevel(int level, string difficulty) +{ + var stateMachine = this.GetSystem(); + + // 获取状态实例并设置数据 + var gameplayState = stateMachine.GetState(); + if (gameplayState != null) + { + gameplayState.Level = level; + gameplayState.Difficulty = difficulty; + } + + // 切换状态 + await stateMachine.ChangeToAsync(); +} +``` + +### 状态事件通知 + +```csharp +// 定义状态变更事件 +public class StateChangedEvent +{ + public IState? From { get; set; } + public IState To { get; set; } +} + +// 自定义状态机系统 +public class CustomStateMachineSystem : StateMachineSystem +{ + protected override async Task OnStateChangedAsync(IState? from, IState to) + { + // 发送状态变更事件 + this.SendEvent(new StateChangedEvent + { + From = from, + To = to + }); + + await base.OnStateChangedAsync(from, to); + } +} +``` + +### 条件状态转换 + +```csharp +public class BattleState : ContextAwareStateBase +{ + public override bool CanTransitionTo(IState target) + { + // 战斗中不能直接退出,必须先结束战斗 + if (target is MenuState) + { + var battleModel = this.GetModel(); + return battleModel.IsBattleEnded; + } + + return true; + } +} + +// 尝试切换状态 +public async Task TryExitBattle() +{ + var stateMachine = this.GetSystem(); + + // 检查是否可以切换 + var canChange = await stateMachine.CanChangeToAsync(); + + if (canChange) + { + await stateMachine.ChangeToAsync(); + } + else + { + Console.WriteLine("战斗尚未结束,无法退出"); + } +} +``` + +## 最佳实践 + +1. **使用基类创建状态**:继承 `ContextAwareStateBase` 或 `AsyncContextAwareStateBase` + ```csharp + ✓ public class MyState : ContextAwareStateBase { } + ✗ public class MyState : IState { } // 需要手动实现所有接口 + ``` + +2. **在 OnEnter 中初始化,在 OnExit 中清理**:保持状态的独立性 + ```csharp + public override void OnEnter(IState? from) + { + // 初始化状态相关资源 + LoadUI(); + StartBackgroundMusic(); + } + + public override void OnExit(IState? to) + { + // 清理状态相关资源 + UnloadUI(); + StopBackgroundMusic(); + } + ``` + +3. **使用转换验证控制状态流**:避免非法状态转换 + ```csharp + public override bool CanTransitionTo(IState target) + { + // 定义明确的转换规则 + return target is AllowedState1 or AllowedState2; + } + ``` + +4. **异步操作使用异步状态**:避免阻塞主线程 + ```csharp + ✓ public class LoadingState : AsyncContextAwareStateBase + { + public override async Task OnEnterAsync(IState? from) + { + await LoadDataAsync(); + } + } + + ✗ public class LoadingState : ContextAwareStateBase + { + public override void OnEnter(IState? from) + { + LoadDataAsync().Wait(); // 阻塞主线程 + } + } + ``` + +5. **合理使用状态历史**:避免历史记录过大 + ```csharp + // 创建状态机时设置历史大小 + var stateMachine = new StateMachineSystem(maxHistorySize: 10); + ``` + +6. **状态保持单一职责**:每个状态只负责一个场景或功能 + ```csharp + ✓ MenuState, GameplayState, PauseState // 职责清晰 + ✗ GameState // 职责不明确,包含太多逻辑 + ``` + +## 常见问题 + +### 问题:状态切换失败怎么办? + +**解答**: +`ChangeToAsync` 返回 `false` 表示切换失败,通常是因为 `CanTransitionTo` 返回 `false`: + +```csharp +var success = await stateMachine.ChangeToAsync(); +if (!success) +{ + Console.WriteLine("状态切换被拒绝"); + // 检查转换规则 +} +``` + +### 问题:如何在状态之间传递数据? + +**解答**: +有几种方式: + +1. **通过状态属性**: + +```csharp +var state = stateMachine.GetState(); +state.Level = 5; +await stateMachine.ChangeToAsync(); +``` + +2. **通过 Model**: + +```csharp +// 在切换前设置 Model +var gameModel = this.GetModel(); +gameModel.CurrentLevel = 5; + +// 在状态中读取 +public override void OnEnter(IState? from) +{ + var gameModel = this.GetModel(); + var level = gameModel.CurrentLevel; +} +``` + +3. **通过事件**: + +```csharp +this.SendEvent(new LevelSelectedEvent { Level = 5 }); +await stateMachine.ChangeToAsync(); +``` + +### 问题:状态机系统和普通状态机有什么区别? + +**解答**: + +- **StateMachine**:纯状态机,不依赖架构 +- **StateMachineSystem**:集成到架构中,状态可以访问所有架构组件 + +```csharp +// 使用 StateMachineSystem(推荐) +RegisterSystem(new StateMachineSystem()); + +// 使用 StateMachine(独立使用) +var stateMachine = new StateMachine(); +``` + +### 问题:如何处理状态切换动画? + +**解答**: +在 `OnExit` 和 `OnEnter` 中使用协程: + +```csharp +public class MenuState : AsyncContextAwareStateBase +{ + public override async Task OnExitAsync(IState? to) + { + // 播放淡出动画 + await PlayFadeOutAnimation(); + } +} + +public class GameplayState : AsyncContextAwareStateBase +{ + public override async Task OnEnterAsync(IState? from) + { + // 播放淡入动画 + await PlayFadeInAnimation(); + } +} +``` + +### 问题:可以在状态中切换到其他状态吗? + +**解答**: +可以,但要注意避免递归切换: + +```csharp +public override async void OnEnter(IState? from) +{ + // 检查条件后自动切换 + if (ShouldSkip()) + { + var stateMachine = this.GetSystem(); + await stateMachine.ChangeToAsync(); + } +} +``` + +### 问题:状态机是线程安全的吗? + +**解答**: +是的,状态机的所有操作都是线程安全的,使用了内部锁机制。 + +### 问题:如何实现状态栈(多层状态)? + +**解答**: +使用状态历史功能: + +```csharp +// 进入子状态 +await stateMachine.ChangeToAsync(); + +// 返回上一层 +await stateMachine.GoBackAsync(); +``` + +## 相关文档 + +- [生命周期管理](/zh-CN/core/lifecycle) - 状态的初始化和销毁 +- [事件系统](/zh-CN/core/events) - 状态变更通知 +- [协程系统](/zh-CN/core/coroutine) - 异步状态操作 +- [状态机实现教程](/zh-CN/tutorials/state-machine-tutorial) - 完整示例 diff --git a/docs/zh-CN/game/scene.md b/docs/zh-CN/game/scene.md new file mode 100644 index 0000000..0a226ed --- /dev/null +++ b/docs/zh-CN/game/scene.md @@ -0,0 +1,652 @@ +--- +title: 场景系统 +description: 场景系统提供了完整的场景生命周期管理、路由导航和转换控制功能。 +--- + +# 场景系统 + +## 概述 + +场景系统是 GFramework.Game 中用于管理游戏场景的核心组件。它提供了场景的加载、卸载、切换、暂停和恢复等完整生命周期管理,以及基于栈的场景导航机制。 + +通过场景系统,你可以轻松实现场景之间的平滑切换,管理场景栈(如主菜单 -> 游戏 -> 暂停菜单),并在场景转换时执行自定义逻辑。 + +**主要特性**: + +- 完整的场景生命周期管理 +- 基于栈的场景导航 +- 场景转换管道和钩子 +- 路由守卫(Route Guard) +- 场景工厂和行为模式 +- 异步加载和卸载 + +## 核心概念 + +### 场景接口 + +`IScene` 定义了场景的完整生命周期: + +```csharp +public interface IScene +{ + ValueTask OnLoadAsync(ISceneEnterParam? param); // 加载资源 + ValueTask OnEnterAsync(); // 进入场景 + ValueTask OnPauseAsync(); // 暂停场景 + ValueTask OnResumeAsync(); // 恢复场景 + ValueTask OnExitAsync(); // 退出场景 + ValueTask OnUnloadAsync(); // 卸载资源 +} +``` + +### 场景路由 + +`ISceneRouter` 管理场景的导航和切换: + +```csharp +public interface ISceneRouter : ISystem +{ + ISceneBehavior? Current { get; } // 当前场景 + string? CurrentKey { get; } // 当前场景键 + IEnumerable Stack { get; } // 场景栈 + bool IsTransitioning { get; } // 是否正在切换 + + ValueTask ReplaceAsync(string sceneKey, ISceneEnterParam? param = null); + ValueTask PushAsync(string sceneKey, ISceneEnterParam? param = null); + ValueTask PopAsync(); + ValueTask ClearAsync(); +} +``` + +### 场景行为 + +`ISceneBehavior` 封装了场景的具体实现和引擎集成: + +```csharp +public interface ISceneBehavior +{ + string Key { get; } // 场景唯一标识 + IScene Scene { get; } // 场景实例 + ValueTask LoadAsync(ISceneEnterParam? param); + ValueTask UnloadAsync(); +} +``` + +## 基本用法 + +### 定义场景 + +实现 `IScene` 接口创建场景: + +```csharp +using GFramework.Game.Abstractions.scene; + +public class MainMenuScene : IScene +{ + public async ValueTask OnLoadAsync(ISceneEnterParam? param) + { + // 加载场景资源 + Console.WriteLine("加载主菜单资源"); + await Task.Delay(100); // 模拟加载 + } + + public async ValueTask OnEnterAsync() + { + // 进入场景 + Console.WriteLine("进入主菜单"); + // 显示 UI、播放音乐等 + await Task.CompletedTask; + } + + public async ValueTask OnPauseAsync() + { + // 暂停场景 + Console.WriteLine("暂停主菜单"); + await Task.CompletedTask; + } + + public async ValueTask OnResumeAsync() + { + // 恢复场景 + Console.WriteLine("恢复主菜单"); + await Task.CompletedTask; + } + + public async ValueTask OnExitAsync() + { + // 退出场景 + Console.WriteLine("退出主菜单"); + // 隐藏 UI、停止音乐等 + await Task.CompletedTask; + } + + public async ValueTask OnUnloadAsync() + { + // 卸载场景资源 + Console.WriteLine("卸载主菜单资源"); + await Task.Delay(50); // 模拟卸载 + } +} +``` + +### 注册场景 + +在场景注册表中注册场景: + +```csharp +using GFramework.Game.Abstractions.scene; + +public class GameSceneRegistry : IGameSceneRegistry +{ + private readonly Dictionary _scenes = new(); + + public GameSceneRegistry() + { + // 注册场景 + Register("MainMenu", typeof(MainMenuScene)); + Register("Gameplay", typeof(GameplayScene)); + Register("Pause", typeof(PauseScene)); + } + + public void Register(string key, Type sceneType) + { + _scenes[key] = sceneType; + } + + public Type? GetSceneType(string key) + { + return _scenes.TryGetValue(key, out var type) ? type : null; + } +} +``` + +### 切换场景 + +使用场景路由进行导航: + +```csharp +public class GameController : IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public async Task StartGame() + { + var sceneRouter = this.GetSystem(); + + // 替换当前场景(清空场景栈) + await sceneRouter.ReplaceAsync("Gameplay"); + } + + public async Task ShowPauseMenu() + { + var sceneRouter = this.GetSystem(); + + // 压入新场景(保留当前场景) + await sceneRouter.PushAsync("Pause"); + } + + public async Task ClosePauseMenu() + { + var sceneRouter = this.GetSystem(); + + // 弹出当前场景(恢复上一个场景) + await sceneRouter.PopAsync(); + } +} +``` + +## 高级用法 + +### 场景参数传递 + +通过 `ISceneEnterParam` 传递数据: + +```csharp +// 定义场景参数 +public class GameplayEnterParam : ISceneEnterParam +{ + public int Level { get; set; } + public string Difficulty { get; set; } +} + +// 在场景中接收参数 +public class GameplayScene : IScene +{ + private int _level; + private string _difficulty; + + public async ValueTask OnLoadAsync(ISceneEnterParam? param) + { + if (param is GameplayEnterParam gameplayParam) + { + _level = gameplayParam.Level; + _difficulty = gameplayParam.Difficulty; + Console.WriteLine($"加载关卡 {_level},难度: {_difficulty}"); + } + + await Task.CompletedTask; + } + + // ... 其他生命周期方法 +} + +// 切换场景时传递参数 +await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam +{ + Level = 1, + Difficulty = "Normal" +}); +``` + +### 路由守卫 + +使用路由守卫控制场景切换: + +```csharp +using GFramework.Game.Abstractions.scene; + +public class SaveGameGuard : ISceneRouteGuard +{ + public async ValueTask CanLeaveAsync( + ISceneBehavior from, + string toKey, + ISceneEnterParam? param) + { + // 离开游戏场景前检查是否需要保存 + if (from.Key == "Gameplay") + { + var needsSave = CheckIfNeedsSave(); + if (needsSave) + { + await SaveGameAsync(); + } + } + + return true; // 允许离开 + } + + public async ValueTask CanEnterAsync( + string toKey, + ISceneEnterParam? param) + { + // 进入场景前的验证 + if (toKey == "Gameplay") + { + // 检查是否满足进入条件 + var canEnter = CheckGameplayRequirements(); + return canEnter; + } + + return true; + } + + private bool CheckIfNeedsSave() => true; + private async Task SaveGameAsync() => await Task.Delay(100); + private bool CheckGameplayRequirements() => true; +} + +// 注册守卫 +sceneRouter.AddGuard(new SaveGameGuard()); +``` + +### 场景转换处理器 + +自定义场景转换逻辑: + +```csharp +using GFramework.Game.Abstractions.scene; + +public class FadeTransitionHandler : ISceneTransitionHandler +{ + public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event) + { + Console.WriteLine($"准备加载场景: {@event.ToKey}"); + // 显示加载画面 + await ShowLoadingScreen(); + } + + public async ValueTask OnAfterLoadAsync(SceneTransitionEvent @event) + { + Console.WriteLine($"场景加载完成: {@event.ToKey}"); + await Task.CompletedTask; + } + + public async ValueTask OnBeforeEnterAsync(SceneTransitionEvent @event) + { + Console.WriteLine($"准备进入场景: {@event.ToKey}"); + // 播放淡入动画 + await PlayFadeIn(); + } + + public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event) + { + Console.WriteLine($"已进入场景: {@event.ToKey}"); + // 隐藏加载画面 + await HideLoadingScreen(); + } + + public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event) + { + Console.WriteLine($"准备退出场景: {@event.FromKey}"); + // 播放淡出动画 + await PlayFadeOut(); + } + + public async ValueTask OnAfterExitAsync(SceneTransitionEvent @event) + { + Console.WriteLine($"已退出场景: {@event.FromKey}"); + await Task.CompletedTask; + } + + private async Task ShowLoadingScreen() => await Task.Delay(100); + private async Task HideLoadingScreen() => await Task.Delay(100); + private async Task PlayFadeIn() => await Task.Delay(200); + private async Task PlayFadeOut() => await Task.Delay(200); +} + +// 注册转换处理器 +sceneRouter.AddTransitionHandler(new FadeTransitionHandler()); +``` + +### 场景栈管理 + +```csharp +public class SceneNavigationController : IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public async Task NavigateToSettings() + { + var sceneRouter = this.GetSystem(); + + // 检查场景是否已在栈中 + if (sceneRouter.Contains("Settings")) + { + Console.WriteLine("设置场景已打开"); + return; + } + + // 压入设置场景 + await sceneRouter.PushAsync("Settings"); + } + + public void ShowSceneStack() + { + var sceneRouter = this.GetSystem(); + + Console.WriteLine("当前场景栈:"); + foreach (var scene in sceneRouter.Stack) + { + Console.WriteLine($"- {scene.Key}"); + } + } + + public async Task ReturnToMainMenu() + { + var sceneRouter = this.GetSystem(); + + // 清空所有场景并加载主菜单 + await sceneRouter.ClearAsync(); + await sceneRouter.ReplaceAsync("MainMenu"); + } +} +``` + +### 场景加载进度 + +```csharp +public class GameplayScene : IScene +{ + public async ValueTask OnLoadAsync(ISceneEnterParam? param) + { + var resourceManager = GetResourceManager(); + + // 加载多个资源并报告进度 + var resources = new[] + { + "textures/player.png", + "textures/enemy.png", + "audio/bgm.mp3", + "models/level.obj" + }; + + for (int i = 0; i < resources.Length; i++) + { + await resourceManager.LoadAsync(resources[i]); + + // 报告进度 + var progress = (i + 1) / (float)resources.Length; + ReportProgress(progress); + } + } + + private void ReportProgress(float progress) + { + // 发送进度事件 + Console.WriteLine($"加载进度: {progress * 100:F0}%"); + } + + // ... 其他生命周期方法 +} +``` + +### 场景预加载 + +```csharp +public class PreloadController : IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public async Task PreloadNextLevel() + { + var sceneFactory = this.GetUtility(); + + // 预加载下一关场景 + var scene = sceneFactory.Create("Level2"); + await scene.OnLoadAsync(null); + + Console.WriteLine("下一关预加载完成"); + } +} +``` + +## 最佳实践 + +1. **在 OnLoad 中加载资源,在 OnUnload 中释放**:保持资源管理清晰 + ```csharp + public async ValueTask OnLoadAsync(ISceneEnterParam? param) + { + _texture = await LoadTextureAsync("player.png"); + } + + public async ValueTask OnUnloadAsync() + { + _texture?.Dispose(); + _texture = null; + } + ``` + +2. **使用 Push/Pop 管理临时场景**:如暂停菜单、设置界面 + ```csharp + // 打开暂停菜单(保留游戏场景) + await sceneRouter.PushAsync("Pause"); + + // 关闭暂停菜单(恢复游戏场景) + await sceneRouter.PopAsync(); + ``` + +3. **使用 Replace 切换主要场景**:如从菜单到游戏 + ```csharp + // 开始游戏(清空场景栈) + await sceneRouter.ReplaceAsync("Gameplay"); + ``` + +4. **在 OnPause/OnResume 中管理状态**:暂停和恢复游戏逻辑 + ```csharp + public async ValueTask OnPauseAsync() + { + // 暂停游戏逻辑 + _gameTimer.Pause(); + _audioSystem.PauseBGM(); + } + + public async ValueTask OnResumeAsync() + { + // 恢复游戏逻辑 + _gameTimer.Resume(); + _audioSystem.ResumeBGM(); + } + ``` + +5. **使用路由守卫处理业务逻辑**:如保存检查、权限验证 + ```csharp + public async ValueTask CanLeaveAsync(...) + { + if (HasUnsavedChanges()) + { + var confirmed = await ShowSaveDialog(); + if (confirmed) + { + await SaveAsync(); + } + return confirmed; + } + return true; + } + ``` + +6. **避免在场景切换时阻塞**:使用异步操作 + ```csharp + ✓ await sceneRouter.ReplaceAsync("Gameplay"); + ✗ sceneRouter.ReplaceAsync("Gameplay").Wait(); // 可能死锁 + ``` + +## 常见问题 + +### 问题:Replace、Push、Pop 有什么区别? + +**解答**: + +- **Replace**:清空场景栈,加载新场景(用于主要场景切换) +- **Push**:压入新场景,暂停当前场景(用于临时场景) +- **Pop**:弹出当前场景,恢复上一个场景(用于关闭临时场景) + +```csharp +// 场景栈示例 +await sceneRouter.ReplaceAsync("MainMenu"); // [MainMenu] +await sceneRouter.PushAsync("Settings"); // [MainMenu, Settings] +await sceneRouter.PushAsync("About"); // [MainMenu, Settings, About] +await sceneRouter.PopAsync(); // [MainMenu, Settings] +await sceneRouter.PopAsync(); // [MainMenu] +``` + +### 问题:如何在场景之间传递数据? + +**解答**: +有几种方式: + +1. **通过场景参数**: + +```csharp +await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam +{ + Level = 5 +}); +``` + +2. **通过 Model**: + +```csharp +var gameModel = this.GetModel(); +gameModel.CurrentLevel = 5; +await sceneRouter.ReplaceAsync("Gameplay"); +``` + +3. **通过事件**: + +```csharp +this.SendEvent(new LevelSelectedEvent { Level = 5 }); +await sceneRouter.ReplaceAsync("Gameplay"); +``` + +### 问题:场景切换时如何显示加载画面? + +**解答**: +使用场景转换处理器: + +```csharp +public class LoadingScreenHandler : ISceneTransitionHandler +{ + public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event) + { + await ShowLoadingScreen(); + } + + public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event) + { + await HideLoadingScreen(); + } + + // ... 其他方法 +} +``` + +### 问题:如何防止用户在场景切换时操作? + +**解答**: +检查 `IsTransitioning` 状态: + +```csharp +public async Task ChangeScene(string sceneKey) +{ + var sceneRouter = this.GetSystem(); + + if (sceneRouter.IsTransitioning) + { + Console.WriteLine("场景正在切换中,请稍候"); + return; + } + + await sceneRouter.ReplaceAsync(sceneKey); +} +``` + +### 问题:场景切换失败怎么办? + +**解答**: +使用 try-catch 捕获异常: + +```csharp +try +{ + await sceneRouter.ReplaceAsync("Gameplay"); +} +catch (Exception ex) +{ + Console.WriteLine($"场景切换失败: {ex.Message}"); + // 回退到安全场景 + await sceneRouter.ReplaceAsync("MainMenu"); +} +``` + +### 问题:如何实现场景预加载? + +**解答**: +在后台预先加载场景资源: + +```csharp +// 在当前场景中预加载下一个场景 +var factory = this.GetUtility(); +var nextScene = factory.Create("NextLevel"); +await nextScene.OnLoadAsync(null); + +// 稍后快速切换 +await sceneRouter.ReplaceAsync("NextLevel"); +``` + +## 相关文档 + +- [UI 系统](/zh-CN/game/ui) - UI 页面管理 +- [资源管理系统](/zh-CN/core/resource) - 场景资源加载 +- [状态机系统](/zh-CN/core/state-machine) - 场景状态管理 +- [Godot 场景系统](/zh-CN/godot/scene) - Godot 引擎集成 +- [存档系统实现教程](/zh-CN/tutorials/save-system) - 场景切换时保存数据 diff --git a/docs/zh-CN/game/ui.md b/docs/zh-CN/game/ui.md new file mode 100644 index 0000000..7113bb4 --- /dev/null +++ b/docs/zh-CN/game/ui.md @@ -0,0 +1,496 @@ +--- +title: UI 系统 +description: UI 系统提供了完整的 UI 页面管理、路由导航和多层级显示功能。 +--- + +# UI 系统 + +## 概述 + +UI 系统是 GFramework.Game 中用于管理游戏 UI 界面的核心组件。它提供了 UI 页面的生命周期管理、基于栈的导航机制,以及多层级的 +UI 显示系统(Page、Overlay、Modal、Toast、Topmost)。 + +通过 UI 系统,你可以轻松实现 UI 页面之间的切换,管理 UI 栈(如主菜单 -> 设置 -> 关于),以及在不同层级显示各种类型的 +UI(对话框、提示、加载界面等)。 + +**主要特性**: + +- 完整的 UI 生命周期管理 +- 基于栈的 UI 导航 +- 多层级 UI 显示(5 个层级) +- UI 转换管道和钩子 +- 路由守卫(Route Guard) +- UI 工厂和行为模式 + +## 核心概念 + +### UI 页面接口 + +`IUiPage` 定义了 UI 页面的生命周期: + +```csharp +public interface IUiPage +{ + void OnEnter(IUiPageEnterParam? param); // 进入页面 + void OnExit(); // 退出页面 + void OnPause(); // 暂停页面 + void OnResume(); // 恢复页面 + void OnShow(); // 显示页面 + void OnHide(); // 隐藏页面 +} +``` + +### UI 路由 + +`IUiRouter` 管理 UI 的导航和切换: + +```csharp +public interface IUiRouter : ISystem +{ + int Count { get; } // UI 栈深度 + IUiPageBehavior? Peek(); // 栈顶 UI + + ValueTask PushAsync(string uiKey, IUiPageEnterParam? param = null); + ValueTask PopAsync(UiPopPolicy policy = UiPopPolicy.Destroy); + ValueTask ReplaceAsync(string uiKey, IUiPageEnterParam? param = null); + ValueTask ClearAsync(); +} +``` + +### UI 层级 + +UI 系统支持 5 个显示层级: + +```csharp +public enum UiLayer +{ + Page, // 页面层(栈管理,不可重入) + Overlay, // 浮层(可重入,对话框等) + Modal, // 模态层(可重入,带遮罩) + Toast, // 提示层(可重入,轻量提示) + Topmost // 顶层(不可重入,系统级) +} +``` + +## 基本用法 + +### 定义 UI 页面 + +实现 `IUiPage` 接口创建 UI 页面: + +```csharp +using GFramework.Game.Abstractions.ui; + +public class MainMenuPage : IUiPage +{ + public void OnEnter(IUiPageEnterParam? param) + { + Console.WriteLine("进入主菜单"); + // 初始化 UI、绑定事件 + } + + public void OnExit() + { + Console.WriteLine("退出主菜单"); + // 清理资源、解绑事件 + } + + public void OnPause() + { + Console.WriteLine("暂停主菜单"); + // 暂停动画、停止交互 + } + + public void OnResume() + { + Console.WriteLine("恢复主菜单"); + // 恢复动画、启用交互 + } + + public void OnShow() + { + Console.WriteLine("显示主菜单"); + // 显示 UI 元素 + } + + public void OnHide() + { + Console.WriteLine("隐藏主菜单"); + // 隐藏 UI 元素 + } +} +``` + +### 切换 UI 页面 + +使用 UI 路由进行导航: + +```csharp +public class UiController : IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public async Task ShowSettings() + { + var uiRouter = this.GetSystem(); + + // 压入设置页面(保留当前页面) + await uiRouter.PushAsync("Settings"); + } + + public async Task CloseSettings() + { + var uiRouter = this.GetSystem(); + + // 弹出当前页面(返回上一页) + await uiRouter.PopAsync(); + } + + public async Task ShowMainMenu() + { + var uiRouter = this.GetSystem(); + + // 替换所有页面(清空 UI 栈) + await uiRouter.ReplaceAsync("MainMenu"); + } +} +``` + +### 显示不同层级的 UI + +```csharp +public class UiController : IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public void ShowDialog() + { + var uiRouter = this.GetSystem(); + + // 在 Modal 层显示对话框 + var handle = uiRouter.Show("ConfirmDialog", UiLayer.Modal); + } + + public void ShowToast(string message) + { + var uiRouter = this.GetSystem(); + + // 在 Toast 层显示提示 + var handle = uiRouter.Show("ToastMessage", UiLayer.Toast, + new ToastParam { Message = message }); + } + + public void ShowLoading() + { + var uiRouter = this.GetSystem(); + + // 在 Topmost 层显示加载界面 + var handle = uiRouter.Show("LoadingScreen", UiLayer.Topmost); + } +} +``` + +## 高级用法 + +### UI 参数传递 + +```csharp +// 定义 UI 参数 +public class SettingsEnterParam : IUiPageEnterParam +{ + public string Category { get; set; } +} + +// 在 UI 中接收参数 +public class SettingsPage : IUiPage +{ + private string _category; + + public void OnEnter(IUiPageEnterParam? param) + { + if (param is SettingsEnterParam settingsParam) + { + _category = settingsParam.Category; + Console.WriteLine($"打开设置分类: {_category}"); + } + } + + // ... 其他生命周期方法 +} + +// 传递参数 +await uiRouter.PushAsync("Settings", new SettingsEnterParam +{ + Category = "Audio" +}); +``` + +### 路由守卫 + +```csharp +using GFramework.Game.Abstractions.ui; + +public class UnsavedChangesGuard : IUiRouteGuard +{ + public async ValueTask CanLeaveAsync( + IUiPageBehavior from, + string toKey, + IUiPageEnterParam? param) + { + // 检查是否有未保存的更改 + if (from.Key == "Settings" && HasUnsavedChanges()) + { + var confirmed = await ShowConfirmDialog(); + return confirmed; + } + + return true; + } + + public async ValueTask CanEnterAsync( + string toKey, + IUiPageEnterParam? param) + { + // 进入前的验证 + return true; + } + + private bool HasUnsavedChanges() => true; + private async Task ShowConfirmDialog() => await Task.FromResult(true); +} + +// 注册守卫 +uiRouter.AddGuard(new UnsavedChangesGuard()); +``` + +### UI 转换处理器 + +```csharp +using GFramework.Game.Abstractions.ui; + +public class FadeTransitionHandler : IUiTransitionHandler +{ + public async ValueTask OnBeforeEnterAsync(UiTransitionEvent @event) + { + Console.WriteLine($"准备进入 UI: {@event.ToKey}"); + await PlayFadeIn(); + } + + public async ValueTask OnAfterEnterAsync(UiTransitionEvent @event) + { + Console.WriteLine($"已进入 UI: {@event.ToKey}"); + } + + public async ValueTask OnBeforeExitAsync(UiTransitionEvent @event) + { + Console.WriteLine($"准备退出 UI: {@event.FromKey}"); + await PlayFadeOut(); + } + + public async ValueTask OnAfterExitAsync(UiTransitionEvent @event) + { + Console.WriteLine($"已退出 UI: {@event.FromKey}"); + } + + private async Task PlayFadeIn() => await Task.Delay(200); + private async Task PlayFadeOut() => await Task.Delay(200); +} + +// 注册转换处理器 +uiRouter.RegisterHandler(new FadeTransitionHandler()); +``` + +### UI 句柄管理 + +```csharp +public class DialogController : IController +{ + private UiHandle? _dialogHandle; + + public void ShowDialog() + { + var uiRouter = this.GetSystem(); + + // 显示对话框并保存句柄 + _dialogHandle = uiRouter.Show("ConfirmDialog", UiLayer.Modal); + } + + public void CloseDialog() + { + if (_dialogHandle.HasValue) + { + var uiRouter = this.GetSystem(); + + // 使用句柄关闭对话框 + uiRouter.Hide(_dialogHandle.Value, UiLayer.Modal, destroy: true); + _dialogHandle = null; + } + } +} +``` + +### UI 栈管理 + +```csharp +public class NavigationController : IController +{ + public void ShowUiStack() + { + var uiRouter = this.GetSystem(); + + Console.WriteLine($"UI 栈深度: {uiRouter.Count}"); + + var current = uiRouter.Peek(); + if (current != null) + { + Console.WriteLine($"当前 UI: {current.Key}"); + } + } + + public bool IsSettingsOpen() + { + var uiRouter = this.GetSystem(); + return uiRouter.Contains("Settings"); + } + + public bool IsTopPage(string uiKey) + { + var uiRouter = this.GetSystem(); + return uiRouter.IsTop(uiKey); + } +} +``` + +### 多层级 UI 管理 + +```csharp +public class LayerController : IController +{ + public void ShowMultipleToasts() + { + var uiRouter = this.GetSystem(); + + // Toast 层支持重入,可以同时显示多个 + uiRouter.Show("Toast1", UiLayer.Toast); + uiRouter.Show("Toast2", UiLayer.Toast); + uiRouter.Show("Toast3", UiLayer.Toast); + } + + public void ClearAllToasts() + { + var uiRouter = this.GetSystem(); + + // 清空 Toast 层的所有 UI + uiRouter.ClearLayer(UiLayer.Toast, destroy: true); + } + + public void HideAllDialogs() + { + var uiRouter = this.GetSystem(); + + // 隐藏 Modal 层的所有对话框 + uiRouter.HideByKey("ConfirmDialog", UiLayer.Modal, hideAll: true); + } +} +``` + +## 最佳实践 + +1. **使用合适的层级**:根据 UI 类型选择正确的层级 + ```csharp + ✓ Page: 主要页面(主菜单、设置、游戏界面) + ✓ Overlay: 浮层(信息面板、小窗口) + ✓ Modal: 模态对话框(确认框、输入框) + ✓ Toast: 轻量提示(消息、通知) + ✓ Topmost: 系统级(加载界面、全屏遮罩) + ``` + +2. **使用 Push/Pop 管理临时 UI**:如设置、帮助页面 + ```csharp + // 打开设置(保留当前页面) + await uiRouter.PushAsync("Settings"); + + // 关闭设置(返回上一页) + await uiRouter.PopAsync(); + ``` + +3. **使用 Replace 切换主要页面**:如从菜单到游戏 + ```csharp + // 开始游戏(清空 UI 栈) + await uiRouter.ReplaceAsync("Gameplay"); + ``` + +4. **在 OnEnter/OnExit 中管理资源**:保持资源管理清晰 + ```csharp + public void OnEnter(IUiPageEnterParam? param) + { + // 加载资源、绑定事件 + BindEvents(); + } + + public void OnExit() + { + // 清理资源、解绑事件 + UnbindEvents(); + } + ``` + +5. **使用句柄管理非栈 UI**:对于 Overlay、Modal、Toast 层 + ```csharp + // 保存句柄 + var handle = uiRouter.Show("Dialog", UiLayer.Modal); + + // 使用句柄关闭 + uiRouter.Hide(handle, UiLayer.Modal, destroy: true); + ``` + +6. **避免在 UI 切换时阻塞**:使用异步操作 + ```csharp + ✓ await uiRouter.PushAsync("Settings"); + ✗ uiRouter.PushAsync("Settings").Wait(); // 可能死锁 + ``` + +## 常见问题 + +### 问题:Push、Pop、Replace 有什么区别? + +**解答**: + +- **Push**:压入新 UI,暂停当前 UI(用于临时页面) +- **Pop**:弹出当前 UI,恢复上一个 UI(用于关闭临时页面) +- **Replace**:清空 UI 栈,加载新 UI(用于主要页面切换) + +### 问题:什么时候使用不同的 UI 层级? + +**解答**: + +- **Page**:主要页面,使用栈管理 +- **Overlay**:浮层,可叠加显示 +- **Modal**:模态对话框,阻挡下层交互 +- **Toast**:轻量提示,不阻挡交互 +- **Topmost**:系统级,最高优先级 + +### 问题:如何在 UI 之间传递数据? + +**解答**: + +1. 通过 UI 参数 +2. 通过 Model +3. 通过事件 + +### 问题:UI 切换时如何显示过渡动画? + +**解答**: +使用 UI 转换处理器在 `OnBeforeEnter`/`OnAfterExit` 中播放动画。 + +### 问题:如何防止用户在 UI 切换时操作? + +**解答**: +在转换处理器中显示遮罩或禁用输入。 + +## 相关文档 + +- [场景系统](/zh-CN/game/scene) - 场景管理 +- [Godot UI 系统](/zh-CN/godot/ui) - Godot 引擎集成 +- [事件系统](/zh-CN/core/events) - UI 事件通信 +- [状态机系统](/zh-CN/core/state-machine) - UI 状态管理 From b3838ce8c7e556e0999f44e0dfd10695b8680b8e Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:02:19 +0800 Subject: [PATCH 2/8] =?UTF-8?q?docs(godot):=20=E6=B7=BB=E5=8A=A0=20Godot?= =?UTF-8?q?=20=E6=9E=B6=E6=9E=84=E9=9B=86=E6=88=90=E5=92=8C=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E7=B3=BB=E7=BB=9F=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Godot 架构集成文档,介绍 AbstractArchitecture 和 ArchitectureAnchor - 添加 Godot 场景系统文档,涵盖 SceneBehavior 和场景生命周期管理 - 包含数据与存档系统文档,介绍 IDataRepository 和 ISaveRepository 接口 - 提供完整的代码示例和最佳实践指南 - 覆盖多架构支持、热重载和场景参数传递等高级功能 - 包含常见问题解答和相关文档链接 --- docs/zh-CN/game/data.md | 588 +++++++++++ docs/zh-CN/godot/architecture.md | 590 +++++++++++ docs/zh-CN/godot/scene.md | 583 +++++++++++ docs/zh-CN/godot/ui.md | 643 ++++++++++++ docs/zh-CN/tutorials/coroutine-tutorial.md | 598 +++++++++++ .../zh-CN/tutorials/godot-complete-project.md | 813 +++++++++++++++ docs/zh-CN/tutorials/resource-management.md | 814 +++++++++++++++ docs/zh-CN/tutorials/save-system.md | 966 ++++++++++++++++++ .../zh-CN/tutorials/state-machine-tutorial.md | 746 ++++++++++++++ 9 files changed, 6341 insertions(+) create mode 100644 docs/zh-CN/game/data.md create mode 100644 docs/zh-CN/godot/architecture.md create mode 100644 docs/zh-CN/godot/scene.md create mode 100644 docs/zh-CN/godot/ui.md create mode 100644 docs/zh-CN/tutorials/coroutine-tutorial.md create mode 100644 docs/zh-CN/tutorials/godot-complete-project.md create mode 100644 docs/zh-CN/tutorials/resource-management.md create mode 100644 docs/zh-CN/tutorials/save-system.md create mode 100644 docs/zh-CN/tutorials/state-machine-tutorial.md diff --git a/docs/zh-CN/game/data.md b/docs/zh-CN/game/data.md new file mode 100644 index 0000000..f594fd6 --- /dev/null +++ b/docs/zh-CN/game/data.md @@ -0,0 +1,588 @@ +--- +title: 数据与存档系统 +description: 数据与存档系统提供了完整的数据持久化解决方案,支持多槽位存档、版本管理和数据迁移。 +--- + +# 数据与存档系统 + +## 概述 + +数据与存档系统是 GFramework.Game 中用于管理游戏数据持久化的核心组件。它提供了统一的数据加载和保存接口,支持多槽位存档管理、数据版本控制和自动迁移,让你可以轻松实现游戏存档、设置保存等功能。 + +通过数据系统,你可以将游戏数据保存到本地存储,支持多个存档槽位,并在数据结构变化时自动进行版本迁移。 + +**主要特性**: + +- 统一的数据持久化接口 +- 多槽位存档管理 +- 数据版本控制和迁移 +- 异步加载和保存 +- 批量数据操作 +- 与存储系统集成 + +## 核心概念 + +### 数据接口 + +`IData` 标记数据类型: + +```csharp +public interface IData +{ + // 标记接口,用于标识可持久化的数据 +} +``` + +### 数据仓库 + +`IDataRepository` 提供通用的数据操作: + +```csharp +public interface IDataRepository : IUtility +{ + Task LoadAsync(IDataLocation location) where T : class, IData, new(); + Task SaveAsync(IDataLocation location, T data) where T : class, IData; + Task ExistsAsync(IDataLocation location); + Task DeleteAsync(IDataLocation location); + Task SaveAllAsync(IEnumerable<(IDataLocation, IData)> dataList); +} +``` + +### 存档仓库 + +`ISaveRepository` 专门用于管理游戏存档: + +```csharp +public interface ISaveRepository : IUtility + where TSaveData : class, IData, new() +{ + Task ExistsAsync(int slot); + Task LoadAsync(int slot); + Task SaveAsync(int slot, TSaveData data); + Task DeleteAsync(int slot); + Task> ListSlotsAsync(); +} +``` + +### 版本化数据 + +`IVersionedData` 支持数据版本管理: + +```csharp +public interface IVersionedData : IData +{ + int Version { get; set; } +} +``` + +## 基本用法 + +### 定义数据类型 + +```csharp +using GFramework.Game.Abstractions.data; + +// 简单数据 +public class PlayerData : IData +{ + public string Name { get; set; } + public int Level { get; set; } + public int Experience { get; set; } +} + +// 版本化数据 +public class SaveData : IVersionedData +{ + public int Version { get; set; } = 1; + public PlayerData Player { get; set; } + public DateTime SaveTime { get; set; } +} +``` + +### 使用存档仓库 + +```csharp +public class SaveController : IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public async Task SaveGame(int slot) + { + var saveRepo = this.GetUtility>(); + + // 创建存档数据 + var saveData = new SaveData + { + Player = new PlayerData + { + Name = "Player1", + Level = 10, + Experience = 1000 + }, + SaveTime = DateTime.Now + }; + + // 保存到指定槽位 + await saveRepo.SaveAsync(slot, saveData); + Console.WriteLine($"游戏已保存到槽位 {slot}"); + } + + public async Task LoadGame(int slot) + { + var saveRepo = this.GetUtility>(); + + // 检查存档是否存在 + if (!await saveRepo.ExistsAsync(slot)) + { + Console.WriteLine($"槽位 {slot} 不存在存档"); + return; + } + + // 加载存档 + var saveData = await saveRepo.LoadAsync(slot); + Console.WriteLine($"加载存档: {saveData.Player.Name}, 等级 {saveData.Player.Level}"); + } + + public async Task DeleteSave(int slot) + { + var saveRepo = this.GetUtility>(); + + // 删除存档 + await saveRepo.DeleteAsync(slot); + Console.WriteLine($"已删除槽位 {slot} 的存档"); + } +} +``` + +### 注册存档仓库 + +```csharp +using GFramework.Game.data; + +public class GameArchitecture : Architecture +{ + protected override void Init() + { + // 获取存储系统 + var storage = this.GetUtility(); + + // 创建存档配置 + var saveConfig = new SaveConfiguration + { + SaveRoot = "saves", + SaveSlotPrefix = "slot_", + SaveFileName = "save.json" + }; + + // 注册存档仓库 + var saveRepo = new SaveRepository(storage, saveConfig); + RegisterUtility>(saveRepo); + } +} +``` + +## 高级用法 + +### 列出所有存档 + +```csharp +public async Task ShowSaveList() +{ + var saveRepo = this.GetUtility>(); + + // 获取所有存档槽位 + var slots = await saveRepo.ListSlotsAsync(); + + Console.WriteLine($"找到 {slots.Count} 个存档:"); + foreach (var slot in slots) + { + var saveData = await saveRepo.LoadAsync(slot); + Console.WriteLine($"槽位 {slot}: {saveData.Player.Name}, " + + $"等级 {saveData.Player.Level}, " + + $"保存时间 {saveData.SaveTime}"); + } +} +``` + +### 自动保存 + +```csharp +public class AutoSaveController : IController +{ + private CancellationTokenSource? _autoSaveCts; + + public void StartAutoSave(int slot, TimeSpan interval) + { + _autoSaveCts = new CancellationTokenSource(); + + Task.Run(async () => + { + while (!_autoSaveCts.Token.IsCancellationRequested) + { + await Task.Delay(interval, _autoSaveCts.Token); + + try + { + await SaveGame(slot); + Console.WriteLine("自动保存完成"); + } + catch (Exception ex) + { + Console.WriteLine($"自动保存失败: {ex.Message}"); + } + } + }, _autoSaveCts.Token); + } + + public void StopAutoSave() + { + _autoSaveCts?.Cancel(); + _autoSaveCts?.Dispose(); + _autoSaveCts = null; + } + + private async Task SaveGame(int slot) + { + var saveRepo = this.GetUtility>(); + var saveData = CreateSaveData(); + await saveRepo.SaveAsync(slot, saveData); + } + + private SaveData CreateSaveData() + { + // 从游戏状态创建存档数据 + return new SaveData(); + } +} +``` + +### 数据版本迁移 + +```csharp +// 版本 1 的数据 +public class SaveDataV1 : IVersionedData +{ + public int Version { get; set; } = 1; + public string PlayerName { get; set; } + public int Level { get; set; } +} + +// 版本 2 的数据(添加了新字段) +public class SaveDataV2 : IVersionedData +{ + public int Version { get; set; } = 2; + public string PlayerName { get; set; } + public int Level { get; set; } + public int Experience { get; set; } // 新增字段 + public DateTime LastPlayTime { get; set; } // 新增字段 +} + +// 数据迁移器 +public class SaveDataMigrator +{ + public SaveDataV2 Migrate(SaveDataV1 oldData) + { + return new SaveDataV2 + { + Version = 2, + PlayerName = oldData.PlayerName, + Level = oldData.Level, + Experience = oldData.Level * 100, // 根据等级计算经验 + LastPlayTime = DateTime.Now + }; + } +} + +// 加载时自动迁移 +public async Task LoadWithMigration(int slot) +{ + var saveRepo = this.GetUtility>(); + var data = await saveRepo.LoadAsync(slot); + + if (data.Version < 2) + { + // 需要迁移 + var oldData = data as SaveDataV1; + var migrator = new SaveDataMigrator(); + var newData = migrator.Migrate(oldData); + + // 保存迁移后的数据 + await saveRepo.SaveAsync(slot, newData); + return newData; + } + + return data; +} +``` + +### 使用数据仓库 + +```csharp +public class SettingsController : IController +{ + public async Task SaveSettings() + { + var dataRepo = this.GetUtility(); + + var settings = new GameSettings + { + MasterVolume = 0.8f, + MusicVolume = 0.6f, + SfxVolume = 0.7f + }; + + // 定义数据位置 + var location = new DataLocation("settings", "game_settings.json"); + + // 保存设置 + await dataRepo.SaveAsync(location, settings); + } + + public async Task LoadSettings() + { + var dataRepo = this.GetUtility(); + var location = new DataLocation("settings", "game_settings.json"); + + // 检查是否存在 + if (!await dataRepo.ExistsAsync(location)) + { + return new GameSettings(); // 返回默认设置 + } + + // 加载设置 + return await dataRepo.LoadAsync(location); + } +} +``` + +### 批量保存数据 + +```csharp +public async Task SaveAllGameData() +{ + var dataRepo = this.GetUtility(); + + var dataList = new List<(IDataLocation, IData)> + { + (new DataLocation("player", "profile.json"), playerData), + (new DataLocation("inventory", "items.json"), inventoryData), + (new DataLocation("quests", "progress.json"), questData) + }; + + // 批量保存 + await dataRepo.SaveAllAsync(dataList); + Console.WriteLine("所有数据已保存"); +} +``` + +### 存档备份 + +```csharp +public async Task BackupSave(int slot) +{ + var saveRepo = this.GetUtility>(); + + if (!await saveRepo.ExistsAsync(slot)) + { + Console.WriteLine("存档不存在"); + return; + } + + // 加载原存档 + var saveData = await saveRepo.LoadAsync(slot); + + // 保存到备份槽位 + int backupSlot = slot + 100; + await saveRepo.SaveAsync(backupSlot, saveData); + + Console.WriteLine($"存档已备份到槽位 {backupSlot}"); +} + +public async Task RestoreBackup(int slot) +{ + int backupSlot = slot + 100; + var saveRepo = this.GetUtility>(); + + if (!await saveRepo.ExistsAsync(backupSlot)) + { + Console.WriteLine("备份不存在"); + return; + } + + // 加载备份 + var backupData = await saveRepo.LoadAsync(backupSlot); + + // 恢复到原槽位 + await saveRepo.SaveAsync(slot, backupData); + + Console.WriteLine($"已从备份恢复到槽位 {slot}"); +} +``` + +## 最佳实践 + +1. **使用版本化数据**:为存档数据实现 `IVersionedData` + ```csharp + ✓ public class SaveData : IVersionedData { public int Version { get; set; } = 1; } + ✗ public class SaveData : IData { } // 无法进行版本管理 + ``` + +2. **定期自动保存**:避免玩家数据丢失 + ```csharp + // 每 5 分钟自动保存 + StartAutoSave(currentSlot, TimeSpan.FromMinutes(5)); + ``` + +3. **保存前验证数据**:确保数据完整性 + ```csharp + public async Task SaveGame(int slot) + { + var saveData = CreateSaveData(); + + if (!ValidateSaveData(saveData)) + { + throw new InvalidOperationException("存档数据无效"); + } + + await saveRepo.SaveAsync(slot, saveData); + } + ``` + +4. **处理保存失败**:使用 try-catch 捕获异常 + ```csharp + try + { + await saveRepo.SaveAsync(slot, saveData); + } + catch (Exception ex) + { + Logger.Error($"保存失败: {ex.Message}"); + ShowErrorMessage("保存失败,请重试"); + } + ``` + +5. **提供多个存档槽位**:让玩家可以管理多个存档 + ```csharp + // 支持 10 个存档槽位 + for (int i = 1; i <= 10; i++) + { + if (await saveRepo.ExistsAsync(i)) + { + ShowSaveSlot(i); + } + } + ``` + +6. **在关键时刻保存**:场景切换、关卡完成等 + ```csharp + public async Task OnLevelComplete() + { + // 关卡完成时自动保存 + await SaveGame(currentSlot); + } + ``` + +## 常见问题 + +### 问题:如何实现多个存档槽位? + +**解答**: +使用 `ISaveRepository` 的槽位参数: + +```csharp +// 保存到不同槽位 +await saveRepo.SaveAsync(1, saveData); // 槽位 1 +await saveRepo.SaveAsync(2, saveData); // 槽位 2 +await saveRepo.SaveAsync(3, saveData); // 槽位 3 +``` + +### 问题:如何处理数据版本升级? + +**解答**: +实现 `IVersionedData` 并在加载时检查版本: + +```csharp +var data = await saveRepo.LoadAsync(slot); +if (data.Version < CurrentVersion) +{ + data = MigrateData(data); + await saveRepo.SaveAsync(slot, data); +} +``` + +### 问题:存档数据保存在哪里? + +**解答**: +由存储系统决定,通常在: + +- Windows: `%AppData%/GameName/saves/` +- Linux: `~/.local/share/GameName/saves/` +- macOS: `~/Library/Application Support/GameName/saves/` + +### 问题:如何实现云存档? + +**解答**: +实现自定义的 `IStorage`,将数据保存到云端: + +```csharp +public class CloudStorage : IStorage +{ + public async Task WriteAsync(string path, byte[] data) + { + await UploadToCloud(path, data); + } + + public async Task ReadAsync(string path) + { + return await DownloadFromCloud(path); + } +} +``` + +### 问题:如何加密存档数据? + +**解答**: +在保存和加载时进行加密/解密: + +```csharp +public async Task SaveEncrypted(int slot, SaveData data) +{ + var json = JsonSerializer.Serialize(data); + var encrypted = Encrypt(json); + await storage.WriteAsync(path, encrypted); +} + +public async Task LoadEncrypted(int slot) +{ + var encrypted = await storage.ReadAsync(path); + var json = Decrypt(encrypted); + return JsonSerializer.Deserialize(json); +} +``` + +### 问题:存档损坏怎么办? + +**解答**: +实现备份和恢复机制: + +```csharp +public async Task SaveWithBackup(int slot, SaveData data) +{ + // 先备份旧存档 + if (await saveRepo.ExistsAsync(slot)) + { + var oldData = await saveRepo.LoadAsync(slot); + await saveRepo.SaveAsync(slot + 100, oldData); + } + + // 保存新存档 + await saveRepo.SaveAsync(slot, data); +} +``` + +## 相关文档 + +- [存储系统](/zh-CN/game/storage) - 底层存储实现 +- [序列化系统](/zh-CN/game/serialization) - 数据序列化 +- [场景系统](/zh-CN/game/scene) - 场景切换时保存 +- [存档系统实现教程](/zh-CN/tutorials/save-system) - 完整示例 diff --git a/docs/zh-CN/godot/architecture.md b/docs/zh-CN/godot/architecture.md new file mode 100644 index 0000000..7ca9a50 --- /dev/null +++ b/docs/zh-CN/godot/architecture.md @@ -0,0 +1,590 @@ +--- +title: Godot 架构集成 +description: Godot 架构集成提供了 GFramework 与 Godot 引擎的无缝连接,实现生命周期同步和模块化开发。 +--- + +# Godot 架构集成 + +## 概述 + +Godot 架构集成是 GFramework.Godot 中连接框架与 Godot 引擎的核心组件。它提供了架构与 Godot 场景树的生命周期绑定、模块化扩展系统,以及与 +Godot 节点系统的深度集成。 + +通过 Godot 架构集成,你可以在 Godot 项目中使用 GFramework 的所有功能,同时保持与 Godot 引擎的完美兼容。 + +**主要特性**: + +- 架构与 Godot 生命周期自动同步 +- 模块化的 Godot 扩展系统 +- 架构锚点节点管理 +- 自动资源清理 +- 热重载支持 +- 与 Godot 场景树深度集成 + +## 核心概念 + +### 抽象架构 + +`AbstractArchitecture` 是 Godot 项目中架构的基类: + +```csharp +public abstract class AbstractArchitecture : Architecture +{ + protected Node ArchitectureRoot { get; } + protected abstract void InstallModules(); + protected Task InstallGodotModule(TModule module); +} +``` + +### 架构锚点 + +`ArchitectureAnchor` 是连接架构与 Godot 场景树的桥梁: + +```csharp +public partial class ArchitectureAnchor : Node +{ + public void Bind(Action onExit); + public override void _ExitTree(); +} +``` + +### Godot 模块 + +`IGodotModule` 定义了 Godot 特定的模块接口: + +```csharp +public interface IGodotModule : IArchitectureModule +{ + Node Node { get; } + void OnPhase(ArchitecturePhase phase, IArchitecture architecture); + void OnAttach(Architecture architecture); + void OnDetach(); +} +``` + +## 基本用法 + +### 创建 Godot 架构 + +```csharp +using GFramework.Godot.architecture; +using GFramework.Core.Abstractions.architecture; + +public class GameArchitecture : AbstractArchitecture +{ + // 单例实例 + public static GameArchitecture Interface { get; private set; } + + public GameArchitecture() + { + Interface = this; + } + + protected override void InstallModules() + { + // 注册 Model + RegisterModel(new PlayerModel()); + RegisterModel(new GameModel()); + + // 注册 System + RegisterSystem(new GameplaySystem()); + RegisterSystem(new AudioSystem()); + + // 注册 Utility + RegisterUtility(new StorageUtility()); + } +} +``` + +### 在 Godot 场景中初始化架构 + +```csharp +using Godot; +using GFramework.Godot.architecture; + +public partial class GameRoot : Node +{ + private GameArchitecture _architecture; + + public override void _Ready() + { + // 创建并初始化架构 + _architecture = new GameArchitecture(); + _architecture.InitializeAsync().AsTask().Wait(); + + GD.Print("架构已初始化"); + } +} +``` + +### 使用架构锚点 + +架构锚点会自动创建并绑定到场景树: + +```csharp +// 架构会自动创建锚点节点 +// 节点名称格式: __GFramework__GameArchitecture__[HashCode]__ArchitectureAnchor__ + +// 当场景树销毁时,锚点会自动触发架构清理 +``` + +## 高级用法 + +### 创建 Godot 模块 + +```csharp +using GFramework.Godot.architecture; +using Godot; + +public class CoroutineModule : AbstractGodotModule +{ + private Node _coroutineNode; + + public override Node Node => _coroutineNode; + + public CoroutineModule() + { + _coroutineNode = new Node { Name = "CoroutineScheduler" }; + } + + public override void Install(IArchitecture architecture) + { + // 注册协程调度器 + var scheduler = new CoroutineScheduler(new GodotTimeSource()); + architecture.RegisterSystem(scheduler); + + GD.Print("协程模块已安装"); + } + + public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) + { + if (phase == ArchitecturePhase.Ready) + { + GD.Print("协程模块已就绪"); + } + } + + public override void OnDetach() + { + GD.Print("协程模块已分离"); + _coroutineNode?.QueueFree(); + } +} +``` + +### 安装 Godot 模块 + +```csharp +public class GameArchitecture : AbstractArchitecture +{ + protected override void InstallModules() + { + // 安装核心模块 + RegisterModel(new PlayerModel()); + RegisterSystem(new GameplaySystem()); + + // 安装 Godot 模块 + InstallGodotModule(new CoroutineModule()).Wait(); + InstallGodotModule(new SceneModule()).Wait(); + InstallGodotModule(new UiModule()).Wait(); + } +} +``` + +### 访问架构根节点 + +```csharp +public class SceneModule : AbstractGodotModule +{ + private Node _sceneRoot; + + public override Node Node => _sceneRoot; + + public SceneModule() + { + _sceneRoot = new Node { Name = "SceneRoot" }; + } + + public override void Install(IArchitecture architecture) + { + // 访问架构根节点 + if (architecture is AbstractArchitecture godotArch) + { + var root = godotArch.ArchitectureRoot; + root.AddChild(_sceneRoot); + } + } +} +``` + +### 监听架构阶段 + +```csharp +public class AnalyticsModule : AbstractGodotModule +{ + private Node _analyticsNode; + + public override Node Node => _analyticsNode; + + public AnalyticsModule() + { + _analyticsNode = new Node { Name = "Analytics" }; + } + + public override void Install(IArchitecture architecture) + { + // 安装分析系统 + } + + public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) + { + switch (phase) + { + case ArchitecturePhase.Initializing: + GD.Print("架构正在初始化"); + break; + + case ArchitecturePhase.Ready: + GD.Print("架构已就绪,开始追踪"); + StartTracking(); + break; + + case ArchitecturePhase.Destroying: + GD.Prin构正在销毁,停止追踪"); + StopTracking(); + break; + } + } + + private void StartTracking() { } + private void StopTracking() { } +} +``` + +### 自定义架构配置 + +```csharp +using GFramework.Core.Abstractions.architecture; +using GFramework.Core.Abstractions.environment; + +public class GameArchitecture : AbstractArchitecture +{ + public GameArchitecture() : base( + configuration: CreateConfiguration(), + environment: CreateEnvironment() + ) + { + } + + private static IArchitectureConfiguration CreateConfiguration() + { + return new ArchitectureConfiguration + { + EnableLogging + LogLevel = LogLevel.Debug + }; + } + + private static IEnvironment CreateEnvironment() + { + return new DefaultEnvironment + { + IsDevelopment = OS.IsDebugBuild() + }; + } + + protected override void InstallModules() + { + // 根据环境配置安装模块 + if (Environment.IsDevelopment) + { + InstallGodotModule(new DebugModule()).Wait(); + } + + // 安装核心模块 + RegisterModel(new PlayerModel()); + RegisterSystem(new GameplaySystem()); + } +} +``` + +### 热重载支持 + +```csharp +public class GameArchitecture : AbstractArchitecture +{ + private static bool _initialized; + + protected override void OnInitialize() + { + // 防止热重载时重复初始化 + if (_initialized) + { + GD.Print("架构已初始化,跳过重复初始化"); + return; + } + + base.OnInitialize(); + _initialized = true; + } + + protected override async ValueTask OnDestroyAsync() + { + await base.OnDestroyAsync(); + _initialized = false; + } +} +``` + +### 在节点中使用架构 + +```csharp +using Godot; +using GFramework.Godot.extensions; + +public partial class Player : CharacterBody2D +{ + public override void _Ready() + { + // 通过扩展方法访问架构组件 + var playerModel = this.GetModel(); + var gameplaySystem = this.GetSystem(); + + // 发送事件 + this.SendEvent(new PlayerSpawnedEvent()); + + // 执行命令 + this.SendCommand(new InitPlayerCommand()); + } + + public override void _Process(double delta) + { + // 在 Process 中使用架构组件 + var inputSystem = this.GetSystem(); + var movement = inputSystem.GetMovementInput(); + + Velocity = movement * 200; + MoveAndSlide(); + } +} +``` + +### 多架构支持 + +```csharp +// 游戏架构 +public class GameArchitecture : AbstractArchitecture +{ + public static GameArchitecture Interface { get; private set; } + + public GameArchitecture() + { + Interface = this; + } + + protected override void InstallModules() + { + RegisterModel(new PlayerModel()); + RegisterSystem(new GameplaySystem()); + } +} + +// UI 架构 +public class UiArchitecture : AbstractArchitecture +{ + public static UiArchitecture Interface { get; private set; } + + public UiArchitecture() + { + Interface = this; + } + + protected override void InstallModules() + { + RegisterModel(new UiModel()); + RegisterSystem(new UiSystem()); + } +} + +// 在不同节点中使用不同架构 +public partial class GameNode : Node, IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; +} + +public partial class UiNode : Control, IController +{ + public IArchitecture GetArchitecture() => UiArchitecture.Interface; +} +``` + +## 最佳实践 + +1. **使用单例模式**:为架构提供全局访问点 + ```csharp + public class GameArchitecture : AbstractArchitecture + { + public static GameArchitecture Interface { get; private set; } + + public GameArchitecture() + { + Interface = this; + } + } + ``` + +2. **在根节点初始化架构**:确保架构在所有节点之前就绪 + ```csharp + public partial class GameRoot : Node + { + public override void _Ready() + { + new GameArchitecture().InitializeAsync().AsTask().Wait(); + } + } + ``` + +3. **使用 Godot 模块组织功能**:将相关功能封装为模块 + ```csharp + InstallGodotModule(new CoroutineModule()).Wait(); + InstallGodotModule(new SceneModule()).Wait(); + InstallGodotModule(new UiModule()).Wait(); + ``` + +4. **利用架构阶段钩子**:在适当的时机执行逻辑 + ```csharp + public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture) + { + if (phase == ArchitecturePhase.Ready) + { + // 架构就绪后的初始化 + } + } + ``` + +5. **正确清理资源**:在 OnDetach 中释放 Godot 节点 + ```csharp + public override void OnDetach() + { + _node?.QueueFree(); + _node = null; + } + ``` + +6. **避免在构造函数中访问架构**:使用 _Ready 或 OnPhase + ```csharp + ✗ public Player() + { + var model = this.GetModel(); // 架构可能未就绪 + } + + ✓ public override void _Ready() + { + var model = this.GetModel(); // 安全 + } + ``` + +## 常见问题 + +### 问题:架构什么时候初始化? + +**解答**: +在根节点的 `_Ready` 方法中初始化: + +```csharp +public partial class GameRoot : Node +{ + public override void _Ready() + { + new GameArchitecture().InitializeAsync().AsTask().Wait(); + } +} +``` + +### 问题:如何在节点中访问架构? + +**解答**: +实现 `IController` 接口或使用扩展方法: + +```csharp +// 方式 1: 实现 IController +public partial class Player : Node, IController +{ + public IArchitecture GetArchitecture() => GameArchitecture.Interface; + + public override void _Ready() + { + var model = this.GetModel(); + } +} + +// 方式 2: 直接使用单例 +public partial class Enemy : Node +{ + public override void _Ready() + { + var model = GameArchitecture.Interface.GetModel(); + } +} +``` + +### 问题:架构锚点节点是什么? + +**解答**: +架构锚点是一个隐藏的节点,用于将架构绑定到 Godot 场景树。当场景树销毁时,锚点会自动触发架构清理。 + +### 问题:如何支持热重载? + +**解答**: +使用静态标志防止重复初始化: + +```csharp +private static bool _initialized; + +protected override void OnInitialize() +{ + if (_initialized) return; + base.OnInitialize(); + _initialized = true; +} +``` + +### 问题:可以有多个架构吗? + +**解答**: +可以,但通常一个游戏只需要一个主架构。如果需要多个架构,为每个架构提供独立的单例: + +```csharp +public class GameArchitecture : AbstractArchitecture +{ + public static GameArchitecture Interface { get; private set; } +} + +public class UiArchitecture : AbstractArchitecture +{ + public static UiArchitecture Interface { get; private set; } +} +``` + +### 问题:Godot 模块和普通模块有什么区别? + +**解答**: + +- **普通模块**:纯 C# 逻辑,不依赖 Godot +- **Godot 模块**:包含 Godot 节点,与场景树集成 + +```csharp +// 普通模块 +InstallModule(new CoreModule()); + +// Godot 模块 +InstallGodotModule(new SceneModule()).Wait(); +``` + +## 相关文档 + +- [架构组件](/zh-CN/core/architecture) - 核心架构系统 +- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期 +- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成 +- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成 +- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法 diff --git a/docs/zh-CN/godot/scene.md b/docs/zh-CN/godot/scene.md new file mode 100644 index 0000000..2555943 --- /dev/null +++ b/docs/zh-CN/godot/scene.md @@ -0,0 +1,583 @@ +--- +title: Godot 场景系统 +description: Godot 场景系统提供了 GFramework 场景管理与 Godot 场景树的完整集成。 +--- + +# Godot 场景系统 + +## 概述 + +Godot 场景系统是 GFramework.Godot 中连接框架场景管理与 Godot 场景树的核心组件。它提供了场景行为封装、场景工厂、场景注册表等功能,让你可以在 +Godot 项目中使用 GFramework 的场景管理系统。 + +通过 Godot 场景系统,你可以使用 GFramework 的场景路由、生命周期管理等功能,同时保持与 Godot 场景系统的完美兼容。 + +**主要特性**: + +- 场景行为封装(SceneBehavior) +- 场景工厂和注册表 +- 与 Godot PackedScene 集成 +- 多种场景行为类型(Node2D、Node3D、Control) +- 场景生命周期管理 +- 场景根节点管理 + +## 核心概念 + +### 场景行为 + +`SceneBehaviorBase` 封装了 Godot 节点的场景行为: + +```csharp +public abstract class SceneBehaviorBase : ISceneBehavior + where T : Node +{ + protected readonly T Owner; + public string Key { get; } + public IScene Scene { get; } +} +``` + +### 场景工厂 + +`GodotSceneFactory` 负责创建场景实例: + +```csharp +public class GodotSceneFactory : ISceneFactory +{ + public ISceneBehavior Create(string sceneKey); +} +``` + +### 场景注册表 + +`IGodotSceneRegistry` 管理场景资源: + +```csharp +public interface IGodotSceneRegistry +{ + void Register(string key, PackedScene scene); + PackedScene Get(string key); +} +``` + +## 基本用法 + +### 创建场景脚本 + +```csharp +using Godot; +using GFramework.Game.Abstractions.scene; + +public partial class MainMenuScene : Control, IScene +{ + public async ValueTask OnLoadAsync(ISceneEnterParam? param) + { + GD.Print("加载主菜单资源"); + await Task.CompletedTask; + } + + public async ValueTask OnEnterAsync() + { + GD.Print("进入主菜单"); + Show(); + await Task.CompletedTask; + } + + public async ValueTask OnPauseAsync() + { + GD.Print("暂停主菜单"); + await Task.CompletedTask; + } + + public async ValueTask OnResumeAsync() + { + GD.Print("恢复主菜单"); + await Task.CompletedTask; + } + + public async ValueTask OnExitAsync() + { + GD.Print("退出主菜单"); + Hide(); + await Task.CompletedTask; + } + + public async ValueTask OnUnloadAsync() + { + GD.Print("卸载主菜单资源"); + await Task.CompletedTask; + } +} +``` + +### 注册场景 + +```csharp +using GFramework.Godot.scene; +using Godot; + +public class GameSceneRegistry : GodotSceneRegistry +{ + publieneRegistry() + { + // 注册场景资源 + Register("MainMenu", GD.Load("res://scenes/MainMenu.tscn")); + Register("Gameplay", GD.Load("res://scenes/Gameplay.tscn")); + Register("Pause", GD.Load("res://scenes/Pause.tscn")); + } +} +``` + +### 设置场景系统 + +```csharp +using GFramework.Godot.architecture; +using GFramework.Godot.scene; + +public class GameArchitecture : AbstractArchitecture +{ + protected override void InstallModules() + { + // 注册场景注册表 + var sceneRegistry = new GameSceneRegistry(); + RegisterUtility(sceneRegistry); + + // 注册场景工厂 + var sceneFactory = new GodotSceneFactory(); + RegisterUtility(sceneFactory); + + // 注册场景路由 + var sceneRouter = new GodotSceneRouter(); + RegisterSystem(sceneRouter); + } +} +``` + +### 使用场景路由 + +```csharp +using Godot; +using GFramework.Godot.extensions; + +public partial class GameController : Node +{ + public override void _Ready() + { + // 切换到主菜单 + SwitchToMainMenu(); + } + + private async void SwitchToMainMenu() + { + var sceneRouter = this.GetSystem(); + await sceneRouter.ReplaceAsync("MainMenu"); + } + + private async void StartGame() + { + var sceneRouter = this.GetSystem(); + await sceneRouter.ReplaceAsync("Gameplay"); + } + + private async void ShowPause() + { + var sceneRouter = this.GetSystem(); + await sceneRouter.PushAsync("Pause"); + } +} +``` + +## 高级用法 + +### 使用场景行为提供者 + +```csharp +using Godot; +using GFramework.Game.Abstractions.scene; +using GFramework.Godot.scene; + +public partial class GameplayScene : Node2D, ISceneBehaviorProvider +{ + private GameplaySceneBehavior _behavior; + + public override void _Ready() + { + _behavior = new GameplaySceneBehavior(this, "Gameplay"); + } + + public ISceneBehavior GetScene() + { + return _behavior; + } +} + +// 自定义场景行为 +public class GameplaySceneBehavior : Node2DSceneBehavior +{ + public GameplaySceneBehavior(Node2D owner, string key) : base(owner, key) + { + } + + protected override async ValueTask OnLoadInternalAsync(ISceneEnterParam? param) + { + GD.Print("加载游戏场景"); + // 加载游戏资源 + await Task.CompletedTask; + } + + protected override async ValueTask OnEnterInternalAsync() + { + GD.Print("进入游戏场景"); + Owner.Show(); + await Task.CompletedTask; + } +} +``` + +### 不同类型的场景行为 + +```csharp +// Node2D 场景 +public class Node2DSceneBehavior : SceneBehaviorBase +{ + public Node2DSceneBehavior(Node2D owner, string key) : base(owner, key) + { + } +} + +// Node3D 场景 +public class Node3DSceneBehavior : SceneBehaviorBase +{ + public Node3DSceneBehavior(Node3D owner, string key) : base(owner, key) + { + } +} + +// Control 场景(UI) +public class ControlSceneBehavior : SceneBehaviorBase +{ + public ControlSceneBehavior(Control owner, string key) : base(owner, key) + { + } +} +``` + +### 场景根节点管理 + +```csharp +using Godot; +using GFramework.Godot.scene; + +public partial class SceneRoot : Node, ISceneRoot +{ + private Node _currentSceneNode; + + public void AttachScene(Node sceneNode) + { + // 移除旧场景 + if (_currentSceneNode != null) + { + RemoveChild(_currentSceneNode); + _currentSceneNode.QueueFree(); + } + + // 添加新场景 + _currentSceneNode = sceneNode; + AddChild(_currentSceneNode); + } + + public void DetachScene(Node sceneNode) + { + if (_currentSceneNode == sceneNode) + { + RemoveChild(_currentSceneNode); + _currentSceneNode = null; + } + } +} +``` + +### 场景参数传递 + +```csharp +// 定义场景参数 +public class GameplayEnterParam : ISceneEnterParam +{ + public int Level { get; set; } + public string Difficulty { get; set; } +} + +// 在场景中接收参数 +public partial class GameplayScene : Node2D, IScene +{ + private int _level; + private string _difficulty; + + public async ValueTask OnLoadAsync(ISceneEnterParam? param) + { + if (param is GameplayEnterParam gameplayParam) + { + _level = gameplayParam.Level; + _difficulty = gameplayParam.Difficulty; + GD.Print($"加载关卡 {_level},难度: {_difficulty}"); + } + + await Task.CompletedTask; + } + + // ... 其他生命周期方法 +} + +// 切换场景时传递参数 +var sceneRouter = this.GetSystem(); +await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam +{ + Level = 1, + Difficulty = "Normal" +}); +``` + +### 场景预加载 + +```csharp +public partial class LoadingScene : Control +{ + public override async void _Ready() + { + // 预加载下一个场景 + await PreloadNextScene(); + + // 切换到预加载的场景 + var sceneRouter = this.GetSystem(); + await sceneRouter.ReplaceAsync("Gameplay"); + } + + private async Task PreloadNextScene() + { + var sceneFactory = this.GetUtility(); + var sceneBehavior = sceneFactory.Create("Gameplay"); + + // 预加载场景资源 + await sceneBehavior.LoadAsync(null); + + GD.Print("场景预加载完成"); + } +} +``` + +### 场景转换动画 + +```csharp +using Godot; +using GFramework.Game.Abstractions.scene; + +public class FadeTransitionHandler : ISceneTransitionHandler +{ + private ColorRect _fadeRect; + + public FadeTransitionHandler(ColorRect fadeRect) + { + _fadeRect = fadeRect; + } + + public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event) + { + // 淡出动画 + var tween = _fadeRect.CreateTween(); + tween.TweenProperty(_fadeRect, "modulate:a", 1.0f, 0.3f); + await tween.ToSignal(tween, Tween.SignalName.Finished); + } + + public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event) + { + // 淡入动画 + var tween = _fadeRect.CreateTween(); + tween.TweenProperty(_fadeRect, "modulate:a", 0.0f, 0.3f); + await tween.ToSignal(tween, Tween.SignalName.Finished); + } + + // ... 其他方法 +} +``` + +### 场景间通信 + +```csharp +// 通过事件通信 +public partial class GameplayScene : Node2D, IScene +{ + public async ValueTask OnEnterAsync() + { + // 发送场景进入事件 + this.SendEvent(new GameplaySceneEnteredEvent()); + await Task.CompletedTask; + } +} + +// 在其他地方监听 +public partial class HUD : Control +{ + public override void _Ready() + { + this.RegisterEvent(OnGameplayEntered); + } + + private void OnGameplayEntered(GameplaySceneEnteredEvent evt) + { + GD.Print("游戏场景已进入,显示 HUD"); + Show(); + } +} +``` + +## 最佳实践 + +1. **场景脚本实现 IScene 接口**:获得完整的生命周期管理 + ```csharp + ✓ public partial class MyScene : Node2D, IScene { } + ✗ public partial class MyScene : Node2D { } // 无生命周期管理 + ``` + +2. **使用场景注册表管理场景资源**:集中管理所有场景 + ```csharp + public class GameSceneRegistry : GodotSceneRegistry + { + public GameSceneRegistry() + { + Register("MainMenu", GD.Load("res://scenes/MainMenu.tscn")); + Register("Gameplay", GD.Load("res://scenes/Gameplay.tscn")); + } + } + ``` + +3. **在 OnLoadAsync 中加载资源**:避免场景切换卡顿 + ```csharp + public async ValueTask OnLoadAsync(ISceneEnterParam? param) + { + // 异步加载资源 + await LoadTexturesAsync(); + await LoadAudioAsync(); + } + ``` + +4. **使用场景根节点管理场景树**:保持场景树结构清晰 + ```csharp + // 创建场景根节点 + var sceneRoot = new Node { Name = "SceneRoot" }; + AddChild(sceneRoot); + + // 绑定到场景路由 + sceneRouter.BindRoot(sceneRoot); + ``` + +5. **正确清理场景资源**:在 OnUnloadAsync 中释放资源 + ```csharp + public async ValueTask OnUnloadAsync() + { + // 释放资源 + _texture?.Dispose(); + _audioStream?.Dispose(); + await Task.CompletedTask; + } + ``` + +6. **使用场景参数传递数据**:避免使用全局变量 + ```csharp + ✓ await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam { Level = 1 }); + ✗ GlobalData.CurrentLevel = 1; // 避免全局状态 + ``` + +## 常见问题 + +### 问题:如何在 Godot 场景中使用 GFramework? + +**解答**: +场景脚本实现 `IScene` 接口: + +```csharp +public partial class MyScene : Node2D, IScene +{ + public async ValueTask OnLoadAsync(ISceneEnterParam? param) { } + public async ValueTask OnEnterAsync() { } + // ... 实现其他方法 +} +``` + +### 问题:场景切换时节点如何管理? + +**解答**: +使用场景根节点管理: + +```csharp +// 场景路由会自动管理节点的添加和移除 +await sceneRouter.ReplaceAsync("NewScene"); +// 旧场景节点会被移除,新场景节点会被添加 +``` + +### 问题:如何实现场景预加载? + +**解答**: +使用场景工厂提前创建场景: + +```csharp +var sceneFactory = this.GetUtility(); +var sceneBehavior = sceneFactory.Create("NextScene"); +await sceneBehavior.LoadAsync(null); +``` + +### 问题:场景生命周期方法的调用顺序是什么? + +**解答**: + +- 进入场景:`OnLoadAsync` -> `OnEnterAsync` -> `OnShow` +- 暂停场景:`OnPause` -> `OnHide` +- 恢复场景:`OnShow` -> `OnResume` +- 退出场景:`OnHide` -> `OnExitAsync` -> `OnUnloadAsync` + +### 问题:如何在场景中访问架构组件? + +**解答**: +使用扩展方法: + +```csharp +public partial class MyScene : Node2D, IScene +{ + public async ValueTask OnEnterAsync() + { + var playerModel = this.GetModel(); + var gameSystem = this.GetSystem(); + await Task.CompletedTask; + } +} +``` + +### 问题:场景切换时如何显示加载界面? + +**解答**: +使用场景转换处理器: + +```csharp +public class LoadingScreenHandler : ISceneTransitionHandler +{ + public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event) + { + // 显示加载界面 + ShowLoadingScreen(); + await Task.CompletedTask; + } + + public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event) + { + // 隐藏加载界面 + HideLoadingScreen(); + await Task.CompletedTask; + } +} +``` + +## 相关文档 + +- [场景系统](/zh-CN/game/scene) - 核心场景管理 +- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础 +- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成 +- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法 diff --git a/docs/zh-CN/godot/ui.md b/docs/zh-CN/godot/ui.md new file mode 100644 index 0000000..aca8a08 --- /dev/null +++ b/docs/zh-CN/godot/ui.md @@ -0,0 +1,643 @@ +--- +title: Godot UI 系统 +description: Godot UI 系统提供了 GFramework UI 管理与 Godot Control 节点的完整集成。 +--- + +# Godot UI 系统 + +## 概述 + +Godot UI 系统是 GFramework.Godot 中连接框架 UI 管理与 Godot Control 节点的核心组件。它提供了 UI 页面行为封装、UI 工厂、UI +注册表等功能,支持多层级 UI 显示,让你可以在 Godot 项目中使用 GFramework 的 UI 管理系统。 + +通过 Godot UI 系统,你可以使用 GFramework 的 UI 路由、生命周期管理、多层级显示等功能,同时保持与 Godot UI 系统的完美兼容。 + +**主要特性**: + +- UI 页面行为封装 +- UI 工厂和注册表 +- 与 Godot PackedScene 集成 +- 多层级 UI 支持(Page、Overlay、Modal、Toast、Topmost) +- UI 生命周期管理 +- UI 根节点管理 + +## 核心概念 + +### UI 页面行为 + +`CanvasItemUiPageBehaviorBase` 封装了 Godot Control 节点的 UI 行为: + +```csharp +public abstract class CanvasItemUiPageBehaviorBase : IUiPageBehavior + where T : CanvasItem +{ + protected readonly T Owner; + public string Key { get; } + public UiLayer Layer { get; } + public bool IsReentrant { get; } +} +``` + +### UI 工厂 + +`GodotUiFactory` 负责创建 UI 实例: + +```csharp +public class GodotUiFactory : IUiFactory +{ + public IUiPageBehavior Create(string uiKey); +} +``` + +### UI 层级行为 + +不同层级的 UI 有不同的行为类: + +```csharp +// Page 层(栈管理) +public class PageLayerUiPageBehavior : CanvasItemUiPageBehaviorBase +{ + public override UiLayer Layer => UiLayer.Page; + public override bool IsReentrant => false; +} + +// Modal 层(模态对话框) +public class ModalLayerUiPageBehavior : CanvasItemUiPageBehaviorBase +{ + public override UiLayer Layer => UiLayer.Modal; + public override bool IsReentrant => true; +} +``` + +## 基本用法 + +### 创建 UI 脚本 + +```csharp +using Godot; +using GFramework.Game.Abstractions.ui; + +public partial class MainMenuPage : Control, IUiPage +{ + public void OnEnter(IUiPageEnterParam? param) + { + GD.Print("进入主菜单"); + Show(); + } + + public void OnExit() + { + GD.Print("退出主菜单"); + Hide(); + } + + public void OnPause() + { + GD.Print("暂停主菜单"); + } + + public void OnResume() + { + GD.Print("恢复主菜单"); + } + + public void OnShow() + { + Show(); + } + + public void OnHide() + { + Hide(); + } +} +``` + +### 实现 UI 页面行为提供者 + +```csharp +using Godot; +using GFramework.Game.Abstractions.ui; +using GFramework.Godot.ui; + +public partial class MainMenuPage : Control, IUiPageBehaviorProvider +{ + private PageLayerUiPageBehavior _behavior; + + public override void _Ready() + { + _behavior = new PageLayerUiPageBehavior(this, "MainMenu"); + } + + public IUiPageBehavior GetPage() + { + return _behavior; + } +} +``` + +### 注册 UI + +```csharp +using GFramework.Godot.ui; +using Godot; + +public class GameUiRegistry : GodotUiRegistry +{ + public GameUiRegistry() + { + // 注册 UI 资源 + Register("MainMenu", GD.Load("res://ui/MainMenu.tscn")); + Register("Settings", GD.Load("res://ui/Settings.tscn")); + Register("ConfirmDialog", GD.Load("res://ui/ConfirmDialog.tscn")); + Register("Toast", GD.Load("res://ui/Toast.tscn")); + } +} +``` + +### 设置 UI 系统 + +```csharp +using GFramework.Godot.architecture; +using GFramework.Godot.ui; + +public class GameArchitecture : AbstractArchitecture +{ + protected override void InstallModules() + { + // 注册 UI 注册表 + var uiRegistry = new GameUiRegistry(); + RegisterUtility(uiRegistry); + + // 注册 UI 工厂 + var uiFactory = new GodotUiFactory(); + RegisterUtility(uiFactory); + + // 注册 UI 路由 + var uiRouter = new GodotUiRouter(); + RegisterSystem(uiRouter); + } +} +``` + +### 使用 UI 路由 + +```csharp +using Godot; +using GFramework.Godot.extensions; + +public partial class GameController : Node +{ + public override void _Ready() + { + ShowMainMenu(); + } + + private async void ShowMainMenu() + { + var uiRouter = this.GetSystem(); + await uiRouter.PushAsync("MainMenu"); + } + + private async void ShowSettings() + { + var uiRouter = this.GetSystem(); + await uiRouter.PushAsync("Settings"); + } + + private void ShowDialog() + { + var uiRouter = this.GetSystem(); + uiRouter.Show("ConfirmDialog", UiLayer.Modal); + } + + private void ShowToast(string message) + { + var uiRouter = this.GetSystem(); + uiRouter.Show("Toast", UiLayer.Toast, new ToastParam { Message = message }); + } +} +``` + +## 高级用法 + +### 不同层级的 UI 行为 + +```csharp +// Page 层 UI(栈管理,不可重入) +public partial class MainMenuPage : Control, IUiPageBehaviorProvider +{ + public IUiPageBehavior GetPage() + { + return new PageLayerUiPageBehavior(this, "MainMenu"); + } +} + +// Overlay 层 UI(浮层,可重入) +public partial class InfoPanel : Control, IUiPageBehaviorProvider +{ + public IUiPageBehavior GetPage() + { + return new OverlayLayerUiPageBehavior(this, "InfoPanel"); + } +} + +// Modal 层 UI(模态对话框,可重入) +public partial class ConfirmDialog : Control, IUiPageBehaviorProvider +{ + public IUiPageBehavior GetPage() + { + return new ModalLayerUiPageBehavior(this, "ConfirmDialog"); + } +} + +// Toast 层 UI(提示,可重入) +public partial class ToastMessage : Control, IUiPageBehaviorProvider +{ + public IUiPageBehavior GetPage() + { + return new ToastLayerUiPageBehavior(this, "Toast"); + } +} + +// Topmost 层 UI(顶层,不可重入) +public partial class LoadingScreen : Control, IUiPageBehaviorProvider +{ + public IUiPageBehavior GetPage() + { + return new TopmostLayerUiPageBehavior(this, "Loading"); + } +} +``` + +### UI 参数传递 + +```csharp +// 定义 UI 参数 +public class ConfirmDialogParam : IUiPageEnterParam +{ + public string Title { get; set; } + public string Message { get; set; } + public Action OnConfirm { get; set; } + public Action OnCancel { get; set; } +} + +// 在 UI 中接收参数 +public partial class ConfirmDialog : Control, IUiPage +{ + private Label _titleLabel; + private Label _messageLabel; + private Action _onConfirm; + private Action _onCancel; + + public override void _Ready() + { + _titleLabel = GetNode