GeWuYou bbb91d597a docs: 添加协程系统、CQRS与Mediator、生命周期管理、资源管理系统文档
- 新增协程系统文档,介绍协程调度器、等待指令、基本用法和最佳实践
- 新增CQRS与Mediator文档,涵盖命令查询职责分离、处理器实现和管道行为
- 新增生命周期管理文档,说明同步异步初始化和销毁机制
- 新增资源管理系统文档,介绍资源加载、缓存和引用计数管理功能
2026-03-05 21:53:55 +08:00

16 KiB
Raw Blame History

title, description
title description
场景系统 场景系统提供了完整的场景生命周期管理、路由导航和转换控制功能。

场景系统

概述

场景系统是 GFramework.Game 中用于管理游戏场景的核心组件。它提供了场景的加载、卸载、切换、暂停和恢复等完整生命周期管理,以及基于栈的场景导航机制。

通过场景系统,你可以轻松实现场景之间的平滑切换,管理场景栈(如主菜单 -> 游戏 -> 暂停菜单),并在场景转换时执行自定义逻辑。

主要特性

  • 完整的场景生命周期管理
  • 基于栈的场景导航
  • 场景转换管道和钩子
  • 路由守卫Route Guard
  • 场景工厂和行为模式
  • 异步加载和卸载

核心概念

场景接口

IScene 定义了场景的完整生命周期:

public interface IScene
{
    ValueTask OnLoadAsync(ISceneEnterParam? param);    // 加载资源
    ValueTask OnEnterAsync();                          // 进入场景
    ValueTask OnPauseAsync();                          // 暂停场景
    ValueTask OnResumeAsync();                         // 恢复场景
    ValueTask OnExitAsync();                           // 退出场景
    ValueTask OnUnloadAsync();                         // 卸载资源
}

场景路由

ISceneRouter 管理场景的导航和切换:

public interface ISceneRouter : ISystem
{
    ISceneBehavior? Current { get; }              // 当前场景
    string? CurrentKey { get; }                   // 当前场景键
    IEnumerable<ISceneBehavior> Stack { get; }    // 场景栈
    bool IsTransitioning { get; }                 // 是否正在切换

    ValueTask ReplaceAsync(string sceneKey, ISceneEnterParam? param = null);
    ValueTask PushAsync(string sceneKey, ISceneEnterParam? param = null);
    ValueTask PopAsync();
    ValueTask ClearAsync();
}

场景行为

ISceneBehavior 封装了场景的具体实现和引擎集成:

public interface ISceneBehavior
{
    string Key { get; }                    // 场景唯一标识
    IScene Scene { get; }                  // 场景实例
    ValueTask LoadAsync(ISceneEnterParam? param);
    ValueTask UnloadAsync();
}

基本用法

定义场景

实现 IScene 接口创建场景:

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); // 模拟卸载
    }
}

注册场景

在场景注册表中注册场景:

using GFramework.Game.Abstractions.scene;

public class GameSceneRegistry : IGameSceneRegistry
{
    private readonly Dictionary<string, Type> _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;
    }
}

切换场景

使用场景路由进行导航:

public class GameController : IController
{
    public IArchitecture GetArchitecture() => GameArchitecture.Interface;

    public async Task StartGame()
    {
        var sceneRouter = this.GetSystem<ISceneRouter>();

        // 替换当前场景(清空场景栈)
        await sceneRouter.ReplaceAsync("Gameplay");
    }

    public async Task ShowPauseMenu()
    {
        var sceneRouter = this.GetSystem<ISceneRouter>();

        // 压入新场景(保留当前场景)
        await sceneRouter.PushAsync("Pause");
    }

    public async Task ClosePauseMenu()
    {
        var sceneRouter = this.GetSystem<ISceneRouter>();

        // 弹出当前场景(恢复上一个场景)
        await sceneRouter.PopAsync();
    }
}

高级用法

场景参数传递

通过 ISceneEnterParam 传递数据:

// 定义场景参数
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"
});

路由守卫

使用路由守卫控制场景切换:

using GFramework.Game.Abstractions.scene;

public class SaveGameGuard : ISceneRouteGuard
{
    public async ValueTask<bool> CanLeaveAsync(
        ISceneBehavior from,
        string toKey,
        ISceneEnterParam? param)
    {
        // 离开游戏场景前检查是否需要保存
        if (from.Key == "Gameplay")
        {
            var needsSave = CheckIfNeedsSave();
            if (needsSave)
            {
                await SaveGameAsync();
            }
        }

        return true; // 允许离开
    }

    public async ValueTask<bool> 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());

场景转换处理器

自定义场景转换逻辑:

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());

场景栈管理

public class SceneNavigationController : IController
{
    public IArchitecture GetArchitecture() => GameArchitecture.Interface;

    public async Task NavigateToSettings()
    {
        var sceneRouter = this.GetSystem<ISceneRouter>();

        // 检查场景是否已在栈中
        if (sceneRouter.Contains("Settings"))
        {
            Console.WriteLine("设置场景已打开");
            return;
        }

        // 压入设置场景
        await sceneRouter.PushAsync("Settings");
    }

    public void ShowSceneStack()
    {
        var sceneRouter = this.GetSystem<ISceneRouter>();

        Console.WriteLine("当前场景栈:");
        foreach (var scene in sceneRouter.Stack)
        {
            Console.WriteLine($"- {scene.Key}");
        }
    }

    public async Task ReturnToMainMenu()
    {
        var sceneRouter = this.GetSystem<ISceneRouter>();

        // 清空所有场景并加载主菜单
        await sceneRouter.ClearAsync();
        await sceneRouter.ReplaceAsync("MainMenu");
    }
}

场景加载进度

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<object>(resources[i]);

            // 报告进度
            var progress = (i + 1) / (float)resources.Length;
            ReportProgress(progress);
        }
    }

    private void ReportProgress(float progress)
    {
        // 发送进度事件
        Console.WriteLine($"加载进度: {progress * 100:F0}%");
    }

    // ... 其他生命周期方法
}

场景预加载

public class PreloadController : IController
{
    public IArchitecture GetArchitecture() => GameArchitecture.Interface;

    public async Task PreloadNextLevel()
    {
        var sceneFactory = this.GetUtility<ISceneFactory>();

        // 预加载下一关场景
        var scene = sceneFactory.Create("Level2");
        await scene.OnLoadAsync(null);

        Console.WriteLine("下一关预加载完成");
    }
}

最佳实践

  1. 在 OnLoad 中加载资源,在 OnUnload 中释放:保持资源管理清晰

    public async ValueTask OnLoadAsync(ISceneEnterParam? param)
    {
        _texture = await LoadTextureAsync("player.png");
    }
    
    public async ValueTask OnUnloadAsync()
    {
        _texture?.Dispose();
        _texture = null;
    }
    
  2. 使用 Push/Pop 管理临时场景:如暂停菜单、设置界面

    // 打开暂停菜单(保留游戏场景)
    await sceneRouter.PushAsync("Pause");
    
    // 关闭暂停菜单(恢复游戏场景)
    await sceneRouter.PopAsync();
    
  3. 使用 Replace 切换主要场景:如从菜单到游戏

    // 开始游戏(清空场景栈)
    await sceneRouter.ReplaceAsync("Gameplay");
    
  4. 在 OnPause/OnResume 中管理状态:暂停和恢复游戏逻辑

    public async ValueTask OnPauseAsync()
    {
        // 暂停游戏逻辑
        _gameTimer.Pause();
        _audioSystem.PauseBGM();
    }
    
    public async ValueTask OnResumeAsync()
    {
        // 恢复游戏逻辑
        _gameTimer.Resume();
        _audioSystem.ResumeBGM();
    }
    
  5. 使用路由守卫处理业务逻辑:如保存检查、权限验证

    public async ValueTask<bool> CanLeaveAsync(...)
    {
        if (HasUnsavedChanges())
        {
            var confirmed = await ShowSaveDialog();
            if (confirmed)
            {
                await SaveAsync();
            }
            return confirmed;
        }
        return true;
    }
    
  6. 避免在场景切换时阻塞:使用异步操作

     await sceneRouter.ReplaceAsync("Gameplay");
     sceneRouter.ReplaceAsync("Gameplay").Wait(); // 可能死锁
    

常见问题

问题Replace、Push、Pop 有什么区别?

解答

  • Replace:清空场景栈,加载新场景(用于主要场景切换)
  • Push:压入新场景,暂停当前场景(用于临时场景)
  • Pop:弹出当前场景,恢复上一个场景(用于关闭临时场景)
// 场景栈示例
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. 通过场景参数
await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam
{
    Level = 5
});
  1. 通过 Model
var gameModel = this.GetModel<GameModel>();
gameModel.CurrentLevel = 5;
await sceneRouter.ReplaceAsync("Gameplay");
  1. 通过事件
this.SendEvent(new LevelSelectedEvent { Level = 5 });
await sceneRouter.ReplaceAsync("Gameplay");

问题:场景切换时如何显示加载画面?

解答 使用场景转换处理器:

public class LoadingScreenHandler : ISceneTransitionHandler
{
    public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event)
    {
        await ShowLoadingScreen();
    }

    public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
    {
        await HideLoadingScreen();
    }

    // ... 其他方法
}

问题:如何防止用户在场景切换时操作?

解答 检查 IsTransitioning 状态:

public async Task ChangeScene(string sceneKey)
{
    var sceneRouter = this.GetSystem<ISceneRouter>();

    if (sceneRouter.IsTransitioning)
    {
        Console.WriteLine("场景正在切换中,请稍候");
        return;
    }

    await sceneRouter.ReplaceAsync(sceneKey);
}

问题:场景切换失败怎么办?

解答 使用 try-catch 捕获异常:

try
{
    await sceneRouter.ReplaceAsync("Gameplay");
}
catch (Exception ex)
{
    Console.WriteLine($"场景切换失败: {ex.Message}");
    // 回退到安全场景
    await sceneRouter.ReplaceAsync("MainMenu");
}

问题:如何实现场景预加载?

解答 在后台预先加载场景资源:

// 在当前场景中预加载下一个场景
var factory = this.GetUtility<ISceneFactory>();
var nextScene = factory.Create("NextLevel");
await nextScene.OnLoadAsync(null);

// 稍后快速切换
await sceneRouter.ReplaceAsync("NextLevel");

相关文档