# FlowScope 安装与最小接入 日期:2026-05-26 本文说明 FlowScope 在 P2 第一阶段面向使用者的安装路径、项目内开发模式,以及基于当前真实 API 的最小 Bootstrap 写法。当前仓库尚未完成标准 Unity Package 物理搬迁,package path 与 Git URL 方式是目标接入形态;项目内 `Assets/FlowScope` 开发模式是当前可用形态。 ## 安装方式 ### 本地 package path 当后续完成标准包目录后,可在使用方 Unity 项目的 `Packages/manifest.json` 中使用本地路径: ```json { "dependencies": { "com.flowscope.gamecore": "file:../FlowScope/Packages/com.flowscope.gamecore" } } ``` 适用场景: - 本机同时开发 FlowScope 与游戏项目。 - 需要在包仓库中调试 Runtime、Addressables、Editor 或 Samples。 - 希望由 Unity Package Manager 识别包结构与 `Samples~`。 注意:P2 第一阶段当前仓库还没有 `Packages/com.flowscope.gamecore` 物理目录,因此此方式需要等第二阶段本地 package 试迁移完成后再用于实装验证。 ### Git URL 当包目录与发布分支稳定后,可在 `Packages/manifest.json` 中使用 Git URL: ```json { "dependencies": { "com.flowscope.gamecore": "<最终发布仓库 Git URL>?path=/Packages/com.flowscope.gamecore#v0.2.0" } } ``` 适用场景: - 使用方只消费稳定版本,不直接修改 FlowScope 源码。 - 团队希望通过 tag 或固定 commit 控制接入版本。 当前仓库 `origin` 仍带有旧项目名,不应直接写入对外安装文档。发布前需要由团队确认最终 FlowScope 包仓库地址;使用 Git URL 时应固定 tag 或 commit,避免使用浮动分支导致使用方项目在不同时间解析到不同代码。 ### 项目内 Assets 开发模式 当前可用模式是将 FlowScope 放在 Unity 项目内: ```text Assets/ FlowScope/ Runtime/ Addressables/ Samples/ Tests/ ``` 本仓库当前路径为: ```text My project/Assets/FlowScope ``` 适用场景: - P2 第一阶段继续验证 P0/P1 Runtime 行为。 - 并行开发 Addressables 验收、Editor 诊断与 package 文档。 - 在包迁移前保持 `MainMenuP0` 示例与测试路径稳定。 使用此模式时,不需要在 `manifest.json` 中声明 `com.flowscope.gamecore`。仍需要确保项目已有当前代码依赖的包,例如 `com.cysharp.r3`、`com.unity.addressables` 与 `com.unity.ugui`。 ## 最小 Bootstrap 示例 下面示例只使用当前代码中已经存在的 API 名称。示例使用 Addressables 作为资源后端,适合未来包接入;如果项目暂时没有 Addressables 资源,也可以参考 `MainMenuP0` 示例中的自定义 `IResourceBackend` 做本地验证。 ```csharp using System.Collections.Generic; using System; using System.Threading; using System.Threading.Tasks; using FlowScope.Addressables; using FlowScope.Audio; using FlowScope.Config; using FlowScope.Flow; using FlowScope.Resources; using FlowScope.Save; using FlowScope.UI; using UnityEngine; using ContainerType = FlowScope.Container.Container; public sealed class FlowScopeBootstrap : MonoBehaviour { [SerializeField] private Canvas hudCanvas; private ContainerType _root; private GameFlow _gameFlow; private UIPreloadService _preloadService; private CancellationTokenSource _lifetime; public Task StartupTask { get; private set; } private void Awake() { _lifetime = new CancellationTokenSource(); StartupTask = RunStartupWithLoggingAsync(_lifetime.Token); } public async Task StartupAsync(CancellationToken cancellationToken) { _root = new ContainerType(); _gameFlow = new GameFlow(); IResourceService resources = new AddressablesResourceService( new AddressablesResourceBackend()); _preloadService = new UIPreloadService(resources); var uiManager = new UIManager(resources, _preloadService); uiManager.RegisterLayer("hud", hudCanvas, 0, PanelStrategy.Destroy); var screenNavigator = new UIScreenNavigator(uiManager); var configProvider = new JsonConfigProvider(new[] { JsonConfigProvider.Mapping( "example.json", new InMemoryConfigSource( new Dictionary { { "example.json", "[{\"Id\":1,\"Name\":\"Start\"}]" } }, "Bootstrap"), new JsonConfigParser()) }); var saveService = new SaveService( new PlayerPrefsSaveStorage(), new JsonSaveSerializer()); var audioService = gameObject .AddComponent() .Initialize(resources); _root.RegisterInstance(_root); _root.RegisterInstance(configProvider); _root.RegisterInstance(resources); _root.RegisterInstance(saveService); _root.RegisterInstance(audioService); _root.RegisterInstance(uiManager); _root.RegisterInstance(_preloadService); _root.RegisterInstance(screenNavigator); _root.RegisterTransient(_ => new ExampleFeature(screenNavigator)); await _gameFlow.StartupAsync(_root, cancellationToken); } public async Task ShutdownAsync(CancellationToken cancellationToken) { if (_gameFlow != null && _gameFlow.State != GameFlowState.Disposed) { await _gameFlow.ShutdownAsync(cancellationToken); } _preloadService?.ReleaseAll(); _lifetime?.Cancel(); _lifetime?.Dispose(); _lifetime = null; } private void OnApplicationQuit() { _ = RunShutdownWithLoggingAsync(CancellationToken.None); } private async Task RunStartupWithLoggingAsync(CancellationToken cancellationToken) { try { await StartupAsync(cancellationToken); } catch (Exception exception) { Debug.LogException(exception); throw; } } private async Task RunShutdownWithLoggingAsync(CancellationToken cancellationToken) { try { await ShutdownAsync(cancellationToken); } catch (Exception exception) { Debug.LogException(exception); } } private sealed class ExampleConfigRow : IConfigRow { public int Id { get; set; } public string Name { get; set; } } private sealed class ExampleFeature : IFeature { private readonly IUIScreenNavigator _screenNavigator; public ExampleFeature(IUIScreenNavigator screenNavigator) { _screenNavigator = screenNavigator; } public Task LoadAsync(FeatureContext context) { return Task.CompletedTask; } public Task EnterAsync(FeatureContext context) { return Task.CompletedTask; } public Task ExitAsync(FeatureContext context) { return Task.CompletedTask; } public void Dispose() { } } } ``` 接入要点: - `GameFlow.StartupAsync(Container root, CancellationToken cancellationToken)` 会先解析并加载 `IConfigProvider`。 - 初始 Feature 必须实现 `IFeature`,并能从 Feature scope 中解析;示例使用 `_root.RegisterTransient(_ => new ExampleFeature(screenNavigator))`。 - 业务资源统一通过 `IResourceService` 或 `IResourceGroup` 加载,不直接在业务层调用 Addressables。 - UI 层使用 `UIManager.RegisterLayer(...)` 建立层级,再由 `UIScreenNavigator` 或业务 Feature 打开面板。 - Shutdown 时应调用 `GameFlow.ShutdownAsync(...)`,并释放 UI 预加载资源,例如 `_preloadService.ReleaseAll()`。 ## Addressables 接入说明 使用 Addressables 时,推荐注册: ```csharp IResourceService resources = new AddressablesResourceService( new AddressablesResourceBackend()); ``` 这会把业务层依赖收敛到 `IResourceService`。`AddressablesResourceBackend` 只位于 Addressables 适配程序集,Runtime 不需要知道 Addressables 的存在。 如果某个系统需要独立资源生命周期,应通过 `resources.CreateGroup()` 创建 `IResourceGroup`,让 Feature 的资源释放跟随 `FeatureContext.Resources.Dispose()`。 ## 常见误用 - 不要让业务 Feature 直接调用 `UnityEngine.AddressableAssets.Addressables`;应通过 `IResourceService` 或 `IResourceGroup` 访问资源。 - 不要把短生命周期对象注册成 `RegisterInstance` 或 `RegisterSingletonFactory`;临时对象优先使用 `RegisterTransient`,需要随 Feature 生命周期释放的对象放在 Feature scope。 - 不要跳过 `GameFlow.ShutdownAsync(...)`;它负责退出当前 Feature、释放 Feature scope,并处置 root container。 - 不要把 `UIScreenNavigator` 当成完整页面路由系统;它是当前 UI 导航辅助,不负责 URL、跨 Feature 页面恢复或全局路由。 - 不要让 `FlowScope.Runtime` 反向依赖 Addressables、Editor、Samples 或 Tests。 - 不要在没有注册 UI layer 的情况下打开面板;`UIManager.OpenAsync` 会要求面板声明的 layer 已经通过 `RegisterLayer` 注册。 - 不要在未初始化 `AudioService.Initialize(IResourceService, AudioServiceConfig)` 前调用播放接口。 - 不要假设 package path 与 Git URL 在 P2 第一阶段已经可安装;当前真实可用形态仍是项目内 `Assets/FlowScope` 开发模式。 ## 当前限制 P2 第一阶段文档完成后,FlowScope 仍需要后续 worker 或阶段完成以下验证后,才能把 package 安装方式视为可交付: 1. 建立真实 package 根目录与 `package.json`。 2. 通过本地 package path 在干净 Unity 项目中验证安装。 3. 验证 Git URL 安装与依赖解析。 4. 将 `MainMenuP0` 从 `Assets/FlowScope/Samples` 整理为可导入 package sample。 5. 在干净 Unity 项目中复验 Addressables 测试资源、settings 与 PlayMode 用例;主项目内的真实 load/release 验收记录已完成。 6. 保持 generated csproj build 与 Unity Test Runner 记录为 0 error。