Files
FlowScope/docs/guides/flowscope-installation.md
2026-06-08 16:38:02 +08:00

8.8 KiB
Raw Blame History

FlowScope 安装与最小接入

日期2026-06-06

本文说明 FlowScope 在 P2 package/editor 阶段的当前安装方式、最小接入写法和已知限制。当前仓库已经存在真实 Unity Package 目录 Packages/com.flowscope.gamecore,同时仍保留 My project/Assets/FlowScope 作为开发工程内的测试与样例源。

当前可用安装方式

Git URL

消费方项目推荐通过 SSH Git URL 安装,并固定到 preview tag 或明确 commit

{
  "scopedRegistries": [
    {
      "name": "Unity NuGet",
      "url": "https://unitynuget-registry.openupm.com",
      "scopes": [
        "org.nuget"
      ]
    }
  ],
  "dependencies": {
    "com.cysharp.r3": "https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity#1.3.0",
    "com.flowscope.gamecore": "ssh://git@git.zz.com:2222/Developer/FlowScope.git?path=/Packages/com.flowscope.gamecore#v0.2.0-preview.2",
    "org.nuget.r3": "1.3.0"
  }
}

注意:

  • com.cysharp.r3 提供 Unity 适配层。
  • org.nuget.r3 提供 R3.dll 和相关 BCL DLL。
  • Unity Package Manager 不可靠支持在 package 的 package.json 中声明 Git dependency因此 R3 必须由消费方 manifest 显式声明。
  • 当前 v0.2.0-preview.2 覆盖空白项目 Git URL 安装、最小 smoke probe 编译、Documentation~ 导入内容,以及 Samples~/MainMenuP0 手动导入验收。

本地 package path

本机调试时可在消费方 Unity 项目的 Packages/manifest.json 中使用本地路径:

{
  "dependencies": {
    "com.flowscope.gamecore": "file:../FlowScope/Packages/com.flowscope.gamecore"
  }
}

适用场景:

  • 同时开发 FlowScope 和消费方项目。
  • 需要快速验证 Runtime、Addressables、Editor、Samples 或 Tests package 布局。
  • 不希望每次小改都重新打 Git tag。

项目内 Assets 开发模式

FlowScope 自身开发工程仍保留:

My project/Assets/FlowScope

该路径继续承担:

  • P0/P1 Runtime 回归测试。
  • 主项目内 Addressables 真实 fixture 验收。
  • MainMenuP0 Assets 样例源。
  • 迁移到 package Samples~ / Tests~ 前的稳定参照。

不要在消费方项目同时复制 Assets/FlowScope 并安装 Git package否则会混淆“项目内源码导入”和“package 消费”两条验证路径。

当前 package 内容

Packages/com.flowscope.gamecore/
  package.json
  Runtime/
  Addressables/
  Editor/
  Samples~/
  Tests~/
  Documentation~/
  README.md
  CHANGELOG.md

已完成:

  • Runtime / Addressables / Editor 镜像到 package。
  • Samples~/MainMenuP0 作为 package sample 候选。
  • Tests~/EditMode 第一刀最小 smoke 测试。
  • Git package 在 FishingFrenzy 中完成最小消费方验证。

仍未完成:

  • package sample 导入后的人工验收。
  • package Tests~ 全量策略与 PlayMode 测试迁移。
  • 真正空白 Unity 项目的 Git URL 安装复验。
  • 下一个 preview tag。

最小 Bootstrap 示例

下面示例只使用当前真实 API 名称。示例使用 Addressables 作为资源后端;如果项目暂时没有 Addressables 资源,也可以参考 MainMenuP0 中的 SampleResourceBackend 做本地验证。

using System;
using System.Collections.Generic;
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) => Task.CompletedTask;
        public Task EnterAsync(FeatureContext context) => Task.CompletedTask;
        public Task ExitAsync(FeatureContext context) => Task.CompletedTask;
        public void Dispose() { }
    }
}

接入要点

  • GameFlow.StartupAsync<TInitialFeature> 会先解析并加载 IConfigProvider
  • 初始 Feature 必须实现 IFeature,并能从 root 或 Feature scope 中解析。
  • 业务资源统一通过 IResourceServiceIResourceGroup 加载,不直接调用 Addressables。
  • UI 层使用 UIManager.RegisterLayer(...) 建立层级,再由 UIScreenNavigator 或业务 Feature 打开面板。
  • Shutdown 时应调用 GameFlow.ShutdownAsync(...),并释放 UI 预加载资源,例如 _preloadService.ReleaseAll()

常见误用

  • 不要让业务 Feature 直接调用 UnityEngine.AddressableAssets.Addressables
  • 不要把短生命周期对象注册为 RegisterInstanceRegisterSingletonFactory
  • 不要跳过 GameFlow.ShutdownAsync(...)
  • 不要把 UIScreenNavigator 当作完整页面路由系统。
  • 不要让 FlowScope.Runtime 反向依赖 Addressables、Editor、Samples 或 Tests。
  • 不要在没有注册 UI layer 的情况下打开面板。
  • 不要在未初始化 AudioService.Initialize(...) 前调用播放接口。

当前限制

P2 收口前仍需要完成:

  1. package sample 导入验收。
  2. package Tests~ 全量策略和 PlayMode 迁移策略。
  3. 真正空白 Unity 项目 Git URL 安装复验。
  4. 下一个 preview tag。
  5. Unity Test Runner 或明确记录无法执行原因。