Files
FlowScope/docs/guides/flowscope-installation.md
2026-06-04 11:33:59 +08:00

280 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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 时,推荐注册:
```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。