10 KiB
FlowScope 安装与最小接入
日期:2026-05-26
本文说明 FlowScope 在 P2 第一阶段面向使用者的安装路径、项目内开发模式,以及基于当前真实 API 的最小 Bootstrap 写法。当前仓库尚未完成标准 Unity Package 物理搬迁,package path 与 Git URL 方式是目标接入形态;项目内 Assets/FlowScope 开发模式是当前可用形态。
安装方式
本地 package path
当后续完成标准包目录后,可在使用方 Unity 项目的 Packages/manifest.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:
{
"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 项目内:
Assets/
FlowScope/
Runtime/
Addressables/
Samples/
Tests/
本仓库当前路径为:
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 做本地验证。
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<ExampleConfigRow>(
"example.json",
new InMemoryConfigSource(
new Dictionary<string, string>
{
{ "example.json", "[{\"Id\":1,\"Name\":\"Start\"}]" }
},
"Bootstrap"),
new JsonConfigParser())
});
var saveService = new SaveService(
new PlayerPrefsSaveStorage(),
new JsonSaveSerializer());
var audioService = gameObject
.AddComponent<AudioService>()
.Initialize(resources);
_root.RegisterInstance(_root);
_root.RegisterInstance<IConfigProvider>(configProvider);
_root.RegisterInstance<IResourceService>(resources);
_root.RegisterInstance<ISaveService>(saveService);
_root.RegisterInstance<IAudioService>(audioService);
_root.RegisterInstance(uiManager);
_root.RegisterInstance<IUIPreloadService>(_preloadService);
_root.RegisterInstance<IUIScreenNavigator>(screenNavigator);
_root.RegisterTransient(_ => new ExampleFeature(screenNavigator));
await _gameFlow.StartupAsync<ExampleFeature>(_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<TInitialFeature>(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 时,推荐注册:
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 安装方式视为可交付:
- 建立真实 package 根目录与
package.json。 - 通过本地 package path 在干净 Unity 项目中验证安装。
- 验证 Git URL 安装与依赖解析。
- 将
MainMenuP0从Assets/FlowScope/Samples整理为可导入 package sample。 - 在干净 Unity 项目中复验 Addressables 测试资源、settings 与 PlayMode 用例;主项目内的真实 load/release 验收记录已完成。
- 保持 generated csproj build 与 Unity Test Runner 记录为 0 error。