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 状态管理