48 Commits

Author SHA1 Message Date
JSD\13999
491eb27cd2 提交文档 2026-06-18 15:11:42 +08:00
JSD\13999
b87465d80f refactor(scope): 收束架构作用域容器边界 2026-06-18 12:20:20 +08:00
JSD\13999
76016d35c1 feat(core): 实现架构作用域容器核心 2026-06-18 10:25:32 +08:00
JSD\13999
6e45152250 收束 P1 资源后端适配层 2026-05-20 20:49:29 +08:00
JSD\13999
1c53319961 完成 Game Core P1 生产化集成 2026-05-20 16:31:21 +08:00
JSD\13999
a879badd2a 升级 P1 主菜单样例和使用规范 2026-05-20 16:27:08 +08:00
JSD\13999
a0508ff630 扩展 P1 UI 导航和预加载 2026-05-20 16:22:10 +08:00
JSD\13999
1efe6c9974 扩展 P1 配置来源和存档迁移 2026-05-20 16:16:08 +08:00
JSD\13999
1e7ae22ddc 拆分 P1 资源后端适配层 2026-05-20 16:08:20 +08:00
JSD\13999
ff3dc749f7 定义 Game Core P1 生产化契约 2026-05-20 15:36:10 +08:00
JSD\13999
f855c34bf9 修正 P1 资源后端泛型契约 2026-05-20 15:28:27 +08:00
JSD\13999
6da0a7822c 制定 Game Core P1 生产化实施计划 2026-05-20 12:08:11 +08:00
JSD\13999
e1197b5c0f 完成 Game Core P0 验收收口 2026-05-20 11:57:00 +08:00
JSD\13999
e866e75e86 修正音频异步取消语义 2026-05-20 11:05:33 +08:00
JSD\13999
05ecb0916c 修正资源加载取消与同步音频语义 2026-05-19 21:20:52 +08:00
JSD\13999
afcb02591d 补充 AudioService 异步播放入口 2026-05-19 20:42:36 +08:00
JSD\13999
436cf6ef56 修正 UIManager 运行时销毁逻辑 2026-05-19 20:31:04 +08:00
JSD\13999
a51145ae2c 完善 MainMenuP0 纵向切片验收 2026-05-19 19:59:57 +08:00
JSD\13999
f7473ab016 补充 GameFlow 根容器清理异常日志断言 2026-05-19 11:33:23 +08:00
JSD\13999
0257529a6f 修复 GameFlow 清理异常测试日志断言 2026-05-19 11:29:36 +08:00
JSD\13999
c0163d4d1c 稳定 Audio PlayMode 测试 2026-05-19 11:22:07 +08:00
JSD\13999
29ebeb57b2 修复 PlayMode 全量测试断言 2026-05-19 10:59:16 +08:00
JSD\13999
23d235dad6 修复 PlayMode 测试主线程阻塞 2026-05-19 10:53:26 +08:00
JSD\13999
48785f9f6a 修复 Unity Test Runner 测试签名 2026-05-19 10:41:31 +08:00
JSD\13999
210715d0cd 修复测试程序集编译兼容性 2026-05-18 14:56:10 +08:00
JSD\13999
686dec326e 修正 FlowScope 测试程序集引用边界 2026-05-18 14:42:34 +08:00
JSD\13999
e35981d246 修复 Unity 编译依赖和测试程序集配置 2026-05-18 14:18:19 +08:00
JSD\13999
fac8e9a7f3 集成 Game Core P0 纵向切片 2026-05-18 12:11:57 +08:00
JSD\13999
06d7b0dc75 修正 P0 UI Attribute 契约文档 2026-05-18 12:11:50 +08:00
JSD\13999
00a3368df5 合并 P0 主菜单示例 2026-05-18 12:10:29 +08:00
JSD\13999
d30120c6cd 添加 P0 主菜单纵向切片示例 2026-05-18 12:10:16 +08:00
JSD\13999
040721c713 合并 P0 生命周期编排
# Conflicts:
#	My project/Assets/FlowScope/Tests.meta
#	My project/Assets/FlowScope/Tests/EditMode.meta
2026-05-18 12:05:22 +08:00
JSD\13999
e4074cf388 合并 P0 音频服务
# Conflicts:
#	My project/Assets/FlowScope/Runtime.meta
#	My project/Assets/FlowScope/Tests.meta
#	My project/Assets/FlowScope/Tests/PlayMode.meta
2026-05-18 12:04:38 +08:00
JSD\13999
d3fd0706f1 合并 P0 UI 管理器
# Conflicts:
#	My project/Assets/FlowScope/Tests.meta
#	My project/Assets/FlowScope/Tests/PlayMode.meta
2026-05-18 12:04:07 +08:00
JSD\13999
912ad25c67 合并 P0 资源服务
# Conflicts:
#	My project/Assets/FlowScope/Runtime.meta
#	My project/Assets/FlowScope/Tests.meta
#	My project/Assets/FlowScope/Tests/EditMode.meta
2026-05-18 12:01:31 +08:00
JSD\13999
eff2ad1b3a 合并 P0 数据配置和存档
# Conflicts:
#	My project/Assets/FlowScope/Tests.meta
#	My project/Assets/FlowScope/Tests/EditMode.meta
2026-05-18 12:00:10 +08:00
JSD\13999
4befd6655b 合并 P0 容器实现 2026-05-18 11:59:09 +08:00
JSD\13999
23905fcbea 实现 P0 音频服务 2026-05-18 11:58:49 +08:00
JSD\13999
d37faae707 实现 P0 UI 管理器 2026-05-18 11:58:49 +08:00
JSD\13999
dfd1c4cfae 实现 P0 资源加载和引用计数 2026-05-18 11:58:49 +08:00
JSD\13999
742b8aba25 实现 P0 数据配置和存档 2026-05-18 11:58:49 +08:00
JSD\13999
b1018c120d 实现 P0 容器核心能力 2026-05-18 11:58:49 +08:00
JSD\13999
135c5a071d 实现 P0 生命周期编排 2026-05-18 10:28:55 +08:00
JSD\13999
49318c50a4 定义 Game Core P0 共享契约 2026-05-15 15:57:30 +08:00
JSD\13999
76c35d8d99 忽略本地工作树目录 2026-05-15 15:39:09 +08:00
JSD\13999
019703d2a8 文档记录 2026-05-15 15:38:40 +08:00
JSD\13999
d7b09891dc unity sample scene 2026-05-15 15:36:47 +08:00
JSD\13999
e84f437f40 git ignore 2026-05-15 15:36:31 +08:00
102 changed files with 16863 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
Confs
Client/ServerData
/assetbundles
.DS_Store
.vscode/
/cfgPack/encryptcfg
.gemini/
.worktrees/
GEMINI.md
CLAUDE.md
AGENTS.md
QWEN.md
Client/.claude/
dev-ab-svr/debugconfig.json

101
My project/.gitignore vendored Normal file
View File

@@ -0,0 +1,101 @@
# This .gitignore file should be placed at the root of your Unity project directory
#
# Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore
#
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/
/[Bb]in/
/SerializedBuildInfo/
# ignore all files and folders ends with "~"
*~
# MemoryCaptures can get excessive in size.
# They also could contain extremely sensitive data
/[Mm]emoryCaptures/
# Asset meta data should only be ignored when the corresponding asset is also ignored
!/[Aa]ssets/**/*.meta
# Uncomment this line if you wish to ignore the asset store tools plugin
# /[Aa]ssets/AssetStoreTools*
# Autogenerated Jetbrains Rider plugin
/[Aa]ssets/Plugins/Editor/JetBrains*
# Visual Studio cache directory
.vs/
# Gradle cache directory
.gradle/
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db
.vsconfig
.vshistory
.vscode/
# Unity3D generated meta files
*.pidb.meta
*.pdb.meta
*.mdb.meta
# Unity3D generated file on crash reports
sysinfo.txt
# Builds
*.apk
*.unitypackage
# Crashlytics generated file
crashlytics-build.properties
# ignore Test Scripts & resources in test folder
/[Aa]ssets/[Tt]emp[Tt]est/
/[Aa]ssets/[Tt]emp[Tt]est
/[Aa]ssets/[Tt]emp[Tt]est.meta
# ignore build info from reporter
/[Aa]ssets/StreamingAssets/build_info/
/[Aa]ssets/StreamingAssets/build_info
/[Aa]ssets/StreamingAssets/build_info.meta
## ignore generated playfab async scripts
#/LocalPackages/asap.playfab.async/Runtime/Gen.meta
#/LocalPackages/asap.playfab.async/Runtime/Gen/
#/LocalPackages/asap.playfab.async/Runtime/Gen
# ignore core source code
/LocalPackages/GameCore/
/LocalPackages/asap.core/
# HybridCLR Temp project
/HybridCLRData/StrippedAOTDllsTempProj
# ignore idea files
.idea/
# ignore new slnx files
*.slnx

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6f15a20a42b04f9a86de633ca4831e25
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: eab40aadc4ea4632930155f91c6faca4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c35227ceb2c96da44873807b1908facf
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4bd6a11cc58df434eb82db6a3fe8362f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
namespace FlowScope.Core.Container
{
internal sealed class ArchitectureScope : IArchitectureScope
{
private readonly ArchitectureScope _parent;
private readonly global::FlowScope.Container.Container _container;
private readonly List<ArchitectureScope> _children = new List<ArchitectureScope>();
public ArchitectureScope(string name)
: this(name, null, new global::FlowScope.Container.Container())
{
}
private ArchitectureScope(
string name,
ArchitectureScope parent,
global::FlowScope.Container.Container container)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Scope name cannot be empty.", nameof(name));
}
Name = name;
_parent = parent;
_container = container;
Path = parent == null ? name : $"{parent.Path}/{name}";
}
public string Name { get; }
public IScopeInfo Parent => _parent;
public string Path { get; }
public bool IsDisposed { get; private set; }
public IArchitectureScope CreateScope(string name)
{
ThrowIfDisposed();
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Scope name cannot be empty.", nameof(name));
}
if (_children.Exists(child => !child.IsDisposed && child.Name == name))
{
throw new InvalidOperationException($"Scope '{name}' already exists under '{Path}'.");
}
var child = new ArchitectureScope(name, this, _container.CreateScope(name));
_children.Add(child);
return child;
}
public IArchitectureScope CreateFeature<TFeature>()
where TFeature : IFeature, new()
{
return CreateFeature<TFeature>(typeof(TFeature).Name);
}
public IArchitectureScope CreateFeature<TFeature>(string name)
where TFeature : IFeature, new()
{
ThrowIfDisposed();
var featureScope = (ArchitectureScope)CreateScope(name);
try
{
var feature = new TFeature();
feature.Install(featureScope);
return featureScope;
}
catch
{
featureScope.Dispose();
throw;
}
}
public IRegistrationBuilder Register<TImplementation>()
{
ThrowIfDisposed();
return new RegistrationBuilderAdapter(_container.Register<TImplementation>());
}
public IRegistrationBuilder Register<TImplementation>(string name)
{
ThrowIfDisposed();
return new RegistrationBuilderAdapter(_container.Register<TImplementation>(name));
}
public IRegistrationBuilder Register<TService, TImplementation>()
where TImplementation : TService
{
ThrowIfDisposed();
return new RegistrationBuilderAdapter(_container.Register<TService, TImplementation>());
}
public IRegistrationBuilder Register<TService, TImplementation>(string name)
where TImplementation : TService
{
ThrowIfDisposed();
return new RegistrationBuilderAdapter(_container.Register<TService, TImplementation>(name));
}
public void RegisterInstance<TService>(TService instance)
{
RegisterInstance(instance, null);
}
public void RegisterInstance<TService>(TService instance, string name)
{
ThrowIfDisposed();
_container.RegisterInstance(instance, name);
}
public TService Resolve<TService>()
{
ThrowIfDisposed();
return _container.Resolve<TService>();
}
public TService Resolve<TService>(string name)
{
ThrowIfDisposed();
return _container.Resolve<TService>(name);
}
public bool TryResolve<TService>(out TService value)
{
ThrowIfDisposed();
return _container.TryResolve(out value);
}
public bool TryResolve<TService>(out TService value, string name)
{
ThrowIfDisposed();
return _container.TryResolve(out value, name);
}
public void Dispose()
{
if (IsDisposed)
{
return;
}
IsDisposed = true;
for (var i = _children.Count - 1; i >= 0; i--)
{
_children[i].Dispose();
}
_children.Clear();
_container.Dispose();
_parent?._children.Remove(this);
}
private void ThrowIfDisposed()
{
if (IsDisposed)
{
throw new ObjectDisposedException(Path);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cce1873306eafbf41b8db1123fd2bd49
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,330 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace FlowScope.Container
{
public sealed class Container : IDisposable
{
[ThreadStatic]
private static Stack<Type> _resolutionStack;
private readonly Container _parent;
private readonly Dictionary<ServiceKey, ContainerRegistration> _registrations =
new Dictionary<ServiceKey, ContainerRegistration>();
private readonly List<Container> _children = new List<Container>();
private readonly List<IDisposable> _ownedDisposables = new List<IDisposable>();
private bool _disposed;
public Container()
{
}
private Container(Container parent, string name)
{
_parent = parent;
Name = string.IsNullOrWhiteSpace(name) ? null : name;
}
public string Name { get; }
public void RegisterInstance<T>(T instance)
{
RegisterInstance(instance, null);
}
public void RegisterInstance<T>(T instance, string name)
{
ThrowIfDisposed();
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
RegisterLocal(new ServiceKey(typeof(T), name), ContainerRegistration.ForInstance(instance));
}
public void RegisterFactory<T>(Func<Container, T> factory)
{
RegisterFactory(null, factory);
}
public void RegisterFactory<T>(string name, Func<Container, T> factory)
{
ThrowIfDisposed();
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
RegisterLocal(
new ServiceKey(typeof(T), name),
ContainerRegistration.ForFactory(this, c => factory(c), ServiceLifetime.Singleton));
}
public ContainerRegistrationBuilder Register<TImplementation>()
{
return Register<TImplementation>(null);
}
public ContainerRegistrationBuilder Register<TImplementation>(string name)
{
ThrowIfDisposed();
return RegisterBuiltFactory(
new ServiceKey(typeof(TImplementation), name),
typeof(TImplementation));
}
public ContainerRegistrationBuilder Register<TInterface, TImplementation>()
where TImplementation : TInterface
{
return Register<TInterface, TImplementation>(null);
}
public ContainerRegistrationBuilder Register<TInterface, TImplementation>(string name)
where TImplementation : TInterface
{
ThrowIfDisposed();
var serviceType = typeof(TInterface);
var implementationType = typeof(TImplementation);
if (!serviceType.IsAssignableFrom(implementationType))
{
throw new InvalidOperationException($"{FormatType(implementationType)} cannot be assigned to {FormatType(serviceType)}.");
}
return RegisterBuiltFactory(new ServiceKey(serviceType, name), implementationType);
}
public T Resolve<T>()
{
return Resolve<T>(null);
}
public T Resolve<T>(string name)
{
ThrowIfDisposed();
return (T)Resolve(typeof(T), name);
}
public bool TryResolve<T>(out T value)
{
return TryResolve(out value, null);
}
public bool TryResolve<T>(out T value, string name)
{
ThrowIfDisposed();
var key = new ServiceKey(typeof(T), name);
if (!TryFindRegistration(key, out var registration))
{
value = default;
return false;
}
value = (T)ResolveRegistration(key, registration);
return true;
}
public Container CreateScope()
{
ThrowIfDisposed();
var child = new Container(this, null);
_children.Add(child);
return child;
}
public Container CreateScope(string name)
{
ThrowIfDisposed();
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Scope name cannot be empty.", nameof(name));
}
if (_children.Exists(child => !child._disposed && child.Name == name))
{
throw new InvalidOperationException($"Scope '{name}' already exists in this container.");
}
var child = new Container(this, name);
_children.Add(child);
return child;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
var exceptions = new List<Exception>();
for (var i = _children.Count - 1; i >= 0; i--)
{
TryDispose(_children[i], exceptions);
}
for (var i = _ownedDisposables.Count - 1; i >= 0; i--)
{
TryDispose(_ownedDisposables[i], exceptions);
}
_children.Clear();
_ownedDisposables.Clear();
_registrations.Clear();
if (_parent != null)
{
_parent._children.Remove(this);
}
if (exceptions.Count > 0)
{
throw new AggregateException("One or more container-owned instances failed to dispose.", exceptions);
}
}
internal object Resolve(Type serviceType)
{
return Resolve(serviceType, null);
}
internal object Resolve(Type serviceType, string name)
{
ThrowIfDisposed();
var key = new ServiceKey(serviceType, name);
if (!TryFindRegistration(key, out var registration))
{
throw new InvalidOperationException($"No registration found for {key}.");
}
return ResolveRegistration(key, registration);
}
internal void RegisterBuilt(ServiceKey serviceKey, ContainerRegistration registration)
{
ThrowIfDisposed();
RegisterLocal(serviceKey, registration);
}
internal void TrackOwnedDisposable(IDisposable disposable)
{
_ownedDisposables.Add(disposable);
}
private ContainerRegistrationBuilder RegisterBuiltFactory(
ServiceKey serviceKey,
Type implementationType)
{
var factory = BuildFactory(implementationType);
return new ContainerRegistrationBuilder(this, serviceKey, factory);
}
private static Func<Container, object> BuildFactory(Type implementationType)
{
var constructors = implementationType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
if (constructors.Length != 1)
{
throw new InvalidOperationException($"{FormatType(implementationType)} must declare exactly one public constructor.");
}
var constructor = constructors[0];
var parameters = constructor.GetParameters();
return container =>
{
var arguments = parameters
.Select(parameter => container.Resolve(parameter.ParameterType))
.ToArray();
return constructor.Invoke(arguments);
};
}
private void RegisterLocal(ServiceKey serviceKey, ContainerRegistration registration)
{
if (_registrations.ContainsKey(serviceKey))
{
throw new InvalidOperationException($"{serviceKey} is already registered in this container scope.");
}
_registrations.Add(serviceKey, registration);
}
private object ResolveRegistration(ServiceKey serviceKey, ContainerRegistration registration)
{
_resolutionStack = _resolutionStack ?? new Stack<Type>();
if (_resolutionStack.Contains(serviceKey.ServiceType))
{
var chain = _resolutionStack.Reverse()
.Concat(new[] { serviceKey.ServiceType })
.Select(t => t.Name);
throw new InvalidOperationException($"Circular dependency detected: {string.Join(" -> ", chain)}");
}
_resolutionStack.Push(serviceKey.ServiceType);
try
{
return registration.Resolve(this);
}
finally
{
_resolutionStack.Pop();
if (_resolutionStack.Count == 0)
{
_resolutionStack = null;
}
}
}
private bool TryFindRegistration(ServiceKey serviceKey, out ContainerRegistration registration)
{
if (_registrations.TryGetValue(serviceKey, out registration))
{
return true;
}
if (_parent != null)
{
return _parent.TryFindRegistration(serviceKey, out registration);
}
return false;
}
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(Container));
}
}
private static void TryDispose(IDisposable disposable, List<Exception> exceptions)
{
try
{
disposable.Dispose();
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
private static string FormatType(Type type)
{
return type.FullName ?? type.Name;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 554409c4a7e84909a72a89d17aeb2b0b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
namespace FlowScope.Container
{
public enum ServiceLifetime
{
Singleton,
Scoped,
Transient
}
public readonly struct ServiceKey : IEquatable<ServiceKey>
{
public ServiceKey(Type serviceType, string name)
{
ServiceType = serviceType ?? throw new ArgumentNullException(nameof(serviceType));
Name = string.IsNullOrWhiteSpace(name) ? null : name;
}
public Type ServiceType { get; }
public string Name { get; }
public bool Equals(ServiceKey other)
{
return ServiceType == other.ServiceType && string.Equals(Name, other.Name, StringComparison.Ordinal);
}
public override bool Equals(object obj)
{
return obj is ServiceKey other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
return ((ServiceType != null ? ServiceType.GetHashCode() : 0) * 397) ^
(Name != null ? StringComparer.Ordinal.GetHashCode(Name) : 0);
}
}
public override string ToString()
{
var typeName = ServiceType.FullName ?? ServiceType.Name;
return Name == null ? typeName : $"{typeName} named '{Name}'";
}
}
public sealed class ContainerRegistrationBuilder
{
private readonly Container _owner;
private readonly ServiceKey _serviceKey;
private readonly Func<Container, object> _factory;
private bool _committed;
internal ContainerRegistrationBuilder(
Container owner,
ServiceKey serviceKey,
Func<Container, object> factory)
{
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
_serviceKey = serviceKey;
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
public void AsSingleton()
{
Commit(ServiceLifetime.Singleton);
}
public void PerScope()
{
Commit(ServiceLifetime.Scoped);
}
public void AsTransient()
{
Commit(ServiceLifetime.Transient);
}
private void Commit(ServiceLifetime lifetime)
{
if (_committed)
{
throw new InvalidOperationException($"{_serviceKey} registration has already been committed.");
}
_committed = true;
_owner.RegisterBuilt(_serviceKey, ContainerRegistration.ForFactory(_owner, _factory, lifetime));
}
}
internal sealed class ContainerRegistration
{
private readonly Container _owner;
private readonly Func<Container, object> _factory;
private readonly ServiceLifetime _lifetime;
private readonly Dictionary<Container, object> _scopedInstances;
private object _singletonInstance;
private bool _singletonCreated;
private ContainerRegistration(
Container owner,
Func<Container, object> factory,
object instance,
bool created,
ServiceLifetime lifetime)
{
_owner = owner;
_factory = factory;
_singletonInstance = instance;
_singletonCreated = created;
_lifetime = lifetime;
_scopedInstances = lifetime == ServiceLifetime.Scoped
? new Dictionary<Container, object>()
: null;
}
public static ContainerRegistration ForInstance(object instance)
{
return new ContainerRegistration(null, null, instance, true, ServiceLifetime.Singleton);
}
public static ContainerRegistration ForFactory(Container owner, Func<Container, object> factory)
{
return ForFactory(owner, factory, ServiceLifetime.Singleton);
}
public static ContainerRegistration ForFactory(
Container owner,
Func<Container, object> factory,
ServiceLifetime lifetime)
{
return new ContainerRegistration(owner, factory, null, false, lifetime);
}
public object Resolve(Container requester)
{
switch (_lifetime)
{
case ServiceLifetime.Singleton:
return ResolveSingleton();
case ServiceLifetime.Scoped:
return ResolveScoped(requester);
case ServiceLifetime.Transient:
return CreateAndTrack(requester);
default:
throw new InvalidOperationException($"Unknown service lifetime {_lifetime}.");
}
}
private object ResolveSingleton()
{
if (_singletonCreated)
{
return _singletonInstance;
}
_singletonInstance = CreateAndTrack(_owner);
_singletonCreated = true;
return _singletonInstance;
}
private object ResolveScoped(Container requester)
{
if (requester == null)
{
requester = _owner;
}
if (_scopedInstances.TryGetValue(requester, out var instance))
{
return instance;
}
instance = CreateAndTrack(requester);
_scopedInstances.Add(requester, instance);
return instance;
}
private object CreateAndTrack(Container owner)
{
var instance = _factory(owner);
if (instance is IDisposable disposable)
{
owner.TrackOwnedDisposable(disposable);
}
return instance;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d024d7ed8fd642b6948a216b3a676e22
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
using FlowScope.Container;
namespace FlowScope.Core.Container
{
internal sealed class RegistrationBuilderAdapter : IRegistrationBuilder
{
private readonly ContainerRegistrationBuilder _inner;
public RegistrationBuilderAdapter(ContainerRegistrationBuilder inner)
{
_inner = inner;
}
public void AsSingleton() => _inner.AsSingleton();
public void PerScope() => _inner.PerScope();
public void AsTransient() => _inner.AsTransient();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 988d85a7d7fa65f4786596348f72e053
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
using FlowScope.Core.Container;
namespace FlowScope
{
public static class FlowScope
{
public static IArchitectureScope Create()
{
return Create("Root");
}
public static IArchitectureScope Create(string rootName)
{
return new ArchitectureScope(rootName);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c3102de921ecf4a40acc99c9bd099adc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
using System;
namespace FlowScope
{
public interface IArchitectureScope : IScopeInfo, IDisposable
{
IArchitectureScope CreateScope(string name);
IArchitectureScope CreateFeature<TFeature>() where TFeature : IFeature, new();
IArchitectureScope CreateFeature<TFeature>(string name) where TFeature : IFeature, new();
IRegistrationBuilder Register<TImplementation>();
IRegistrationBuilder Register<TImplementation>(string name);
IRegistrationBuilder Register<TService, TImplementation>() where TImplementation : TService;
IRegistrationBuilder Register<TService, TImplementation>(string name) where TImplementation : TService;
void RegisterInstance<TService>(TService instance);
void RegisterInstance<TService>(TService instance, string name);
TService Resolve<TService>();
TService Resolve<TService>(string name);
bool TryResolve<TService>(out TService value);
bool TryResolve<TService>(out TService value, string name);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4aac2cbe9a0b55b4a847cc7eb58e21e5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
namespace FlowScope
{
public interface IFeature
{
void Install(IArchitectureScope scope);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d65ef41768038504fb7b3c5b5712d3ad
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,9 @@
namespace FlowScope
{
public interface IRegistrationBuilder
{
void AsSingleton();
void PerScope();
void AsTransient();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6e26efad6333ec6469df54dbf85637e0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
namespace FlowScope
{
public interface IScopeInfo
{
string Name { get; }
IScopeInfo Parent { get; }
string Path { get; }
bool IsDisposed { get; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a005e79c7b865c4458eb72135769a42f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
{
"name": "FlowScope.Runtime",
"rootNamespace": "FlowScope",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4f077ee742b346a188dc0c93b419f3ee
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 283379933e6c4e008981e1de556732cd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1d4bc61cf30a4b319edd9c885eb07341
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1f19d44b51ad9214693ce4ebaa68a5fa
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,109 @@
using NUnit.Framework;
namespace FlowScope.Tests.EditMode.Core
{
public sealed class ArchitectureScopeDisposeTests
{
[Test]
public void Singleton_FromParentIsSharedByChildrenAndDisposedByParent()
{
using var root = global::FlowScope.FlowScope.Create();
var first = root.CreateScope("First");
var second = root.CreateScope("Second");
root.Register<IDisposableService, DisposableService>().AsSingleton();
var rootInstance = root.Resolve<IDisposableService>();
Assert.That(first.Resolve<IDisposableService>(), Is.SameAs(rootInstance));
Assert.That(second.Resolve<IDisposableService>(), Is.SameAs(rootInstance));
first.Dispose();
Assert.That(rootInstance.IsDisposed, Is.False);
root.Dispose();
Assert.That(rootInstance.IsDisposed, Is.True);
}
[Test]
public void PerScope_FromParentCreatesDifferentInstancesPerChildScope()
{
using var root = global::FlowScope.FlowScope.Create();
var first = root.CreateScope("First");
var second = root.CreateScope("Second");
root.Register<IDisposableService, DisposableService>().PerScope();
var firstInstance = first.Resolve<IDisposableService>();
var secondInstance = second.Resolve<IDisposableService>();
Assert.That(first.Resolve<IDisposableService>(), Is.SameAs(firstInstance));
Assert.That(second.Resolve<IDisposableService>(), Is.SameAs(secondInstance));
Assert.That(firstInstance, Is.Not.SameAs(secondInstance));
first.Dispose();
Assert.That(firstInstance.IsDisposed, Is.True);
Assert.That(secondInstance.IsDisposed, Is.False);
}
[Test]
public void Dispose_ParentDisposesChildrenBeforeOwnInstancesAndIsIdempotent()
{
DisposeRecorder.Order.Clear();
var root = global::FlowScope.FlowScope.Create();
var child = root.CreateScope("Child");
root.Register<ITracked, RootTracked>().AsSingleton();
child.Register<IChildTracked, ChildTracked>().AsSingleton();
root.Resolve<ITracked>();
child.Resolve<IChildTracked>();
root.Dispose();
root.Dispose();
CollectionAssert.AreEqual(new[] { "child", "root" }, DisposeRecorder.Order);
}
[Test]
public void RegisterInstance_DefaultDoesNotDisposeExternalInstance()
{
var root = global::FlowScope.FlowScope.Create();
var service = new DisposableService();
root.RegisterInstance<IDisposableService>(service);
root.Dispose();
Assert.That(service.IsDisposed, Is.False);
}
private interface IDisposableService
{
bool IsDisposed { get; }
}
private sealed class DisposableService : IDisposableService, System.IDisposable
{
public bool IsDisposed { get; private set; }
public void Dispose() => IsDisposed = true;
}
private interface ITracked { }
private interface IChildTracked { }
private static class DisposeRecorder
{
public static readonly System.Collections.Generic.List<string> Order =
new System.Collections.Generic.List<string>();
}
private sealed class RootTracked : ITracked, System.IDisposable
{
public void Dispose() => DisposeRecorder.Order.Add("root");
}
private sealed class ChildTracked : IChildTracked, System.IDisposable
{
public void Dispose() => DisposeRecorder.Order.Add("child");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7e708e4fc0c860b46b4c51d589dca9ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,114 @@
using System;
using NUnit.Framework;
namespace FlowScope.Tests.EditMode.Core
{
public sealed class ArchitectureScopeFeatureTests
{
[Test]
public void CreateFeature_UsesFeatureTypeNameAndCallsInstall()
{
using var root = global::FlowScope.FlowScope.Create();
using var feature = root.CreateFeature<BossFightFeature>();
Assert.That(feature.Name, Is.EqualTo(nameof(BossFightFeature)));
Assert.That(feature.Path, Is.EqualTo("Root/BossFightFeature"));
Assert.That(feature.Resolve<BossFightModel>(), Is.Not.Null);
}
[Test]
public void CreateFeature_WithNameUsesExplicitName()
{
using var root = global::FlowScope.FlowScope.Create();
using var feature = root.CreateFeature<BossFightFeature>("BossFight");
Assert.That(feature.Name, Is.EqualTo("BossFight"));
Assert.That(feature.Resolve<BossFightModel>(), Is.Not.Null);
}
[Test]
public void CreateFeature_WhenInstallFailsDisposesCreatedScope()
{
using var root = global::FlowScope.FlowScope.Create();
Assert.Throws<InvalidOperationException>(() => root.CreateFeature<BrokenFeature>());
using var feature = root.CreateFeature<BossFightFeature>(nameof(BrokenFeature));
Assert.That(feature.Name, Is.EqualTo(nameof(BrokenFeature)));
}
[Test]
public void FeatureScope_CanResolveRootServices()
{
using var root = global::FlowScope.FlowScope.Create();
var clock = new RootClock(10);
root.RegisterInstance<IRootClock>(clock);
using var feature = root.CreateFeature<ClockFeature>();
Assert.That(feature.Resolve<IRootClock>(), Is.SameAs(clock));
Assert.That(feature.Resolve<ClockModel>().Value, Is.EqualTo(10));
}
[Test]
public void TwoFeatureScopes_DoNotSharePerScopeFeatureServices()
{
using var root = global::FlowScope.FlowScope.Create();
root.Register<IFeatureService, FeatureService>().PerScope();
using var first = root.CreateFeature<EmptyFeature>("FirstActivity");
using var second = root.CreateFeature<EmptyFeature>("SecondActivity");
Assert.That(first.Resolve<IFeatureService>(), Is.SameAs(first.Resolve<IFeatureService>()));
Assert.That(first.Resolve<IFeatureService>(), Is.Not.SameAs(second.Resolve<IFeatureService>()));
}
private sealed class BossFightFeature : IFeature
{
public void Install(IArchitectureScope scope)
{
scope.Register<BossFightModel>().PerScope();
}
}
private sealed class BrokenFeature : IFeature
{
public void Install(IArchitectureScope scope)
{
throw new InvalidOperationException("install failed");
}
}
private sealed class BossFightModel { }
private interface IRootClock { int Value { get; } }
private sealed class RootClock : IRootClock
{
public RootClock(int value) => Value = value;
public int Value { get; }
}
private sealed class ClockFeature : IFeature
{
public void Install(IArchitectureScope scope)
{
scope.Register<ClockModel>().PerScope();
}
}
private sealed class ClockModel
{
public ClockModel(IRootClock clock) => Value = clock.Value;
public int Value { get; }
}
private interface IFeatureService { }
private sealed class FeatureService : IFeatureService { }
private sealed class EmptyFeature : IFeature
{
public void Install(IArchitectureScope scope) { }
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8c5d54b1595c9e4488ca5e4558e87b60
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,46 @@
using System;
using NUnit.Framework;
namespace FlowScope.Tests.EditMode.Core
{
public sealed class ArchitectureScopeRegistrationTests
{
[Test]
public void Create_ReturnsRootArchitectureScope()
{
using var root = global::FlowScope.FlowScope.Create();
Assert.That(root.Name, Is.EqualTo("Root"));
Assert.That(root.Parent, Is.Null);
Assert.That(root.Path, Is.EqualTo("Root"));
Assert.That(root.IsDisposed, Is.False);
}
[Test]
public void Register_WritesOnlyCurrentScope()
{
using var root = global::FlowScope.FlowScope.Create();
using var child = root.CreateScope("Child");
root.Register<IService, RootService>().AsSingleton();
child.Register<IService, ChildService>().AsSingleton();
Assert.That(root.Resolve<IService>(), Is.TypeOf<RootService>());
Assert.That(child.Resolve<IService>(), Is.TypeOf<ChildService>());
}
[Test]
public void Register_DuplicateKeyInSameScopeThrows()
{
using var root = global::FlowScope.FlowScope.Create();
root.Register<IService, RootService>().AsSingleton();
Assert.Throws<InvalidOperationException>(() =>
root.Register<IService, ChildService>().AsSingleton());
}
private interface IService { }
private sealed class RootService : IService { }
private sealed class ChildService : IService { }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 853e2ec507a40b743b8c415496908b3a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,116 @@
using System;
using System.Reflection;
using NUnit.Framework;
namespace FlowScope.Tests.EditMode.Core
{
public sealed class ArchitectureScopeResolveTests
{
[Test]
public void CreateScope_CreatesChildWithParentAndPath()
{
using var root = global::FlowScope.FlowScope.Create();
using var feature = root.CreateScope("BossFightFeature");
using var child = feature.CreateScope("RevivePanel");
Assert.That(feature.Parent, Is.SameAs(root));
Assert.That(child.Parent, Is.SameAs(feature));
Assert.That(feature.Path, Is.EqualTo("Root/BossFightFeature"));
Assert.That(child.Path, Is.EqualTo("Root/BossFightFeature/RevivePanel"));
}
[Test]
public void CreateScope_DuplicateSiblingNameThrows()
{
using var root = global::FlowScope.FlowScope.Create();
using var first = root.CreateScope("BossFightFeature");
Assert.Throws<InvalidOperationException>(() => root.CreateScope("BossFightFeature"));
}
[Test]
public void Resolve_FallsBackToParentWhenCurrentMissing()
{
using var root = global::FlowScope.FlowScope.Create();
using var feature = root.CreateScope("BossFightFeature");
var clock = new RootClock(42);
root.RegisterInstance<IRootClock>(clock);
Assert.That(feature.Resolve<IRootClock>(), Is.SameAs(clock));
}
[Test]
public void Resolve_ChildRegistrationOverridesParent()
{
using var root = global::FlowScope.FlowScope.Create();
using var feature = root.CreateScope("BossFightFeature");
root.Register<IRewardService, GlobalRewardService>().AsSingleton();
feature.Register<IRewardService, BossRewardService>().AsSingleton();
Assert.That(root.Resolve<IRewardService>(), Is.TypeOf<GlobalRewardService>());
Assert.That(feature.Resolve<IRewardService>(), Is.TypeOf<BossRewardService>());
}
[Test]
public void TryResolve_MissingRegistrationReturnsFalse()
{
using var root = global::FlowScope.FlowScope.Create();
Assert.That(root.TryResolve<IRewardService>(out var value), Is.False);
Assert.That(value, Is.Null);
}
[Test]
public void TryResolve_ConstructorFailureThrows()
{
using var root = global::FlowScope.FlowScope.Create();
root.Register<BrokenService>().AsTransient();
Assert.Throws<TargetInvocationException>(() =>
root.TryResolve<BrokenService>(out _));
}
[Test]
public void NamedRegistration_ResolvesByTypeAndNameFromFeature()
{
using var root = global::FlowScope.FlowScope.Create();
root.Register<IRewardService, BossRewardService>("boss").PerScope();
root.Register<IRewardService, ScratchRewardService>("scratch").PerScope();
using var boss = root.CreateFeature<EmptyFeature>("BossFight");
using var scratch = root.CreateFeature<EmptyFeature>("ScratchTicket");
Assert.That(boss.Resolve<IRewardService>("boss"), Is.TypeOf<BossRewardService>());
Assert.That(scratch.Resolve<IRewardService>("scratch"), Is.TypeOf<ScratchRewardService>());
Assert.That(boss.Resolve<IRewardService>("boss"), Is.SameAs(boss.Resolve<IRewardService>("boss")));
Assert.That(boss.Resolve<IRewardService>("boss"), Is.Not.SameAs(scratch.Resolve<IRewardService>("boss")));
}
private interface IRootClock { int Value { get; } }
private sealed class RootClock : IRootClock
{
public RootClock(int value) => Value = value;
public int Value { get; }
}
private interface IRewardService { }
private sealed class GlobalRewardService : IRewardService { }
private sealed class BossRewardService : IRewardService { }
private sealed class ScratchRewardService : IRewardService { }
private sealed class EmptyFeature : IFeature
{
public void Install(IArchitectureScope scope) { }
}
private sealed class BrokenService
{
public BrokenService()
{
throw new InvalidOperationException("broken");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0723a2e5a83c0c04bb6ba5aee064485a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0bea5f9c12b040369bc322e19d230e50
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,246 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
using FlowScope.Container;
namespace FlowScope.Tests.EditMode.Container
{
public sealed class ContainerTests
{
[Test]
public void RegisterInstance_Resolve_ReturnsSameInstanceAndDoesNotDisposeExternalInstance()
{
var container = new global::FlowScope.Container.Container();
var service = new DisposableService();
container.RegisterInstance<IService>(service);
Assert.AreSame(service, container.Resolve<IService>());
Assert.IsTrue(container.TryResolve<IService>(out var resolved));
Assert.AreSame(service, resolved);
container.Dispose();
Assert.IsFalse(service.IsDisposed);
}
[Test]
public void RegisterFactory_Resolve_CreatesOnceAndDisposesInReverseCreationOrder()
{
var disposeOrder = new List<string>();
var container = new global::FlowScope.Container.Container();
container.RegisterFactory<FirstDisposable>(_ => new FirstDisposable(disposeOrder));
container.RegisterFactory<SecondDisposable>(_ => new SecondDisposable(disposeOrder));
var first = container.Resolve<FirstDisposable>();
var second = container.Resolve<SecondDisposable>();
Assert.AreSame(first, container.Resolve<FirstDisposable>());
Assert.AreSame(second, container.Resolve<SecondDisposable>());
container.Dispose();
container.Dispose();
CollectionAssert.AreEqual(new[] { "second", "first" }, disposeOrder);
}
[Test]
public void CreateScope_Resolve_ReadsParentAndCanOverrideWithoutPollutingParent()
{
var parent = new global::FlowScope.Container.Container();
var parentService = new NamedService("parent");
var childService = new NamedService("child");
parent.RegisterInstance<IService>(parentService);
var child = parent.CreateScope();
Assert.AreSame(parentService, child.Resolve<IService>());
child.RegisterInstance<IService>(childService);
Assert.AreSame(childService, child.Resolve<IService>());
Assert.AreSame(parentService, parent.Resolve<IService>());
}
[Test]
public void RegisterBuilder_AsSingleton_ReusesOneRootOwnedInstanceAcrossScopes()
{
var parent = new global::FlowScope.Container.Container();
parent.Register<IService, DisposableService>().AsSingleton();
var firstScope = parent.CreateScope("first");
var secondScope = parent.CreateScope("second");
var rootResolved = parent.Resolve<IService>();
var firstResolved = firstScope.Resolve<IService>();
var secondResolved = secondScope.Resolve<IService>();
Assert.AreSame(rootResolved, firstResolved);
Assert.AreSame(rootResolved, secondResolved);
firstScope.Dispose();
Assert.IsFalse(((DisposableService)rootResolved).IsDisposed);
parent.Dispose();
Assert.IsTrue(((DisposableService)rootResolved).IsDisposed);
}
[Test]
public void RegisterBuilder_PerScope_CreatesOneInstancePerResolvingScope()
{
var parent = new global::FlowScope.Container.Container();
parent.Register<IService, DisposableService>().PerScope();
var firstScope = parent.CreateScope("first");
var secondScope = parent.CreateScope("second");
var firstResolved = firstScope.Resolve<IService>();
var secondResolved = secondScope.Resolve<IService>();
Assert.AreSame(firstResolved, firstScope.Resolve<IService>());
Assert.AreSame(secondResolved, secondScope.Resolve<IService>());
Assert.AreNotSame(firstResolved, secondResolved);
firstScope.Dispose();
Assert.IsTrue(((DisposableService)firstResolved).IsDisposed);
Assert.IsFalse(((DisposableService)secondResolved).IsDisposed);
}
[Test]
public void RegisterBuilder_AsTransient_CreatesNewInstanceOnEveryResolve()
{
var container = new global::FlowScope.Container.Container();
container.Register<IService, DisposableService>().AsTransient();
var first = container.Resolve<IService>();
var second = container.Resolve<IService>();
Assert.AreNotSame(first, second);
container.Dispose();
Assert.IsTrue(((DisposableService)first).IsDisposed);
Assert.IsTrue(((DisposableService)second).IsDisposed);
}
[Test]
public void NamedRegistration_ResolvesByTypeAndName()
{
var container = new global::FlowScope.Container.Container();
container.Register<IService, NamedService>("boss").AsSingleton();
container.Register<IService, OtherNamedService>("scratch").AsSingleton();
Assert.AreEqual("named", container.Resolve<IService>("boss").Name);
Assert.AreEqual("other", container.Resolve<IService>("scratch").Name);
Assert.Throws<InvalidOperationException>(() => container.Resolve<IService>());
}
[Test]
public void RegisterInstance_WhenSameScopeAlreadyRegistered_Throws()
{
var container = new global::FlowScope.Container.Container();
container.RegisterInstance<IService>(new NamedService("first"));
var ex = Assert.Throws<InvalidOperationException>(
() => container.RegisterInstance<IService>(new NamedService("second")));
StringAssert.Contains("already registered", ex.Message);
}
[Test]
public void Dispose_ParentDisposesChildScopesBeforeOwnInstances()
{
var disposeOrder = new List<string>();
var parent = new global::FlowScope.Container.Container();
parent.RegisterFactory<FirstDisposable>(_ => new FirstDisposable(disposeOrder));
var child = parent.CreateScope();
child.RegisterFactory<SecondDisposable>(_ => new SecondDisposable(disposeOrder));
parent.Resolve<FirstDisposable>();
child.Resolve<SecondDisposable>();
parent.Dispose();
CollectionAssert.AreEqual(new[] { "second", "first" }, disposeOrder);
}
[Test]
public void Resolve_WhenDependencyCycleExists_ThrowsWithCompleteDependencyChain()
{
var container = new global::FlowScope.Container.Container();
container.Register<CycleA>().AsSingleton();
container.Register<CycleB>().AsSingleton();
var ex = Assert.Throws<InvalidOperationException>(() => container.Resolve<CycleA>());
StringAssert.Contains(nameof(CycleA), ex.Message);
StringAssert.Contains(nameof(CycleB), ex.Message);
StringAssert.Contains($"{nameof(CycleA)} -> {nameof(CycleB)} -> {nameof(CycleA)}", ex.Message);
}
[Test]
public void DisposedContainer_RegisterAndResolveThrowObjectDisposedException()
{
var container = new global::FlowScope.Container.Container();
container.Dispose();
Assert.Throws<ObjectDisposedException>(() => container.RegisterInstance<IService>(new NamedService("late")));
Assert.Throws<ObjectDisposedException>(() => container.RegisterFactory<IService>(_ => new NamedService("late")));
Assert.Throws<ObjectDisposedException>(() => container.Resolve<IService>());
Assert.Throws<ObjectDisposedException>(() => container.CreateScope());
}
private interface IService
{
string Name { get; }
}
private sealed class NamedService : IService
{
public NamedService()
: this("named")
{
}
public NamedService(string name) => Name = name;
public string Name { get; }
}
private sealed class OtherNamedService : IService
{
public string Name => "other";
}
private sealed class DisposableService : IService, IDisposable
{
public string Name => "disposable";
public bool IsDisposed { get; private set; }
public void Dispose() => IsDisposed = true;
}
private sealed class FirstDisposable : IDisposable
{
private readonly List<string> _disposeOrder;
public FirstDisposable(List<string> disposeOrder) => _disposeOrder = disposeOrder;
public void Dispose() => _disposeOrder.Add("first");
}
private sealed class SecondDisposable : IDisposable
{
private readonly List<string> _disposeOrder;
public SecondDisposable(List<string> disposeOrder) => _disposeOrder = disposeOrder;
public void Dispose() => _disposeOrder.Add("second");
}
private sealed class CycleA
{
public CycleA(CycleB dependency) => Dependency = dependency;
public CycleB Dependency { get; }
}
private sealed class CycleB
{
public CycleB(CycleA dependency) => Dependency = dependency;
public CycleA Dependency { get; }
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d5d96cf26ce24c8988c911eb9e73f1fb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
{
"name": "FlowScope.Tests.EditMode",
"rootNamespace": "FlowScope.Tests",
"references": [
"FlowScope.Runtime"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"optionalUnityReferences": [
"TestAssemblies"
],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4292f95f100145edb6c1ebc8e206eb59
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 33170343ec80b764ea01c486eb60f2bc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,267 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 705507994}
m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_FinalGather: 0
m_FinalGatherFiltering: 1
m_FinalGatherRayCount: 256
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 500
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 500
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 2
m_PVRDenoiserTypeDirect: 0
m_PVRDenoiserTypeIndirect: 0
m_PVRDenoiserTypeAO: 0
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 0
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
m_PVRFilteringGaussRadiusAO: 2
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 2
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
accuratePlacement: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &705507993
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 705507995}
- component: {fileID: 705507994}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!108 &705507994
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 705507993}
m_Enabled: 1
serializedVersion: 8
m_Type: 1
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_CookieSize: 10
m_Shadows:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
m_RenderMode: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_Lightmapping: 1
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 6570
m_UseColorTemperature: 0
m_ShadowRadius: 0
m_ShadowAngle: 0
--- !u!4 &705507995
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 705507993}
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
--- !u!1 &963194225
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 963194228}
- component: {fileID: 963194227}
- component: {fileID: 963194226}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!81 &963194226
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 963194225}
m_Enabled: 1
--- !u!20 &963194227
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 963194225}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_GateFitMode: 2
m_FocalLength: 50
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &963194228
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 963194225}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9fc0d4010bbf28b4594072e72b8655ab
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,53 @@
{
"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.unity.collab-proxy": "2.12.4",
"com.unity.feature.development": "1.0.1",
"com.unity.addressables": "1.21.21",
"com.unity.textmeshpro": "3.0.7",
"com.unity.timeline": "1.7.7",
"com.unity.ugui": "1.0.0",
"com.unity.visualscripting": "1.9.4",
"com.unity.modules.ai": "1.0.0",
"com.unity.modules.androidjni": "1.0.0",
"com.unity.modules.animation": "1.0.0",
"com.unity.modules.assetbundle": "1.0.0",
"com.unity.modules.audio": "1.0.0",
"com.unity.modules.cloth": "1.0.0",
"com.unity.modules.director": "1.0.0",
"com.unity.modules.imageconversion": "1.0.0",
"com.unity.modules.imgui": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0",
"com.unity.modules.particlesystem": "1.0.0",
"com.unity.modules.physics": "1.0.0",
"com.unity.modules.physics2d": "1.0.0",
"com.unity.modules.screencapture": "1.0.0",
"com.unity.modules.terrain": "1.0.0",
"com.unity.modules.terrainphysics": "1.0.0",
"com.unity.modules.tilemap": "1.0.0",
"com.unity.modules.ui": "1.0.0",
"com.unity.modules.uielements": "1.0.0",
"com.unity.modules.umbra": "1.0.0",
"com.unity.modules.unityanalytics": "1.0.0",
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.modules.unitywebrequestassetbundle": "1.0.0",
"com.unity.modules.unitywebrequestaudio": "1.0.0",
"com.unity.modules.unitywebrequesttexture": "1.0.0",
"com.unity.modules.unitywebrequestwww": "1.0.0",
"com.unity.modules.vehicles": "1.0.0",
"com.unity.modules.video": "1.0.0",
"com.unity.modules.vr": "1.0.0",
"com.unity.modules.wind": "1.0.0",
"com.unity.modules.xr": "1.0.0",
"org.nuget.r3": "1.3.0"
}
}

View File

@@ -0,0 +1,501 @@
{
"dependencies": {
"com.cysharp.r3": {
"version": "https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity#1.3.0",
"depth": 0,
"source": "git",
"dependencies": {
"com.unity.modules.imgui": "1.0.0"
},
"hash": "329ca7915713417f2e0837b0e0a80b4da074db4a"
},
"com.unity.addressables": {
"version": "1.21.21",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.scriptablebuildpipeline": "1.21.23",
"com.unity.modules.assetbundle": "1.0.0",
"com.unity.modules.imageconversion": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0",
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.modules.unitywebrequestassetbundle": "1.0.0"
},
"url": "https://packages.unity.cn"
},
"com.unity.collab-proxy": {
"version": "2.12.4",
"depth": 0,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.cn"
},
"com.unity.editorcoroutines": {
"version": "1.0.0",
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.cn"
},
"com.unity.ext.nunit": {
"version": "1.0.6",
"depth": 2,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.cn"
},
"com.unity.feature.development": {
"version": "1.0.1",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.ide.visualstudio": "2.0.22",
"com.unity.ide.rider": "3.0.36",
"com.unity.ide.vscode": "1.2.5",
"com.unity.editorcoroutines": "1.0.0",
"com.unity.performance.profile-analyzer": "1.2.3",
"com.unity.test-framework": "1.1.33",
"com.unity.testtools.codecoverage": "1.2.6"
}
},
"com.unity.ide.rider": {
"version": "3.0.36",
"depth": 1,
"source": "registry",
"dependencies": {
"com.unity.ext.nunit": "1.0.6"
},
"url": "https://packages.unity.cn"
},
"com.unity.ide.visualstudio": {
"version": "2.0.22",
"depth": 1,
"source": "registry",
"dependencies": {
"com.unity.test-framework": "1.1.9"
},
"url": "https://packages.unity.cn"
},
"com.unity.ide.vscode": {
"version": "1.2.5",
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.cn"
},
"com.unity.performance.profile-analyzer": {
"version": "1.2.3",
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.cn"
},
"com.unity.scriptablebuildpipeline": {
"version": "1.21.25",
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.cn"
},
"com.unity.settings-manager": {
"version": "2.1.0",
"depth": 2,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.cn"
},
"com.unity.test-framework": {
"version": "1.1.33",
"depth": 1,
"source": "registry",
"dependencies": {
"com.unity.ext.nunit": "1.0.6",
"com.unity.modules.imgui": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0"
},
"url": "https://packages.unity.cn"
},
"com.unity.testtools.codecoverage": {
"version": "1.2.6",
"depth": 1,
"source": "registry",
"dependencies": {
"com.unity.test-framework": "1.0.16",
"com.unity.settings-manager": "1.0.1"
},
"url": "https://packages.unity.cn"
},
"com.unity.textmeshpro": {
"version": "3.0.7",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.ugui": "1.0.0"
},
"url": "https://packages.unity.cn"
},
"com.unity.timeline": {
"version": "1.7.7",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.modules.audio": "1.0.0",
"com.unity.modules.director": "1.0.0",
"com.unity.modules.animation": "1.0.0",
"com.unity.modules.particlesystem": "1.0.0"
},
"url": "https://packages.unity.cn"
},
"com.unity.ugui": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.ui": "1.0.0",
"com.unity.modules.imgui": "1.0.0"
}
},
"com.unity.visualscripting": {
"version": "1.9.4",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.ugui": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0"
},
"url": "https://packages.unity.cn"
},
"org.nuget.microsoft.bcl.asyncinterfaces": {
"version": "6.0.0",
"depth": 2,
"source": "registry",
"dependencies": {
"org.nuget.system.threading.tasks.extensions": "4.5.4"
},
"url": "https://unitynuget-registry.openupm.com"
},
"org.nuget.microsoft.bcl.timeprovider": {
"version": "8.0.0",
"depth": 1,
"source": "registry",
"dependencies": {
"org.nuget.microsoft.bcl.asyncinterfaces": "6.0.0"
},
"url": "https://unitynuget-registry.openupm.com"
},
"org.nuget.r3": {
"version": "1.3.0",
"depth": 0,
"source": "registry",
"dependencies": {
"org.nuget.microsoft.bcl.timeprovider": "8.0.0",
"org.nuget.system.buffers": "4.5.1",
"org.nuget.system.componentmodel.annotations": "5.0.0",
"org.nuget.system.memory": "4.5.5",
"org.nuget.system.runtime.compilerservices.unsafe": "6.0.0",
"org.nuget.system.threading.channels": "8.0.0"
},
"url": "https://unitynuget-registry.openupm.com"
},
"org.nuget.system.buffers": {
"version": "4.5.1",
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://unitynuget-registry.openupm.com"
},
"org.nuget.system.componentmodel.annotations": {
"version": "5.0.0",
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://unitynuget-registry.openupm.com"
},
"org.nuget.system.memory": {
"version": "4.5.5",
"depth": 1,
"source": "registry",
"dependencies": {
"org.nuget.system.buffers": "4.5.1",
"org.nuget.system.numerics.vectors": "4.4.0",
"org.nuget.system.runtime.compilerservices.unsafe": "4.5.3"
},
"url": "https://unitynuget-registry.openupm.com"
},
"org.nuget.system.numerics.vectors": {
"version": "4.4.0",
"depth": 2,
"source": "registry",
"dependencies": {},
"url": "https://unitynuget-registry.openupm.com"
},
"org.nuget.system.runtime.compilerservices.unsafe": {
"version": "6.0.0",
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://unitynuget-registry.openupm.com"
},
"org.nuget.system.threading.channels": {
"version": "8.0.0",
"depth": 1,
"source": "registry",
"dependencies": {
"org.nuget.system.threading.tasks.extensions": "4.5.4"
},
"url": "https://unitynuget-registry.openupm.com"
},
"org.nuget.system.threading.tasks.extensions": {
"version": "4.5.4",
"depth": 2,
"source": "registry",
"dependencies": {
"org.nuget.system.runtime.compilerservices.unsafe": "4.5.3"
},
"url": "https://unitynuget-registry.openupm.com"
},
"com.unity.modules.ai": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.androidjni": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.animation": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.assetbundle": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.audio": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.cloth": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.physics": "1.0.0"
}
},
"com.unity.modules.director": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.audio": "1.0.0",
"com.unity.modules.animation": "1.0.0"
}
},
"com.unity.modules.imageconversion": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.imgui": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.jsonserialize": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.particlesystem": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.physics": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.physics2d": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.screencapture": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.imageconversion": "1.0.0"
}
},
"com.unity.modules.subsystems": {
"version": "1.0.0",
"depth": 1,
"source": "builtin",
"dependencies": {
"com.unity.modules.jsonserialize": "1.0.0"
}
},
"com.unity.modules.terrain": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.terrainphysics": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.physics": "1.0.0",
"com.unity.modules.terrain": "1.0.0"
}
},
"com.unity.modules.tilemap": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.physics2d": "1.0.0"
}
},
"com.unity.modules.ui": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.uielements": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.ui": "1.0.0",
"com.unity.modules.imgui": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0"
}
},
"com.unity.modules.umbra": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.unityanalytics": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0"
}
},
"com.unity.modules.unitywebrequest": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.unitywebrequestassetbundle": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.assetbundle": "1.0.0",
"com.unity.modules.unitywebrequest": "1.0.0"
}
},
"com.unity.modules.unitywebrequestaudio": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.modules.audio": "1.0.0"
}
},
"com.unity.modules.unitywebrequesttexture": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.modules.imageconversion": "1.0.0"
}
},
"com.unity.modules.unitywebrequestwww": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.unitywebrequest": "1.0.0",
"com.unity.modules.unitywebrequestassetbundle": "1.0.0",
"com.unity.modules.unitywebrequestaudio": "1.0.0",
"com.unity.modules.audio": "1.0.0",
"com.unity.modules.assetbundle": "1.0.0",
"com.unity.modules.imageconversion": "1.0.0"
}
},
"com.unity.modules.vehicles": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.physics": "1.0.0"
}
},
"com.unity.modules.video": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.audio": "1.0.0",
"com.unity.modules.ui": "1.0.0",
"com.unity.modules.unitywebrequest": "1.0.0"
}
},
"com.unity.modules.vr": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.jsonserialize": "1.0.0",
"com.unity.modules.physics": "1.0.0",
"com.unity.modules.xr": "1.0.0"
}
},
"com.unity.modules.wind": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {}
},
"com.unity.modules.xr": {
"version": "1.0.0",
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.modules.physics": "1.0.0",
"com.unity.modules.jsonserialize": "1.0.0",
"com.unity.modules.subsystems": "1.0.0"
}
}
}
}

View File

@@ -0,0 +1,19 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!11 &1
AudioManager:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Volume: 1
Rolloff Scale: 1
Doppler Factor: 1
Default Speaker Mode: 2
m_SampleRate: 0
m_DSPBufferSize: 1024
m_VirtualVoiceCount: 512
m_RealVoiceCount: 32
m_SpatializerPlugin:
m_AmbisonicDecoderPlugin:
m_DisableAudio: 0
m_VirtualizeEffects: 1
m_RequestedDSPBufferSize: 1024

View File

@@ -0,0 +1,6 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!236 &1
ClusterInputManager:
m_ObjectHideFlags: 0
m_Inputs: []

View File

@@ -0,0 +1,34 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!55 &1
PhysicsManager:
m_ObjectHideFlags: 0
serializedVersion: 11
m_Gravity: {x: 0, y: -9.81, z: 0}
m_DefaultMaterial: {fileID: 0}
m_BounceThreshold: 2
m_SleepThreshold: 0.005
m_DefaultContactOffset: 0.01
m_DefaultSolverIterations: 6
m_DefaultSolverVelocityIterations: 1
m_QueriesHitBackfaces: 0
m_QueriesHitTriggers: 1
m_EnableAdaptiveForce: 0
m_ClothInterCollisionDistance: 0
m_ClothInterCollisionStiffness: 0
m_ContactsGeneration: 1
m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
m_AutoSimulation: 1
m_AutoSyncTransforms: 0
m_ReuseCollisionCallbacks: 1
m_ClothInterCollisionSettingsToggle: 0
m_ContactPairsMode: 0
m_BroadphaseType: 0
m_WorldBounds:
m_Center: {x: 0, y: 0, z: 0}
m_Extent: {x: 250, y: 250, z: 250}
m_WorldSubdivisions: 8
m_FrictionType: 0
m_EnableEnhancedDeterminism: 0
m_EnableUnifiedHeightmaps: 1
m_DefaultMaxAngluarSpeed: 7

View File

@@ -0,0 +1,8 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1045 &1
EditorBuildSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Scenes: []
m_configObjects: {}

View File

@@ -0,0 +1,30 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!159 &1
EditorSettings:
m_ObjectHideFlags: 0
serializedVersion: 11
m_ExternalVersionControlSupport: Visible Meta Files
m_SerializationMode: 2
m_LineEndingsForNewScripts: 0
m_DefaultBehaviorMode: 0
m_PrefabRegularEnvironment: {fileID: 0}
m_PrefabUIEnvironment: {fileID: 0}
m_SpritePackerMode: 0
m_SpritePackerPaddingPower: 1
m_EtcTextureCompressorBehavior: 1
m_EtcTextureFastCompressor: 1
m_EtcTextureNormalCompressor: 2
m_EtcTextureBestCompressor: 4
m_ProjectGenerationIncludedExtensions: txt;xml;fnt;cd;asmdef;rsp;asmref
m_ProjectGenerationRootNamespace:
m_CollabEditorSettings:
inProgressEnabled: 1
m_EnableTextureStreamingInEditMode: 1
m_EnableTextureStreamingInPlayMode: 1
m_AsyncShaderCompilation: 1
m_EnterPlayModeOptionsEnabled: 0
m_EnterPlayModeOptions: 3
m_ShowLightmapResolutionOverlay: 1
m_UseLegacyProbeSampleCount: 0
m_SerializeInlineMappingsOnOneLine: 1

View File

@@ -0,0 +1,63 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!30 &1
GraphicsSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_Deferred:
m_Mode: 1
m_Shader: {fileID: 69, guid: 0000000000000000f000000000000000, type: 0}
m_DeferredReflections:
m_Mode: 1
m_Shader: {fileID: 74, guid: 0000000000000000f000000000000000, type: 0}
m_ScreenSpaceShadows:
m_Mode: 1
m_Shader: {fileID: 64, guid: 0000000000000000f000000000000000, type: 0}
m_LegacyDeferred:
m_Mode: 1
m_Shader: {fileID: 63, guid: 0000000000000000f000000000000000, type: 0}
m_DepthNormals:
m_Mode: 1
m_Shader: {fileID: 62, guid: 0000000000000000f000000000000000, type: 0}
m_MotionVectors:
m_Mode: 1
m_Shader: {fileID: 75, guid: 0000000000000000f000000000000000, type: 0}
m_LightHalo:
m_Mode: 1
m_Shader: {fileID: 105, guid: 0000000000000000f000000000000000, type: 0}
m_LensFlare:
m_Mode: 1
m_Shader: {fileID: 102, guid: 0000000000000000f000000000000000, type: 0}
m_AlwaysIncludedShaders:
- {fileID: 7, guid: 0000000000000000f000000000000000, type: 0}
- {fileID: 15104, guid: 0000000000000000f000000000000000, type: 0}
- {fileID: 15105, guid: 0000000000000000f000000000000000, type: 0}
- {fileID: 15106, guid: 0000000000000000f000000000000000, type: 0}
- {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0}
- {fileID: 10770, guid: 0000000000000000f000000000000000, type: 0}
m_PreloadedShaders: []
m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000,
type: 0}
m_CustomRenderPipeline: {fileID: 0}
m_TransparencySortMode: 0
m_TransparencySortAxis: {x: 0, y: 0, z: 1}
m_DefaultRenderingPath: 1
m_DefaultMobileRenderingPath: 1
m_TierSettings: []
m_LightmapStripping: 0
m_FogStripping: 0
m_InstancingStripping: 0
m_LightmapKeepPlain: 1
m_LightmapKeepDirCombined: 1
m_LightmapKeepDynamicPlain: 1
m_LightmapKeepDynamicDirCombined: 1
m_LightmapKeepShadowMask: 1
m_LightmapKeepSubtractive: 1
m_FogKeepLinear: 1
m_FogKeepExp: 1
m_FogKeepExp2: 1
m_AlbedoSwatchInfos: []
m_LightsUseLinearIntensity: 0
m_LightsUseColorTemperature: 0
m_LogWhenShaderIsCompiled: 0
m_AllowEnlightenSupportForUpgradedProject: 0

View File

@@ -0,0 +1,295 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!13 &1
InputManager:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Axes:
- serializedVersion: 3
m_Name: Horizontal
descriptiveName:
descriptiveNegativeName:
negativeButton: left
positiveButton: right
altNegativeButton: a
altPositiveButton: d
gravity: 3
dead: 0.001
sensitivity: 3
snap: 1
invert: 0
type: 0
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Vertical
descriptiveName:
descriptiveNegativeName:
negativeButton: down
positiveButton: up
altNegativeButton: s
altPositiveButton: w
gravity: 3
dead: 0.001
sensitivity: 3
snap: 1
invert: 0
type: 0
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Fire1
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton: left ctrl
altNegativeButton:
altPositiveButton: mouse 0
gravity: 1000
dead: 0.001
sensitivity: 1000
snap: 0
invert: 0
type: 0
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Fire2
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton: left alt
altNegativeButton:
altPositiveButton: mouse 1
gravity: 1000
dead: 0.001
sensitivity: 1000
snap: 0
invert: 0
type: 0
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Fire3
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton: left shift
altNegativeButton:
altPositiveButton: mouse 2
gravity: 1000
dead: 0.001
sensitivity: 1000
snap: 0
invert: 0
type: 0
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Jump
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton: space
altNegativeButton:
altPositiveButton:
gravity: 1000
dead: 0.001
sensitivity: 1000
snap: 0
invert: 0
type: 0
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Mouse X
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton:
altNegativeButton:
altPositiveButton:
gravity: 0
dead: 0
sensitivity: 0.1
snap: 0
invert: 0
type: 1
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Mouse Y
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton:
altNegativeButton:
altPositiveButton:
gravity: 0
dead: 0
sensitivity: 0.1
snap: 0
invert: 0
type: 1
axis: 1
joyNum: 0
- serializedVersion: 3
m_Name: Mouse ScrollWheel
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton:
altNegativeButton:
altPositiveButton:
gravity: 0
dead: 0
sensitivity: 0.1
snap: 0
invert: 0
type: 1
axis: 2
joyNum: 0
- serializedVersion: 3
m_Name: Horizontal
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton:
altNegativeButton:
altPositiveButton:
gravity: 0
dead: 0.19
sensitivity: 1
snap: 0
invert: 0
type: 2
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Vertical
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton:
altNegativeButton:
altPositiveButton:
gravity: 0
dead: 0.19
sensitivity: 1
snap: 0
invert: 1
type: 2
axis: 1
joyNum: 0
- serializedVersion: 3
m_Name: Fire1
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton: joystick button 0
altNegativeButton:
altPositiveButton:
gravity: 1000
dead: 0.001
sensitivity: 1000
snap: 0
invert: 0
type: 0
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Fire2
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton: joystick button 1
altNegativeButton:
altPositiveButton:
gravity: 1000
dead: 0.001
sensitivity: 1000
snap: 0
invert: 0
type: 0
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Fire3
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton: joystick button 2
altNegativeButton:
altPositiveButton:
gravity: 1000
dead: 0.001
sensitivity: 1000
snap: 0
invert: 0
type: 0
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Jump
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton: joystick button 3
altNegativeButton:
altPositiveButton:
gravity: 1000
dead: 0.001
sensitivity: 1000
snap: 0
invert: 0
type: 0
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Submit
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton: return
altNegativeButton:
altPositiveButton: joystick button 0
gravity: 1000
dead: 0.001
sensitivity: 1000
snap: 0
invert: 0
type: 0
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Submit
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton: enter
altNegativeButton:
altPositiveButton: space
gravity: 1000
dead: 0.001
sensitivity: 1000
snap: 0
invert: 0
type: 0
axis: 0
joyNum: 0
- serializedVersion: 3
m_Name: Cancel
descriptiveName:
descriptiveNegativeName:
negativeButton:
positiveButton: escape
altNegativeButton:
altPositiveButton: joystick button 1
gravity: 1000
dead: 0.001
sensitivity: 1000
snap: 0
invert: 0
type: 0
axis: 0
joyNum: 0

View File

@@ -0,0 +1,35 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!387306366 &1
MemorySettings:
m_ObjectHideFlags: 0
m_EditorMemorySettings:
m_MainAllocatorBlockSize: -1
m_ThreadAllocatorBlockSize: -1
m_MainGfxBlockSize: -1
m_ThreadGfxBlockSize: -1
m_CacheBlockSize: -1
m_TypetreeBlockSize: -1
m_ProfilerBlockSize: -1
m_ProfilerEditorBlockSize: -1
m_BucketAllocatorGranularity: -1
m_BucketAllocatorBucketsCount: -1
m_BucketAllocatorBlockSize: -1
m_BucketAllocatorBlockCount: -1
m_ProfilerBucketAllocatorGranularity: -1
m_ProfilerBucketAllocatorBucketsCount: -1
m_ProfilerBucketAllocatorBlockSize: -1
m_ProfilerBucketAllocatorBlockCount: -1
m_TempAllocatorSizeMain: -1
m_JobTempAllocatorBlockSize: -1
m_BackgroundJobTempAllocatorBlockSize: -1
m_JobTempAllocatorReducedBlockSize: -1
m_TempAllocatorSizeGIBakingWorker: -1
m_TempAllocatorSizeNavMeshWorker: -1
m_TempAllocatorSizeAudioWorker: -1
m_TempAllocatorSizeCloudWorker: -1
m_TempAllocatorSizeGfx: -1
m_TempAllocatorSizeJobWorker: -1
m_TempAllocatorSizeBackgroundWorker: -1
m_TempAllocatorSizePreloadManager: -1
m_PlatformMemorySettings: {}

View File

@@ -0,0 +1,91 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!126 &1
NavMeshProjectSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
areas:
- name: Walkable
cost: 1
- name: Not Walkable
cost: 1
- name: Jump
cost: 2
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
- name:
cost: 1
m_LastAgentTypeID: -887442657
m_Settings:
- serializedVersion: 2
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.75
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
accuratePlacement: 0
debug:
m_Flags: 0
m_SettingNames:
- Humanoid

View File

@@ -0,0 +1,44 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &1
MonoBehaviour:
m_ObjectHideFlags: 61
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0}
m_Name:
m_EditorClassIdentifier:
m_EnablePreReleasePackages: 0
m_AdvancedSettingsExpanded: 1
m_ScopedRegistriesSettingsExpanded: 1
m_SeeAllPackageVersions: 0
m_DismissPreviewPackagesInUse: 0
oneTimeWarningShown: 0
m_Registries:
- m_Id: main
m_Name:
m_Url: https://packages.unity.cn
m_Scopes: []
m_IsDefault: 1
m_Capabilities: 7
m_ConfigSource: 0
- m_Id: scoped:project:Unity NuGet
m_Name: Unity NuGet
m_Url: https://unitynuget-registry.openupm.com
m_Scopes:
- org.nuget
m_IsDefault: 0
m_Capabilities: 0
m_ConfigSource: 4
m_UserSelectedRegistryName: Unity NuGet
m_UserAddingNewScopedRegistry: 0
m_RegistryInfoDraft:
m_Modified: 0
m_ErrorMessage:
m_UserModificationsInstanceId: -834
m_OriginalInstanceId: -836
m_LoadAssets: 0

View File

@@ -0,0 +1,5 @@
{
"m_Dictionary": {
"m_DictionaryValues": []
}
}

View File

@@ -0,0 +1,56 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!19 &1
Physics2DSettings:
m_ObjectHideFlags: 0
serializedVersion: 4
m_Gravity: {x: 0, y: -9.81}
m_DefaultMaterial: {fileID: 0}
m_VelocityIterations: 8
m_PositionIterations: 3
m_VelocityThreshold: 1
m_MaxLinearCorrection: 0.2
m_MaxAngularCorrection: 8
m_MaxTranslationSpeed: 100
m_MaxRotationSpeed: 360
m_BaumgarteScale: 0.2
m_BaumgarteTimeOfImpactScale: 0.75
m_TimeToSleep: 0.5
m_LinearSleepTolerance: 0.01
m_AngularSleepTolerance: 2
m_DefaultContactOffset: 0.01
m_JobOptions:
serializedVersion: 2
useMultithreading: 0
useConsistencySorting: 0
m_InterpolationPosesPerJob: 100
m_NewContactsPerJob: 30
m_CollideContactsPerJob: 100
m_ClearFlagsPerJob: 200
m_ClearBodyForcesPerJob: 200
m_SyncDiscreteFixturesPerJob: 50
m_SyncContinuousFixturesPerJob: 50
m_FindNearestContactsPerJob: 100
m_UpdateTriggerContactsPerJob: 100
m_IslandSolverCostThreshold: 100
m_IslandSolverBodyCostScale: 1
m_IslandSolverContactCostScale: 10
m_IslandSolverJointCostScale: 10
m_IslandSolverBodiesPerJob: 50
m_IslandSolverContactsPerJob: 50
m_AutoSimulation: 1
m_QueriesHitTriggers: 1
m_QueriesStartInColliders: 1
m_CallbacksOnDisable: 1
m_ReuseCollisionCallbacks: 1
m_AutoSyncTransforms: 0
m_AlwaysShowColliders: 0
m_ShowColliderSleep: 1
m_ShowColliderContacts: 0
m_ShowColliderAABB: 0
m_ContactArrowScale: 0.2
m_ColliderAwakeColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.7529412}
m_ColliderAsleepColor: {r: 0.5686275, g: 0.95686275, b: 0.54509807, a: 0.36078432}
m_ColliderContactColor: {r: 1, g: 0, b: 1, a: 0.6862745}
m_ColliderAABBColor: {r: 1, g: 1, b: 0, a: 0.2509804}
m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

View File

@@ -0,0 +1,7 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1386491679 &1
PresetManager:
m_ObjectHideFlags: 0
serializedVersion: 2
m_DefaultPresets: {}

View File

@@ -0,0 +1,961 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!129 &1
PlayerSettings:
m_ObjectHideFlags: 0
serializedVersion: 26
productGUID: 849f03f34461eb7458d452aad37c304a
AndroidProfiler: 0
AndroidFilterTouchesWhenObscured: 0
AndroidEnableSustainedPerformanceMode: 0
defaultScreenOrientation: 4
targetDevice: 2
useOnDemandResources: 0
accelerometerFrequency: 60
companyName: DefaultCompany
productName: My project
defaultCursor: {fileID: 0}
cursorHotspot: {x: 0, y: 0}
m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1}
m_ShowUnitySplashScreen: 1
m_ShowUnitySplashLogo: 1
m_SplashScreenOverlayOpacity: 1
m_SplashScreenAnimation: 1
m_SplashScreenLogoStyle: 1
m_SplashScreenDrawMode: 0
m_SplashScreenBackgroundAnimationZoom: 1
m_SplashScreenLogoAnimationZoom: 1
m_SplashScreenBackgroundLandscapeAspect: 1
m_SplashScreenBackgroundPortraitAspect: 1
m_SplashScreenBackgroundLandscapeUvs:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
m_SplashScreenBackgroundPortraitUvs:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
m_SplashScreenLogos: []
m_VirtualRealitySplashScreen: {fileID: 0}
m_HolographicTrackingLossScreen: {fileID: 0}
defaultScreenWidth: 1920
defaultScreenHeight: 1080
defaultScreenWidthWeb: 960
defaultScreenHeightWeb: 600
m_StereoRenderingPath: 0
m_ActiveColorSpace: 1
unsupportedMSAAFallback: 0
m_SpriteBatchVertexThreshold: 300
m_MTRendering: 1
mipStripping: 0
numberOfMipsStripped: 0
numberOfMipsStrippedPerMipmapLimitGroup: {}
m_StackTraceTypes: 010000000100000001000000010000000100000001000000
iosShowActivityIndicatorOnLoading: -1
androidShowActivityIndicatorOnLoading: -1
iosUseCustomAppBackgroundBehavior: 0
allowedAutorotateToPortrait: 1
allowedAutorotateToPortraitUpsideDown: 1
allowedAutorotateToLandscapeRight: 1
allowedAutorotateToLandscapeLeft: 1
useOSAutorotation: 1
use32BitDisplayBuffer: 1
preserveFramebufferAlpha: 0
disableDepthAndStencilBuffers: 0
androidStartInFullscreen: 1
androidRenderOutsideSafeArea: 1
androidUseSwappy: 1
androidBlitType: 0
androidResizableWindow: 0
androidDefaultWindowWidth: 1920
androidDefaultWindowHeight: 1080
androidMinimumWindowWidth: 400
androidMinimumWindowHeight: 300
androidFullscreenMode: 1
androidAutoRotationBehavior: 1
androidPredictiveBackSupport: 1
defaultIsNativeResolution: 1
macRetinaSupport: 1
runInBackground: 1
captureSingleScreen: 0
muteOtherAudioSources: 0
Prepare IOS For Recording: 0
Force IOS Speakers When Recording: 0
audioSpatialExperience: 0
deferSystemGesturesMode: 0
hideHomeButton: 0
submitAnalytics: 1
usePlayerLog: 1
dedicatedServerOptimizations: 0
bakeCollisionMeshes: 0
forceSingleInstance: 0
useFlipModelSwapchain: 1
resizableWindow: 0
useMacAppStoreValidation: 0
macAppStoreCategory: public.app-category.games
gpuSkinning: 1
xboxPIXTextureCapture: 0
xboxEnableAvatar: 0
xboxEnableKinect: 0
xboxEnableKinectAutoTracking: 0
xboxEnableFitness: 0
visibleInBackground: 1
allowFullscreenSwitch: 1
fullscreenMode: 1
xboxSpeechDB: 0
xboxEnableHeadOrientation: 0
xboxEnableGuest: 0
xboxEnablePIXSampling: 0
metalFramebufferOnly: 0
xboxOneResolution: 0
xboxOneSResolution: 0
xboxOneXResolution: 3
xboxOneMonoLoggingLevel: 0
xboxOneLoggingLevel: 1
xboxOneDisableEsram: 0
xboxOneEnableTypeOptimization: 0
xboxOnePresentImmediateThreshold: 0
switchQueueCommandMemory: 0
switchQueueControlMemory: 16384
switchQueueComputeMemory: 262144
switchNVNShaderPoolsGranularity: 33554432
switchNVNDefaultPoolsGranularity: 16777216
switchNVNOtherPoolsGranularity: 16777216
switchGpuScratchPoolGranularity: 2097152
switchAllowGpuScratchShrinking: 0
switchNVNMaxPublicTextureIDCount: 0
switchNVNMaxPublicSamplerIDCount: 0
switchNVNGraphicsFirmwareMemory: 32
switchMaxWorkerMultiple: 8
stadiaPresentMode: 0
stadiaTargetFramerate: 0
vulkanNumSwapchainBuffers: 3
vulkanEnableSetSRGBWrite: 0
vulkanEnablePreTransform: 1
vulkanEnableLateAcquireNextImage: 0
vulkanEnableCommandBufferRecycling: 1
loadStoreDebugModeEnabled: 0
visionOSBundleVersion: 1.0
tvOSBundleVersion: 1.0
bundleVersion: 0.1
preloadedAssets: []
metroInputSource: 0
wsaTransparentSwapchain: 0
m_HolographicPauseOnTrackingLoss: 1
xboxOneDisableKinectGpuReservation: 1
xboxOneEnable7thCore: 1
vrSettings:
enable360StereoCapture: 0
isWsaHolographicRemotingEnabled: 0
enableFrameTimingStats: 0
enableOpenGLProfilerGPURecorders: 1
allowHDRDisplaySupport: 0
useHDRDisplay: 0
hdrBitDepth: 0
m_ColorGamuts: 00000000
targetPixelDensity: 30
resolutionScalingMode: 0
resetResolutionOnWindowResize: 0
androidSupportedAspectRatio: 1
androidMaxAspectRatio: 2.1
applicationIdentifier: {}
buildNumber:
Standalone: 0
VisionOS: 0
iPhone: 0
tvOS: 0
overrideDefaultApplicationIdentifier: 0
AndroidBundleVersionCode: 1
AndroidMinSdkVersion: 22
AndroidTargetSdkVersion: 0
AndroidPreferredInstallLocation: 1
aotOptions:
stripEngineCode: 1
iPhoneStrippingLevel: 0
iPhoneScriptCallOptimization: 0
ForceInternetPermission: 0
ForceSDCardPermission: 0
CreateWallpaper: 0
APKExpansionFiles: 0
keepLoadedShadersAlive: 0
StripUnusedMeshComponents: 1
strictShaderVariantMatching: 0
VertexChannelCompressionMask: 4054
iPhoneSdkVersion: 988
iOSSimulatorArchitecture: 0
iOSTargetOSVersionString: 12.0
tvOSSdkVersion: 0
tvOSSimulatorArchitecture: 0
tvOSRequireExtendedGameController: 0
tvOSTargetOSVersionString: 12.0
VisionOSSdkVersion: 0
VisionOSTargetOSVersionString: 1.0
uIPrerenderedIcon: 0
uIRequiresPersistentWiFi: 0
uIRequiresFullScreen: 1
uIStatusBarHidden: 1
uIExitOnSuspend: 0
uIStatusBarStyle: 0
appleTVSplashScreen: {fileID: 0}
appleTVSplashScreen2x: {fileID: 0}
tvOSSmallIconLayers: []
tvOSSmallIconLayers2x: []
tvOSLargeIconLayers: []
tvOSLargeIconLayers2x: []
tvOSTopShelfImageLayers: []
tvOSTopShelfImageLayers2x: []
tvOSTopShelfImageWideLayers: []
tvOSTopShelfImageWideLayers2x: []
iOSLaunchScreenType: 0
iOSLaunchScreenPortrait: {fileID: 0}
iOSLaunchScreenLandscape: {fileID: 0}
iOSLaunchScreenBackgroundColor:
serializedVersion: 2
rgba: 0
iOSLaunchScreenFillPct: 100
iOSLaunchScreenSize: 100
iOSLaunchScreenCustomXibPath:
iOSLaunchScreeniPadType: 0
iOSLaunchScreeniPadImage: {fileID: 0}
iOSLaunchScreeniPadBackgroundColor:
serializedVersion: 2
rgba: 0
iOSLaunchScreeniPadFillPct: 100
iOSLaunchScreeniPadSize: 100
iOSLaunchScreeniPadCustomXibPath:
iOSLaunchScreenCustomStoryboardPath:
iOSLaunchScreeniPadCustomStoryboardPath:
iOSDeviceRequirements: []
iOSURLSchemes: []
macOSURLSchemes: []
iOSBackgroundModes: 0
iOSMetalForceHardShadows: 0
metalEditorSupport: 1
metalAPIValidation: 1
metalCompileShaderBinary: 0
iOSRenderExtraFrameOnPause: 0
iosCopyPluginsCodeInsteadOfSymlink: 0
appleDeveloperTeamID:
iOSManualSigningProvisioningProfileID:
tvOSManualSigningProvisioningProfileID:
VisionOSManualSigningProvisioningProfileID:
iOSManualSigningProvisioningProfileType: 0
tvOSManualSigningProvisioningProfileType: 0
VisionOSManualSigningProvisioningProfileType: 0
appleEnableAutomaticSigning: 0
iOSRequireARKit: 0
iOSAutomaticallyDetectAndAddCapabilities: 1
appleEnableProMotion: 0
shaderPrecisionModel: 0
clonedFromGUID: c0afd0d1d80e3634a9dac47e8a0426ea
templatePackageId: com.unity.template.3d@8.1.3
templateDefaultScene: Assets/Scenes/SampleScene.unity
useCustomMainManifest: 0
useCustomLauncherManifest: 0
useCustomMainGradleTemplate: 0
useCustomLauncherGradleManifest: 0
useCustomBaseGradleTemplate: 0
useCustomGradlePropertiesTemplate: 0
useCustomGradleSettingsTemplate: 0
useCustomProguardFile: 0
AndroidTargetArchitectures: 1
AndroidTargetDevices: 0
AndroidSplashScreenScale: 0
androidSplashScreen: {fileID: 0}
AndroidKeystoreName:
AndroidKeyaliasName:
AndroidEnableArmv9SecurityFeatures: 0
AndroidBuildApkPerCpuArchitecture: 0
AndroidTVCompatibility: 0
AndroidIsGame: 1
AndroidEnableTango: 0
androidEnableBanner: 1
androidUseLowAccuracyLocation: 0
androidUseCustomKeystore: 0
m_AndroidBanners:
- width: 320
height: 180
banner: {fileID: 0}
androidGamepadSupportLevel: 0
chromeosInputEmulation: 1
AndroidMinifyRelease: 0
AndroidMinifyDebug: 0
AndroidValidateAppBundleSize: 1
AndroidAppBundleSizeToValidate: 150
m_BuildTargetIcons: []
m_BuildTargetPlatformIcons:
- m_BuildTarget: iPhone
m_Icons:
- m_Textures: []
m_Width: 180
m_Height: 180
m_Kind: 0
m_SubKind: iPhone
- m_Textures: []
m_Width: 120
m_Height: 120
m_Kind: 0
m_SubKind: iPhone
- m_Textures: []
m_Width: 167
m_Height: 167
m_Kind: 0
m_SubKind: iPad
- m_Textures: []
m_Width: 152
m_Height: 152
m_Kind: 0
m_SubKind: iPad
- m_Textures: []
m_Width: 76
m_Height: 76
m_Kind: 0
m_SubKind: iPad
- m_Textures: []
m_Width: 120
m_Height: 120
m_Kind: 3
m_SubKind: iPhone
- m_Textures: []
m_Width: 80
m_Height: 80
m_Kind: 3
m_SubKind: iPhone
- m_Textures: []
m_Width: 80
m_Height: 80
m_Kind: 3
m_SubKind: iPad
- m_Textures: []
m_Width: 40
m_Height: 40
m_Kind: 3
m_SubKind: iPad
- m_Textures: []
m_Width: 87
m_Height: 87
m_Kind: 1
m_SubKind: iPhone
- m_Textures: []
m_Width: 58
m_Height: 58
m_Kind: 1
m_SubKind: iPhone
- m_Textures: []
m_Width: 29
m_Height: 29
m_Kind: 1
m_SubKind: iPhone
- m_Textures: []
m_Width: 58
m_Height: 58
m_Kind: 1
m_SubKind: iPad
- m_Textures: []
m_Width: 29
m_Height: 29
m_Kind: 1
m_SubKind: iPad
- m_Textures: []
m_Width: 60
m_Height: 60
m_Kind: 2
m_SubKind: iPhone
- m_Textures: []
m_Width: 40
m_Height: 40
m_Kind: 2
m_SubKind: iPhone
- m_Textures: []
m_Width: 40
m_Height: 40
m_Kind: 2
m_SubKind: iPad
- m_Textures: []
m_Width: 20
m_Height: 20
m_Kind: 2
m_SubKind: iPad
- m_Textures: []
m_Width: 1024
m_Height: 1024
m_Kind: 4
m_SubKind: App Store
- m_BuildTarget: Android
m_Icons:
- m_Textures: []
m_Width: 432
m_Height: 432
m_Kind: 2
m_SubKind:
- m_Textures: []
m_Width: 324
m_Height: 324
m_Kind: 2
m_SubKind:
- m_Textures: []
m_Width: 216
m_Height: 216
m_Kind: 2
m_SubKind:
- m_Textures: []
m_Width: 162
m_Height: 162
m_Kind: 2
m_SubKind:
- m_Textures: []
m_Width: 108
m_Height: 108
m_Kind: 2
m_SubKind:
- m_Textures: []
m_Width: 81
m_Height: 81
m_Kind: 2
m_SubKind:
- m_Textures: []
m_Width: 192
m_Height: 192
m_Kind: 1
m_SubKind:
- m_Textures: []
m_Width: 144
m_Height: 144
m_Kind: 1
m_SubKind:
- m_Textures: []
m_Width: 96
m_Height: 96
m_Kind: 1
m_SubKind:
- m_Textures: []
m_Width: 72
m_Height: 72
m_Kind: 1
m_SubKind:
- m_Textures: []
m_Width: 48
m_Height: 48
m_Kind: 1
m_SubKind:
- m_Textures: []
m_Width: 36
m_Height: 36
m_Kind: 1
m_SubKind:
- m_Textures: []
m_Width: 192
m_Height: 192
m_Kind: 0
m_SubKind:
- m_Textures: []
m_Width: 144
m_Height: 144
m_Kind: 0
m_SubKind:
- m_Textures: []
m_Width: 96
m_Height: 96
m_Kind: 0
m_SubKind:
- m_Textures: []
m_Width: 72
m_Height: 72
m_Kind: 0
m_SubKind:
- m_Textures: []
m_Width: 48
m_Height: 48
m_Kind: 0
m_SubKind:
- m_Textures: []
m_Width: 36
m_Height: 36
m_Kind: 0
m_SubKind:
m_BuildTargetBatching:
- m_BuildTarget: Standalone
m_StaticBatching: 1
m_DynamicBatching: 0
- m_BuildTarget: tvOS
m_StaticBatching: 1
m_DynamicBatching: 0
- m_BuildTarget: Android
m_StaticBatching: 1
m_DynamicBatching: 0
- m_BuildTarget: iPhone
m_StaticBatching: 1
m_DynamicBatching: 0
- m_BuildTarget: WebGL
m_StaticBatching: 0
m_DynamicBatching: 0
m_BuildTargetShaderSettings: []
m_BuildTargetGraphicsJobs:
- m_BuildTarget: MacStandaloneSupport
m_GraphicsJobs: 0
- m_BuildTarget: Switch
m_GraphicsJobs: 1
- m_BuildTarget: MetroSupport
m_GraphicsJobs: 1
- m_BuildTarget: AppleTVSupport
m_GraphicsJobs: 0
- m_BuildTarget: BJMSupport
m_GraphicsJobs: 1
- m_BuildTarget: LinuxStandaloneSupport
m_GraphicsJobs: 1
- m_BuildTarget: PS4Player
m_GraphicsJobs: 1
- m_BuildTarget: iOSSupport
m_GraphicsJobs: 0
- m_BuildTarget: WindowsStandaloneSupport
m_GraphicsJobs: 1
- m_BuildTarget: XboxOnePlayer
m_GraphicsJobs: 1
- m_BuildTarget: LuminSupport
m_GraphicsJobs: 0
- m_BuildTarget: AndroidPlayer
m_GraphicsJobs: 0
- m_BuildTarget: WebGLSupport
m_GraphicsJobs: 0
m_BuildTargetGraphicsJobMode:
- m_BuildTarget: PS4Player
m_GraphicsJobMode: 0
- m_BuildTarget: XboxOnePlayer
m_GraphicsJobMode: 0
m_BuildTargetGraphicsAPIs:
- m_BuildTarget: AndroidPlayer
m_APIs: 150000000b000000
m_Automatic: 1
- m_BuildTarget: iOSSupport
m_APIs: 10000000
m_Automatic: 1
- m_BuildTarget: AppleTVSupport
m_APIs: 10000000
m_Automatic: 1
- m_BuildTarget: WebGLSupport
m_APIs: 0b000000
m_Automatic: 1
m_BuildTargetVRSettings:
- m_BuildTarget: Standalone
m_Enabled: 0
m_Devices:
- Oculus
- OpenVR
m_DefaultShaderChunkSizeInMB: 16
m_DefaultShaderChunkCount: 0
openGLRequireES31: 0
openGLRequireES31AEP: 0
openGLRequireES32: 0
m_TemplateCustomTags: {}
mobileMTRendering:
Android: 1
iPhone: 1
tvOS: 1
m_BuildTargetGroupLightmapEncodingQuality:
- m_BuildTarget: Android
m_EncodingQuality: 1
- m_BuildTarget: iPhone
m_EncodingQuality: 1
- m_BuildTarget: tvOS
m_EncodingQuality: 1
m_BuildTargetGroupHDRCubemapEncodingQuality:
- m_BuildTarget: Android
m_EncodingQuality: 1
- m_BuildTarget: iPhone
m_EncodingQuality: 1
- m_BuildTarget: tvOS
m_EncodingQuality: 1
m_BuildTargetGroupLightmapSettings: []
m_BuildTargetGroupLoadStoreDebugModeSettings: []
m_BuildTargetNormalMapEncoding:
- m_BuildTarget: Android
m_Encoding: 1
- m_BuildTarget: iPhone
m_Encoding: 1
- m_BuildTarget: tvOS
m_Encoding: 1
m_BuildTargetDefaultTextureCompressionFormat:
- m_BuildTarget: Android
m_Format: 3
playModeTestRunnerEnabled: 0
runPlayModeTestAsEditModeTest: 0
actionOnDotNetUnhandledException: 1
enableInternalProfiler: 0
logObjCUncaughtExceptions: 1
enableCrashReportAPI: 0
cameraUsageDescription:
locationUsageDescription:
microphoneUsageDescription:
bluetoothUsageDescription:
macOSTargetOSVersion: 10.13.0
switchNMETAOverride:
switchNetLibKey:
switchSocketMemoryPoolSize: 6144
switchSocketAllocatorPoolSize: 128
switchSocketConcurrencyLimit: 14
switchScreenResolutionBehavior: 2
switchUseCPUProfiler: 0
switchEnableFileSystemTrace: 0
switchLTOSetting: 0
switchApplicationID: 0x01004b9000490000
switchNSODependencies:
switchCompilerFlags:
switchTitleNames_0:
switchTitleNames_1:
switchTitleNames_2:
switchTitleNames_3:
switchTitleNames_4:
switchTitleNames_5:
switchTitleNames_6:
switchTitleNames_7:
switchTitleNames_8:
switchTitleNames_9:
switchTitleNames_10:
switchTitleNames_11:
switchTitleNames_12:
switchTitleNames_13:
switchTitleNames_14:
switchTitleNames_15:
switchPublisherNames_0:
switchPublisherNames_1:
switchPublisherNames_2:
switchPublisherNames_3:
switchPublisherNames_4:
switchPublisherNames_5:
switchPublisherNames_6:
switchPublisherNames_7:
switchPublisherNames_8:
switchPublisherNames_9:
switchPublisherNames_10:
switchPublisherNames_11:
switchPublisherNames_12:
switchPublisherNames_13:
switchPublisherNames_14:
switchPublisherNames_15:
switchIcons_0: {fileID: 0}
switchIcons_1: {fileID: 0}
switchIcons_2: {fileID: 0}
switchIcons_3: {fileID: 0}
switchIcons_4: {fileID: 0}
switchIcons_5: {fileID: 0}
switchIcons_6: {fileID: 0}
switchIcons_7: {fileID: 0}
switchIcons_8: {fileID: 0}
switchIcons_9: {fileID: 0}
switchIcons_10: {fileID: 0}
switchIcons_11: {fileID: 0}
switchIcons_12: {fileID: 0}
switchIcons_13: {fileID: 0}
switchIcons_14: {fileID: 0}
switchIcons_15: {fileID: 0}
switchSmallIcons_0: {fileID: 0}
switchSmallIcons_1: {fileID: 0}
switchSmallIcons_2: {fileID: 0}
switchSmallIcons_3: {fileID: 0}
switchSmallIcons_4: {fileID: 0}
switchSmallIcons_5: {fileID: 0}
switchSmallIcons_6: {fileID: 0}
switchSmallIcons_7: {fileID: 0}
switchSmallIcons_8: {fileID: 0}
switchSmallIcons_9: {fileID: 0}
switchSmallIcons_10: {fileID: 0}
switchSmallIcons_11: {fileID: 0}
switchSmallIcons_12: {fileID: 0}
switchSmallIcons_13: {fileID: 0}
switchSmallIcons_14: {fileID: 0}
switchSmallIcons_15: {fileID: 0}
switchManualHTML:
switchAccessibleURLs:
switchLegalInformation:
switchMainThreadStackSize: 1048576
switchPresenceGroupId:
switchLogoHandling: 0
switchReleaseVersion: 0
switchDisplayVersion: 1.0.0
switchStartupUserAccount: 0
switchSupportedLanguagesMask: 0
switchLogoType: 0
switchApplicationErrorCodeCategory:
switchUserAccountSaveDataSize: 0
switchUserAccountSaveDataJournalSize: 0
switchApplicationAttribute: 0
switchCardSpecSize: -1
switchCardSpecClock: -1
switchRatingsMask: 0
switchRatingsInt_0: 0
switchRatingsInt_1: 0
switchRatingsInt_2: 0
switchRatingsInt_3: 0
switchRatingsInt_4: 0
switchRatingsInt_5: 0
switchRatingsInt_6: 0
switchRatingsInt_7: 0
switchRatingsInt_8: 0
switchRatingsInt_9: 0
switchRatingsInt_10: 0
switchRatingsInt_11: 0
switchRatingsInt_12: 0
switchLocalCommunicationIds_0:
switchLocalCommunicationIds_1:
switchLocalCommunicationIds_2:
switchLocalCommunicationIds_3:
switchLocalCommunicationIds_4:
switchLocalCommunicationIds_5:
switchLocalCommunicationIds_6:
switchLocalCommunicationIds_7:
switchParentalControl: 0
switchAllowsScreenshot: 1
switchAllowsVideoCapturing: 1
switchAllowsRuntimeAddOnContentInstall: 0
switchDataLossConfirmation: 0
switchUserAccountLockEnabled: 0
switchSystemResourceMemory: 16777216
switchSupportedNpadStyles: 22
switchNativeFsCacheSize: 32
switchIsHoldTypeHorizontal: 0
switchSupportedNpadCount: 8
switchEnableTouchScreen: 1
switchSocketConfigEnabled: 0
switchTcpInitialSendBufferSize: 32
switchTcpInitialReceiveBufferSize: 64
switchTcpAutoSendBufferSizeMax: 256
switchTcpAutoReceiveBufferSizeMax: 256
switchUdpSendBufferSize: 9
switchUdpReceiveBufferSize: 42
switchSocketBufferEfficiency: 4
switchSocketInitializeEnabled: 1
switchNetworkInterfaceManagerInitializeEnabled: 1
switchDisableHTCSPlayerConnection: 0
switchUseNewStyleFilepaths: 1
switchUseLegacyFmodPriorities: 0
switchUseMicroSleepForYield: 1
switchEnableRamDiskSupport: 0
switchMicroSleepForYieldTime: 25
switchRamDiskSpaceSize: 12
ps4NPAgeRating: 12
ps4NPTitleSecret:
ps4NPTrophyPackPath:
ps4ParentalLevel: 11
ps4ContentID: ED1633-NPXX51362_00-0000000000000000
ps4Category: 0
ps4MasterVersion: 01.00
ps4AppVersion: 01.00
ps4AppType: 0
ps4ParamSfxPath:
ps4VideoOutPixelFormat: 0
ps4VideoOutInitialWidth: 1920
ps4VideoOutBaseModeInitialWidth: 1920
ps4VideoOutReprojectionRate: 60
ps4PronunciationXMLPath:
ps4PronunciationSIGPath:
ps4BackgroundImagePath:
ps4StartupImagePath:
ps4StartupImagesFolder:
ps4IconImagesFolder:
ps4SaveDataImagePath:
ps4SdkOverride:
ps4BGMPath:
ps4ShareFilePath:
ps4ShareOverlayImagePath:
ps4PrivacyGuardImagePath:
ps4ExtraSceSysFile:
ps4NPtitleDatPath:
ps4RemotePlayKeyAssignment: -1
ps4RemotePlayKeyMappingDir:
ps4PlayTogetherPlayerCount: 0
ps4EnterButtonAssignment: 1
ps4ApplicationParam1: 0
ps4ApplicationParam2: 0
ps4ApplicationParam3: 0
ps4ApplicationParam4: 0
ps4DownloadDataSize: 0
ps4GarlicHeapSize: 2048
ps4ProGarlicHeapSize: 2560
playerPrefsMaxSize: 32768
ps4Passcode: frAQBc8Wsa1xVPfvJcrgRYwTiizs2trQ
ps4pnSessions: 1
ps4pnPresence: 1
ps4pnFriends: 1
ps4pnGameCustomData: 1
playerPrefsSupport: 0
enableApplicationExit: 0
resetTempFolder: 1
restrictedAudioUsageRights: 0
ps4UseResolutionFallback: 0
ps4ReprojectionSupport: 0
ps4UseAudio3dBackend: 0
ps4UseLowGarlicFragmentationMode: 1
ps4SocialScreenEnabled: 0
ps4ScriptOptimizationLevel: 0
ps4Audio3dVirtualSpeakerCount: 14
ps4attribCpuUsage: 0
ps4PatchPkgPath:
ps4PatchLatestPkgPath:
ps4PatchChangeinfoPath:
ps4PatchDayOne: 0
ps4attribUserManagement: 0
ps4attribMoveSupport: 0
ps4attrib3DSupport: 0
ps4attribShareSupport: 0
ps4attribExclusiveVR: 0
ps4disableAutoHideSplash: 0
ps4videoRecordingFeaturesUsed: 0
ps4contentSearchFeaturesUsed: 0
ps4CompatibilityPS5: 0
ps4AllowPS5Detection: 0
ps4GPU800MHz: 1
ps4attribEyeToEyeDistanceSettingVR: 0
ps4IncludedModules: []
ps4attribVROutputEnabled: 0
monoEnv:
splashScreenBackgroundSourceLandscape: {fileID: 0}
splashScreenBackgroundSourcePortrait: {fileID: 0}
blurSplashScreenBackground: 1
spritePackerPolicy:
webGLMemorySize: 16
webGLExceptionSupport: 1
webGLNameFilesAsHashes: 0
webGLShowDiagnostics: 0
webGLDataCaching: 1
webGLDebugSymbols: 0
webGLEmscriptenArgs:
webGLModulesDirectory:
webGLTemplate: APPLICATION:Default
webGLAnalyzeBuildSize: 0
webGLUseEmbeddedResources: 0
webGLCompressionFormat: 1
webGLWasmArithmeticExceptions: 0
webGLLinkerTarget: 1
webGLThreadsSupport: 0
webGLDecompressionFallback: 0
webGLInitialMemorySize: 32
webGLMaximumMemorySize: 2048
webGLMemoryGrowthMode: 2
webGLMemoryLinearGrowthStep: 16
webGLMemoryGeometricGrowthStep: 0.2
webGLMemoryGeometricGrowthCap: 96
webGLPowerPreference: 2
scriptingDefineSymbols: {}
additionalCompilerArguments: {}
platformArchitecture: {}
scriptingBackend: {}
il2cppCompilerConfiguration: {}
il2cppCodeGeneration: {}
managedStrippingLevel:
EmbeddedLinux: 1
GameCoreScarlett: 1
GameCoreXboxOne: 1
Nintendo Switch: 1
PS4: 1
PS5: 1
QNX: 1
Stadia: 1
VisionOS: 1
WebGL: 1
Windows Store Apps: 1
XboxOne: 1
iPhone: 1
tvOS: 1
incrementalIl2cppBuild: {}
suppressCommonWarnings: 1
allowUnsafeCode: 0
useDeterministicCompilation: 1
additionalIl2CppArgs:
scriptingRuntimeVersion: 1
gcIncremental: 1
gcWBarrierValidation: 0
apiCompatibilityLevelPerPlatform: {}
m_RenderingPath: 1
m_MobileRenderingPath: 1
metroPackageName: My project
metroPackageVersion:
metroCertificatePath:
metroCertificatePassword:
metroCertificateSubject:
metroCertificateIssuer:
metroCertificateNotAfter: 0000000000000000
metroApplicationDescription: My project
wsaImages: {}
metroTileShortName:
metroTileShowName: 0
metroMediumTileShowName: 0
metroLargeTileShowName: 0
metroWideTileShowName: 0
metroSupportStreamingInstall: 0
metroLastRequiredScene: 0
metroDefaultTileSize: 1
metroTileForegroundText: 2
metroTileBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21568628, a: 0}
metroSplashScreenBackgroundColor: {r: 0.12941177, g: 0.17254902, b: 0.21568628, a: 1}
metroSplashScreenUseBackgroundColor: 0
syncCapabilities: 0
platformCapabilities: {}
metroTargetDeviceFamilies: {}
metroFTAName:
metroFTAFileTypes: []
metroProtocolName:
vcxProjDefaultLanguage:
XboxOneProductId:
XboxOneUpdateKey:
XboxOneSandboxId:
XboxOneContentId:
XboxOneTitleId:
XboxOneSCId:
XboxOneGameOsOverridePath:
XboxOnePackagingOverridePath:
XboxOneAppManifestOverridePath:
XboxOneVersion: 1.0.0.0
XboxOnePackageEncryption: 0
XboxOnePackageUpdateGranularity: 2
XboxOneDescription:
XboxOneLanguage:
- enus
XboxOneCapability: []
XboxOneGameRating: {}
XboxOneIsContentPackage: 0
XboxOneEnhancedXboxCompatibilityMode: 0
XboxOneEnableGPUVariability: 1
XboxOneSockets: {}
XboxOneSplashScreen: {fileID: 0}
XboxOneAllowedProductIds: []
XboxOnePersistentLocalStorageSize: 0
XboxOneXTitleMemory: 8
XboxOneOverrideIdentityName:
XboxOneOverrideIdentityPublisher:
vrEditorSettings: {}
cloudServicesEnabled:
UNet: 1
luminIcon:
m_Name:
m_ModelFolderPath:
m_PortalFolderPath:
luminCert:
m_CertPath:
m_SignPackage: 1
luminIsChannelApp: 0
luminVersion:
m_VersionCode: 1
m_VersionName:
hmiPlayerDataPath:
hmiForceSRGBBlit: 1
embeddedLinuxEnableGamepadInput: 1
hmiLogStartupTiming: 0
hmiCpuConfiguration:
apiCompatibilityLevel: 6
activeInputHandler: 0
windowsGamepadBackendHint: 0
cloudProjectId: 9d1c8a3f-1980-42b0-8177-7ad62a001224
framebufferDepthMemorylessMode: 0
qualitySettingsNames: []
projectName: My project
organizationId: jisd8018
cloudEnabled: 0
legacyClampBlendShapeWeights: 0
hmiLoadingImage: {fileID: 0}
platformRequiresReadableAssets: 0
virtualTexturingSupportEnabled: 0
insecureHttpOption: 0

View File

@@ -0,0 +1,2 @@
m_EditorVersion: 2022.3.62f2c1
m_EditorVersionWithRevision: 2022.3.62f2c1 (92e6e6be66dc)

View File

@@ -0,0 +1,234 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!47 &1
QualitySettings:
m_ObjectHideFlags: 0
serializedVersion: 5
m_CurrentQuality: 5
m_QualitySettings:
- serializedVersion: 2
name: Very Low
pixelLightCount: 0
shadows: 0
shadowResolution: 0
shadowProjection: 1
shadowCascades: 1
shadowDistance: 15
shadowNearPlaneOffset: 3
shadowCascade2Split: 0.33333334
shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667}
shadowmaskMode: 0
blendWeights: 1
textureQuality: 1
anisotropicTextures: 0
antiAliasing: 0
softParticles: 0
softVegetation: 0
realtimeReflectionProbes: 0
billboardsFaceCameraPosition: 0
vSyncCount: 0
lodBias: 0.3
maximumLODLevel: 0
streamingMipmapsActive: 0
streamingMipmapsAddAllCameras: 1
streamingMipmapsMemoryBudget: 512
streamingMipmapsRenderersPerFrame: 512
streamingMipmapsMaxLevelReduction: 2
streamingMipmapsMaxFileIORequests: 1024
particleRaycastBudget: 4
asyncUploadTimeSlice: 2
asyncUploadBufferSize: 16
asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1
excludedTargetPlatforms: []
- serializedVersion: 2
name: Low
pixelLightCount: 0
shadows: 0
shadowResolution: 0
shadowProjection: 1
shadowCascades: 1
shadowDistance: 20
shadowNearPlaneOffset: 3
shadowCascade2Split: 0.33333334
shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667}
shadowmaskMode: 0
blendWeights: 2
textureQuality: 0
anisotropicTextures: 0
antiAliasing: 0
softParticles: 0
softVegetation: 0
realtimeReflectionProbes: 0
billboardsFaceCameraPosition: 0
vSyncCount: 0
lodBias: 0.4
maximumLODLevel: 0
streamingMipmapsActive: 0
streamingMipmapsAddAllCameras: 1
streamingMipmapsMemoryBudget: 512
streamingMipmapsRenderersPerFrame: 512
streamingMipmapsMaxLevelReduction: 2
streamingMipmapsMaxFileIORequests: 1024
particleRaycastBudget: 16
asyncUploadTimeSlice: 2
asyncUploadBufferSize: 16
asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1
excludedTargetPlatforms: []
- serializedVersion: 2
name: Medium
pixelLightCount: 1
shadows: 1
shadowResolution: 0
shadowProjection: 1
shadowCascades: 1
shadowDistance: 20
shadowNearPlaneOffset: 3
shadowCascade2Split: 0.33333334
shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667}
shadowmaskMode: 0
blendWeights: 2
textureQuality: 0
anisotropicTextures: 1
antiAliasing: 0
softParticles: 0
softVegetation: 0
realtimeReflectionProbes: 0
billboardsFaceCameraPosition: 0
vSyncCount: 1
lodBias: 0.7
maximumLODLevel: 0
streamingMipmapsActive: 0
streamingMipmapsAddAllCameras: 1
streamingMipmapsMemoryBudget: 512
streamingMipmapsRenderersPerFrame: 512
streamingMipmapsMaxLevelReduction: 2
streamingMipmapsMaxFileIORequests: 1024
particleRaycastBudget: 64
asyncUploadTimeSlice: 2
asyncUploadBufferSize: 16
asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1
excludedTargetPlatforms: []
- serializedVersion: 2
name: High
pixelLightCount: 2
shadows: 2
shadowResolution: 1
shadowProjection: 1
shadowCascades: 2
shadowDistance: 40
shadowNearPlaneOffset: 3
shadowCascade2Split: 0.33333334
shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667}
shadowmaskMode: 1
blendWeights: 2
textureQuality: 0
anisotropicTextures: 1
antiAliasing: 0
softParticles: 0
softVegetation: 1
realtimeReflectionProbes: 1
billboardsFaceCameraPosition: 1
vSyncCount: 1
lodBias: 1
maximumLODLevel: 0
streamingMipmapsActive: 0
streamingMipmapsAddAllCameras: 1
streamingMipmapsMemoryBudget: 512
streamingMipmapsRenderersPerFrame: 512
streamingMipmapsMaxLevelReduction: 2
streamingMipmapsMaxFileIORequests: 1024
particleRaycastBudget: 256
asyncUploadTimeSlice: 2
asyncUploadBufferSize: 16
asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1
excludedTargetPlatforms: []
- serializedVersion: 2
name: Very High
pixelLightCount: 3
shadows: 2
shadowResolution: 2
shadowProjection: 1
shadowCascades: 2
shadowDistance: 70
shadowNearPlaneOffset: 3
shadowCascade2Split: 0.33333334
shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667}
shadowmaskMode: 1
blendWeights: 4
textureQuality: 0
anisotropicTextures: 2
antiAliasing: 2
softParticles: 1
softVegetation: 1
realtimeReflectionProbes: 1
billboardsFaceCameraPosition: 1
vSyncCount: 1
lodBias: 1.5
maximumLODLevel: 0
streamingMipmapsActive: 0
streamingMipmapsAddAllCameras: 1
streamingMipmapsMemoryBudget: 512
streamingMipmapsRenderersPerFrame: 512
streamingMipmapsMaxLevelReduction: 2
streamingMipmapsMaxFileIORequests: 1024
particleRaycastBudget: 1024
asyncUploadTimeSlice: 2
asyncUploadBufferSize: 16
asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1
excludedTargetPlatforms: []
- serializedVersion: 2
name: Ultra
pixelLightCount: 4
shadows: 2
shadowResolution: 2
shadowProjection: 1
shadowCascades: 4
shadowDistance: 150
shadowNearPlaneOffset: 3
shadowCascade2Split: 0.33333334
shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667}
shadowmaskMode: 1
blendWeights: 4
textureQuality: 0
anisotropicTextures: 2
antiAliasing: 2
softParticles: 1
softVegetation: 1
realtimeReflectionProbes: 1
billboardsFaceCameraPosition: 1
vSyncCount: 1
lodBias: 2
maximumLODLevel: 0
streamingMipmapsActive: 0
streamingMipmapsAddAllCameras: 1
streamingMipmapsMemoryBudget: 512
streamingMipmapsRenderersPerFrame: 512
streamingMipmapsMaxLevelReduction: 2
streamingMipmapsMaxFileIORequests: 1024
particleRaycastBudget: 4096
asyncUploadTimeSlice: 2
asyncUploadBufferSize: 16
asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1
excludedTargetPlatforms: []
m_PerPlatformDefaultQuality:
Android: 2
Lumin: 5
GameCoreScarlett: 5
GameCoreXboxOne: 5
Nintendo 3DS: 5
Nintendo Switch: 5
PS4: 5
PS5: 5
Stadia: 5
Standalone: 5
WebGL: 3
Windows Store Apps: 5
XboxOne: 5
iPhone: 2
tvOS: 2

View File

@@ -0,0 +1,121 @@
{
"templatePinStates": [],
"dependencyTypeInfos": [
{
"userAdded": false,
"type": "UnityEngine.AnimationClip",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEditor.Animations.AnimatorController",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.AnimatorOverrideController",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEditor.Audio.AudioMixerController",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.ComputeShader",
"defaultInstantiationMode": 1
},
{
"userAdded": false,
"type": "UnityEngine.Cubemap",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.GameObject",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEditor.LightingDataAsset",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.LightingSettings",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.Material",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEditor.MonoScript",
"defaultInstantiationMode": 1
},
{
"userAdded": false,
"type": "UnityEngine.PhysicMaterial",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.PhysicsMaterial2D",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.Rendering.PostProcessing.PostProcessProfile",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.Rendering.PostProcessing.PostProcessResources",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.Rendering.VolumeProfile",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEditor.SceneAsset",
"defaultInstantiationMode": 1
},
{
"userAdded": false,
"type": "UnityEngine.Shader",
"defaultInstantiationMode": 1
},
{
"userAdded": false,
"type": "UnityEngine.ShaderVariantCollection",
"defaultInstantiationMode": 1
},
{
"userAdded": false,
"type": "UnityEngine.Texture",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.Texture2D",
"defaultInstantiationMode": 0
},
{
"userAdded": false,
"type": "UnityEngine.Timeline.TimelineAsset",
"defaultInstantiationMode": 0
}
],
"defaultDependencyTypeInfo": {
"userAdded": false,
"type": "<default_scene_template_dependencies>",
"defaultInstantiationMode": 1
},
"newSceneOverride": 0
}

View File

@@ -0,0 +1,43 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!78 &1
TagManager:
serializedVersion: 2
tags: []
layers:
- Default
- TransparentFX
- Ignore Raycast
-
- Water
- UI
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
m_SortingLayers:
- name: Default
uniqueID: 0
locked: 0

View File

@@ -0,0 +1,9 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!5 &1
TimeManager:
m_ObjectHideFlags: 0
Fixed Timestep: 0.02
Maximum Allowed Timestep: 0.33333334
m_TimeScale: 1
Maximum Particle Timestep: 0.03

View File

@@ -0,0 +1,38 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!310 &1
UnityConnectSettings:
m_ObjectHideFlags: 0
serializedVersion: 1
m_Enabled: 0
m_TestMode: 0
m_EventOldUrl: https://api.uca.cloud.unity3d.com/v1/events
m_EventUrl: https://cdp.cloud.unity3d.com/v1/events
m_ConfigUrl: https://config.uca.cloud.unity3d.com
m_DashboardUrl: https://dashboard.unity3d.com
m_CNEventUrl: https://cdp.cloud.unity.cn/v1/events
m_CNConfigUrl: https://cdp.cloud.unity.cn/config
m_TestInitMode: 0
CrashReportingSettings:
m_EventUrl: https://perf-events.cloud.unity.cn
m_Enabled: 0
m_LogBufferSize: 10
m_CaptureEditorExceptions: 1
UnityPurchasingSettings:
m_Enabled: 0
m_TestMode: 0
UnityAnalyticsSettings:
m_Enabled: 0
m_TestMode: 0
m_InitializeOnStartup: 1
m_PackageRequiringCoreStatsPresent: 0
UnityAdsSettings:
m_Enabled: 0
m_InitializeOnStartup: 1
m_TestMode: 0
m_IosGameId:
m_AndroidGameId:
m_GameIds: {}
m_GameId:
PerformanceReportingSettings:
m_Enabled: 0

View File

@@ -0,0 +1,12 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!937362698 &1
VFXManager:
m_ObjectHideFlags: 0
m_IndirectShader: {fileID: 0}
m_CopyBufferShader: {fileID: 0}
m_SortShader: {fileID: 0}
m_StripUpdateShader: {fileID: 0}
m_RenderPipeSettingsPath:
m_FixedTimeStep: 0.016666668
m_MaxDeltaTime: 0.05

View File

@@ -0,0 +1,8 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!890905787 &1
VersionControlSettings:
m_ObjectHideFlags: 0
m_Mode: Visible Meta Files
m_CollabEditorSettings:
inProgressEnabled: 1

View File

@@ -0,0 +1,10 @@
{
"m_SettingKeys": [
"VR Device Disabled",
"VR Device User Alert"
],
"m_SettingValues": [
"False",
"False"
]
}

View File

@@ -0,0 +1,102 @@
# AudioService 取消语义修复说明
日期2026-05-20
范围:`My project/Assets/FlowScope/Runtime/Audio/AudioService.cs`
优先级P0 小修复
结论:建议立即顺手修复
## 问题概述
`AudioService.PlayBgmAsync``AudioService.PlaySfxAsync` 支持传入 `CancellationToken`,但当前内部 `LoadClipAsync` 会捕获所有异常,并统一包装为 `InvalidOperationException`
这会导致调用方主动取消音频加载时,拿到的不是 `OperationCanceledException`,而是普通失败异常。
## 影响
取消语义被破坏后,调用方无法可靠区分以下两类情况:
- 生命周期或用户操作导致的正常取消。
- 资源缺失、Addressables 失败、类型不匹配等真实加载错误。
可能造成的问题:
- 正常取消被记录成错误日志。
- UI 或业务层误判为资源加载失败。
- 后续重试、错误兜底、统计埋点出现噪音。
- 框架内不同服务的取消语义不一致,增加异步生命周期排查成本。
## 推荐修改
`AudioService.LoadClipAsync` 中单独保留 `OperationCanceledException`,不要包装。
目标代码形态:
```csharp
private async Task<IResourceHandle<AudioClip>> LoadClipAsync(
string key,
CancellationToken cancellationToken)
{
try
{
var handle = await _resources.LoadAsync<AudioClip>(key, cancellationToken);
return ValidateLoadedClip(key, handle);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exception)
{
throw new InvalidOperationException($"Failed to load audio clip '{key}'.", exception);
}
}
```
## 测试建议
`AudioServiceTests` 中补充异步取消测试。
建议覆盖:
1. `PlayBgmAsync_WhenCanceled_ThrowsOperationCanceledException`
2. `PlaySfxAsync_WhenCanceled_ThrowsOperationCanceledException`
测试要点:
- 创建 `CancellationTokenSource`
- 调用 `Cancel()`
- 调用 `PlayBgmAsync``PlaySfxAsync`
- 断言抛出 `OperationCanceledException`
- 断言没有创建新的 `AudioSource`
示例结构:
```csharp
[UnityTest]
public IEnumerator PlayBgmAsync_WhenCanceled_ThrowsOperationCanceledException()
{
var service = CreateService();
using var cts = new CancellationTokenSource();
cts.Cancel();
var sourceCountBeforePlay = Object.FindObjectsOfType<AudioSource>().Length;
yield return ThrowsAsync<OperationCanceledException>(
service.PlayBgmAsync("bgm-a", cancellationToken: cts.Token));
Assert.That(Object.FindObjectsOfType<AudioSource>().Length, Is.EqualTo(sourceCountBeforePlay));
}
```
如果现有测试工具方法只支持 `Func<Task>`,也可以按当前测试文件风格新增一个接收 `Task``ThrowsAsync<TException>` helper。
## 验收标准
- `PlayBgmAsync` 在取消时抛出 `OperationCanceledException`
- `PlaySfxAsync` 在取消时抛出 `OperationCanceledException`
- 取消时不创建 `AudioSource`
- 取消时不把异常包装成 `InvalidOperationException`
- 现有 PlayMode 音频测试继续通过。
## 后续备注
该问题不是架构阻塞项,但修复成本很低。建议本轮直接修掉,避免后续异步服务取消语义不一致。

View File

@@ -0,0 +1,141 @@
# 通用游戏 Core 需求分档文档
## 文档说明
本文档定义 FlowScope Game Core 的需求分档。P0 的详细需求已经收敛到:
- `docs/requirements/p0-requirements-set.md`
- `docs/requirements/p0-container.md`
- `docs/requirements/p0-gameflow.md`
- `docs/requirements/p0-feature.md`
- `docs/requirements/p0-data-r3.md`
- `docs/requirements/p0-configprovider.md`
- `docs/requirements/p0-saveservice.md`
- `docs/requirements/p0-resourceservice.md`
- `docs/requirements/p0-uimanager.md`
- `docs/requirements/p0-audioservice.md`
如本文档与上述 P0 详细文档冲突,以 P0 详细文档为准。
---
## 分档原则
1. 先满足游戏开发闭环,再追求通用框架完整性。
2. 显式接口优先,不暗猜业务模型,不预设具体 Data 类。
3. P0 必须能支撑一个真实休闲游戏的最小纵向切片。
4. P1 扩展常用生产能力,但不破坏 P0 核心接口。
5. P2 才考虑包分发、编辑器体验、多后端生态和跨项目复用。
6. P3 作为长期演进方向,不进入近期交付承诺。
---
## P0最小可运行核心
目标:跑通一个休闲/超休闲 Unity 游戏的启动、配置加载、存档加载、进入初始 Feature、打开 MVVM UI、响应 ViewModel/Data 变化、退出并保存。
P0 包含:
| 模块 | 决策 |
|------|------|
| Container | 显式工厂、Attribute + Source Generator、Attribute + 运行时反射均进入 P0 |
| GameFlow | 负责创建 Feature scope、FeatureContext并编排启动/切换/关闭 |
| Feature | 只负责业务行为,消费 GameFlow 提供的 FeatureContext |
| R3 + Data | 直接使用 R3不自研响应式系统 |
| ConfigProvider | P0 只支持 JSON 强类型读取 |
| SaveService | P0 只支持本地轻量存档File 或 PlayerPrefs |
| ResourceService | P0 支持引用计数和 ResourceGroup默认只支持 Addressables |
| UIManager | P0 支持可配置层级和层内 LIFO 栈 |
| AudioService | P0 支持 BGM/SFX、音量、静音、简单 SFX 池和轻量 AudioHandle |
P0 不包含:
- CSV / Luban / ScriptableObject 配置。
- 云存档、加密、压缩、复杂版本迁移。
- Resources / AssetBundle / YooAsset 资源后端。
- 完整 UI 路由系统。
- 全局 EventBus。
- AudioMixer、3D 音频、动态音乐。
- 编辑器配置检查和资源分析工具。
P0 完成后应能做到:
- Unity 启动后进入 `GameBootstrap`
- 注册核心服务和业务 Data。
- 加载 JSON 配置。
- 加载或创建存档数据。
- 进入 `MainMenuFeature`
- 打开 `MainMenuPanel`
- ViewModel 使用 R3 驱动 UI 刷新。
- 切换或关闭 Feature 时释放资源和订阅。
- 关闭游戏时保存数据。
---
## P1常用生产能力
目标:在 P0 核心稳定后,扩展常用生产能力,仍然保持 Core 主接口稳定。
候选能力:
| 能力 | 建议形式 |
|------|----------|
| 按模块配置加载 | `IConfigSource` 扩展 |
| CSV / Luban 配置 | `IConfigParser` 插件 |
| 存档版本迁移 | `ISaveMigration` 插件 |
| Resources 后端 | `IResourceBackend` 插件 |
| YooAsset 后端 | `IResourceBackend` 插件 |
| UI 返回策略 | `IUIScreenNavigator` 扩展 |
| Panel 预加载 | UIManager 扩展服务 |
| AudioMixer 分组 | AudioService 扩展 |
| 3D 音频 | AudioService 扩展 |
---
## P2包分发与生态化
目标:当多个项目复用需求明确后,把 Game Core 整理为更标准的 Unity Package 和可插拔生态。
候选能力:
- `com.flowscope.gamecore` 包结构。
- `package.json` / `README.md` / `CHANGELOG.md` / `Samples~`
- 独立 asmdef 拆分。
- Editor 导入向导。
- 配置校验工具。
- 资源扫描和分析工具。
- 云存档适配包。
- AssetBundle 适配包。
- UI 路由包。
---
## P3长期演进方向
目标:只作为方向保留,不进入近期交付范围。
候选能力:
- Source Generator 深度优化。
- `[Inject]` 属性/方法注入。
- 全局 EventBus 并支持 R3/默认实现切换。
- 完整网络长连接、断线重连、心跳机制。
- 云存档冲突解决。
- 热更新框架。
- UI 自动绑定框架。
- 大型项目资源预算和内存分析工具。
---
## 当前实施建议
当前只实施 P0。P0 的并行实施计划见:
- `docs/superpowers/plans/2026-05-15-p0-parallel-implementation-plan.md`
执行顺序:
1. 先完成 `p0-contracts`
2. 合并共享契约。
3. 再并行执行各模块 worktree。
4. 最后由 `p0-integration` 做统一集成和 Sample 验证。

View File

@@ -0,0 +1,487 @@
# 通用游戏 Core 需求文档
## 文档说明
本文档描述一份**通用的游戏 Core 模块模板**——不绑定具体游戏类型,适用于 RPG、FPS、RTS、解谜等任何品类。每个项目基于此模板补充具体的数值和细节即可。
---
## 1. 架构总览
Core 是游戏的底层基础设施,不包含任何业务逻辑。它的职责是为上层业务提供可复用的系统服务。
```
┌──────────────────────────────────────────────┐
│ 业务层 (Game Logic) │
│ 各游戏自行定义,不归 Core 管 │
├──────────────────────────────────────────────┤
│ Core 层 │
├──────────┬──────────┬──────────┬─────────────┤
│ Game │ State │ Input │ Scene │
│ Loop │ Machine │ System │ Manager │
├──────────┼──────────┼──────────┼─────────────┤
│ Event │ Data │ Audio │ UI │
│ System │ Manager │ System │ Framework │
├──────────┼──────────┼──────────┼─────────────┤
│ Save │ Config │ Time │ Asset │
│ System │ System │ System │ Loader │
└──────────┴──────────┴──────────┴─────────────┘
```
---
## 2. 模块依赖与构建顺序
Core 模块之间存在依赖关系,必须按层构建:
```
第一层无依赖Game Loop, Event System, Time System, Asset Loader
第二层依赖一层Config System, Input System, State Machine
第三层依赖前两层Data Manager, Scene Manager, Audio System, UI Framework
第四层依赖前三层Save System
```
---
## 3. 通用验收规则
这些规则适用于 Core 的所有模块,不分优先级:
| 规则 | 说明 |
|------|------|
| 零业务耦合 | Core 不包含任何游戏业务逻辑,只提供服务 |
| 接口稳定 | Core API 一旦确定,业务层不因 Core 变动而改动 |
| 可独立测试 | 每个模块可在无其他模块的情况下单元测试 |
| 日志完备 | 关键操作(加载、切换、错误)有结构化日志 |
| 文档自洽 | 每个接口有参数说明和返回值约定 |
---
## 4. 模块详细描述
每个模块按统一模板编写:目标 → 职责 → 接口 → 依赖 → 验收标准。
---
### 4.1 Game Loop主循环
#### 目标
驱动整个游戏帧更新,控制初始化、运行、暂停、退出流程。
#### 状态
```
Init → Loading → Running → Paused → Quitting
```
#### 职责
- 固定频率调用各子系统 Tick逻辑帧
- 渲染帧与逻辑帧解耦(可选)
#### 接口
| 接口 | 说明 |
|------|------|
| `Start()` | 启动游戏循环 |
| `Pause()` | 暂停所有子系统 Tick |
| `Resume()` | 恢复所有子系统 Tick |
| `Quit()` | 退出并释放资源 |
| `OnFixedUpdate(dt)` | 逻辑帧回调 |
| `OnUpdate(dt)` | 渲染帧回调 |
#### 依赖
无(最底层)
#### 验收标准
- [ ] 启动 → Loading → Running 流程正常
- [ ] 暂停时所有子系统停止 Tick
- [ ] 退出时正确释放资源
---
### 4.2 State Machine状态机
#### 目标
管理游戏全局状态和各模块局部状态的切换。
#### 职责
- 状态注册、切换、查询
- 状态切换时触发 Enter/Exit 回调
- 支持状态栈push/pop用于暂停/恢复场景
#### 接口
| 接口 | 说明 |
|------|------|
| `Register(stateId, state)` | 注册一个状态 |
| `SwitchTo(stateId)` | 切换到指定状态 |
| `Push(stateId)` | 压栈当前状态并切换 |
| `Pop()` | 弹栈恢复上一个状态 |
| `CurrentState` | 当前状态 ID只读 |
#### 依赖
Event System
#### 验收标准
- [ ] 切换状态时 Enter/Exit 回调正确执行
- [ ] Push/Pop 不破坏前一个状态
- [ ] 不存在的状态切换抛出明确错误
---
### 4.3 Input System输入系统
#### 目标
统一处理键鼠、手柄、触屏输入,向上层提供抽象的输入动作。
#### 职责
- 原始输入 → 逻辑动作映射Move/Jump/Attack
- 支持运行时切换输入方案
- 支持输入录制/回放(用于测试/回放系统)
#### 接口
| 接口 | 说明 |
|------|------|
| `BindAction(actionName, callback)` | 绑定动作回调 |
| `EnableScheme(schemeName)` | 启用指定输入方案 |
| `GetAxis(axisName) → float` | 获取轴向值 |
| `IsPressed(actionName) → bool` | 查询动作是否按下 |
#### 依赖
Config System键位配置读取
#### 验收标准
- [ ] 键鼠和手柄可无缝切换
- [ ] 自定义键位后立即生效
- [ ] 同一帧多次读取结果一致
---
### 4.4 Scene Manager场景管理
#### 目标
管理场景/关卡的加载、卸载、切换。
#### 职责
- 同步/异步加载场景
- 切换时显示 Loading 界面
- 支持附加式加载Additive实现流式开放世界
#### 接口
| 接口 | 说明 |
|------|------|
| `LoadScene(sceneName, additive)` | 加载场景 |
| `UnloadScene(sceneName)` | 卸载场景 |
| `OnSceneLoaded → callback` | 场景加载完成回调 |
| `OnSceneUnloading → callback` | 场景卸载前回调 |
#### 依赖
Asset Loader, Event System
#### 验收标准
- [ ] 场景切换时无黑屏闪烁
- [ ] 异步加载期间有进度反馈
- [ ] 卸载场景后内存正确释放
---
### 4.5 Event System事件总线
#### 目标
模块间解耦通信,替代直接引用。
#### 职责
- 事件注册、派发、注销
- 支持泛型事件(带 payload
- 支持事件优先级
#### 接口
| 接口 | 说明 |
|------|------|
| `Subscribe(eventId, handler)` | 订阅事件 |
| `Unsubscribe(eventId, handler)` | 取消订阅 |
| `Dispatch(eventId, payload)` | 派发事件 |
#### 依赖
#### 验收标准
- [ ] handler 注销后不再收到事件
- [ ] 循环派发不会死循环(深度限制或检测)
- [ ] 事件丢失时有日志警告
---
### 4.6 Data Manager数据管理
#### 目标
统一管理运行时数据(配置表、存档、缓存)。
#### 职责
- 配置表加载与热更新
- 运行时数据容器的 CRUD
- 数据变更通知
#### 接口
| 接口 | 说明 |
|------|------|
| `LoadConfig(tableName)` | 加载配置表 |
| `Get<T>(key) → T` | 按主键获取数据 |
| `Set(key, value)` | 写入数据 |
| `OnDataChanged(key) → event` | 数据变更通知 |
#### 依赖
Asset Loader, Event System
#### 验收标准
- [ ] 配置表加载失败使用默认值并报警
- [ ] Get 不存在的 key 返回 default(T) 而非报错
---
### 4.7 Audio System音频系统
#### 目标
统一管理 BGM、SFX、环境音的播放控制。
#### 职责
- 音频资源池化
- 分通道音量控制Master/BGM/SFX/Voice
- 3D 空间音频
- 音频淡入淡出
#### 接口
| 接口 | 说明 |
|------|------|
| `PlayBGM(clipId, fadeIn)` | 播放背景音乐 |
| `PlaySFX(clipId, position)` | 播放音效(支持 3D 位置) |
| `SetVolume(channel, value)` | 设置通道音量 |
| `StopAll()` | 停止所有音频 |
#### 依赖
Data Manager, Config System
#### 验收标准
- [ ] 同一 SFX 快速重复播放不卡顿
- [ ] 场景切换时 BGM 可配置是否延续
- [ ] 音量设置持久化到存档
---
### 4.8 UI FrameworkUI 框架)
#### 目标
管理 UI 面板的打开、关闭、层级、动画。
#### 职责
- 面板栈管理(打开新面板压栈,关闭弹栈)
- 面板缓存与复用
- 统一的打开/关闭动画
- 遮罩与模态控制
#### 接口
| 接口 | 说明 |
|------|------|
| `Open(panelId, params)` | 打开面板 |
| `Close(panelId)` | 关闭面板 |
| `CloseAll()` | 关闭所有面板 |
| `IsOpen(panelId) → bool` | 查询面板状态 |
#### 依赖
Event System, Asset Loader
#### 验收标准
- [ ] 打开新面板时下层面板交互自动屏蔽
- [ ] 连续打开/关闭 10 次无内存泄漏
- [ ] 面板动画未播完时可打断
---
### 4.9 Save System存档系统
#### 目标
游戏数据的持久化存储与读取。
#### 职责
- 多存档位管理
- 增量存档(只存变更)
- 存档版本迁移(旧版存档兼容)
- 自动存档触发
#### 接口
| 接口 | 说明 |
|------|------|
| `Save(slotId)` | 保存到指定存档位 |
| `Load(slotId)` | 从指定存档位加载 |
| `Delete(slotId)` | 删除存档 |
| `ListSaves() → saveInfo[]` | 列出所有存档 |
| `OnAutoSaveTrigger → event` | 自动存档触发事件 |
#### 依赖
Data Manager, Event System
#### 验收标准
- [ ] 存档过程中断电,旧存档不被损坏(双写机制)
- [ ] 旧版本存档能正确迁移到新版本
- [ ] 自动存档不造成可感知的卡顿
---
### 4.10 Config System配置系统
#### 目标
管理游戏全局配置和用户偏好设置。
#### 职责
- 分层配置:默认值 → 项目配置 → 用户配置
- 运行时修改用户配置并持久化
- 配置项变更通知
#### 接口
| 接口 | 说明 |
|------|------|
| `Get(key) → value` | 获取配置值 |
| `Set(key, value, persist)` | 设置配置值(可选持久化) |
| `ResetToDefault()` | 重置为默认配置 |
#### 依赖
Save System
#### 验收标准
- [ ] 用户配置覆盖优先级正确
- [ ] 非法配置值回退到默认值
- [ ] 配置修改后立即生效
---
### 4.11 Time System时间系统
#### 目标
提供统一的时间源,控制游戏时间流速。
#### 职责
- 真实时间 vs 游戏时间分离
- 时间缩放(慢动作、加速)
- 帧间 deltaTime 提供者
- 定时器/冷却器管理
#### 接口
| 接口 | 说明 |
|------|------|
| `deltaTime → float` | 受缩放影响的帧间隔 |
| `unscaledDeltaTime → float` | 不受缩放影响的帧间隔 |
| `timeScale` | 时间缩放倍率(读写) |
| `SetTimer(duration, callback) → timerId` | 设置定时器 |
| `CancelTimer(timerId)` | 取消定时器 |
#### 依赖
#### 验收标准
- [ ] timeScale = 0 时所有依赖 deltaTime 的逻辑冻结
- [ ] 定时器在暂停后恢复时正确续计
- [ ] 时间精度不随游戏运行时长漂移
---
### 4.12 Asset Loader资源加载
#### 目标
统一管理资源的加载、缓存、卸载、引用计数。
#### 职责
- 同步/异步加载
- 引用计数自动回收
- 加载优先级
- 内存监控与阈值卸载
#### 接口
| 接口 | 说明 |
|------|------|
| `LoadAsync<T>(assetPath) → Task<T>` | 异步加载资源 |
| `LoadSync<T>(assetPath) → T` | 同步加载资源 |
| `Release(asset)` | 释放资源引用 |
| `ForceUnloadUnused()` | 强制卸载未使用资源 |
#### 依赖
#### 验收标准
- [ ] 引用归零的资源在下一帧自动卸载
- [ ] 异步加载失败有重试机制
- [ ] 同一资源并发加载只触发一次实际 IO
---
## 5. 术语表
| 术语 | 定义 |
|------|------|
| Game Loop | 游戏主循环,驱动每帧更新的入口 |
| Feature | 自包含的业务特性模块,拥有独立的资源和生命周期 |
| Scene | 游戏场景,可包含一个或多个 Feature |
| Data Class | 按业务域拆分的运行时数据容器,保持纯 C# |
| ViewModel | MVVM 模式中的视图模型,连接 Data 和 UI Panel |
| Panel | UI 面板,由 UI Framework 管理生命周期 |
| Scope | DI 容器的子作用域,随 Feature 创建和销毁 |
| Handle | 资源引用句柄Dispose 时减少引用计数 |
| Config Row | 配置表中的单行数据,必须带有 Id 字段 |
| Tick | 一次逻辑帧更新调用 |

View File

@@ -0,0 +1,239 @@
# Unity Package 开发与 Git 分发指南
## 概述
本文档描述如何将 Unity 代码组织为 Package 结构,并通过 Git 仓库进行版本管理和分发更新。适用于 FlowScope 框架各模块的独立开发与集成。
## Package 标准结构
```text
com.flowscope.xxx/
├── package.json # 必须 — 包的元数据与依赖声明
├── README.md # 包说明
├── CHANGELOG.md # 版本变更记录
├── LICENSE # 许可证
├── Runtime/
│ ├── ExampleScript.cs
│ └── com.flowscope.xxx.Runtime.asmdef
├── Editor/
│ └── com.flowscope.xxx.Editor.asmdef
├── Tests/
│ ├── Runtime/
│ │ └── com.flowscope.xxx.Tests.Runtime.asmdef
│ └── Editor/
│ └── com.flowscope.xxx.Tests.Editor.asmdef
└── Samples~/ # 可选示例(~ 后缀表示在 Package Manager 中隐藏)
└── Demo/
└── ...
```
## package.json 说明
```json
{
"name": "com.flowscope.xxx",
"version": "0.1.0",
"displayName": "FlowScope XXX",
"description": "模块描述",
"unity": "2022.3",
"dependencies": {
"com.flowscope.core": "0.1.0"
},
"author": {
"name": "FlowScope Team"
},
"repository": {
"type": "git",
"url": "https://github.com/flowscope/xxx.git"
}
}
```
关键字段:
| 字段 | 说明 |
|------|------|
| `name` | 反向域名格式,全局唯一 |
| `version` | 语义化版本SemVer |
| `unity` | 最低兼容 Unity 版本 |
| `dependencies` | 该包依赖的其他 Package |
## 开发工作流
### 推荐:在 Unity 项目内开发
直接在目标 Unity 项目的 `Packages/` 目录下创建包文件夹Unity 自动识别,开发体验与 `Assets/` 中写脚本一致。
```text
MyUnityProject/
├── Assets/
│ └── Scenes/
│ └── Test.unity # 测试场景
├── Packages/
│ ├── manifest.json
│ └── com.flowscope.xxx/ ← 在这里开发
│ ├── package.json
│ ├── Runtime/
│ └── Editor/
```
`manifest.json` 中自动生成引用:
```json
{
"dependencies": {
"com.flowscope.xxx": "file:com.flowscope.xxx"
}
}
```
### 迁移已有代码
如果代码已在 `Assets/` 中:
1.`Packages/` 下建好包结构和 `package.json`
2. 将脚本从 `Assets/` **移动**到 `Packages/com.flowscope.xxx/Runtime/`
3. 创建 `.asmdef`Assembly DefinitionPackages 下的脚本不会自动编译到 `Assembly-CSharp.dll`
4. Unity 重新编译,确认引用无误
## Assembly Definition (.asmdef)
Packages 下的代码必须通过 `.asmdef` 显式声明程序集。
**Runtime 层:**
```json
{
"name": "FlowScope.XXX.Runtime",
"rootNamespace": "FlowScope.XXX",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
```
**Editor 层:**
```json
{
"name": "FlowScope.XXX.Editor",
"rootNamespace": "FlowScope.XXX.Editor",
"references": ["FlowScope.XXX.Runtime"],
"includePlatforms": ["Editor"],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
```
## Git 分发与版本管理
### 推送到 Git 仓库
```powershell
cd Packages/com.flowscope.xxx
git init
git add .
git commit -m "初始化 FlowScope XXX Package"
git remote add origin https://github.com/flowscope/xxx.git
git push -u origin main
```
### 在其他项目中引用
修改目标项目的 `Packages/manifest.json`
```json
{
"dependencies": {
"com.flowscope.xxx": "https://github.com/flowscope/xxx.git"
}
}
```
### 版本引用方式
| 方式 | 写法 | 适用场景 |
|------|------|----------|
| 最新 main | `"https://github.com/flowscope/xxx.git"` | 内部开发 |
| 指定版本 | `"https://github.com/flowscope/xxx.git#v1.2.0"` | 正式项目 |
| 指定分支 | `"https://github.com/flowscope/xxx.git#dev"` | 测试分支 |
| 指定 commit | `"https://github.com/flowscope/xxx.git#abc1234"` | 精确锁定 |
| 本地路径 | `"file:../../com.flowscope.xxx"` | 本地联调 |
### 版本发布流程
```powershell
# 开发完成后打 tag
git tag v1.0.0
git push origin v1.0.0
```
其他项目引用指定版本:
```json
"com.flowscope.xxx": "https://github.com/flowscope/xxx.git#v1.0.0"
```
### 更新已安装的 Package
- Unity 编辑器Window → Package Manager → 选择对应包 → 点击 Update
- 手动:删除 `Library/PackageCache/` 中对应缓存后重启 Unity
## .gitignore 注意事项
Package 仓库的 `.gitignore`
```gitignore
# Unity 生成的 meta 文件不要忽略Package 需要)
# 但 Library/ 下的内容不提交
[Ll]ibrary/
[Tt]emp/
[Oo]bj/
[Bb]uild/
[Bb]uilds/
[Ll]ogs/
[Uu]ser[Ss]ettings/
# IDE
.vs/
.vscode/
.idea/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.pidb
*.booproj
```
## FlowScope 模块化策略
根据 FlowScope Core Kernel 的架构原则,各模块独立为 Package
```text
FlowScope/
├── Core/ → com.flowscope.core (纯 C#,无 Unity 依赖)
├── UI/ → com.flowscope.ui MVVM + uGUI 适配)
├── Flow/ → com.flowscope.flow (时间线编排)
├── Events/ → com.flowscope.events (事件流)
├── Config/ → com.flowscope.config (配置加载隔离)
├── Res/ → com.flowscope.res (资源生命周期)
└── Modules/ → 各扩展模块
```
每个模块为独立 Git 仓库,通过 `package.json``dependencies` 声明模块间依赖关系。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
# FlowScope Runtime 使用规范
## Bootstrap
Bootstrap 负责组合运行时依赖,而不是把业务流程写进场景脚本。推荐顺序是创建根 `Container`、注册 Config/Resource/Save/UI/Audio 等服务、注册首个 Feature 的工厂,最后调用 `GameFlow.StartupAsync<TInitialFeature>`
Bootstrap 必须持有生命周期 `CancellationToken`,退出时先保存必要状态,再调用 `GameFlow.ShutdownAsync`,最后释放预加载资源、音频句柄或其他根级缓存。
## Container 注册规则
容器注册应显式、可读、可追踪。稳定服务使用 `RegisterInstance`,带运行时参数或 Feature 私有依赖使用 `RegisterFactory`。不要依赖类型扫描或名称约定来猜测注册关系。
Feature 只从自己的 scope 解析依赖。跨 Feature 的共享服务放在 root containerFeature 内部临时对象放在 Feature scope并随 `FeatureContext` 一起释放。
## Feature 生命周期
`LoadAsync` 只准备数据、ViewModel 和轻量状态;`EnterAsync` 打开 UI、订阅输入或启动表现`ExitAsync` 必须关闭本 Feature 打开的 UI 并释放订阅;`Dispose` 只做最后的托底清理。
不要在 `Dispose` 中启动新的异步工作。需要异步关闭的行为放在 `ExitAsync`
## 资源所有权
P1 的资源入口是 `ResourceService`,后端由 `IResourceBackend` 适配。业务层只持有 `IResourceHandle<T>``IResourceGroup`,不直接调用 Addressables 或样例 backend。
谁创建 handle谁负责释放。预加载资源由 `UIPreloadService` 持有UI 实例关闭时不释放预加载 handleBootstrap 或上层流程退出时调用 `Release<TPanel>``ReleaseAll`
## UI 路径和层级
Panel 类型必须声明 `UIPanelAttribute`。优先显式填写资源路径,例如 `FlowScope/Samples/MainMenuP0/MainMenuPanel`;未填写时才使用 `UI/{PanelNameWithoutSuffix}/Prefab` 约定。
`UIManager` 维护层内 LIFO。`UIScreenNavigator` 只管理它自己 push 的页面,`GoBack` 保留最后一个页面,`Clear` 清空导航器打开的页面。
## 配置 DTO 约束
配置行实现 `IConfigRow``Id` 在同一表内必须唯一。DTO 应保持简单字段或属性不把运行时对象、Unity 组件、服务实例放进配置。
配置加载通过 `IConfigSource` 读取文本,通过 `IConfigParser` 解析。生产环境可接文件或远端来源,测试和样例可使用 `InMemoryConfigSource`
## 存档版本迁移
`JsonSaveSerializer``currentVersion` 大于 1 时会写入版本 envelope。旧版裸 JSON 被视为版本 1并通过 `SaveMigrationRegistry` 迁移到当前版本。
每个迁移实现 `ISaveMigration``Key` 默认使用存档 DTO 类型全名,迁移链必须逐版本连续,例如 1 -> 2 -> 3。
## Addressables 后端接入
生产资源后端使用 `FlowScope.Addressables.AddressablesResourceBackend`。项目需要在 `Packages/manifest.json` 中包含 `com.unity.addressables`,并让 Unity 完成包解析后再运行 PlayMode 全量测试。
业务代码不直接依赖 Addressables API。需要替换后端时只调整 Bootstrap 或平台装配层的 `IResourceBackend` 注册。
## 测试建议
窄单元测试优先覆盖接口契约:重复加载共享、取消不释放他人 handle、配置重复 ID、存档迁移缺链、UI LIFO 和预加载释放。
样例验收至少覆盖启动可见、按钮即时更新、关闭保存、重启恢复、资源释放。Unity Test Runner 全绿前,不要宣布生产化验收完成。

View File

@@ -0,0 +1,350 @@
# Loxodon Framework Core 分析
> 分析对象:<https://github.com/vovgou/loxodon-framework/tree/master/Loxodon.Framework/Assets/LoxodonFramework>
>
> 分析日期2026-06-12
## 结论
Loxodon Framework 的 Core 本质上是一个 **Unity MVVM + DataBinding 运行时核心**,而不是覆盖完整游戏生命周期的游戏框架内核。
它的核心价值集中在:
-`Context``ServiceContainer` 提供全局/局部上下文与轻量服务注册。
-`BindingServiceBundle` 装配 DataBinding 所需的 Parser、Converter、SourceProxy、TargetProxy、Binder。
-`ViewModelBase``ObservableObject`、Observable Collection 支撑 ViewModel 状态变化。
-`UIView``Window``WindowManager` 提供 UI 视图、窗口状态和转场管理。
-`Messenger``Command``Async``Prefs``Localization` 等模块补齐 MVVM UI 开发所需的基础设施。
因此Loxodon 更适合作为 FlowScope 的 **UI / Binding / ServiceBundle 外围模块参考**,不适合作为 FlowScope Kernel 的边界模板。
## Core 模块拆解
### Context
源码位置:
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Contexts/Context.cs>
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Contexts/ApplicationContext.cs>
`Context` 维护静态 `ApplicationContext` 和按 key 注册的多个 `Context`,每个 Context 内部有:
- `IServiceContainer`
- 属性字典
- 可级联查询的 parent context
`ApplicationContext``Context` 基础上补了:
- `IMainLoopExecutor`
- Global Preferences
- User Preferences
这个设计方便应用层快速拿到全局能力,但也说明 Loxodon 的 Core 并不是纯机制内核。它把主线程执行器和 Prefs 这类应用级能力直接放进了全局上下文。
### ServiceContainer
源码位置:
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Services/ServiceContainer.cs>
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Services/AbstractServiceBundle.cs>
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Services/IServiceBundle.cs>
`ServiceContainer` 是轻量服务定位器,支持:
-`Type` 注册对象或 factory。
-`string name` 注册对象或 factory。
- `Resolve<T>()` / `Resolve(string)` 解析服务。
- `Unregister<T>()` / `Unregister(string)` 注销服务。
`ServiceBundle` 是模块装配入口:
```csharp
public interface IServiceBundle
{
void Start();
void Stop();
}
```
`AbstractServiceBundle` 把容器传给 `OnStart` / `OnStop`,由具体 bundle 负责注册和注销一组服务。
这个模式对 FlowScope 有参考价值:模块可以用显式 `Start/Stop` 完成能力注册,避免散落的静态初始化。
### Binding
源码位置:
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Binding/BindingServiceBundle.cs>
- <https://github.com/vovgou/loxodon-framework/tree/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Binding>
Binding 是 Loxodon Core 最厚、最核心的部分。
`BindingServiceBundle` 会装配:
- `PathParser`
- `ExpressionPathFinder`
- `ConverterRegistry`
- `ObjectSourceProxyFactory`
- `SourceProxyFactory`
- `TargetProxyFactory`
- `BindingFactory`
- `StandardBinder`
然后注册:
- `IBinder`
- `IBindingFactory`
- `IConverterRegistry`
- `IExpressionPathFinder`
- `IPathParser`
- `INodeProxyFactory`
- `ISourceProxyFactory`
- `ITargetProxyFactory`
这说明 Loxodon 的 Binding 不是简单的反射赋值,而是一条可扩展管线:
```text
BindingDescription
-> Path / Expression
-> SourceProxy
-> Converter
-> TargetProxy
-> Binding
```
README 也强调该框架围绕 DataBinding 做了性能优化:减少装箱、降低 GC、通过动态委托或静态织入降低反射成本。
### ViewModel / Observable
源码位置:
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/ViewModels/ViewModelBase.cs>
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Observables/ObservableObject.cs>
- <https://github.com/vovgou/loxodon-framework/tree/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Observables>
`ViewModelBase` 继承 `ObservableObject`,核心职责是:
- 封装属性变更。
- 触发 `PropertyChanged`
- 可选通过 `IMessenger` 广播 `PropertyChangedMessage<T>`
典型路径是:
```text
ViewModel property set
-> Set<T>
-> field updated
-> RaisePropertyChanged
-> optional Messenger.Publish(PropertyChangedMessage<T>)
-> Binding updates target UI
```
这是标准 MVVM 的数据驱动 UI 模型。
### View / Window
源码位置:
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Views/UIView.cs>
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Views/Window.cs>
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Views/WindowManager.cs>
`UIView` 负责 Unity UI 基础行为:
- `RectTransform`
- `CanvasGroup`
- `Visibility`
- Enter / Exit Animation
- Enable / Disable event
`Window``UIView` 基础上增加:
- `WindowType`
- `windowPriority`
- `WindowState`
- activated / dismissed / visibility changed event
- state broadcast
`WindowManager` 管理窗口集合、当前窗口、可见窗口和转场执行。
这部分是 UI 框架能力,不是游戏 Feature 生命周期管理。
### Messaging / Command / Async / Prefs
源码位置:
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Messaging/Messenger.cs>
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Commands/RelayCommand.cs>
- <https://github.com/vovgou/loxodon-framework/tree/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Asynchronous>
- <https://github.com/vovgou/loxodon-framework/blob/master/Loxodon.Framework/Assets/LoxodonFramework/Runtime/Framework/Prefs/Preferences.cs>
这些模块服务于 MVVM/UI 工程化:
- `Messenger`:按消息类型和 channel 做发布订阅。
- `RelayCommand`:给 UI 事件绑定命令。
- `AsyncResult` / `ProgressResult` / `CoroutineTask`:把异步、协程、进度结果统一成可等待对象。
- `Preferences`:默认基于 `PlayerPrefs`,支持全局和用户偏好。
这些能力有实用价值,但它们放在 Core 中会让 Core 变厚。对 FlowScope 来说,应当拆成独立模块或适配层,而不是进入 Kernel。
## 架构优点
### 1. MVVM UI 工程化成熟
Binding、ViewModel、Command、Window、Messenger 形成了完整闭环。对于 UI 重、状态同步复杂、需要数据绑定的 Unity 项目Loxodon 能显著减少手写 UI 刷新代码。
### 2. Binding 管线拆得足够细
`SourceProxy``TargetProxy``Converter``PathParser``Binder` 的拆分使绑定系统具备较强扩展性。不同 UI 系统或不同数据源可以通过 proxy/factory 接入,而不是改主流程。
### 3. ServiceBundle 模式简单可控
`Start/Stop` 注册/注销一组服务,适合插件化装配。相比全局静态初始化,这种方式更容易表达模块边界。
### 4. 有性能意识
README 中明确强调减少 value type 装箱、减少 GC、优化绑定性能。这说明 Loxodon 的设计目标不是单纯追求抽象,而是面向 Unity UI 高频更新场景。
## 架构局限
### 1. Core 边界偏厚
`ApplicationContext` 直接提供主线程执行器和 Preferences`Preferences` 默认落到 `PlayerPrefs``Messenger.Default` 也带有全局静态倾向。
这些设计让使用更方便,但会让 Core 逐步吸收平台、存储、消息、UI 等应用级能力。
### 2. 不是完整游戏框架内核
Loxodon Core 没有把以下能力作为主轴:
- 游戏 Feature 生命周期
- 模块启动顺序
- 资源后端边界
- 配置表加载边界
- 存档版本迁移
- 热更新策略
- 场景/流程切换策略
它的插件生态里有 Addressable、Data、XLua、ILRuntime 等扩展,但 Core 本体的主轴仍然是 MVVM/DataBinding。
### 3. 对小团队很省事,对强边界内核有侵蚀风险
如果项目目标是快速做 UI 驱动业务Loxodon 的全局上下文和内置 Prefs 会很方便。
但如果目标是 FlowScope 这种机制型内核,直接照搬会让 Kernel 同时承担 UI、Prefs、Messaging、Async、Binding 等职责,边界会变得越来越难控。
## 对 FlowScope 的借鉴建议
### 可以借鉴
1. **ServiceBundle 的模块装配方式**
FlowScope 的 P1/P2 模块可以采用类似显式装配模式:
```text
ModuleBundle.Start(container)
-> register contracts
-> register default implementations
-> register adapters
ModuleBundle.Stop(container)
-> unregister contracts
-> release module-owned state
```
2. **Binding 管线的分层思想**
如果 FlowScope 后续需要 UI Binding 或 Editor Binding可以参考 Loxodon 的拆分:
```text
Source
Target
Path
Converter
Binder
BindingContext
```
但这应该作为 UI 扩展模块,而不是 Kernel 机制。
3. **ViewModelBase 的属性变更封装**
`Set<T>` 模式可以降低 ViewModel 代码重复,也能统一 PropertyChanged 和消息广播策略。
4. **Window 状态模型**
`Window` / `WindowManager` 对弹窗、页面、转场、可见性和激活状态有参考价值,可用于 FlowScope UI 模块设计。
### 不建议照搬
1. **不要把 Prefs 放进 Kernel**
FlowScope 的 Save / Prefs / PlayerPrefs 应保持在 Save 模块或 Unity Adapter 中。
2. **不要把 Messenger 作为 Kernel 默认通信机制**
消息总线容易变成隐式耦合通道。Kernel 应保持 Feature 生命周期和 Slot/Policy 机制清晰,事件系统应作为 P1/P2 模块单独设计。
3. **不要把 UI Window 放进 Kernel**
UI 是 Feature/Module 的外围能力。Kernel 只需要管理 Feature 的启动、替换、停止策略,不应知道 Window 栈。
4. **不要用全局 ApplicationContext 承载所有能力**
FlowScope 更适合显式 Bootstrap + Container + FeatureContext。全局上下文可以作为上层便利入口但不应成为 Kernel 的事实中心。
## 与 FlowScope Kernel 边界的关系
FlowScope 当前更适合保持:
```text
Kernel
-> Container
-> AppKernel
-> StartupPipeline
-> FeatureHost
-> FeatureInstance
-> FeatureContext
-> FeatureLifetime
-> FeatureSlot
-> FeaturePolicy
```
Loxodon 的能力更适合放在:
```text
FlowScope.UI
-> ViewModel
-> Binding
-> Window / Panel
-> Command
FlowScope.Events
-> Messenger / EventBus
FlowScope.Save.Unity
-> PlayerPrefs adapter
FlowScope.Runtime.Extensions
-> ServiceBundle helpers
```
换句话说Loxodon 对 FlowScope 的启发是:**外围模块可以做厚Kernel 必须保持薄。**
## 最终判断
Loxodon Framework 的 Core 是一个成熟的 Unity MVVM/UI 运行时。它适合解决 UI 数据绑定、ViewModel 状态传播、窗口管理和 UI 事件命令化问题。
但它不是 FlowScope Kernel 的直接模板。对 FlowScope 来说,最值得吸收的是:
- `ServiceBundle` 式模块装配。
- Binding 管线拆分方式。
- ViewModel 属性变更封装。
- Window 状态管理经验。
最需要避免的是:
- 把 Prefs、UI、Messaging、Binding 一起塞进 Core。
- 用全局上下文替代显式生命周期。
- 让 Kernel 从机制层滑向应用便利层。

View File

@@ -0,0 +1,33 @@
# MainMenuP0 P1 样例验收
## 打开场景
在 Unity 中打开:
```text
My project/Assets/FlowScope/Samples/MainMenuP0/Scenes/MainMenuP0.unity
```
进入 Play Mode 后,`GameBootstrap` 会注册 P1 运行时服务,预加载 `MainMenuPanel`,并启动 `MainMenuFeature`
## 预期画面
画面中应出现主菜单面板、Gold 数字文本和一个递增按钮。若场景或 prefab 引用缺失,样例仍会创建运行时 fallback UI但人工验收应优先检查场景和 prefab 引用是否正确。
## Gold 递增
按钮点击调用 `MainMenuViewModel.IncrementGold`,立即修改 `PlayerData.Gold`,并通过面板订阅同步刷新 Gold 文本。点击一次后,文本应从 `0` 变为 `1`
## 保存与恢复
`ShutdownAsync` 会把 `PlayerData` 写入 `main-menu-player` 存档。再次启动样例时,`SaveService` 会读取同一 key 并恢复 Gold。
P1 样例使用 `JsonSaveSerializer` 版本 envelope旧版裸 JSON 会通过 no-op 的 1 -> 2 迁移链继续读取。
## 资源释放
样例资源通过 `ResourceService` 和样例 `IResourceBackend` 加载。`UIPreloadService` 持有预加载 handleFeature 退出时关闭面板实例Bootstrap 关闭时调用 `ReleaseAll` 释放预加载资源。
## Console 输出
验收期间不应出现未预期的 Error 或 Exception。Unity 引用版本冲突警告如果只出现在外部 `dotnet build` 中,属于当前 Unity 生成项目的已知警告,不作为样例失败条件。

View File

@@ -0,0 +1,501 @@
# QFramework Core 细读与 FlowScope 借鉴边界
更新时间2026-06-12
参考源码:<https://github.com/liangxiegame/QFramework/blob/master/QFramework.cs>
本文分析 QFramework 单文件 Core 的架构设计、适用范围、优缺点,以及它对 FlowScope Kernel / CoreModules 的可借鉴部分。本文不是选型结论,也不是实现计划;它用于后续讨论 FlowScope 业务架构层、事件层、状态层时作为参考。
## 1. 总结
QFramework Core 的价值不在于提供完整商业游戏运行时,而在于用很少的类型固定 Unity 业务代码的依赖方向。
它的核心模型可以概括为:
```text
Architecture<T>
-> IOCContainer
-> Model / System / Utility
-> Command / Query
-> Event / BindableProperty
Controller / MonoBehaviour
-> SendCommand / SendQuery
-> GetModel / GetSystem
-> RegisterEvent
```
QFramework Core 解决的是 Unity 项目里常见的“表现层乱改状态、MonoBehaviour 互相引用、业务逻辑没有入口、通知链路散落”的问题。它不是资源框架、UI 框架、热更框架、构建框架,也不负责 Feature、场景、流程或模块生命周期编排。
对 FlowScope 来说QFramework Core 最值得借鉴的是:
- 状态变更必须有入口。
- 上层可以调用下层,下层不直接引用上层。
- 下层通过事件或可观察状态通知上层。
- Command / Query 是短生命周期行为对象,不承载长期状态。
- 用能力接口限制对象可访问的架构能力。
不适合直接照搬的是:
- 静态全局 `Architecture<T>.Interface` 入口。
- 强制 `Model / System / Utility / Command` 成为 Kernel 业务分层。
- 没有 RootScope / FeatureScope 的单容器模型。
- 没有 Feature 级生命周期和释放边界。
- 把 Event / BindableProperty 作为所有项目默认基础设施。
## 2. Core 范围
QFramework.cs 里的核心组成大致分为以下几组。
| 组 | 类型 | 职责 |
| --- | --- | --- |
| 架构根 | `IArchitecture`, `Architecture<T>` | 注册和获取 Model / System / Utility执行 Command / Query发送和注册事件 |
| 表现层协议 | `IController` | 通常由 MonoBehaviour 实现,负责接入架构,不直接持有业务状态 |
| 业务逻辑层 | `ISystem`, `AbstractSystem` | 放跨表现层共享的业务逻辑 |
| 数据层 | `IModel`, `AbstractModel` | 放状态和数据操作 |
| 工具层 | `IUtility` | 放外部能力适配例如存储、SDK、序列化 |
| 行为对象 | `ICommand`, `ICommand<TResult>`, `IQuery<TResult>` | 封装写操作和读操作 |
| 能力接口 | `ICanGetModel`, `ICanSendCommand` 等 | 用接口组合限制对象能做什么 |
| 事件系统 | `TypeEventSystem`, `EasyEvent` | 按类型注册、发送、反注册事件 |
| 可绑定状态 | `BindableProperty<T>` | 值变化时通知订阅者 |
| 容器 | `IOCContainer` | 按类型注册和解析实例 |
这个范围说明 QFramework Core 更接近“业务代码架构层”,而不是“应用 Kernel”。它把代码写法约束住但不管理完整应用运行期。
## 3. Architecture<T>
`Architecture<T>` 是 QFramework 的架构根。它使用静态字段保存当前架构实例,`Interface` 首次访问时触发初始化。
典型启动过程:
1. 创建 `T : Architecture<T>` 实例。
2. 调用子类实现的 `Init()`
3.`Init()` 中注册 Model、System、Utility。
4. 执行 `OnRegisterPatch`
5. 初始化所有未初始化的 Model。
6. 初始化所有未初始化的 System。
7. 将架构标记为已初始化。
反初始化过程:
1. 调用 `OnDeinit()`
2. 对已初始化的 System 调用 `Deinit()`
3. 对已初始化的 Model 调用 `Deinit()`
4. 清空容器。
5. 清空静态架构实例。
这个设计很适合中小项目:入口少、初始化简单、读源码很快。它的问题也同样明确:全局静态入口会自然演化成 Service Locator业务对象很容易绕开组合根直接拿依赖。
FlowScope 不应该照搬这个入口。FlowScope 的 Kernel 需要显式 `AppKernel``RootScope``StartupPipeline``FeatureHost`,让启动顺序、依赖边界和关闭顺序都可测试、可替换、可释放。
## 4. Model / System / Utility
QFramework 的分层规则可以简化理解为:
```text
Controller -> Command / Query -> System / Model -> Utility
Model / System -> Event / BindableProperty -> Controller
```
### Model
`IModel` 表示数据层。它可以:
- 持有状态。
- 调用 Utility。
- 发送事件。
- 初始化和反初始化。
它不应该直接引用 Controller也不应该直接操作 UI。
### System
`ISystem` 表示共享业务逻辑层。它可以:
- 获取 Model。
- 获取 Utility。
- 获取其他 System。
- 注册事件。
- 发送事件。
- 初始化和反初始化。
System 适合放跨 UI、跨场景、跨表现层复用的业务逻辑。它比 Model 更偏行为,比 Controller 更偏领域。
### Utility
`IUtility` 是最薄的一层,只是一个标记接口。它通常承载外部能力,例如:
- 本地存储。
- 网络 SDK。
- 平台 SDK。
- JSON 序列化。
- 时间服务。
Utility 本身不被强制注入 Architecture这也意味着它更像纯工具或外部适配器。
### 对 FlowScope 的启发
FlowScope Kernel 不应该规定 `Model / System / Utility`。这是项目业务架构选择,不是 Kernel 机制。
但 FlowScope 可以在 CoreModules 或推荐模板里提供类似规则:
- Feature 内部状态归 Feature 自己或 Feature-scoped state service。
- 跨 Feature 长期状态归 Root-scoped service。
- 外部能力通过接口注册到 RootScope。
- UI 和 MonoBehaviour 不直接写长期状态,而是调用 UseCase、Command 或 Service 方法。
## 5. Command / Query
QFramework 用 Command 作为状态变更入口,用 Query 作为读操作入口。
Command 的特征:
- 短生命周期。
- 执行前被注入 Architecture。
- 可获取 Model / System / Utility。
- 可发送事件。
- 可继续发送其他 Command 或 Query。
- 不保存长期状态。
Query 的特征:
- 短生命周期。
- 执行前被注入 Architecture。
- 主要读取 Model / System。
- 返回查询结果。
这个设计的好处是把“写操作”从 Controller 中拿出来。Controller 不再到处写:
```csharp
model.Coins.Value += 10;
```
而是写:
```csharp
this.SendCommand(new AddCoinCommand(10));
```
这样状态变化可以被搜索、测试和复用。
对 FlowScope 来说,这个思想很值得吸收,但名称和形态不应进 Kernel。Kernel 不应该强制所有状态变更都叫 Command。项目可以选择
- `Command`
- `UseCase`
- `Action`
- `Service Method`
- `Application Service`
Kernel 只需要保证依赖边界和生命周期,不需要规定业务行为对象的命名体系。
## 6. 能力接口组合
QFramework 一个很好的设计点是能力接口组合。
例如:
- `ICanGetModel`
- `ICanGetSystem`
- `ICanGetUtility`
- `ICanSendCommand`
- `ICanSendEvent`
- `ICanRegisterEvent`
- `ICanSendQuery`
这些接口本身几乎没有实现,实际能力通过扩展方法提供。对象只有实现了对应接口,才获得对应扩展方法。
这带来的好处是:
- Controller 能做的事和 Model 能做的事不同。
- Model 可以发事件,但不能随意发 Command。
- Command 可以访问 Model/System/Utility但不变成长期服务。
- 架构规则体现在类型系统里,而不是只写在文档里。
这个方向对 FlowScope 有参考价值。FlowScope 如果后续做业务架构层,可以避免提供一个万能 `FeatureContext`。更好的方式是拆小能力:
```text
ICanResolveService
ICanPublishEvent
ICanReadState
ICanExecuteUseCase
ICanRegisterLifetime
```
但这些应属于 P1/P2 业务层或扩展层。Kernel 的 `FeatureContext` 仍应保持最小,只暴露 Scope、Lifetime、CancellationToken、Handle 等机制概念。
## 7. TypeEventSystem
QFramework 的 `TypeEventSystem` 是一个按事件类型分发的轻量事件系统。
它的核心机制:
```text
TypeEventSystem
-> EasyEvents
-> Dictionary<Type, IEasyEvent>
-> EasyEvent<T>
```
发送事件时按 `EasyEvent<T>` 类型取出事件对象并触发。注册事件时如果没有对应事件对象,就创建一个。
优点:
- 使用简单。
- 类型安全。
- 适合 UI 刷新和轻量业务通知。
- 反注册模型清晰。
风险:
- 没有作用域隔离。
- 没有事件优先级。
- 没有异步派发协议。
- 没有错误隔离。
- 没有事件链路追踪。
- 大型项目里容易变成全局黑箱。
FlowScope 可以借鉴“下层发事件,上层订阅”的方向,但 EventBus 不应进入 Kernel 主依赖。更合理的位置是 `FlowScope.Events` 或 P1 Events 模块:
```text
Kernel
不知道 EventBus
P1 Events
注册 IEventBus 到 RootScope
提供 FeatureLifetime 可登记的订阅句柄
提供同步或异步事件策略
```
这样 Feature 可以使用事件,但 Kernel 不被事件模型绑死。
## 8. BindableProperty<T>
`BindableProperty<T>` 是一个可观察值对象。它在 `Value` 变化时触发 `EasyEvent<T>`,并提供:
- `Register`
- `RegisterWithInitValue`
- `UnRegister`
- `SetValueWithoutEvent`
- 自定义比较器
它适合表达 UI 绑定和简单状态监听,例如金币、生命值、进度条、开关状态。
优点:
- 简单直接。
- UI 订阅体验好。
- `RegisterWithInitValue` 避免 UI 初始刷新遗漏。
- 对中小项目足够实用。
风险:
- 比较器是静态泛型级别配置,使用时要小心全局影响。
- 复杂状态容易被拆成大量可绑定字段,导致状态事务边界不清。
- 不适合表达复杂异步流、错误流、完成流。
- 如果到处暴露可写 `BindableProperty<T>`,仍然会出现状态乱改。
FlowScope 可以把类似能力放到状态或 UI 扩展模块,但不应放进 Kernel。Kernel 只负责 Feature 生命周期;状态响应式属于业务层或表现层。
## 9. IOCContainer
QFramework 的 `IOCContainer` 很小:
- `Register<T>(T instance)``typeof(T)` 注册。
- `Get<T>()``typeof(T)` 获取。
- `GetInstancesByType<T>()` 枚举所有兼容类型实例。
- `Clear()` 清空。
优点:
- 极易理解。
- 调试成本低。
- 足够支撑 QFramework 的 Model/System/Utility 注册。
不足:
- 没有构造函数注入。
- 没有 Scope。
- 没有生命周期释放。
- 没有 `IDisposable` / `IAsyncDisposable` 管理。
- 没有多绑定。
- 没有 named/keyed service。
- 没有循环依赖检测。
- 没有线程安全。
FlowScope 可以借鉴“按类型注册和解析”的最小体验,但实现必须比 QFramework 的容器多一层生命周期能力:
```text
RootScope
长生命周期服务
FeatureScope
Feature 实例和本次启动参数
先查本地,再回退 RootScope
FeatureLifetime
释放运行期句柄、订阅、资源组、临时对象
```
这也是 FlowScope 不能直接采用 QFramework IOC 的核心原因。
## 10. 生命周期对比
| 维度 | QFramework Core | FlowScope Kernel 目标 |
| --- | --- | --- |
| 应用入口 | 静态 `Architecture<T>.Interface` | 显式 `AppKernel` |
| 启动流程 | 首次访问时初始化 | `StartupPipeline` 顺序执行 |
| 依赖容器 | 单个 `IOCContainer` | `RootScope + FeatureScope` |
| 业务运行单元 | Model/System 长驻 | Feature 实例 |
| Feature 生命周期 | 不负责 | `LoadAsync / EnterAsync / ExitAsync` |
| 释放边界 | Model/System Deinit容器 Clear | `FeatureLifetime + FeatureScope + RootScope` |
| 异步关闭 | 不突出 | Kernel 必须处理 async stop/shutdown |
| 运行位置 | 不负责 | `FeatureSlot + FeaturePolicy` |
QFramework 的生命周期适合“一个游戏一个 Architecture若干长驻 Model/System”。FlowScope 的目标是“一个应用 Kernel 管多个 Feature 实例,每个 Feature 有独立依赖边界和释放边界”。
这两个方向不冲突但层级不同。QFramework 更像可选的业务组织方式FlowScope Kernel 更像运行机制。
## 11. 与完整框架的横向比较
| 维度 | QFramework Core | UnityGameFramework | ET Framework | TinaX | FlowScope 应吸收 |
| --- | --- | --- | --- | --- | --- |
| 核心定位 | 轻量业务分层 | 客户端模块全家桶 | 双端大型联网运行时 | 服务化 Package 框架 | 小 Kernel + 可选模块 |
| 启动入口 | Architecture | GameEntry / Procedure | Scene / Fiber / Actor | Core service bootstrap | AppKernel / StartupPipeline |
| 资源系统 | Core 不负责 | 内置 Resource | 可接入包和热更链路 | VFS | P1 Resources 模块 |
| UI | Core 不负责UIKit 在工具生态 | 内置 UI 模块 | 通常通过包扩展 | UIKit | P1 UI 模块 |
| 事件 | TypeEventSystem | Event 模块 | 消息/事件体系 | Event/System service | P1 Events 模块 |
| 状态变更 | Command | 模块 API / Procedure | Entity/System/消息 | Service API | 可选 Command/UseCase |
| 工程流水线 | 不负责 | 部分覆盖 | 强工具链 | Package 化 | Tools/Package 后续模块 |
QFramework 在这张表里的优势是轻量和清晰。它不应该被拿来和 UGF 的资源/UI/Procedure 覆盖度硬比,也不应该被要求承担 BDFramework 的热更构建流水线职责。
## 12. 对 FlowScope 的建议
### 12.1 Kernel 不吸收 QFramework 分层
FlowScope Kernel 继续保持:
```text
AppKernel
StartupPipeline
FeatureHost
Container
FeatureLifetime
FeatureSlot
FeaturePolicy
IFeature
FeatureContext
FeatureHandle
```
Kernel 不加入:
- `IModel`
- `ISystem`
- `IUtility`
- `ICommand`
- `IQuery`
- `BindableProperty`
- `TypeEventSystem`
- `IController`
原因是这些属于业务架构风格,不是 Feature 编排机制。
### 12.2 CoreModules 可吸收状态变更规则
FlowScope 可以在后续 CoreModules 或 template 中提供推荐规则:
```text
View / MonoBehaviour
-> Command / UseCase / Service Method
-> State Service / Feature Local State
-> Event / Observable
-> View / Presenter / Controller
```
这能吸收 QFramework 的优点,同时避免 Kernel 被业务范式锁死。
### 12.3 Events 模块可参考 TypeEventSystem但要补足边界
如果 FlowScope 做 P1 Events不建议只复制 `TypeEventSystem`。至少要明确:
- 事件作用域Root-scoped 还是 Feature-scoped。
- 订阅句柄如何进入 FeatureLifetime。
- 事件处理异常是否聚合、记录或继续派发。
- 是否支持异步事件。
- 是否允许跨 Feature 事件。
- 是否提供调试追踪。
QFramework 的 TypeEventSystem 可作为最小同步事件模型参考,但不是最终模块规格。
### 12.4 State 模块可参考 BindableProperty但不要裸露可写状态
如果 FlowScope 提供类似 BindableProperty 的能力,建议区分:
```text
IReadonlyState<T>
IMutableState<T>
StateWriter<T>
StateChanged<T>
```
UI 层优先拿只读接口,写入通过 UseCase/Command/Service。这样比直接把 `BindableProperty<T>` 暴露给所有人更稳。
### 12.5 业务模板可以提供 QFramework 风格
可以考虑后续提供一个可选模板:
```text
FlowScope.BusinessArchitecture
- ICommand
- ICommand<TResult>
- IQuery<TResult>
- IUseCase
- IStateService
- IEventPublisher
```
但这应是项目选择,不是 Kernel 强制。
## 13. 风险清单
如果直接照搬 QFramework Core 到 FlowScope主要风险是
1. Kernel 变成业务架构框架,失去机制最小化。
2. `Architecture<T>.Interface``RootScope` 形成双全局入口。
3. `IOCContainer` 无法表达 FeatureScope 生命周期。
4. Command/Event 进入 Kernel 后P1/P2 模块会被迫统一业务风格。
5. EventBus 变成隐式跨 Feature 通道,削弱 FeatureHost 的显式编排边界。
6. BindableProperty 进入底层后UI/状态响应式会反向污染 Kernel。
规避方式:
- Kernel 只保留生命周期、依赖边界、Feature 编排。
- QFramework 的分层规则只进入文档、模板或可选模块。
- Event/State/Command 都作为 CoreModules 或扩展层设计。
- FeatureContext 不提供全能业务能力。
## 14. 结论
QFramework Core 是一个优秀的轻量 Unity 业务架构样本。它用很少代码表达了三条关键规则:
- 上层驱动下层。
- 状态变更有入口。
- 下层通过通知影响上层。
FlowScope 应吸收这些规则,但不能把 QFramework 的 Core 形态直接变成自己的 Kernel。FlowScope Kernel 的目标更底层:它负责应用启动、依赖 Scope、Feature 生命周期、运行槽和释放链。QFramework 风格更适合作为 FlowScope 的可选业务层、示例模板或 P1/P2 模块设计参考。
最终边界建议:
```text
FlowScope.Kernel
只负责机制
FlowScope.CoreModules
提供 Events / State / Config / Save / Resources / UI 等模块
FlowScope.BusinessTemplate.QFrameworkLike
可选提供 Command / Query / State / Event 的业务写法参考
```
这样既能保留 QFramework 的轻量分层优点,又不会牺牲 FlowScope 当前“Kernel tiny, modules optional”的主方向。

View File

@@ -0,0 +1,95 @@
# P0-9: IAudioService 需求详细文档
## 对齐说明
本文档以 `docs/requirements/p0-requirements-set.md` 为准。P0 音频系统提供基础 BGM/SFX 能力和轻量 `IAudioHandle`,但不做完整音频框架。
## 目标
提供基础音频播放能力,包括 BGM 播放/停止、SFX 播放、音量控制、静音、简单 SFX AudioSource 池和 BGM 淡入淡出。
## 接口
```csharp
public interface IAudioService
{
IAudioHandle PlayBgm(string key, bool loop = true, float fadeIn = 0f);
IAudioHandle PlaySfx(string key);
void StopBgm(float fadeOut = 0f);
void SetBgmVolume(float volume);
void SetSfxVolume(float volume);
void SetMute(bool mute);
}
public interface IAudioHandle
{
void Stop(float fadeOut = 0f);
bool IsPlaying { get; }
}
```
## 行为规则
- 同一时间只有一首 BGM。
- 播放新 BGM 时停止旧 BGM。
- `fadeIn``fadeOut` 使用线性插值。
- SFX 可同时播放多个。
- SFX 使用简单 AudioSource 池。
- 音量参数 clamp 到 `[0, 1]`
- 最终音量 = 通道音量 * 静音系数。
- 音频资源通过 `IResourceService` 加载。
- 播放失败时抛异常并包含 key。
## SFX 池
```csharp
public sealed class AudioServiceConfig
{
public int SfxPoolSize = 10;
public bool ReuseOldestWhenExhausted = true;
}
```
规则:
- 池未满时创建或复用 AudioSource。
- 池耗尽且允许复用时,复用最早播放完毕或最旧的 AudioSource。
- 池耗尽且不允许复用时,记录警告并跳过本次 SFX。
## 暂不做
- 单个音频 Pause/Resume。
- 单个音频 Volume。
- AudioMixer 分组。
- 3D 空间音频。
- 动态音乐。
- 语音/对话系统。
- 音频配置表。
- 编辑器音频检查工具。
## 验收标准
| # | 标准 | 通过条件 |
|---|------|---------|
| 1 | BGM 播放 | PlayBgm 后 BGM 正常播放 |
| 2 | BGM 切换 | 播放新 BGM 时旧 BGM 停止 |
| 3 | BGM 淡入 | fadeIn > 0 时线性渐入 |
| 4 | BGM 停止 | StopBgm 后 BGM 停止 |
| 5 | BGM 淡出 | fadeOut > 0 时线性渐出后停止 |
| 6 | SFX 播放 | PlaySfx 后音效播放 |
| 7 | SFX 并发 | 多个 SFX 可同时播放 |
| 8 | SFX 池 | 多次播放不无限创建 AudioSource |
| 9 | Handle Stop | handle.Stop 后对应音频停止 |
| 10 | IsPlaying | 播放中 true停止后 false |
| 11 | 音量 clamp | 超出范围时自动 clamp |
| 12 | 静音 | SetMute(true) 后所有音频静音 |
| 13 | 加载失败 | 不存在 key 抛异常并包含 key |
## 依赖关系
```text
IAudioService
├── IResourceService
├── Unity AudioSource / AudioClip
└── 可选 MonoBehaviour Update/Coroutine 驱动 fade
```

View File

@@ -0,0 +1,99 @@
# P0-5: IConfigProvider 需求详细文档
## 对齐说明
本文档以 `docs/requirements/p0-requirements-set.md` 为准。P0 配置系统只交付 JSON 强类型读取CSV、Luban、ScriptableObject、远程配置、热重载和模块化加载全部放到 P1/P2 插件化扩展。
## 目标
提供最小可用的 JSON 配置加载和查询接口,支撑 P0 Sample 和休闲游戏基础配置读取。
## 接口
```csharp
public interface IConfigProvider
{
Task LoadAllAsync(CancellationToken cancellationToken);
T Get<T>(int id) where T : class, IConfigRow;
IReadOnlyList<T> GetAll<T>() where T : class, IConfigRow;
}
public interface IConfigRow
{
int Id { get; }
}
```
## 功能需求
- 启动时通过 `LoadAllAsync` 加载全部 JSON 配置。
- 配置行必须实现 `IConfigRow`
- 查询使用泛型类型 + `Id`
- `Get<T>(missingId)` 返回 null。
- `GetAll<T>()` 返回只读列表;未加载该类型时返回空列表。
- 同一配置类型内重复 Id 必须抛异常。
- JSON 格式错误必须抛异常并包含文件名。
## 配置文件约定
P0 默认采用约定路径,具体路径可由实现固定,例如:
```text
Assets/Configs/
├── weapons.json
├── levels.json
└── settings.json
```
每个 JSON 文件对应一种配置行类型。类型和文件的映射可以在 GameBootstrap 中显式注册P0 不要求目录扫描和自动类型发现。
## 暂不做
- 字符串模块名查询。
- 混合查询模式。
- CSV。
- Luban。
- ScriptableObject 配置。
- 按模块加载/卸载。
- 远程配置。
- 热重载。
- 字段范围校验和引用完整性校验。
- 配置编辑器工具。
## 扩展方向
P1/P2 通过以下接口扩展,不修改业务调用:
```csharp
public interface IConfigParser
{
string Format { get; }
IReadOnlyList<T> Parse<T>(string content) where T : class, IConfigRow;
}
public interface IConfigSource
{
Task<string> LoadTextAsync(string key, CancellationToken cancellationToken);
}
```
## 验收标准
| # | 标准 | 通过条件 |
|---|------|---------|
| 1 | JSON 加载 | `LoadAllAsync` 后配置可查询 |
| 2 | 按 Id 查询 | `Get<WeaponConfig>(101)` 返回正确对象 |
| 3 | 不存在 Id | 返回 null不抛异常 |
| 4 | 获取列表 | `GetAll<WeaponConfig>()` 返回全部配置 |
| 5 | 重复 Id | 加载时抛异常并包含重复 Id |
| 6 | 格式错误 | 抛异常并包含文件名 |
| 7 | 取消加载 | CancellationToken 取消时抛 `OperationCanceledException` |
## 依赖关系
```text
IConfigProvider
├── JSON 库
├── 可选 IResourceService 或文件读取适配
└── 纯 C# 配置行类型
```

View File

@@ -0,0 +1,129 @@
# P0-1: Container 需求详细文档
## 对齐说明
本文档以 `docs/requirements/p0-requirements-set.md` 为准。P0 允许 DI 自动注入进入首版,但必须保持 Container 核心轻量核心只负责注册、解析、作用域和释放Source Generator 与运行时反射只是生成工厂函数的适配层。
## 目标
提供 Game Core 的基础依赖容器支持显式工厂注册、Attribute + Source Generator 自动注入、Attribute + 运行时反射降级、子作用域和可预测释放。
## P0 功能需求
### 1. 显式注册
```csharp
public sealed class Container : IDisposable
{
public void RegisterInstance<T>(T instance);
public void RegisterFactory<T>(Func<Container, T> factory);
public T Resolve<T>();
public bool TryResolve<T>(out T value);
public Container CreateScope();
public void Dispose();
}
```
规则:
- `RegisterInstance<T>` 注册外部实例Container 不负责释放。
- `RegisterFactory<T>` 首次 Resolve 时创建实例,后续返回同一实例。
- 工厂创建且实现 `IDisposable` 的实例由当前 scope 释放。
- 子 scope 可访问父 scope 注册。
- 子 scope 可覆盖父 scope 注册,且不污染父 scope。
- 已 Dispose 的 scope 调用 Register/Resolve 必须抛 `ObjectDisposedException`
### 2. Attribute 自动注入
```csharp
[AttributeUsage(AttributeTargets.Class)]
public sealed class InjectableAttribute : Attribute
{
}
```
```csharp
public sealed class Container : IDisposable
{
public void RegisterType<TInterface, TImplementation>()
where TImplementation : TInterface;
public void RegisterType<TImplementation>();
public void RegisterAssembly();
}
```
规则:
- 自动注入只支持构造函数注入。
- 只允许一个 public 构造函数。
- 构造函数参数从当前 Container 递归 Resolve。
- 循环依赖必须检测,并输出完整依赖链。
- `[Inject]` 属性注入和方法注入不进入 P0。
### 3. Source Generator 推荐路径
Source Generator 是 P0 推荐自动注入路径。
要求:
- 编译期扫描 `[Injectable]` 类型。
- 为每个 Injectable 类型生成等价于手写工厂的静态工厂方法。
- 编译期检查构造函数依赖是否可解析,无法确认时发出诊断。
- Generator 程序集与 Runtime 程序集分离。
- 未启用 Source Generator 时,显式工厂模式仍可独立使用。
### 4. 运行时反射降级路径
运行时反射是 P0 可选降级路径。
要求:
- 仅在启用反射模式时使用。
- 反射只在首次 Resolve 时构造并缓存结果。
- 行为必须与 Source Generator 模式一致。
- IL2CPP/AOT 风险需要在文档中明确提示。
### 5. 释放规则
- 当前 scope Dispose 时按创建逆序释放本 scope 管理的 `IDisposable`
- 父 scope Dispose 时,先释放未释放的子 scope。
- `Dispose()` 可重复调用,后续调用静默忽略。
- 某个实例 Dispose 抛异常时,记录错误并继续释放其余实例。
## 暂不做
- `[Inject]` 属性注入。
- `[Inject]` 方法注入。
- Transient / Scoped / Singleton 多生命周期模式。
- 开放泛型注册。
- 装饰器注册。
- 条件注册。
- 运行时动态切换 DI 模式。
## 验收标准
| # | 标准 | 通过条件 |
|---|------|---------|
| 1 | 显式实例注册 | `RegisterInstance``Resolve` 返回同一实例 |
| 2 | 显式工厂注册 | `RegisterFactory` 首次 Resolve 创建实例,后续复用 |
| 3 | Source Generator 自动注入 | `[Injectable]` 类型可通过 `RegisterType` 正常解析 |
| 4 | 运行时反射自动注入 | 反射模式下行为与 Source Generator 一致 |
| 5 | 递归解析 | A 依赖 B、B 依赖 C 时 Resolve A 可自动解析整条链 |
| 6 | 循环依赖检测 | A -> B -> A 抛异常,错误包含依赖链 |
| 7 | 子 scope 读取父级 | 子 scope 可 Resolve 父 scope 注册 |
| 8 | 子 scope 覆盖父级 | 子 scope 覆盖后父 scope 不受影响 |
| 9 | 逆序释放 | 多个工厂实例按创建逆序 Dispose |
| 10 | 外部实例不释放 | `RegisterInstance` 传入的实例不由 Container Dispose |
| 11 | 重复 Dispose | 第二次 Dispose 不重复释放、不抛异常 |
| 12 | 已释放 scope 防护 | 已 Dispose scope 调用 Register/Resolve 抛 `ObjectDisposedException` |
## 依赖关系
```text
Container Runtime
├── 纯 C#,不依赖 Unity API
├── 不依赖 R3
├── Source Generator 适配层依赖 Roslyn
└── Reflection 适配层依赖 System.Reflection
```

View File

@@ -0,0 +1,88 @@
# P0-4: R3 + Data 类规范需求详细文档
## 对齐说明
本文档以 `docs/requirements/p0-requirements-set.md` 为准。P0 使用 R3 作为响应式基础,不自研响应式系统,不做 UI 自动绑定。
## 目标
建立游戏运行时 Data 的组织方式、响应式状态表达、ViewModel 访问规则和存档序列化约定。
## Data 类规范
```csharp
public sealed class PlayerData
{
public ReactiveProperty<int> Gold { get; } = new(0);
public ReactiveProperty<int> Level { get; } = new(1);
}
```
规则:
- 一个 Data 类对应一个业务域。
- Data 类注册在全局 Container 中。
- Data 类保持纯 C#,不依赖 Unity API。
- Data 类不持有 View 或 ViewModel 引用。
- Data 类不包含复杂业务流程逻辑。
- Data 类之间不互相直接引用。
- Data 类使用 `ReactiveProperty<T>``ReactiveCollection<T>` 表达可观察状态。
## ViewModel 规则
- ViewModel 可以引用 Data。
- ViewModel 负责 UI 表现逻辑和用户操作逻辑。
- ViewModel 不持有 MonoBehaviour 或具体 View 引用。
- ViewModel 订阅必须加入自己的 `CompositeDisposable`,或加入 `FeatureContext.Disposables`
## 序列化规则
- `ReactiveProperty<T>` 序列化为 `.Value`
- `ReactiveCollection<T>` 序列化为 JSON 数组。
- 反序列化到已有 Data 实例时,更新 `.Value` 或集合内容,不替换 Data 实例。
- 反序列化类型不匹配时抛异常并包含字段名。
## 事件流规则
P0 允许使用 R3 `Subject<T>` 做 Feature 内或明确归属的轻量事件流。
规则:
- Subject 生命周期必须有明确所有者。
- Feature 内 Subject 随 Feature Dispose 释放。
- 跨 Feature 持久状态优先使用 Data。
- 不建立全局 Subject 池。
- 不实现独立 EventBus。
## 暂不做
- 自研响应式系统。
- UI 自动绑定框架。
- 全局 EventBus。
- Data 类 Inspector 可视化。
- 订阅泄漏 Analyzer。
- Data 类之间的复杂关系建模。
## 验收标准
| # | 标准 | 通过条件 |
|---|------|---------|
| 1 | Data 创建 | Data 类用 ReactiveProperty 定义字段 |
| 2 | 全局共享 | 两个 ViewModel Resolve 到同一个 Data 实例 |
| 3 | 响应式同步 | 修改 Data 后订阅者收到通知 |
| 4 | ViewModel 隔离 | ViewModel 不引用 MonoBehaviour 或具体 View |
| 5 | 序列化 ReactiveProperty | JSON 只包含值,不包含内部状态 |
| 6 | 反序列化已有实例 | 已注册 Data 实例被更新而不是替换 |
| 7 | ReactiveCollection | 可序列化为数组并恢复 |
| 8 | 订阅释放 | Dispose 后订阅不再触发 |
| 9 | 纯 C# | Data 类可脱离 Unity 编译 |
## 依赖关系
```text
Data / ViewModel
├── R3
├── Container
├── ISaveService
└── JSON Converter
```

View File

@@ -0,0 +1,128 @@
# P0-3: Feature 需求详细文档
## 对齐说明
本文档以 `docs/requirements/p0-requirements-set.md` 为准。Feature 不再自己创建根 scopeGameFlow 创建 FeatureContext 并传入 Feature。Feature 负责自身业务行为和引用清理。
## 目标
定义业务 Feature 的统一生命周期,让每个玩法、页面流或业务入口可以独立加载、进入、退出和释放。
## FeatureContext
```csharp
public sealed class FeatureContext
{
public Container Scope { get; }
public IResourceGroup Resources { get; }
public CompositeDisposable Disposables { get; }
public CancellationToken CancellationToken { get; }
}
```
所有权:
- `Scope` 由 GameFlow 创建和释放。
- `Resources` 由 GameFlow 创建和释放。
- `Disposables` 由 GameFlow 创建Feature 可加入订阅Exit/Dispose 时清理。
- Feature 可以在 `Scope` 内注册自己的临时服务。
## 生命周期接口
```csharp
public interface IFeature : IDisposable
{
Task LoadAsync(FeatureContext context);
Task EnterAsync(FeatureContext context);
Task ExitAsync(FeatureContext context);
}
```
阶段职责:
| 方法 | 职责 |
|------|------|
| `LoadAsync` | 加载资源、创建 ViewModel、准备运行时依赖 |
| `EnterAsync` | 打开 UI、订阅 Data 或事件流 |
| `ExitAsync` | 关闭 UI、取消业务订阅 |
| `Dispose` | 清空自身引用,释放 Feature 自己创建但未交给 context 管理的对象 |
## 数据传递
规则:
- 跨 Feature 持久状态放在 Data 类中,并注册到全局 Container。
- 一次性过渡参数由目标 Feature 定义。
- Feature 不引用其他 Feature 的内部类。
- Feature 间直接通信不进入 P0使用 Data 或 R3 事件流。
## 资源与订阅
规则:
- Feature 加载资源后必须加入 `context.Resources`,或持有明确的 handle 并在 Dispose 中释放。
- Feature 订阅必须加入 `context.Disposables` 或 ViewModel 自己的 `CompositeDisposable`
- `ExitAsync` 负责停止业务订阅和关闭 UI。
- `Dispose` 必须可重复调用。
## FeatureBase 可选基类
```csharp
public abstract class FeatureBase : IFeature
{
protected FeatureContext Context { get; private set; }
public virtual Task LoadAsync(FeatureContext context)
{
Context = context;
return Task.CompletedTask;
}
public virtual Task EnterAsync(FeatureContext context)
{
return Task.CompletedTask;
}
public virtual Task ExitAsync(FeatureContext context)
{
context.Disposables.Clear();
return Task.CompletedTask;
}
public virtual void Dispose()
{
Context = null;
}
}
```
## 暂不做
- Feature 栈。
- 并行 Feature。
- Feature 热重载。
- Feature 预加载。
- Feature 过渡动画。
- Feature 间直接调用。
## 验收标准
| # | 标准 | 通过条件 |
|---|------|---------|
| 1 | Load/Enter/Exit/Dispose 顺序 | GameFlow 严格按顺序调用 |
| 2 | 使用 FeatureContext | Feature 可从 context Resolve 服务、加入资源和订阅 |
| 3 | 资源释放 | Feature 退出后 context.Resources 已释放 |
| 4 | 订阅释放 | Feature 退出后订阅不再触发 |
| 5 | Dispose 幂等 | 重复 Dispose 不抛异常 |
| 6 | Load 失败清理 | LoadAsync 中途失败后 Dispose 可清理部分状态 |
| 7 | Feature 隔离 | Feature 不引用其他 Feature 的内部类 |
## 依赖关系
```text
IFeature
├── FeatureContext
├── Container
├── IResourceGroup
└── R3 CompositeDisposable
```

View File

@@ -0,0 +1,156 @@
# P0-2: GameFlow 需求详细文档
## 对齐说明
本文档以 `docs/requirements/p0-requirements-set.md` 为准。GameFlow 是全局生命周期所有者,负责创建 Feature scope、FeatureContext并编排 Feature 的进入、切换和关闭。
## 目标
提供游戏启动、Feature 切换和关闭流程的编排器,保证生命周期顺序、失败清理、取消和非法状态防护。
## 状态机
```csharp
public enum GameFlowState
{
Idle,
Starting,
Running,
Switching,
NoActiveFeature,
ShuttingDown,
Disposed
}
```
状态转换:
```text
Idle -> Starting -> Running <-> Switching
\-> NoActiveFeature
Running -> ShuttingDown -> Disposed
NoActiveFeature -> Switching -> Running
NoActiveFeature -> ShuttingDown -> Disposed
```
非法状态调用抛 `InvalidOperationException`,错误必须包含当前状态和操作名。
## 接口
```csharp
public sealed class GameFlow
{
public GameFlowState State { get; }
public Task StartupAsync<TInitialFeature>(
Container root,
CancellationToken cancellationToken)
where TInitialFeature : IFeature;
public Task SwitchToAsync<TFeature>(
CancellationToken cancellationToken)
where TFeature : IFeature;
public Task ShutdownAsync(CancellationToken cancellationToken);
}
```
## 启动流程
```text
StartupAsync:
1. State = Starting
2. Resolve IConfigProvider
3. await config.LoadAllAsync(ct)
4. Resolve ISaveService
5. 加载或创建 P0 Sample 所需 Data
6. 注册 Data 到 root Container
7. 创建 TInitialFeature
8. 创建 Feature scope
9. 创建 FeatureContext
10. await feature.LoadAsync(context)
11. await feature.EnterAsync(context)
12. State = Running
```
失败规则:
- 配置加载失败清理已创建对象State 回到 Idle抛异常。
- 存档加载失败:使用默认 Data记录警告继续启动。
- Feature Load/Enter 失败:调用 Dispose 清理State 回到 Idle抛异常。
## Feature 切换流程
```text
SwitchToAsync:
1. State = Switching
2. await current.ExitAsync(currentContext)
3. current.Dispose()
4. Dispose current FeatureContext / scope / resources
5. 创建新 Feature、scope、FeatureContext
6. await next.LoadAsync(nextContext)
7. await next.EnterAsync(nextContext)
8. State = Running
```
失败规则:
- 旧 Feature `ExitAsync` 失败:记录错误,继续 Dispose。
- 旧 Feature `Dispose` 失败:记录错误,继续创建新 Feature。
- 新 Feature `LoadAsync` 失败:清理新 FeatureState = NoActiveFeature抛异常。
- 新 Feature `EnterAsync` 失败:调用新 Feature DisposeState = NoActiveFeature抛异常。
- 切换期间再次调用 `SwitchToAsync`P0 抛 `InvalidOperationException`,不排队。
## 关闭流程
```text
ShutdownAsync:
1. State = ShuttingDown
2. best-effort 调用当前 Feature ExitAsync
3. best-effort 调用当前 Feature Dispose
4. best-effort 保存必要 Data
5. Dispose root Container
6. State = Disposed
```
规则:
- `ShutdownAsync` 一旦进入关闭流程,必须尽量执行到底。
- 保存失败记录错误,不阻塞退出。
- Feature 退出/释放失败记录错误,不阻塞退出。
- Container 释放失败记录错误,不阻塞退出。
## 暂不做
- Feature 栈。
- 并行 Feature。
- Feature 预加载。
- 可配置启动任务列表。
- 切换请求队列。
- Starting/Switching 中断式 Shutdown。
## 验收标准
| # | 标准 | 通过条件 |
|---|------|---------|
| 1 | 正常启动 | Startup 完成后 State == Running初始 Feature 已 Load + Enter |
| 2 | 启动配置失败 | State 回到 Idle异常向上传播 |
| 3 | 启动存档失败 | 使用默认 Data记录警告继续启动 |
| 4 | 正常切换 | 旧 Feature 已 Exit/Dispose新 Feature 已 Load/Enter |
| 5 | 新 Feature Load 失败 | State == NoActiveFeature已清理新 Feature |
| 6 | 新 Feature Enter 失败 | State == NoActiveFeature已 Dispose 新 Feature |
| 7 | 关闭 best-effort | 保存或释放失败时仍进入 Disposed |
| 8 | 非法状态防护 | 非法调用抛异常并包含当前状态 |
| 9 | CancellationToken 传递 | FeatureContext 中可拿到同一个取消信号 |
## 依赖关系
```text
GameFlow
├── Container
├── IFeature
├── FeatureContext
├── IConfigProvider
├── ISaveService
└── IResourceService
```

View File

@@ -0,0 +1,696 @@
# FlowScope Game Core P0 需求集
## 文档目的
本文档定义 FlowScope Game Core 的 P0 范围。P0 的目标不是做一个覆盖所有未来项目的完整框架,而是交付一套能支撑真实休闲/超休闲 Unity 游戏最小纵向切片的 Core
1. Unity 启动后进入 GameBootstrap。
2. 注册核心服务。
3. 加载 JSON 配置。
4. 加载或创建本地存档数据。
5. 进入初始 Feature。
6. 打开一个 MVVM Panel。
7. ViewModel/Data 通过 R3 驱动 UI 刷新。
8. 切换或关闭 Feature 时释放资源、订阅和作用域。
9. 退出时保存必要数据。
P0 允许包含部分开发体验能力,但这些能力必须服务于最小闭环,不能把 P0 扩张成完整包生态。
---
## P0 范围原则
1. 显式接口优先,业务模型不由 Core 暗猜。
2. Core 不包含具体玩法规则、具体 UI 文案、具体数值或业务 Data 类。
3. 默认实现只保留一条主路径,扩展实现通过接口或独立包接入。
4. 生命周期、资源、订阅、作用域必须有明确所有者。
5. 所有异步流程必须定义失败、取消和重复调用行为。
6. P0 文档中的每个模块都必须能通过单元测试或最小 Sample 验收。
---
## 统一架构决策
### 1. DI 自动注入进入 P0
P0 支持三种 DI 注册方式:
| 模式 | P0 定位 | 说明 |
|------|---------|------|
| 显式工厂注册 | 必须支持 | 最小稳定路径,所有项目都可用 |
| Attribute + Source Generator | P0 推荐路径 | 提供自动构造函数注入、RegisterAssembly、编译期检查 |
| Attribute + 运行时反射 | P0 可选降级路径 | 用于开发期、Generator 不可用场景、快速验证 |
约束:
- Container 的核心运行时只关心工厂函数,不直接依赖 Source Generator 或反射实现。
- Source Generator 和 Reflection 必须放在独立程序集或独立适配层中。
- 没有开启自动注入模式时,`RegisterType` / `RegisterAssembly` 必须给出明确错误,引导使用 `RegisterFactory`
- 自动递归解析依赖链必须检测循环依赖,并输出完整依赖路径。
- Source Generator 的编译期检查作为 P0 推荐能力,但不能阻塞显式工厂模式使用。
### 2. UI 层级改为可配置
P0 不再强制固定 `Bottom / Middle / Top` 三层。UIManager 支持运行时注册层级:
```csharp
RegisterLayer(string name, Canvas canvas, int sortOrder, PanelStrategy strategy);
```
P0 仍需提供一个推荐默认预设,方便最小项目快速接入:
```text
background / hud / popup / dialog / system
```
项目可以只注册其中一部分,也可以自定义层级名。
### 3. UI 栈式面板进入 P0但只做层内 LIFO
P0 支持同一层级内的栈式管理:
- 后打开的 Panel 位于同层级顶部。
- 默认只能关闭栈顶 Panel。
- `CloseLayer(layerName)` 关闭指定层。
- `CloseAll()` 关闭全部层级。
P0 暂不做完整导航路由、跨 Feature 页面恢复、URL/路由式导航、复杂返回策略。这些放到 P1/P2。
### 4. 配置系统 P0 只支持 JSON
P0 只交付 JSON 配置读取:
- `LoadAllAsync()`
- `Get<T>(int id)`
- `GetAll<T>()`
- 配置行实现 `IConfigRow`
CSV、Luban、ScriptableObject、远程配置、热重载、字段校验工具不进入 P0。后续通过 `IConfigSource` / `IConfigParser` 扩展包接入,而不是塞进核心实现。
### 5. 存档系统 P0 只保留本地轻量实现
P0 提供:
- `SaveAsync<T>(string key, T data)`
- `LoadAsync<T>(string key, T defaultValue)`
- `Delete(string key)`
默认实现二选一:
- `FileSaveStorage`
- `PlayerPrefsSaveStorage`
`Exists` 可作为轻量辅助进入 P0`ListKeys`、云存档、加密、压缩、版本迁移、冲突解决不进入 P0。后续通过 `ISaveStorage` / `ISaveSerializer` 扩展。
### 6. 音频系统 P0 保留基础播放,不做完整音频框架
P0 提供:
- 播放/停止 BGM
- 播放 SFX
- 设置 BGM/SFX 音量
- 全局静音
- 简单 SFX AudioSource 池
- BGM 淡入淡出
`IAudioHandle` 可进入 P0但只作为播放结果的轻量控制句柄
- `Stop(float fadeOut = 0)`
- `IsPlaying`
单个音频暂停/恢复、单个音量、AudioMixer 路由、3D 音频、语音系统、动态音乐、音频配置表放到 P1/P2。
### 7. 资源系统 P0 支持引用计数,但只支持一个默认后端
P0 支持:
- `LoadAsync<T>(string key)`
- `IResourceHandle<T>`
- `IResourceGroup`
- 同 key 并发加载合并
- 引用计数
- Group 释放
P0 默认后端只支持 Addressables。
Resources / AssetBundle / YooAsset / 远程下载进度 / 资源分析工具不进入 P0。后续通过 `IResourceBackend` 插件化接入。
---
## 统一生命周期所有权
### GameFlow / AppFlow 负责
GameFlow 是全局生命周期编排者,负责:
- 创建 Feature 实例。
- 创建 Feature Scope。
- 创建 FeatureContext。
- 调用 Feature 生命周期。
- 在切换和关闭时保证释放顺序。
- 处理失败、取消和重复调用。
### Feature 负责
Feature 只负责自身业务行为:
-`LoadAsync` 中加载自身资源、创建 ViewModel、准备运行时依赖。
-`EnterAsync` 中打开 UI、订阅数据或事件流。
-`ExitAsync` 中关闭 UI、取消业务订阅。
-`Dispose` 中清空自身引用。
Feature 不直接创建根 scope。Feature 可以使用 GameFlow 提供的 feature scope 注册临时服务。
### FeatureContext
```csharp
public sealed class FeatureContext
{
public Container Scope { get; }
public IResourceGroup Resources { get; }
public CompositeDisposable Disposables { get; }
public CancellationToken CancellationToken { get; }
}
```
Feature 生命周期接口:
```csharp
public interface IFeature : IDisposable
{
Task LoadAsync(FeatureContext context);
Task EnterAsync(FeatureContext context);
Task ExitAsync(FeatureContext context);
}
```
---
## 统一异步策略
P0 接口统一使用 `Task``CancellationToken`
规则:
- `StartupAsync``SwitchToAsync``ShutdownAsync` 必须接受 `CancellationToken`
- Feature 生命周期通过 `FeatureContext.CancellationToken` 获取取消信号。
- 切换期间再次调用 `SwitchToAsync``InvalidOperationException`P0 不做排队。
- `ShutdownAsync``Starting``Switching` 中调用时P0 抛 `InvalidOperationException`P1 可考虑中断式关闭。
- `ShutdownAsync` 一旦进入关闭流程,必须 best-effort 执行到底。
失败处理:
| 场景 | P0 行为 |
|------|---------|
| Startup 配置加载失败 | 回到 Idle抛异常 |
| Startup 存档加载失败 | 使用默认数据并记录警告 |
| 初始 Feature Load 失败 | 清理已创建资源,回到 Idle抛异常 |
| Switch 旧 Feature Exit 失败 | 记录错误,继续 Dispose |
| Switch 旧 Feature Dispose 失败 | 记录错误,继续创建新 Feature |
| Switch 新 Feature Load 失败 | 清理新 Feature进入 NoActiveFeature 状态,抛异常 |
| Switch 新 Feature Enter 失败 | 调用新 Feature Dispose进入 NoActiveFeature 状态,抛异常 |
| Shutdown 保存失败 | 记录错误,不阻塞退出 |
| 取消发生 | 抛 `OperationCanceledException`,并清理已创建资源 |
---
## P0 模块需求
### P0-1 Container
目标:提供轻量 DI 容器,支持显式注册、自动注入、作用域和释放。
P0 必须支持:
- `RegisterInstance<T>(T instance)`
- `RegisterFactory<T>(Func<Container, T> factory)`
- `RegisterType<TInterface, TImplementation>()`
- `RegisterType<TImplementation>()`
- `RegisterAssembly()`
- `Resolve<T>()`
- `TryResolve<T>(out T value)`
- `CreateScope()`
- `Dispose()`
自动注入规则:
- 只允许一个 public 构造函数。
- 构造函数参数从 Container 递归解析。
- 循环依赖必须检测。
- Source Generator 模式提供编译期依赖检查。
- Reflection 模式提供运行时降级。
暂不做:
- `[Inject]` 属性/方法注入。
- 多生命周期模式。
- 开放泛型注册。
- 装饰器注册。
- 运行时动态切换 DI 模式。
验收:
- 显式工厂注册可解析。
- Source Generator 自动注入可解析。
- Reflection 自动注入可解析。
- 子 scope 可访问父 scope。
- 子 scope 覆盖父 scope 不污染父 scope。
- Dispose 按逆序释放本 scope 创建的 `IDisposable`
- 循环依赖抛出包含依赖链的异常。
### P0-2 GameFlow
目标编排游戏启动、Feature 切换和关闭。
状态:
```text
Idle -> Starting -> Running <-> Switching -> ShuttingDown -> Disposed
```
接口:
```csharp
Task StartupAsync<TInitialFeature>(Container root, CancellationToken ct);
Task SwitchToAsync<TFeature>(CancellationToken ct);
Task ShutdownAsync(CancellationToken ct);
```
职责:
- 启动时加载配置、加载存档、注册 Data、进入初始 Feature。
- 切换时按 `Exit -> Dispose -> CreateScope -> Load -> Enter` 执行。
- 关闭时按 `Exit -> Dispose -> Save -> DisposeRootContainer` 执行。
- 创建并传递 `FeatureContext`
- 防止非法状态调用。
暂不做:
- Feature 栈。
- 并行 Feature。
- 可配置启动任务列表。
- Feature 预加载。
### P0-3 Feature
目标:定义业务特性的统一生命周期。
接口:
```csharp
public interface IFeature : IDisposable
{
Task LoadAsync(FeatureContext context);
Task EnterAsync(FeatureContext context);
Task ExitAsync(FeatureContext context);
}
```
规则:
- Feature 不依赖其他 Feature 的内部状态。
- 持久跨 Feature 状态放入 Data 类。
- 一次性过渡参数由目标 Feature 定义。
- 订阅必须进入 `FeatureContext.Disposables` 或 ViewModel 自己的 disposable 集合。
- 资源必须进入 `FeatureContext.Resources` 或由明确 handle 管理。
暂不做:
- Feature 间直接通信。
- Feature 热重载。
- Feature 过渡动画。
- 多 Feature 同时运行。
### P0-4 R3 + Data
目标:建立运行时数据和响应式绑定基础。
规则:
- 一个 Data 类对应一个业务域。
- Data 类是纯 C#,不依赖 Unity API。
- Data 类不引用 View / ViewModel。
- Data 类使用 `ReactiveProperty<T>``ReactiveCollection<T>` 表达可观察状态。
- ViewModel 负责业务表现逻辑,可以修改 Data。
- ViewModel 订阅必须释放。
P0 支持:
- `ReactiveProperty<T>` 序列化为 `.Value`
- `ReactiveCollection<T>` 序列化为数组。
- 存档加载后恢复到 Data 实例。
暂不做:
- 自研响应式系统。
- UI 自动绑定。
- 全局 EventBus。
- Data Inspector 可视化。
### P0-5 ConfigProvider
目标:提供 JSON 配置读取。
接口:
```csharp
public interface IConfigProvider
{
Task LoadAllAsync(CancellationToken ct);
T Get<T>(int id) where T : class, IConfigRow;
IReadOnlyList<T> GetAll<T>() where T : class, IConfigRow;
}
public interface IConfigRow
{
int Id { get; }
}
```
规则:
- P0 默认 JSON。
- 查询不存在 ID 返回 null。
- 重复 ID 抛异常。
- 配置格式错误抛异常并包含文件名。
暂不做:
- CSV / Luban / ScriptableObject。
- 按模块加载和卸载。
- 热重载。
- 配置编辑器校验。
- 远程配置。
扩展方向:
- P1/P2 通过 `IConfigParser``IConfigSource` 插件化接入,不修改业务调用。
### P0-6 SaveService
目标:提供本地轻量存档。
接口:
```csharp
public interface ISaveService
{
Task SaveAsync<T>(string key, T data, CancellationToken ct);
Task<T> LoadAsync<T>(string key, T defaultValue, CancellationToken ct);
void Delete(string key);
bool Exists(string key);
}
```
默认实现:
- `FileSaveStorage``PlayerPrefsSaveStorage` 二选一。
- 默认序列化为 JSON。
规则:
- 不存在 key 返回 defaultValue。
- 反序列化失败返回 defaultValue 并记录警告。
- 写入失败抛异常。
- 删除不存在 key 静默忽略。
暂不做:
- `ListKeys`
- 多存档槽管理。
- 云存档。
- 加密。
- 压缩。
- 版本迁移。
- 自动存档。
扩展方向:
- P1/P2 通过 `ISaveStorage` / `ISaveSerializer` 扩展。
### P0-7 ResourceService
目标:提供资源异步加载、引用计数和分组释放。
接口:
```csharp
public interface IResourceService
{
Task<IResourceHandle<T>> LoadAsync<T>(string key, CancellationToken ct) where T : class;
IResourceGroup CreateGroup();
}
public interface IResourceHandle<T> : IDisposable where T : class
{
string Key { get; }
T Asset { get; }
bool IsDisposed { get; }
}
public interface IResourceGroup : IDisposable
{
void Add<T>(IResourceHandle<T> handle) where T : class;
int Count { get; }
}
```
规则:
- 同 key 并发加载只触发一次底层加载。
- 每次加载返回独立 handle。
- 最后一个 handle Dispose 时释放底层资源。
- Group Dispose 释放组内所有 handle。
- 重复 Dispose 不抛异常。
- Dispose 后访问 Asset 返回 null 或抛明确异常,二选一需在实现前固定。
P0 默认:
- Addressables 后端。
暂不做:
- Resources 后端。
- AssetBundle 后端。
- YooAsset 后端。
- 下载进度。
- 资源预热。
- 资源分析工具。
扩展方向:
- P1/P2 通过 `IResourceBackend` 插件化接入其他资源系统。
### P0-8 UIManager
目标:提供 MVVM Panel 生命周期管理、可配置层级和层内栈。
接口:
```csharp
public sealed class UIManager
{
void RegisterLayer(string name, Canvas canvas, int sortOrder, PanelStrategy strategy);
Task<TPanel> OpenAsync<TPanel, TViewModel>(TViewModel vm, CancellationToken ct)
where TPanel : UIPanelBase<TViewModel>;
void Close<TPanel>();
void CloseLayer(string layerName);
void CloseAll();
bool IsOpen<TPanel>();
}
```
Panel Attribute
```csharp
[AttributeUsage(AttributeTargets.Class)]
public sealed class UIPanelAttribute : Attribute
{
public string Layer { get; }
public string Path { get; }
public PanelStrategy? OverrideStrategy { get; }
public UIPanelAttribute(string layer);
public UIPanelAttribute(string layer, string path);
public UIPanelAttribute(string layer, PanelStrategy overrideStrategy);
public UIPanelAttribute(string layer, string path, PanelStrategy overrideStrategy);
}
```
Panel 基类:
```csharp
public abstract class UIPanelBase<TViewModel> : MonoBehaviour
{
protected TViewModel ViewModel { get; private set; }
protected CompositeDisposable Disposables { get; } = new();
public void Bind(TViewModel viewModel)
{
ViewModel = viewModel;
OnBind(viewModel);
}
public virtual void Unbind()
{
Disposables.Clear();
}
protected abstract void OnBind(TViewModel viewModel);
}
```
规则:
- Panel 必须声明 `UIPanelAttribute`
- Attribute 未指定 path 时,按命名约定推导资源路径。
- C# Attribute 构造参数不支持 `Nullable<T>`,因此 `OverrideStrategy` 属性保持 nullable但构造函数通过重载表达“未指定”和“指定覆盖策略”。
- 同层级采用 LIFO 栈。
- 关闭非栈顶 Panel 抛异常。
- Destroy 策略关闭时释放资源 handle。
- Cache 策略关闭时隐藏对象,复用时重新 Bind。
暂不做:
- 自动 UI 绑定。
- 导航路由系统。
- Panel 预加载。
- SafeArea。
- 多语言切换。
- 红点系统。
- 复杂转场动画。
### P0-9 AudioService
目标:提供基础音频播放能力。
接口:
```csharp
public interface IAudioService
{
IAudioHandle PlayBgm(string key, bool loop = true, float fadeIn = 0f);
IAudioHandle PlaySfx(string key);
void StopBgm(float fadeOut = 0f);
void SetBgmVolume(float volume);
void SetSfxVolume(float volume);
void SetMute(bool mute);
}
public interface IAudioHandle
{
void Stop(float fadeOut = 0f);
bool IsPlaying { get; }
}
```
规则:
- 同一时间只有一首 BGM。
- 播放新 BGM 自动停止旧 BGM。
- SFX 使用简单 AudioSource 池。
- 音量参数 clamp 到 `[0, 1]`
- 资源通过 `IResourceService` 加载。
暂不做:
- 单个音频 Pause/Resume。
- 单个音频 Volume。
- AudioMixer 分组。
- 3D 音频。
- 动态音乐。
- 音频配置表。
---
## P0 最小 Sample 验收
P0 完成时必须提供一个最小 Sample
```text
GameBootstrap
-> 创建根 Container
-> 注册 Container / Config / Save / Resource / UI / Audio
-> 注册 PlayerData
-> GameFlow.StartupAsync<MainMenuFeature>()
MainMenuFeature
-> LoadAsync: 加载 MainMenuPanel 资源,创建 MainMenuViewModel
-> EnterAsync: UIManager.OpenAsync<MainMenuPanel, MainMenuViewModel>()
-> 点击按钮修改 PlayerData.Gold
-> UI 文本响应刷新
-> ShutdownAsync 保存 PlayerData
```
验收标准:
- Sample 可在 Unity 中运行。
- 首次启动创建默认 PlayerData。
- 修改 Gold 后 UI 立即刷新。
- 退出后保存 PlayerData。
- 重新启动后恢复 PlayerData。
- Feature 退出后资源组释放。
- Feature 退出后订阅不再触发。
- 连续打开/关闭 Panel 不残留订阅。
---
## P0 测试矩阵
| 模块 | 必测内容 |
|------|----------|
| Container | 显式工厂、自动注入、scope 覆盖、Dispose、循环依赖 |
| GameFlow | 启动顺序、切换顺序、失败清理、非法状态调用、关闭 best-effort |
| Feature | Load/Enter/Exit/Dispose 顺序、资源和订阅释放 |
| Data/R3 | ReactiveProperty 同步、序列化/反序列化、订阅释放 |
| Config | JSON 解析、重复 Id、不存在 Id、格式错误 |
| Save | 保存/加载、默认值、删除、反序列化失败 |
| Resource | 同 key 并发、引用计数、Group 释放、加载失败 |
| UI | 层级注册、Open/Close、LIFO、Bind/Unbind、Cache/Destroy |
| Audio | BGM 播放/停止、SFX 池、音量 clamp、静音、资源加载失败 |
---
## P1/P2 扩展方向
后续能力优先做成可插拔扩展,而不是污染 P0 核心:
P1 生产化承接边界P1 可以新增小接口、适配层和样例验收能力,但不得重写 P0 的核心生命周期、容器、UI 层内 LIFO、JSON 默认配置、本地存档和资源所有权语义。任何涉及 Addressables、配置来源、存档迁移、UI 导航或预加载的能力,都应先通过 P1 契约进入,再由独立实现或适配程序集承接。
| 能力 | 建议阶段 | 形式 |
|------|----------|------|
| CSV / Luban 配置 | P1/P2 | `IConfigParser` 插件 |
| 按模块配置加载 | P1 | `IConfigSource` 扩展 |
| 存档版本迁移 | P1/P2 | `ISaveMigration` 插件 |
| 云存档 | P2 | `ISaveStorage` 插件 |
| Resources / YooAsset / AssetBundle | P1/P2 | `IResourceBackend` 插件 |
| UI 路由 / 返回栈策略 | P1 | `IUIScreenNavigator` 扩展 |
| Panel 预加载 | P1 | UIManager 扩展服务 |
| AudioMixer / 3D 音频 | P1/P2 | `IAudioBackend` 或 AudioService 扩展 |
| 编辑器导入向导 | P2 | Editor 包 |
| 配置/资源检查工具 | P2 | Editor 工具包 |
---
## 需要同步修正的旧文档
以下旧文档需要按本文档重新收敛:
- `docs/game-core-requirement-tiers.md`
- `docs/requirements/p0-container.md`
- `docs/requirements/p0-gameflow.md`
- `docs/requirements/p0-feature.md`
- `docs/requirements/p0-configprovider.md`
- `docs/requirements/p0-saveservice.md`
- `docs/requirements/p0-resourceservice.md`
- `docs/requirements/p0-uimanager.md`
- `docs/requirements/p0-audioservice.md`
其中主文档必须更新的决策:
- DI 自动注入进入 P0。
- UI 层级从固定三层改为可配置层级。
- UI 层内 LIFO 栈进入 P0。
- JSON 是 P0 唯一默认配置格式。
- 资源 P0 只保留 Addressables 默认后端,其他后端插件化。
- 存档 P0 只保留本地轻量实现,云存档插件化。

View File

@@ -0,0 +1,104 @@
# P0-7: IResourceService 需求详细文档
## 对齐说明
本文档以 `docs/requirements/p0-requirements-set.md` 为准。P0 资源系统包含引用计数和资源组,因为它直接影响 Feature 释放正确性;但 P0 只实现 Addressables 默认后端,其他后端通过插件扩展。
## 目标
提供异步资源加载、同 key 并发合并、引用计数和资源组释放能力。
## 接口
```csharp
public interface IResourceService
{
Task<IResourceHandle<T>> LoadAsync<T>(
string key,
CancellationToken cancellationToken)
where T : class;
IResourceGroup CreateGroup();
}
public interface IResourceHandle<T> : IDisposable where T : class
{
string Key { get; }
T Asset { get; }
bool IsDisposed { get; }
}
public interface IResourceGroup : IDisposable
{
void Add<T>(IResourceHandle<T> handle) where T : class;
int Count { get; }
}
```
## 行为规则
- `LoadAsync<T>` 加载失败时抛异常,异常包含 key 和后端类型。
- 同 key 同类型并发加载只触发一次底层加载。
- 每次 `LoadAsync` 返回独立 handle。
- 每个 handle Dispose 时引用计数减一。
- 最后一个 handle Dispose 时释放底层资源。
- `IResourceGroup.Dispose` 释放组内所有 handle。
- 重复 Dispose 不抛异常。
- Group 重复 Add 同一个 handle 时忽略。
- Dispose 后访问 `Asset` 的行为必须固定为返回 null。
## 默认后端
P0 默认只支持 Addressables。
```text
IResourceService
└── AddressablesResourceBackend
```
## 暂不做
- Resources 后端。
- AssetBundle 后端。
- YooAsset 后端。
- 资源预热。
- 下载进度回调。
- 资源优先级。
- 资源版本管理。
- 编辑器资源扫描和分析工具。
## 扩展方向
P1/P2 通过后端接口扩展:
```csharp
public interface IResourceBackend
{
Task<T> LoadAsync<T>(string key, CancellationToken cancellationToken)
where T : class;
void Release(string key, object asset);
}
```
## 验收标准
| # | 标准 | 通过条件 |
|---|------|---------|
| 1 | 正常加载 | LoadAsync 返回 handleAsset 可用 |
| 2 | 加载失败 | 不存在 key 抛异常并包含 key |
| 3 | 引用计数 | 同 key 加载两次,最后一个 handle Dispose 后才释放底层资源 |
| 4 | 并发合并 | 同 key 并发 LoadAsync 只触发一次底层加载 |
| 5 | Group 释放 | Group Dispose 后组内 handle 全部释放 |
| 6 | 重复 Dispose | handle/group 重复 Dispose 不抛异常 |
| 7 | Dispose 后 Asset | 返回 null |
| 8 | 取消加载 | CancellationToken 取消时抛 `OperationCanceledException` |
## 依赖关系
```text
IResourceService
├── Addressables
├── CancellationToken
└── 被 Feature / UIManager / AudioService 依赖
```

View File

@@ -0,0 +1,96 @@
# P0-6: ISaveService 需求详细文档
## 对齐说明
本文档以 `docs/requirements/p0-requirements-set.md` 为准。P0 存档系统只提供本地轻量存取;云存档、加密、压缩、复杂迁移和冲突解决通过后续扩展接入。
## 目标
提供本地保存、读取、删除和存在性检查能力,支撑 P0 Sample 的 Data 持久化。
## 接口
```csharp
public interface ISaveService
{
Task SaveAsync<T>(string key, T data, CancellationToken cancellationToken);
Task<T> LoadAsync<T>(string key, T defaultValue, CancellationToken cancellationToken);
void Delete(string key);
bool Exists(string key);
}
```
## 默认实现
P0 默认实现二选一:
| 实现 | 说明 |
|------|------|
| `FileSaveStorage` | 使用本地文件,适合普通存档 |
| `PlayerPrefsSaveStorage` | 使用 PlayerPrefs适合小型数据 |
P0 默认序列化为 JSON并支持 R3 Data 中的 `ReactiveProperty<T>` / `ReactiveCollection<T>` 转换。
## 行为规则
- `LoadAsync` 读取不存在 key 时返回 `defaultValue`
- 反序列化失败返回 `defaultValue` 并记录警告。
- 存储读取失败返回 `defaultValue` 并记录警告。
- 写入失败抛异常,异常包含 key。
- `Delete` 删除不存在 key 时静默忽略。
- `Exists` 只检查当前默认存储后端。
## 暂不做
- `ListKeys`
- 多存档槽管理。
- 自动存档。
- 云存档完整实现。
- 本地 + 云双写。
- 加密。
- 压缩。
- 存档版本迁移。
- 冲突解决。
## 扩展方向
后续通过以下接口扩展:
```csharp
public interface ISaveStorage
{
Task WriteAsync(string key, byte[] data, CancellationToken cancellationToken);
Task<byte[]> ReadAsync(string key, CancellationToken cancellationToken);
void Delete(string key);
bool Exists(string key);
}
public interface ISaveSerializer
{
byte[] Serialize<T>(T data);
T Deserialize<T>(byte[] bytes);
}
```
## 验收标准
| # | 标准 | 通过条件 |
|---|------|---------|
| 1 | 保存和读取 | Save 后 Load 返回等价数据 |
| 2 | 默认值 | 不存在 key 时返回传入 defaultValue |
| 3 | 删除 | Delete 后 Exists 为 falseLoad 返回 defaultValue |
| 4 | Exists | 已保存 key 返回 true未保存 key 返回 false |
| 5 | ReactiveProperty 序列化 | 保存 JSON 不包含 ReactiveProperty 内部状态 |
| 6 | 反序列化失败 | 返回 defaultValue 并记录警告 |
| 7 | 写入失败 | 抛异常并包含 key |
| 8 | 取消保存/加载 | CancellationToken 取消时抛 `OperationCanceledException` |
## 依赖关系
```text
ISaveService
├── ISaveStorage
├── ISaveSerializer
├── JSON 库
└── R3 JSON Converter
```

View File

@@ -0,0 +1,166 @@
# P0-8: UIManager 需求详细文档
## 对齐说明
本文档以 `docs/requirements/p0-requirements-set.md` 为准。P0 UIManager 支持可配置层级和层内 LIFO 栈,但不做完整导航路由系统。`UIPanelAttribute` 明确包含 `path` 参数。
## 目标
提供 MVVM Panel 生命周期管理、运行时层级注册、层内栈式打开关闭、手写 Bind/Unbind 和基础缓存/销毁策略。
## 层级注册
```csharp
public sealed class UIManager
{
public void RegisterLayer(
string name,
Canvas canvas,
int sortOrder,
PanelStrategy strategy = PanelStrategy.Destroy);
}
public enum PanelStrategy
{
Destroy,
Cache
}
```
规则:
- P0 不强制固定 `Bottom/Middle/Top`
- 推荐默认层级为 `background / hud / popup / dialog / system`
- 项目可只注册部分层级。
- 未注册层级打开 Panel 时抛异常。
## Panel Attribute
```csharp
[AttributeUsage(AttributeTargets.Class)]
public sealed class UIPanelAttribute : Attribute
{
public string Layer { get; }
public string Path { get; }
public PanelStrategy? OverrideStrategy { get; }
public UIPanelAttribute(string layer);
public UIPanelAttribute(string layer, string path);
public UIPanelAttribute(string layer, PanelStrategy overrideStrategy);
public UIPanelAttribute(string layer, string path, PanelStrategy overrideStrategy);
}
```
规则:
- `Layer` 必填。
- `Path` 可选;未填时按命名约定推导。
- `OverrideStrategy` 可选,优先于层级默认策略。
- 说明C# Attribute 构造参数不支持 `Nullable<T>`,因此 `OverrideStrategy` 属性保持 nullable但通过重载表达“未指定”和“指定覆盖策略”。
## Panel 基类
```csharp
public abstract class UIPanelBase<TViewModel> : MonoBehaviour
{
protected TViewModel ViewModel { get; private set; }
protected CompositeDisposable Disposables { get; } = new();
public void Bind(TViewModel viewModel)
{
ViewModel = viewModel;
OnBind(viewModel);
}
public virtual void Unbind()
{
Disposables.Clear();
}
protected abstract void OnBind(TViewModel viewModel);
}
```
## 打开与关闭接口
```csharp
public sealed class UIManager
{
public Task<TPanel> OpenAsync<TPanel, TViewModel>(
TViewModel viewModel,
CancellationToken cancellationToken)
where TPanel : UIPanelBase<TViewModel>;
public void Close<TPanel>();
public void CloseLayer(string layerName);
public void CloseAll();
public bool IsOpen<TPanel>();
}
```
## 生命周期
```text
OpenAsync:
1. 读取 UIPanelAttribute
2. 校验层级已注册
3. 解析资源路径
4. IResourceService.LoadAsync<GameObject>
5. Instantiate 到目标 Canvas
6. Bind(viewModel)
7. SetActive(true)
8. 压入层内栈
Close:
1. 校验目标 Panel 是所在层级栈顶
2. 从栈弹出
3. Unbind()
4. Destroy 策略Destroy GameObject 并释放资源 handle
5. Cache 策略SetActive(false),保留实例和资源 handle
```
路径约定:
```text
ShopPanel -> UI/Shop/Prefab
ConfirmPanel -> UI/Confirm/Prefab
MainHudPanel -> UI/MainHud/Prefab
```
## 暂不做
- 自动 UI 绑定。
- 导航路由系统。
- 跨 Feature 页面恢复。
- Panel 预加载。
- SafeArea。
- 多语言 UI 切换。
- 红点/通知系统。
- 复杂转场动画。
## 验收标准
| # | 标准 | 通过条件 |
|---|------|---------|
| 1 | 注册层级 | RegisterLayer 后 Panel 可挂载到对应 Canvas |
| 2 | Attribute 层级 | Panel 通过 UIPanelAttribute 找到层级 |
| 3 | path 参数 | Attribute 指定 path 时按指定路径加载 |
| 4 | 路径约定 | 未指定 path 时按命名约定加载 |
| 5 | Open | 异步加载 Prefab、实例化、Bind、入栈 |
| 6 | Close | 栈顶 Panel Unbind 并按策略关闭 |
| 7 | LIFO | 关闭非栈顶 Panel 抛异常 |
| 8 | Cache | Cache Panel 再次打开时复用并重新 Bind |
| 9 | Destroy | Destroy Panel 关闭后释放资源 handle |
| 10 | CloseLayer | 只关闭指定层级 |
| 11 | CloseAll | 关闭所有层级 |
| 12 | Bind 失败 | 清理已创建对象和资源 handle 后抛异常 |
## 依赖关系
```text
UIManager
├── IResourceService
├── R3
├── Unity Canvas / GameObject / MonoBehaviour
└── CancellationToken
```

View File

@@ -0,0 +1,38 @@
# FlowScope Game Core P1 生产化需求
## 目标
P1 在 P0 核心接口稳定后推进,目标是补齐第一批生产化扩展点,而不是扩张成完整框架生态。
## 本轮范围
- `IResourceBackend` 与 Addressables 独立适配。
- `IConfigSource` / `IConfigParser` 配置扩展点。
- `ISaveMigration` 存档迁移扩展点。
- `IUIScreenNavigator` 与 Panel 预加载。
- MainMenuP0 样例验收升级。
- 使用规范文档。
## 已完成
- Runtime 资源核心已拆为 `ResourceService` + `IResourceBackend`Addressables 适配移动到 `FlowScope.Addressables` 独立 asmdef。
- 配置系统支持 source/parser 组合,旧 `Mapping<T>(fileName, loadTextAsync)` API 保持可用。
- 存档序列化支持版本 envelope 和连续 migration chain旧 raw JSON 按版本 1 读取。
- UI 增加 `UIPreloadService``UIScreenNavigator`,并保持 `UIManager` 层内 LIFO 规则。
- MainMenuP0 样例升级为 P1 装配方式,覆盖启动、点击、保存恢复和资源释放测试。
- 新增运行时使用规范与样例验收指南。
## 未进入本轮
- Luban 完整接入。
- YooAsset / AssetBundle 后端实现。
- 云存档。
- AudioMixer / 3D 音频。
- Package 分发。
- URL 式 UI 路由、跨 Feature 页面恢复和完整页面栈框架。
## 当前验收状态
代码级编译验证中,`FlowScope.Runtime``FlowScope.Tests.EditMode` 可通过。`FlowScope.Tests.PlayMode` 在排除 Addressables 适配测试后可通过。
完整 PlayMode 与 Unity Test Runner 验收仍依赖 Unity 完成 `com.unity.addressables` 包解析;当前机器上主工程被其他 Unity 实例占用,且 `Library/PackageCache` 尚未出现 Addressables 包,因此本轮不能标记为完整人工验收通过。

View File

@@ -0,0 +1,578 @@
# FlowScope Scope / Feature 架构需求草案
## 目标
这份文档用于约束下一版 FlowScope 核心架构实现。目标不是复刻 QFramework也不是照搬 ft_3 当前容器用法,而是形成一套能服务真实 Unity 项目的可落地框架:
- 能力向 ft_3 靠拢:支持动态注册、父子 Scope、局部覆盖、运行期灵活组合。
- 规则向 QFramework 靠拢:业务代码默认在当前架构上下文中工作,不鼓励直接拿根容器乱 Resolve。
- 默认使用简单:大多数业务只需要 `Root -> Feature` 两级。
- 保留扩展能力:复杂业务可以继续创建子 Scope但不把每个 UI、流程、函数调用都强制变成 Scope。
## 核心结论
下一版的第一主角应该是统一的 `ArchitectureScope`,不是固定的 `RootContext + FeatureScope` 两层结构。
`RootScope``FeatureScope``ChildScope` 本质都是同一种 Scope只是所在层级和安装内容不同
```text
Root Scope
BossFightFeature Scope
ScratchTicketFeature Scope
```
必要时可以继续细分:
```text
Root Scope
BossFightFeature Scope
BattleRound Scope
RevivePanel Scope
```
但这不是默认推荐路径。默认推荐是每个活动、玩法、业务模块对应一个 Feature ScopeFeature 内部再按 MVC、MVVM、MVP、Controller、ViewModel、Service、State Machine 等方式组织。
## 概念定义
### ArchitectureScope
`ArchitectureScope` 是框架的核心上下文,也是受规则约束的容器节点。
它负责:
- 保存当前 Scope 的注册项。
- 从当前 Scope 开始解析依赖。
- 找不到时向父 Scope 查找。
- 创建子 Scope。
- 创建 Feature Scope。
- 管理当前 Scope 生命周期。
- 允许当前 Scope 覆盖父 Scope 的能力。
它不应该只是一个裸 DI Container而是一个带层级、生命周期和使用规则的运行时上下文。
### RootScope
`RootScope` 是没有父节点的 `ArchitectureScope`
它不应该拥有一套和普通 Scope 不同的能力。它只是树的根:
```text
RootScope = Parent == null 的 ArchitectureScope
```
Root 适合注册跨 Feature 的基础能力,例如:
- 配置服务
- 资源服务
- 存档服务
- 音频服务
- 网络服务
- 全局账号/环境信息
Root 不应该成为业务代码到处直接访问的全局服务定位器。
### Feature
`Feature` 是一个业务模块的安装定义,不是另一种容器。
一个 Feature 代表一个可独立理解的业务边界,例如:
- BossFightFeature
- ScratchTicketFeature
- Event1v1BattleFeature
- MainMenuFeature
创建 Feature 时,框架会自动创建一个新的 `ArchitectureScope`,然后把 Feature 安装到这个 Scope 上。
推荐 API
```csharp
var boss = game.CreateFeature<BossFightFeature>();
```
等价概念:
```csharp
var boss = game.CreateScope("BossFight");
new BossFightFeature().Install(boss);
```
但调用侧不应该手写安装流程。
### Child Scope
Child Scope 是 Feature 内部或任意 Scope 下的局部生命周期工具。
它适合处理:
- 生命周期明显短于 Feature 的对象。
- 需要局部覆盖某个服务的场景。
- 一组临时对象需要一起释放的场景。
- 同一个 Feature 内存在多个并行实例的场景。
- 特殊流程需要隔离依赖解析的场景。
它不适合处理:
- 普通 UI Panel。
- 普通 ViewModel。
- 普通 Controller。
- 普通业务函数调用。
- 只是为了“看起来更分层”的拆分。
默认原则:能放在 Feature 内部 MVC/MVVM 结构里的,不要先拆成 Scope。
## Register / Resolve 规则
这版设计不应该限制只能在安装期 Register。
更准确的规则是:
```text
Register 本身不是问题。
问题是越级注册、越级修改、污染父级、绕开当前 Scope 解析。
```
因此下一版需要遵守:
1. 任意 Scope 都可以随时 Register。
2. Register 永远只写当前 Scope。
3. Resolve 永远从当前 Scope 开始。
4. 当前 Scope 找不到时,才向 Parent 查找。
6. 子 Scope 可以覆盖父 Scope 的注册。
6. 子 Scope 不能修改父 Scope 的注册。
7. Root 只是普通 Scope 的根节点,不是特殊全局服务定位器。
8. Feature 是创建出来并自动安装的 Scope。
示例:
```csharp
var game = FlowScope.Create();
game.Register<IConfigService, ConfigService>().AsSingleton();
var boss = game.CreateFeature<BossFightFeature>();
boss.Register<IRewardService, BossRewardService>().PerScope();
using var revive = boss.CreateScope("RevivePanel");
revive.Register<IRewardService, ReviveRewardService>().PerScope();
var reward = revive.Resolve<IRewardService>();
```
解析顺序:
```text
RevivePanel Scope -> BossFight Scope -> Root Scope
```
此时 `revive.Register` 不会污染 `boss``boss.Register` 不会污染 `root`
## 容器策略
下一版不应该只包装 ft_3 的 `asap.core.Container`,而应该吸收它的核心设计,形成 FlowScope 自己的 Container。
已通过 `asap.core.dll` 元数据确认ft_3 当前底层容器具备这些值得保留的设计:
- `(Type, string)` 作为注册 key。
- `Register<T>().AsSingleton()` 的声明式生命周期 API。
- `Register<TService, TImplementation>().AsSingleton()` 的接口到实现注册。
- `Register<T>(name).PerScope()` 的命名注册。
- `Register(Type serviceType, string name, Type implementationType).PerScope()` 的运行时类型注册。
- `RegisterInstance(instance)` 的现有实例注册。
- `Resolve<T>()` / `Container.IScope.GetInstance(...)` 的解析入口。
- `CreateScope()` 的 Scope 生命周期入口。
- `PerScope()` 通过 Scope lifetime 在具体 Scope 解析时生成/缓存实例。
同时也确认了 ft_3 当前公开接口的限制:
- `Container.IScope` 公开接口只有 `GetInstance((Type, string))`
- `Container.IScope` 没有公开 `Parent` 属性。
- `Container.IScope` 没有公开 `Register` 方法。
- 底层注册入口主要在根 `Container` 上,不是每个 Scope 都能注册到自己。
- 底层只确认有 `AsSingleton()``PerScope()`,没有确认 `AsTransient()`
- 底层没有确认存在 `TryResolve<T>()`
因此下一版不采用“薄包装 asap.core.Container”的方案。
原因是 FlowScope 当前目标和 ft_3 底层公开模型不完全一致:
```text
ft_3 当前模型Root Container 注册Scope 负责按 Scope 生命周期解析/缓存。
FlowScope 目标模型:每个 ArchitectureScope 都可以注册到自己,并从自己开始 Resolve再向父级查找。
```
如果只包装,会出现两套语义:外层说 Scope-local Register内层却只有 Root Register。长期看会让 `ArchitectureScope` 变成补丁层,代码容易拧巴。
下一版应该选择:
```text
借鉴 ft_3 Container 的算法和 API 手感
但实现 FlowScope 自己的 Container / Scope Tree
```
## FlowScope Container 目标模型
FlowScope 的容器节点应该同时承担三件事:
```text
Container 负责能力
ArchitectureScope 负责规则
Feature 工厂负责业务模块创建
```
但实现上不再拆成“外层 Scope 包内层 Container.IScope”而是统一为 FlowScope 自己的 Scope 节点:
```text
ContainerScope / ArchitectureScope
Name
Parent
Children
Dictionary<ServiceKey, Registration>
CreateScope()
CreateFeature<TFeature>()
Register()
Resolve()
TryResolve()
Dispose()
```
其中 `ServiceKey` 继续沿用 ft_3 的核心思路:
```csharp
public readonly struct ServiceKey
{
public Type Type { get; }
public string Name { get; }
}
```
## 生命周期规则
下一版容器至少支持:
```text
Singleton
注册所在 Scope 持有一个实例。
PerScope
每个 Resolve 发起 Scope 持有一个实例。
Transient
每次 Resolve 都创建新实例。
Instance
注册现有实例。是否 Dispose 需要显式规则,默认不销毁外部传入实例更安全。
```
`PerScope` 是从 ft_3 里最值得保留的能力之一。它适合活动、Feature、局部流程这种“同一个注册不同 Scope 下得到不同实例”的场景。
## Register / Resolve 目标规则
FlowScope 自己的 Container 需要直接支持当前讨论确定的规则:
1. 任意 Scope 都可以随时 Register。
2. Register 永远只写当前 Scope。
3. Resolve 永远从当前 Scope 开始。
4. 当前 Scope 找不到时,才向 Parent 查找。
5. 子 Scope 可以覆盖父 Scope 的注册。
6. 子 Scope 不能修改父 Scope 的注册。
7. Root 只是没有 Parent 的普通 Scope。
8. Parent / Children 是 FlowScope 自己容器模型的一部分,不依赖底层外部容器。
示例:
```csharp
var game = FlowScope.Create();
game.Register<IConfigService, ConfigService>().AsSingleton();
var boss = game.CreateFeature<BossFightFeature>();
boss.Register<IRewardService, BossRewardService>().PerScope();
using var revive = boss.CreateScope("RevivePanel");
revive.Register<IRewardService, ReviveRewardService>().PerScope();
var reward = revive.Resolve<IRewardService>();
```
解析顺序:
```text
RevivePanel Scope -> BossFightFeature Scope -> Root Scope
```
`revive.Register` 不污染 `boss``boss.Register` 不污染 `root`
## CreateFeature 规则
`CreateFeature<TFeature>()` 是 Scope 创建 + Feature 安装的语义封装。
调用者不传 name 时,默认使用 `typeof(TFeature).Name`
```csharp
var boss = game.CreateFeature<BossFightFeature>();
// 等价于 game.CreateFeature<BossFightFeature>(typeof(BossFightFeature).Name)
```
推荐 API
```csharp
IArchitectureScope CreateFeature<TFeature>()
where TFeature : IFeature, new();
IArchitectureScope CreateFeature<TFeature>(string name)
where TFeature : IFeature, new();
```
第一版可以先使用 `where TFeature : IFeature, new()`,保持简单:
```csharp
public IArchitectureScope CreateFeature<TFeature>(string name)
where TFeature : IFeature, new()
{
var child = CreateScope(name);
var feature = new TFeature();
feature.Install(child);
return child;
}
```
后续如果 `TFeature` 需要依赖注入构造,可以改为从当前 Scope 创建:
```csharp
child.Register<TFeature>().PerScope();
var feature = child.Resolve<TFeature>();
feature.Install(child);
```
调用侧不应该手写:
```csharp
boss.InstallFeature(new BossFightFeature());
boss.UseArchitecture(new BossFightArchitecture());
```
Feature 的安装和 Scope 的创建应该被 `CreateFeature<TFeature>()` 收住。
## Feature 默认边界
大多数业务默认只需要:
```text
Root Scope
Feature Scope
```
例如:
```text
Root Scope
BossFightFeature Scope
ScratchTicketFeature Scope
```
BossFight 内部的战斗、复活、奖励、UI不应该默认都拆成 Scope。它们可以优先放在 Feature 内部架构里:
```text
BossFightFeature
Model
Service
Controller
ViewModel
ViewBinder
StateMachine
```
只有当局部生命周期、依赖覆盖、并行实例、独立释放这些需求明确存在时,才创建 Child Scope。
## Feature 与 MVC / MVVM
`Feature` 只定义业务模块边界,不强制决定内部表现层架构。
Feature 内部可以选择:
- MVC
- MVVM
- MVP
- Presenter
- Flow Controller
- State Machine
- 简单 Service + View 组合
框架不应该强制所有 Feature 使用同一种 UI 或业务组织模式。
示例:
```csharp
public sealed class BossFightFeature : IFeature
{
public void Install(IArchitectureScope scope)
{
scope.Register<BossFightModel>().PerScope();
scope.Register<IBossFightService, BossFightService>().PerScope();
scope.UseMvvm<BossFightViewModel>();
scope.UseMvc<BossFightController>();
}
}
```
`UseMvvm``UseMvc` 这类 API 可以作为后续扩展模块,不一定属于第一版核心。
## 与 ft_3 Activity 的关系
Activity 不应该等同于大 Feature 内部的所有业务实现。
更合理的职责划分:
```text
Activity
负责活动开关、入口、可见性、预加载、跳转、生命周期接入。
Feature
负责活动作为业务模块的运行边界。
ArchitectureScope
负责该模块的依赖注册、解析、父子继承、局部覆盖和生命周期。
MVC / MVVM / State
负责 Feature 内部具体业务和 UI 组织。
```
也就是说Activity 更像外部接入器Feature 才是业务模块边界。
## 约束方式
约束重点不应该是禁止 Register而是让代码默认只能操作当前 Scope。
建议通过接口和可见性控制:
```csharp
public interface IArchitectureScope : IDisposable
{
string Name { get; }
IScopeInfo Parent { get; }
IArchitectureScope CreateScope(string name);
IArchitectureScope CreateFeature<TFeature>()
where TFeature : IFeature, new();
IArchitectureScope CreateFeature<TFeature>(string name)
where TFeature : IFeature, new();
IRegistrationBuilder Register<TImplementation>();
IRegistrationBuilder Register<TService, TImplementation>()
where TImplementation : TService;
T Resolve<T>();
T Resolve<T>(string name);
}
```
`Parent` 可以只暴露只读元信息,避免业务代码通过 Parent 直接写父 Scope。
```csharp
public interface IScopeInfo
{
string Name { get; }
IScopeInfo Parent { get; }
}
```
如果确实需要高级能力,可以后续通过内部接口或显式高级 API 暴露,而不是默认给所有业务代码。
## 推荐最小核心 API
第一版可以先只实现:
```csharp
public interface IFeature
{
void Install(IArchitectureScope scope);
}
public interface IArchitectureScope : IDisposable
{
string Name { get; }
IScopeInfo Parent { get; }
IArchitectureScope CreateScope(string name);
IArchitectureScope CreateFeature<TFeature>()
where TFeature : IFeature, new();
IArchitectureScope CreateFeature<TFeature>(string name)
where TFeature : IFeature, new();
IRegistrationBuilder Register<TImplementation>();
IRegistrationBuilder Register<TImplementation>(string name);
IRegistrationBuilder Register<TService, TImplementation>()
where TImplementation : TService;
IRegistrationBuilder Register<TService, TImplementation>(string name)
where TImplementation : TService;
void RegisterInstance<T>(T instance);
void RegisterInstance<T>(T instance, string name);
T Resolve<T>();
T Resolve<T>(string name);
bool TryResolve<T>(out T value);
bool TryResolve<T>(out T value, string name);
}
```
工厂方法:
```csharp
var game = FlowScope.Create();
var boss = game.CreateFeature<BossFightFeature>();
```
## 暂不进入的内容
以下能力暂时不作为下一版第一阶段重点:
- Command / Query / Event 通信模型。
- 完整 MVC / MVVM 框架。
- 自动扫描所有 Feature。
- UI Panel 生命周期统一接管。
- 复杂诊断面板。
- 热更新资源接入细节。
原因是当前最核心的问题还在:
```text
Scope / Feature / Container / Architecture 的第一层抽象需要先稳定。
```
通信和表现层规则应该建立在稳定 Scope 模型之上。
## 验收标准
下一版实现完成后,应满足:
1. Root 和普通 Scope 使用同一种 `ArchitectureScope` 模型。
2. `CreateFeature<TFeature>()` 默认使用 `typeof(TFeature).Name` 作为 Scope 名称。
3. `CreateFeature<TFeature>(name)` 会自动创建子 Scope 并调用 `TFeature.Install(scope)`
4. 任意 Scope 可以随时 Register且只能注册到自己。
5. Resolve 从当前 Scope 开始,支持向父 Scope 查找。
6. 子 Scope 可以覆盖父 Scope 注册。
7. 子 Scope Dispose 不会释放父 Scope 对象。
8. 父 Scope Dispose 会释放子 Scope。
9. Feature Scope 默认就是主要业务边界。
10. Child Scope 能用,但文档和测试不鼓励把普通 UI/VM/Controller 都拆成 Scope。
11. 第一阶段不要求实现 Command / Query / Event。
## 当前设计取舍
这套设计选择了:
```text
默认 Root -> Feature复杂时再 Child Scope。
```
而不是:
```text
强制 Root -> Feature -> Flow -> UI -> Task。
```
原因是 FlowScope 应该帮助真实项目收住边界,而不是替业务制造额外层级。框架应提供可递归的 Scope 能力,但默认心智模型必须保持简单。

View File

@@ -0,0 +1,473 @@
# uFrame MVVM Core 分析
日期2026-06-12
## 结论
uFrame MVVM 的核心价值不在它的 Kernel、IOC 启动或代码生成器,而在运行时 MVVM 的几个机制:
- `ViewModel` 持有可观察状态。
- `P<T>` 把 ViewModel 属性包装成可订阅的数据流。
- `Signal<TCommand>` 把 UI 操作表达成命令流。
- `ViewBase` 通过 `Bind / Unbind` 管理订阅生命周期。
- `ViewService` 负责把 View 与 ViewModel 对接。
但这套 MVVM 被 uFrame 自身的全局 Kernel、全局容器、命名约定、`Resources.Load` 和设计器生成代码强绑定。它可以作为反面边界样本和局部机制参考,不适合作为 FlowScope Kernel 或 UI 模块的直接原型。
FlowScope 可以吸收“可观察状态 + 命令流 + 绑定生命周期作用域”,但不应吸收它的全局 IOC 查找、ViewModel 自动解析、Controller 命名约定和资源自动定位。
## 源码范围
本次分析基于 `uFrame/uFrame.MVVM` 当前公开仓库源码,重点阅读:
- [`ViewModel.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/ViewModels/ViewModel.cs)
- [`ModelPropertyBase.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/ViewModels/ModelPropertyBase.cs)
- [`Signal.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/ViewModels/Signal.cs)
- [`ViewBase.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Views/ViewBase.cs)
- [`View.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Views/View.cs)
- [`Controller.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Controllers/Controller.cs)
- [`ViewService.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Services/ViewService.cs)
- [`MVVMKernelExtensions.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/MVVMKernelExtensions.cs)
- [`ViewBindings.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Bindings/ViewBindings.cs)
- [`ViewResolver.cs`](https://github.com/uFrame/uFrame.MVVM/blob/master/uFrame.MVVM/Source/Runtime/Views/ViewResolver.cs)
## 运行时结构
uFrame MVVM 的运行时可以拆成五组对象:
```text
ViewModel 层
ViewModel
P<T>
ModelCollection<T>
Signal<TCommand>
ViewModelCommand
View 层
ViewBase
View<TModel>
ViewComponent
ViewBindings
创建与连接层
Controller
ViewService
ViewResolver
绑定层
Binding
ModelPropertyBinding
ModelViewPropertyBinding
ModelViewModelCollectionBinding
全局基础设施
uFrameKernel.Container
IEventAggregator
SystemService / SystemServiceMonoBehavior
```
它不是一个独立、纯粹的 MVVM runtime。`ViewModel``View``Controller``ViewService` 都会触碰 uFrame Kernel 或全局容器。
## ViewModel
`ViewModel` 是所有生成 ViewModel 的基类。它承担了状态、绑定、序列化、事件发布、销毁通知等多种职责。
关键职责:
- 通过 `FillProperties` 暴露属性元信息。
- 通过 `FillCommands` 暴露命令元信息。
- 持有 `Bindings`,用于释放订阅。
- 持有 `Identifier`,用于全局容器按 id 查找。
- 通过 `PropertyChanged` 通知属性变化。
- `Dispose` 时发布 `ViewModelDestroyedEvent`
问题是它太重。一个干净的 ViewModel 应该主要表达 UI 状态和 UI 意图,但这里的 `ViewModel` 混入了:
- 序列化协议。
- `IBindable`
- `IDisposable`
- 全局事件聚合器。
- binding 容器。
- 引用计数。
- controller wiring hook。
这会让 ViewModel 从“状态对象”变成“框架对象”。长期项目里,这种设计会让测试、复用和生命周期判断都变复杂。
## P<T>
`P<T>` 是 uFrame MVVM 最值得看的部分。它把单个属性封装成 observable property
```csharp
public class P<T> : ISubject<T>, IObservableProperty, ISimpleNotifyPropertyChanged
```
它提供:
- `Value`:当前值。
- `LastValue`:上一次值。
- `ChangedObservable`:只在值变化时通知。
- `ObjectValue`:非泛型访问入口。
- `Owner`:所属 ViewModel。
- `PropertyName`:属性名。
- `ToComputed`:根据其他 observable property 计算派生值。
View 侧可以订阅 `P<T>`,属性变化时刷新 UI。这个机制适合 Unity UI因为它避免了大量手动 `Refresh()` 和每帧轮询。
可取点:
- 属性变化可以显式订阅。
- 派生属性可以声明依赖。
- View 绑定时可以立即应用当前值。
- IDisposable 可以挂到绑定作用域统一释放。
风险点:
- `Value` setter 没有先比较新旧值,任何赋值都会触发通知。
- `Owner` 让 property 反向知道 ViewModel。
- `P<T>` 同时实现 subject、property、notify职责偏多。
- `ChangedObservable` 依赖 `Value``LastValue` 的外部状态,语义不够直观。
FlowScope 如果吸收这个思路,应改成更小的 `ObservableProperty<T>`
```text
只负责保存值、比较值、发布变化、释放订阅。
不要知道全局 Kernel。
不要知道 View。
不要知道资源或 Controller。
```
## Signal<TCommand>
`Signal<TCommand>` 用来表达 ViewModel 命令:
```csharp
public class Signal<TClass> : ISubject<TClass>, ISignal<TClass>
where TClass : IViewModelCommand, new()
```
它的行为是:
1. View 或绑定层构造 command。
2. 调用 `Signal.OnNext(command)`
3. `Signal``Sender` 设置为当前 ViewModel。
4. 执行本地 `Action`
5. 推送给订阅者。
这个方向是好的UI 操作不应该直接调用业务系统,而应该变成可观察的 command 或 intent。
可取点:
- UI 输入被建模为显式命令。
- 命令可以订阅、组合、测试。
- View 与业务处理之间不需要直接互相引用。
风险点:
- command 的 sender 被 `Signal` 自动写入,有隐式副作用。
- 命令流最终仍然容易接到全局 EventAggregator。
- 生成器会为每个命令生成强约定代码,手写维护成本高。
FlowScope 可以吸收 command stream但建议命名为更中性的 `UIIntent` / `ViewAction` / `FeatureCommand`,并放在 feature scope 内,不进入 Kernel core。
## ViewBase / View<TModel>
`ViewBase` 是 View 与 ViewModel 的连接点。它提供:
- `ViewModelObject`:当前绑定的 ViewModel。
- `Bind()`:用户编写订阅逻辑。
- `PreBind()`:生成器插入绑定逻辑。
- `AfterBind()`:绑定完成后的 hook。
- `Unbind()`:释放绑定。
- `AddBinding()`:把 disposable 放到当前 View 的绑定列表。
- `BindOnStart`:是否启动时自动绑定。
- `DisposeViewModelOnDestroy`View 销毁时是否销毁 ViewModel。
`View<TModel>` 只是给 `ViewBase` 加了泛型 Model 访问:
```csharp
public abstract class View<TModel> : ViewBase where TModel : ViewModel, new()
```
可取点:
- View 的绑定逻辑有明确生命周期。
- 订阅统一挂到 binding scope。
- View 只响应 ViewModel 的状态,不需要每帧刷 UI。
- View 销毁时可以集中释放订阅。
问题点:
- `ViewBase.KernelLoaded()` 会调用 `uFrameKernel.Container.Inject(this)`
- `OnDestroy()` 依赖 `uFrameKernel.IsKernelLoaded` 和全局事件。
- View 的 `Bindings` 实际挂在 `ViewModelObject.Bindings[ViewId]`View 和 ViewModel 的生命周期互相缠住。
- `References` 引用计数用于判断 ViewModel 是否销毁,复杂且脆弱。
FlowScope 可以借鉴 `Bind / Unbind / AddBinding`,但绑定容器应属于 View 或 feature-local binding scope而不是塞进 ViewModel。
## Controller
uFrame 的 `Controller` 是特殊的 `SystemService`,主要负责创建和初始化 ViewModel。
流程大致是:
```text
Controller.Create(identifier)
-> CreateEmpty(identifier)
-> RegisterViewModel(vm, identifier)
-> Initialize(vm)
-> Publish ViewModelCreatedEvent
```
问题在于 `CreateEmpty` 内部直接注册到全局容器:
```text
uFrameKernel.Container.RegisterViewModel(vm, identifier)
```
`MVVMKernelExtensions.CreateViewModel(type)` 又会通过 ViewModel 类型名去全局容器找 Controller
```text
Resolve<Controller>(type.Name)
```
这是强命名约定 + 服务定位器。它表面上把 ViewModel 创建集中到了 Controller实际上把依赖关系藏进全局容器和类型名字符串。
FlowScope 不应该吸收这套 Controller 模式。更合适的是:
```text
Feature 明确创建 ViewModel。
Feature 明确创建/绑定 View。
FeatureContext 提供本 feature 需要的服务。
UI module 不通过全局容器按名字找 Controller。
```
## ViewService / ViewResolver
`ViewService` 负责:
- 监听 `InstantiateViewCommand`
- 根据 ViewModel 找 View prefab。
- 实例化 View。
- 在 View 创建时补齐 ViewModel。
- View 销毁时移除记录。
- ViewModel 销毁时从全局容器移除实例。
`ViewResolver` 默认根据 ViewModel 类型名找 prefab
```text
PlayerViewModel -> Player -> Resources.Load("Player")
```
这是老式 Unity 框架常见做法,但不适合 FlowScope
- 资源路径是隐式命名约定。
- 默认绑定到 `Resources.Load`
- 不支持现代资源后端的显式 handle、group、release。
- 不利于 Addressables / YooAsset / AssetBundle / 自定义资源系统适配。
- View 的创建和 ViewModel 的创建混在同一个服务里。
FlowScope 如果需要 UI 模块,可以把它拆成:
```text
IViewFactory
负责创建 View不负责创建 ViewModel。
IViewBinder<TView, TViewModel>
负责绑定和解绑。
IUIViewRegistry 或 IViewCatalog
负责显式注册 view key 到 prefab/address。
IResourceService adapter
负责真实资源加载和释放。
```
## Binding
uFrame 的 binding 层包括:
- `Binding`
- `ModelPropertyBinding`
- `ModelViewPropertyBinding`
- `ModelViewModelCollectionBinding`
- `ViewBindings` 扩展方法
最实用的是 `ViewBindings.BindProperty`
```text
立即用当前值刷新一次 UI。
订阅后续变化。
把 IDisposable 加入 bindable 的绑定列表。
```
这是 FlowScope 可以吸收的重点:
```csharp
bindingScope.Bind(vm.Title, value => titleText.text = value);
```
但不要吸收它的反射绑定和生成器绑定作为 core。反射/生成器可以是工具层能力,不应该成为 UI runtime 的基本前提。
## 与 FlowScope Kernel 的关系
uFrame MVVM 不应该进入 FlowScope Kernel core。
FlowScope Kernel 已经明确应保持机制小核:
```text
Container
AppKernel
StartupPipeline
FeatureHost
FeatureInstance
FeatureContext
FeatureLifetime
FeatureSlot
FeaturePolicy
```
MVVM 属于更高层的 UI module 或 feature-local module。Kernel 最多提供 feature 生命周期和上下文,不提供:
- ViewModel 基类。
- View 自动绑定。
- UI prefab 查找。
- 命令总线。
- 全局 ViewService。
- 全局事件聚合器。
- 全局 IOC 注入 View。
推荐边界:
```text
FlowScope.Kernel
只负责 Feature 生命周期和 Slot/Policy 机制。
FlowScope.UI
提供 ViewModel、ObservableProperty、BindingScope、ViewBinder。
FlowScope.Resources
提供资源加载接口和后端适配。
Game Feature
自己声明 ViewModel、View、Binder、Command handler。
```
## 可吸收清单
可以吸收:
- `ObservableProperty<T>` 思路。
- `ObservableCollection<T>` 思路。
- UI command / intent stream。
- `Bind / Unbind` 生命周期。
- `IDisposable` binding scope。
- View 订阅 ViewModelViewModel 不直接操作 View。
- View 创建和 ViewModel 绑定分离。
- 集合绑定生成子 View 的思路,但要显式管理资源释放。
不应吸收:
- `uFrameKernel.Container` 全局创建 ViewModel。
- `Container.Inject(view)`
-`ViewModel` 类型名查找 `Controller`
-`ViewModel` 类型名 `Resources.Load` prefab。
- ViewModel 持有 View 绑定列表。
- ViewModel 引用计数决定销毁。
- 全局 EventAggregator 作为 UI command 默认通道。
- 代码生成器作为 runtime 必需条件。
- ViewModel 同时承担序列化、绑定、事件、销毁、controller hook。
## FlowScope 可采用的最小 UI MVVM 形态
建议的最小形态:
```text
ObservableProperty<T>
保存值并发布变化。
BindingScope
持有 IDisposable 列表Dispose 时统一释放。
IViewModel
标记 UI 状态对象,不依赖 Kernel。
IViewBinder<TView, TViewModel>
Bind(view, vm, scope)
Unbind 由 scope.Dispose 承担。
UIView<TViewModel>
MonoBehaviour只持有 Unity UI 引用和当前 ViewModel。
UICommand<T>
feature-local command不走全局事件总线。
```
示意:
```csharp
public sealed class BindingScope : IDisposable
{
private readonly List<IDisposable> _items = new();
public T Add<T>(T item) where T : IDisposable
{
_items.Add(item);
return item;
}
public void Dispose()
{
foreach (var item in _items)
item.Dispose();
_items.Clear();
}
}
```
```csharp
public sealed class MainMenuViewModel
{
public ObservableProperty<string> Title { get; } = new("Main Menu");
public Subject<StartGameIntent> StartGame { get; } = new();
}
```
```csharp
public sealed class MainMenuBinder
{
public void Bind(MainMenuView view, MainMenuViewModel vm, BindingScope scope)
{
scope.Bind(vm.Title, value => view.TitleText.text = value);
scope.Add(view.StartButton.OnClickAsObservable()
.Subscribe(_ => vm.StartGame.OnNext(new StartGameIntent())));
}
}
```
关键点是:这里没有全局容器查找,没有 `Resources.Load` 约定,没有 Kernel 注入 View也没有 ViewModel 引用计数。
## 最终判断
uFrame MVVM 的局部机制比它的 Kernel 更有参考价值,但必须拆开看。
真正值得保留的是:
```text
状态可观察
操作命令化
绑定有生命周期
View 不直接写业务
```
必须避免的是:
```text
全局容器创建一切
命名约定连接一切
ViewModel 变成框架对象
资源加载隐藏在 ViewResolver
UI 模块污染 Kernel core
```
对 FlowScope 来说uFrame MVVM 最好的用途不是成为模板而是提醒我们UI MVVM 应该是 Kernel 之上的可选模块,且必须保持 feature-local、显式绑定、显式资源、显式生命周期。

View File

@@ -0,0 +1,261 @@
# FlowScope 框架评审文档
评审日期2026-05-19
评审对象:`My project/Assets/FlowScope`
评审范围Runtime、Tests、Samples、asmdef、核心服务实现与测试覆盖
评审结论:建议有条件通过,定位为 P0/P1 阶段轻量 Unity 游戏基础框架
## 1. 总体结论
FlowScope 当前已经具备一个轻量游戏框架的基本形态,适合用于小型项目、原型项目或作为后续框架演进的 P0 基座。它的优势在于分层清晰、抽象克制、测试意识较好,并且围绕 Unity 常见横切能力建立了可替换接口。
当前不建议直接定义为稳定生产框架。主要原因是资源异步、运行时对象销毁、Addressables 依赖方式和自研 JSON 转换器仍存在生产风险。这些问题不影响它作为框架雏形继续推进,但应作为进入生产复用前的前置整改项。
## 2. 技术基线
- Unity 版本:`2022.3.62f2c1`
- 主要依赖:
- `com.cysharp.r3`
- `org.nuget.r3`
- `com.unity.ugui`
- `com.unity.textmeshpro`
- `com.unity.timeline`
- 目录结构:
- `Runtime`:框架运行时代码
- `Tests/EditMode`:编辑器测试
- `Tests/PlayMode`:运行时测试
- `Samples/MainMenuP0`P0 示例场景与示例业务
- asmdef
- `FlowScope.Runtime`
- `FlowScope.Tests.EditMode`
- `FlowScope.Tests.PlayMode`
- `FlowScope.Samples.MainMenuP0`
## 3. 现有模块评价
### 3.1 Flow
代表文件:`Runtime/Flow/GameFlow.cs`
`GameFlow` 负责 Feature 生命周期编排,包括启动、切换、关闭、失败清理和状态保护。整体职责清晰,和 `IFeature``FeatureContext` 的边界较自然。
优点:
- 状态机明确,避免非法状态调用。
- Feature 使用独立 scope便于做生命周期隔离。
- 失败路径中有资源、容器、Feature 的清理逻辑。
- 测试覆盖了启动、切换、关闭、失败和取消场景。
风险:
- `StartupAsync` 固定依赖 `IConfigProvider.LoadAllAsync`,会让无配置项目、分包配置、热更新配置的启动流程不够灵活。
- `GameFlow` 当前更像“启动编排器 + Feature 生命周期管理器”的混合体,后续业务复杂时可能需要把配置加载职责移回 Bootstrap。
评价:结构良好,建议保留为框架核心,但需要降低对配置加载的硬绑定。
### 3.2 Container
代表文件:`Runtime/Container/Container.cs`
自研容器实现轻量,支持实例注册、工厂注册、作用域、父子容器、循环依赖检测和拥有对象释放。
优点:
- API 简单,学习成本低。
- scope 模型适合 Feature 生命周期。
- dispose 顺序和循环依赖都有测试。
- 没有引入大型 DI 框架,符合轻量框架定位。
风险:
- `GeneratedFactories` 需要外部注册,长期需要配套生成器或明确手写规范。
- 反射工厂适合开发期,但移动端/AOT 环境需要持续验证。
- 当前没有生命周期枚举,例如 transient、scoped、singleton实际行为需要文档明确。
评价P0 阶段可接受。后续重点不是扩大能力,而是写清注册规范和 AOT 策略。
### 3.3 Resources
代表文件:`Runtime/Resources/AddressablesResourceService.cs`
资源层抽象了 `IResourceService``IResourceHandle<T>``IResourceGroup`,并实现了 Addressables 加载、引用计数和分组释放。
优点:
- 使用句柄释放资源,方向正确。
- 支持同 key 同类型加载去重。
- `ResourceGroup` 适合跟随 Feature 生命周期批量释放。
- PlayMode 测试覆盖了复用、引用计数和取消等路径。
风险:
- Addressables 通过字符串反射调用,编译期无法发现 API 变化。
- Runtime asmdef 没有显式引用 Addressables当前设计牺牲了类型安全。
- 取消等待后底层 Addressables 任务可能仍继续执行,后续需要确认资源完成后的释放策略。
评价:抽象方向正确,但 Addressables 适配层应从 Runtime 核心拆出,改为显式依赖的独立 asmdef。
### 3.4 UI
代表文件:`Runtime/UI/UIManager.cs`
UI 模块基于 `UIPanelAttribute` 标记层级和路径,通过 `UIManager` 加载 prefab、挂载到 Canvas、绑定 ViewModel并支持 Destroy/Cache 策略。
优点:
- 使用 Attribute 降低打开面板时的字符串散落。
- 支持按层关闭、关闭全部、缓存复用。
- 打开失败时有句柄释放和实例销毁逻辑。
- PlayMode 测试覆盖了路径、层级、缓存、关闭顺序和 Bind 失败。
风险:
- 运行时代码使用 `DestroyImmediate`,不适合常规 PlayMode/Runtime 生命周期。
- 面板路径约定较隐式,需要文档约束或配置化。
- 当前只允许关闭栈顶面板,适合简单 UI但复杂弹窗管理可能需要更细的策略。
评价:可作为 P0 UI 管理器,但 `DestroyImmediate` 应优先修正。
### 3.5 Audio
代表文件:`Runtime/Audio/AudioService.cs`
音频模块提供 BGM、SFX、音量、静音、淡入淡出和 SFX 池。
优点:
- 接口简单,适合快速接入。
- SFX 池和复用策略清楚。
- PlayMode 测试覆盖了播放、停止、池耗尽、音量和淡入淡出。
风险:
- `LoadClip` 内部使用 `.GetAwaiter().GetResult()` 同步等待异步资源加载,在 Unity 主线程和 Addressables 场景下存在卡顿或死锁风险。
- 音频服务是 `MonoBehaviour`,但依赖通过 `Initialize` 注入,需要文档明确创建流程。
- 当前播放接口是同步方法,与异步资源系统存在模型冲突。
评价:功能够用,但异步资源加载方式必须调整后才适合生产复用。
### 3.6 Save / Config / Data
代表文件:
- `Runtime/Save/SaveService.cs`
- `Runtime/Config/JsonConfigProvider.cs`
- `Runtime/Data/ReactivePropertyJsonConverter.cs`
数据相关模块围绕 R3 的 `ReactiveProperty` 做了 JSON 序列化、存档读写和配置加载。
优点:
- Save 的 storage 和 serializer 可替换。
- Config 支持多表加载和重复 id 检查。
- Data 层能处理 `ReactiveProperty<T>`,契合 R3 数据模型。
- 相关 EditMode 测试较完整。
风险:
- JSON parser/serializer 是自研实现,长期边界成本较高。
- 当前更适合简单 DTO不适合复杂对象图、版本迁移、字段重命名、特殊字符完整兼容等场景。
- `GetAll<T>` 返回新 List顺序来自 Dictionary values不应作为稳定排序依赖。
评价P0 可以使用,但应明确“简单数据模型”边界。生产化建议接入成熟 JSON 库,保留 R3 适配层。
## 4. 测试覆盖评价
现有测试覆盖优于一般框架雏形,尤其是:
- `GameFlowTests` 覆盖生命周期和异常清理。
- `ContainerTests` 覆盖注册、scope、dispose、循环依赖。
- `UIManagerTests` 覆盖 UI 打开、关闭、缓存和异常。
- `AudioServiceTests` 覆盖播放、停止、池和音量。
- `SaveServiceTests``JsonConfigProviderTests``DataSerializationTests` 覆盖数据链路。
仍建议补充:
- Addressables 真实包接入测试或集成验证。
- AOT/linker 场景验证。
- UI 使用 `Destroy` 后的帧级测试调整。
- 音频异步加载接口改造后的取消、失败和重复播放测试。
- Config/Save 数据版本迁移测试。
## 5. 主要风险清单
| 优先级 | 风险 | 影响 | 建议 |
| --- | --- | --- | --- |
| P0 | `AudioService` 同步等待异步资源 | 可能导致主线程卡顿或死锁 | 改为异步播放或预加载机制 |
| P0 | Runtime 使用 `DestroyImmediate` | 运行时生命周期不稳 | 改为 `Destroy`,测试按帧等待 |
| P1 | Addressables 反射调用 | 类型安全弱,升级风险高 | 拆独立 Addressables 适配 asmdef |
| P1 | 自研 JSON 转换器 | 边界和维护成本高 | 明确 DTO 限制或接入成熟 JSON 库 |
| P1 | `GameFlow` 固定加载配置 | 启动流程不够弹性 | 配置加载移到 Bootstrap 或可选策略 |
| P2 | 容器生命周期语义文档不足 | 使用者容易误判对象生命周期 | 补充注册和释放规范 |
## 6. 审批意见
结论:有条件通过。
通过范围:
- 可继续作为 FlowScope P0/P1 基础框架推进。
- 可用于示例项目、小型原型项目和内部验证项目。
- 可作为后续 Runtime 包化、模块拆分和 API 稳定化的基础。
限制条件:
- 暂不建议标记为生产稳定版本。
- 暂不建议在多人长期项目中无约束扩散使用。
- 在进入生产复用前,必须完成 P0 风险整改。
前置整改项:
1.`UIManager` 运行时销毁逻辑从 `DestroyImmediate` 调整为 `Destroy`
2.`AudioService` 的资源加载从同步阻塞改为异步或预加载模型。
3. 为 Addressables 适配明确依赖策略,优先拆出独立 asmdef。
4. 补充框架使用约定文档,包括 Bootstrap、Feature、资源释放、UI 路径、配置 DTO 限制。
## 7. 后续建议
短期建议:
- 先修生命周期和异步资源问题,不急于扩展新模块。
- 保持框架轻量,不引入事件总线、复杂状态机编辑器、大型 DI 等重能力。
- 将 Samples 继续作为验收样板,所有框架规则都应能在 Sample 中体现。
中期建议:
- 将 Runtime 拆为核心层和适配层,例如 `FlowScope.Runtime``FlowScope.Addressables`
- 为容器工厂生成建立明确流程,降低手写注册成本。
- 给 Save/Config 增加数据版本和迁移策略。
长期建议:
- 明确 FlowScope 的定位:小型游戏基础设施,而不是全能游戏引擎框架。
- API 稳定后再考虑 package 化和跨项目复用。
- 保持测试先行,所有生命周期规则和失败路径都应有测试保护。
## 8. 最终评级
| 维度 | 评级 | 说明 |
| --- | --- | --- |
| 架构清晰度 | 良好 | 模块边界清楚,抽象克制 |
| 可测试性 | 良好 | 已有较完整 EditMode/PlayMode 测试 |
| Unity 生命周期安全 | 一般 | `DestroyImmediate` 和同步等待需修正 |
| 生产稳定性 | 一般 | 仍处于 P0/P1 框架雏形 |
| 可演进性 | 良好 | 适合继续拆模块、补规范、稳定 API |
综合评级B+。
结论摘要FlowScope 是一个有工程意识的轻量 Unity 框架骨架,值得继续推进;但在正式生产复用前,需要优先修正异步资源、运行时销毁和 Addressables 依赖方式三个关键问题。
# P1 更新记录2026-05-20
P1 已完成第一批生产化硬化,评审风险状态同步如下:
| 原风险 | P1 状态 | 说明 |
| --- | --- | --- |
| Addressables 反射调用 | 已降低 | Addressables 适配已从 Runtime 核心拆出到 `FlowScope.Addressables` 独立 asmdef核心资源系统通过 `IResourceBackend` 工作。最终验收仍等待 Unity 完成 Addressables 包解析。 |
| 自研 JSON 转换器缺少扩展点 | 已降低 | Config 增加 `IConfigSource` / `IConfigParser`Save 增加版本 envelope 与 `ISaveMigration` 链。长期是否替换成熟 JSON 库仍是 P2 决策。 |
| UI 缺少轻量导航与预加载 | 已降低 | 新增 `UIScreenNavigator``UIPreloadService`,并补充 PlayMode 编译级测试。 |
| 样例不能体现生产化装配 | 已降低 | MainMenuP0 已改为 P1 装配方式,覆盖启动、点击、保存恢复和资源释放。 |
| Package/editor tooling | 保持 P2 | 本轮没有做包分发、编辑器工具和跨项目模板化。 |
当前结论仍是“有条件通过”P1 代码与文档已落地,但完整 Unity Test Runner 和人工场景验收需要在 Unity 工程未被占用、Addressables 包解析完成后补跑。

View File

@@ -0,0 +1,50 @@
# FlowScope Game Core P1 完成报告
## 范围
本轮执行 `docs/superpowers/plans/2026-05-20-p1-production-hardening-plan.md` 中的 P1 生产化硬化任务范围包括资源后端拆分、配置与存档扩展点、UI 导航与预加载、MainMenuP0 样例升级,以及使用规范和完成报告。
## 已完成
- 定义 P1 契约:`IResourceBackend``IConfigSource``IConfigParser``ISaveMigration``IUIScreenNavigator``IUIPreloadService`
- 资源系统拆分:核心 `ResourceService` 保持在 RuntimeAddressables 适配移动到 `FlowScope.Addressables` 独立 asmdef。
- 配置扩展:`JsonConfigProvider` 支持 source/parser新增 `InMemoryConfigSource``FileConfigSource``JsonConfigParser`
- 存档迁移:`JsonSaveSerializer` 支持版本 envelope`SaveMigrationRegistry` 支持连续迁移链。
- UI 扩展:新增 `UIPreloadService``UIScreenNavigator``UIManager` 支持复用预加载 panel handle。
- MainMenuP0 样例:改为 P1 装配方式,测试覆盖启动可见、点击即时更新、关闭保存、重启恢复、资源释放。
- 文档:新增运行时使用规范和 MainMenuP0 P1 样例验收说明。
## 测试结果
- `dotnet build "My project\FlowScope.Runtime.csproj" --no-restore`通过0 错误;存在 Unity 生成项目的引用版本冲突警告。
- `dotnet build "My project\FlowScope.Tests.EditMode.csproj" --no-restore`通过0 错误;存在相同引用警告。
- `dotnet build "My project\FlowScope.Tests.PlayMode.csproj" --no-restore`:完整编译失败,原因是 `FlowScope.Addressables` 生成项目和 `com.unity.addressables` 包尚未由 Unity 解析。
- 临时排除 `AddressablesResourceBackendTests.cs` 后的 PlayMode 编译通过0 错误;用于确认 UI 与样例集成未破坏。
## 人工验收
尚未完成。主工程当前被其他 Unity 实例占用Unity batchmode 无法打开同一 project path无法执行 Test Runner 全量 PlayMode 和 MainMenuP0 场景人工验收。
待 Unity 可用后,需要重新执行:
- Unity 包解析,确认 `Packages/packages-lock.json` 更新并出现 `com.unity.addressables`
- PlayMode: `FlowScope.Tests.PlayMode.Samples.MainMenuP0Tests`
- PlayMode 全量测试。
- 打开 `My project/Assets/FlowScope/Samples/MainMenuP0/Scenes/MainMenuP0.unity` 做人工点击、保存恢复、资源释放验收。
## 未进入本轮
- Luban 完整接入。
- YooAsset / AssetBundle 后端。
- 云存档。
- AudioMixer / 3D 音频。
- Package 分发和编辑器工具链。
- 完整 UI 路由系统。
## P2 建议
- 让 Unity 完成 Addressables 包解析后补一次 P1 验收提交,包含 `packages-lock.json` 和 Unity Test Runner 结果。
- 将 AudioService 的同步资源加载改为异步或预加载模型。
- 为 Runtime/package 分发建立 asmdef、Samples、Tests 的包结构规范。
- 为 Config/Save 引入更成熟 JSON 库或明确长期 DTO 限制。
- 增加 AOT/linker 验证,避免反射和 JSON 构造在移动端出现隐性问题。

View File

@@ -0,0 +1,778 @@
# FlowScope ArchitectureScope / Container 实现规格
## 背景
本文基于 `docs/requirements/scope-feature-architecture-requirements.md`,用于约束下一版 FlowScope 核心架构实现。
本轮设计不再沿用当前 `RootContext + FeatureScope` 的试验模型,也不继续在 `IFeatureScope``ICommandContext``IQueryContext` 上叠加规则。新方向是吸收 ft_3 Container 的核心能力和 API 手感,自研 FlowScope Container / Scope Tree。
第一阶段只解决一件事:
```text
用统一的 ArchitectureScope 建立 Root -> Feature -> Child Scope 的容器树、注册规则、解析规则和生命周期语义。
```
暂时不进入:
- Command / Query / Event。
- 完整 MVC / MVVM。
- UI Panel 生命周期统一接管。
- 自动扫描所有 Feature。
- 复杂诊断面板。
## 设计目标
1. Root、Feature、Child Scope 都是同一种 `ArchitectureScope`
2. Root 只是 `Parent == null` 的 Scope不拥有另一套接口模型。
3. Feature 是业务模块安装定义,不是另一种容器。
4. `CreateFeature<TFeature>()` 默认使用 `typeof(TFeature).Name` 创建 Scope并自动调用 `Feature.Install(scope)`
5. 任意 Scope 可以随时 `Register`,但只能写入自己。
6. `Resolve` 从当前 Scope 开始,找不到再向父 Scope 查找。
7. 子 Scope 可以覆盖父 Scope 的注册。
8. 默认业务边界是 `Root -> Feature`Child Scope 只作为复杂生命周期能力保留。
9. 实现必须避免“外层说 Scope-local Register内层只有 Root Register”的双重语义。
## 核心类型清单
### FlowScope
静态入口类型,负责创建根 Scope。
建议职责:
- `Create()` 创建一个没有 Parent 的 `ArchitectureScope`
- 不持有全局单例。
- 不提供静态 `Resolve<T>()`
建议 API
```csharp
public static class FlowScope
{
public static IArchitectureScope Create();
public static IArchitectureScope Create(string rootName);
}
```
### ArchitectureScope
统一 Scope 节点的内部实现类型。
建议职责:
- 保存 Scope 名称、父节点、子节点。
- 保存当前 Scope 的注册表。
- 保存当前 Scope 持有的实例和释放栈。
- 执行 Register / Resolve / CreateScope / CreateFeature / Dispose。
- 对外通过 `IArchitectureScope` 暴露。
Root、Feature、Child Scope 不应分别实现不同运行时类型。它们的差异来自创建位置和安装内容,而不是来自继承层级。
### IArchitectureScope
业务侧默认接触的 Scope 接口。
它既是容器访问入口,也是规则约束入口。
```csharp
public interface IArchitectureScope : IScopeInfo, IDisposable
{
IArchitectureScope CreateScope(string name);
IArchitectureScope CreateFeature<TFeature>()
where TFeature : IFeature, new();
IArchitectureScope CreateFeature<TFeature>(string name)
where TFeature : IFeature, new();
IRegistrationBuilder Register<TImplementation>();
IRegistrationBuilder Register<TImplementation>(string name);
IRegistrationBuilder Register<TService, TImplementation>()
where TImplementation : TService;
IRegistrationBuilder Register<TService, TImplementation>(string name)
where TImplementation : TService;
void RegisterInstance<TService>(TService instance);
void RegisterInstance<TService>(TService instance, string name);
TService Resolve<TService>();
TService Resolve<TService>(string name);
bool TryResolve<TService>(out TService value);
bool TryResolve<TService>(out TService value, string name);
}
```
### IScopeInfo
只读 Scope 元信息,避免业务代码通过 Parent 写父级。
```csharp
public interface IScopeInfo
{
string Name { get; }
IScopeInfo Parent { get; }
string Path { get; }
bool IsDisposed { get; }
}
```
`Parent` 第一阶段建议只暴露为 `IScopeInfo`,而不是 `IArchitectureScope`。如果后续需要高级管理能力,应通过单独的内部接口或显式管理 API 暴露。
### IFeature
Feature 是业务模块安装定义。
```csharp
public interface IFeature
{
void Install(IArchitectureScope scope);
}
```
第一阶段不要求 `IFeature : IDisposable`。Feature 自身不是生命周期容器Feature 相关对象的生命周期应由安装后的 Scope 管理。
如果某个 Feature 安装对象确实需要释放,应把它注册进 Scope或由它在 `Install` 中注册需要释放的服务。
### ServiceKey
注册和解析使用的键。
```csharp
public readonly struct ServiceKey : IEquatable<ServiceKey>
{
public Type Type { get; }
public string Name { get; }
}
```
语义:
- `Type` 必须非空。
- `Name` 可以为空或空字符串,但内部应归一化为同一种无名状态。
- `(Type, Name)` 完全相同才视为同一个注册。
### Registration
注册记录。
建议字段:
```csharp
internal sealed class Registration
{
public ServiceKey Key { get; }
public Type ImplementationType { get; }
public object Instance { get; }
public ServiceLifetime Lifetime { get; }
public bool OwnsInstance { get; }
}
```
`Registration` 只描述“如何创建”,不应把所有 PerScope 缓存都放在注册所在 Scope 上。
### ServiceLifetime
第一阶段支持四种生命周期。
```csharp
public enum ServiceLifetime
{
Singleton,
PerScope,
Transient,
Instance
}
```
### IRegistrationBuilder
声明生命周期的构建器。
```csharp
public interface IRegistrationBuilder
{
void AsSingleton();
void PerScope();
void AsTransient();
}
```
第一阶段建议每次 `Register` 返回一个待完成 builder只有调用生命周期方法后才写入注册表或者默认 `PerScope`,但必须在 spec 与测试中固定。为了避免隐式行为,推荐第一阶段要求显式调用生命周期方法。
## 接口设计
第一阶段接口应围绕“统一 Scope 节点”组织,而不是围绕 Root / Feature 两种上下文组织。
公开接口分三层:
```text
FlowScope
创建 Root Scope。
IArchitectureScope
默认业务接口,负责创建子 Scope、创建 Feature、注册、解析和释放。
IScopeInfo
只读诊断接口,暴露 Name / Parent / Path / IsDisposed。
```
Feature 接口只负责安装:
```text
IFeature.Install(IArchitectureScope scope)
```
不在第一阶段公开:
- `IRootContext`
- `IFeatureScope`
- `ICommandContext`
- `IQueryContext`
- `IEventContext`
原因是这些接口会把核心重新拉回“Root 特殊、Feature 特殊、通信模型提前进入核心”的旧方向。第一阶段所有业务和测试都应以 `IArchitectureScope` 为主入口。
## 容器生命周期语义
### Singleton
`Singleton` 表示注册所在 Scope 持有唯一实例。
解析规则:
- 在某个 Scope 找到 Singleton 注册后,实例缓存到该注册所在 Scope。
- 子 Scope Resolve 到父 Scope 的 Singleton 注册时,拿到父 Scope 的同一个实例。
- 子 Scope Dispose 不释放父 Scope Singleton。
- 注册所在 Scope Dispose 时释放该 Singleton 实例。
适用:
- Root 注册全局服务。
- Feature 注册该 Feature 内唯一共享服务。
### PerScope
`PerScope` 表示每个发起 Resolve 的 Scope 持有一个实例。
解析规则:
- 从当前 Scope 开始查找注册。
- 找到注册后,实例缓存到“发起 Resolve 的 Scope”不是注册所在 Scope。
- 同一个发起 Scope 多次 Resolve 返回同一个实例。
- 不同子 Scope Resolve 同一个父级 PerScope 注册时,各自得到不同实例。
- 发起 Scope Dispose 时释放自己缓存的 PerScope 实例。
这是吸收 ft_3 Container 后最重要的生命周期能力,用于“同一个注册规则,在不同 Scope 下得到不同运行实例”。
### Transient
`Transient` 表示每次 Resolve 都创建新实例。
解析规则:
- 不缓存。
- 如果创建出的实例实现 `IDisposable`,第一阶段建议由发起 Scope 跟踪并在 Dispose 时释放。
- 如果不跟踪 Transient Disposable必须在接口文档里明确调用方负责释放。为了减少 Unity 项目泄漏风险,推荐跟踪释放。
### Instance
`Instance` 表示注册外部传入实例。
解析规则:
- 每次 Resolve 返回同一个传入对象。
- 默认 `OwnsInstance = false`Scope Dispose 不释放外部传入实例。
- 如后续需要 Scope 接管实例释放,可以增加显式 API例如 `RegisterOwnedInstance<T>()`,第一阶段不做。
### Dispose 顺序
Scope Dispose 必须满足:
1. 幂等,多次 Dispose 不重复释放。
2. 先 Dispose 子 Scope再 Dispose 当前 Scope 自己持有的实例。
3. 子 Scope Dispose 不影响父 Scope。
4. 父 Scope Dispose 会递归 Dispose 所有子 Scope。
5. 当前 Scope Dispose 后不能继续 Register / Resolve / CreateScope / CreateFeature。
6. Dispose 过程中出现多个异常时,第一阶段可以选择:
- 记录第一个异常并继续释放剩余对象,最后抛出聚合异常。
- 或使用 `AggregateException`
建议使用 LIFO 顺序释放当前 Scope 持有的实例,使后创建对象先释放。
## Register 算法
### 规则
1. 任意 Scope 可以随时 Register。
2. Register 永远只写当前 Scope。
3. Register 不会向父 Scope 写入。
4. Register 不会向子 Scope 广播。
5. 当前 Scope 已 Dispose 时 Register 失败。
6. 同一 Scope 内相同 `ServiceKey` 重复注册,第一阶段建议直接失败,避免隐式覆盖。
子 Scope 覆盖父 Scope 不是通过修改父注册实现,而是子 Scope 自己注册相同 `ServiceKey`
### 算法
```text
Register(scope, key, registration):
1. 如果 scope 已 Dispose抛 ObjectDisposedException。
2. 校验 key.Type 非空。
3. 归一化 key.Name。
4. 如果 scope._registrations 已存在 key抛 DuplicateRegistrationException。
5. 把 registration 写入 scope._registrations。
6. 返回或完成 builder。
```
### 命名注册
命名注册必须和无名注册共存。
```text
Register<IRewardService, DefaultRewardService>()
Register<IRewardService, BossRewardService>("boss")
```
以上是两个不同 `ServiceKey`
## Resolve 算法
### 规则
1. Resolve 从当前 Scope 开始。
2. 当前 Scope 找不到时,再向 Parent 查找。
3. 查找路径直到 Root。
4. 子 Scope 的相同 `ServiceKey` 会覆盖父 Scope。
5. Resolve 不允许绕过当前 Scope 直接查 Root。
6. 当前 Scope 已 Dispose 时 Resolve 失败。
### 查找算法
```text
FindRegistration(scope, key):
current = scope
while current != null:
if current._registrations.TryGetValue(key, out registration):
return (registrationOwner: current, registration)
current = current.Parent
return null
```
### 创建算法
```text
Resolve(scope, key):
1. 如果 scope 已 Dispose抛 ObjectDisposedException。
2. result = FindRegistration(scope, key)。
3. 如果找不到,抛 ServiceNotFoundException。
4. 根据 registration.Lifetime 创建或读取实例。
5. 返回实例。
```
生命周期分支:
```text
Singleton:
缓存位置 = registrationOwner
若缓存不存在,在 registrationOwner 中创建并记录释放
返回 registrationOwner 缓存
PerScope:
缓存位置 = resolveStartScope
若缓存不存在,在 resolveStartScope 中创建并记录释放
返回 resolveStartScope 缓存
Transient:
每次创建新实例
若实例需要 Scope 接管释放,记录到 resolveStartScope 释放栈
返回新实例
Instance:
返回 registration.Instance
```
### TryResolve 算法
```text
TryResolve(scope, key, out value):
1. 如果 scope 已 Dispose抛 ObjectDisposedException。
2. 如果找不到注册value = default返回 false。
3. 如果找到注册,走 Resolve 创建流程。
4. 创建成功返回 true。
5. 创建过程中发生构造异常,不吞异常。
```
`TryResolve` 只表示“是否存在注册”,不表示“构造失败也返回 false”。
## CreateScope 算法
### 规则
1. 任意未 Dispose Scope 都可以创建子 Scope。
2. 子 Scope 的 Parent 指向当前 Scope。
3. 子 Scope 加入当前 Scope 的 Children 集合。
4. 子 Scope 名称由调用方传入。
5. 子 Scope 名称用于诊断和 Path不作为服务注册 key。
6. 同一父级下是否允许重名第一阶段建议禁止,便于诊断。
### 算法
```text
CreateScope(parent, name):
1. 如果 parent 已 Dispose抛 ObjectDisposedException。
2. 校验 name 非空。
3. 如果 parent.Children 已存在同名未释放 Scope抛 DuplicateScopeNameException。
4. 创建 child = new ArchitectureScope(name, parent)。
5. parent.Children.Add(child)。
6. 返回 child。
```
`Path` 示例:
```text
Root
Root/BossFightFeature
Root/BossFightFeature/RevivePanel
```
## CreateFeature 算法
### 规则
1. `CreateFeature<TFeature>()` 默认使用 `typeof(TFeature).Name`
2. `CreateFeature<TFeature>(name)` 创建当前 Scope 的子 Scope。
3. 创建成功后实例化 `TFeature`
4. 自动调用 `feature.Install(featureScope)`
5. 如果 Install 失败,必须 Dispose 已创建的 featureScope并从父级 Children 中移除。
6. 调用者不应手写 Scope 创建和 Feature 安装流程。
### 第一阶段接口
```csharp
IArchitectureScope CreateFeature<TFeature>()
where TFeature : IFeature, new();
IArchitectureScope CreateFeature<TFeature>(string name)
where TFeature : IFeature, new();
```
### 算法
```text
CreateFeature<TFeature>(parent):
return CreateFeature<TFeature>(parent, typeof(TFeature).Name)
CreateFeature<TFeature>(parent, name):
1. featureScope = parent.CreateScope(name)。
2. try:
feature = new TFeature()
feature.Install(featureScope)
return featureScope
catch:
featureScope.Dispose()
从 parent.Children 移除 featureScope
rethrow
```
### 后续扩展点
第一阶段先使用 `new()` 约束,避免把构造注入、工厂注册和 Feature 安装绑在一起。
后续如需 Feature 构造依赖注入,可增加:
```csharp
IArchitectureScope CreateFeature<TFeature>(string name, Func<IArchitectureScope, TFeature> factory)
where TFeature : IFeature;
```
不要在第一阶段默认扫描或反射构造 Feature。
## Dispose 算法
### Scope 状态
建议内部状态:
```text
Active
Disposing
Disposed
```
### 算法
```text
Dispose(scope):
1. 如果 scope.State == Disposed直接返回。
2. 如果 scope.State == Disposing直接返回或抛 InvalidOperationException推荐直接返回保证幂等。
3. scope.State = Disposing。
4. 复制 Children 列表。
5. 按创建顺序反向 Dispose 子 Scope。
6. 清空 Children。
7. 按 LIFO Dispose 当前 Scope 持有的实例。
8. 清空当前 Scope 的 PerScope / Singleton / Transient 跟踪缓存。
9. 从 Parent.Children 中移除自己。
10. scope.State = Disposed。
11. 如有释放异常,抛出聚合异常。
```
### 持有关系
当前 Scope 只释放自己持有的对象:
- 当前 Scope 创建的 Singleton。
- 当前 Scope 发起 Resolve 后缓存的 PerScope。
- 当前 Scope 接管的 Transient Disposable。
- 当前 Scope 创建的子 Scope。
当前 Scope 不释放:
- 父 Scope 持有的 Singleton。
- 父 Scope 持有的 PerScope 实例。
- 默认 RegisterInstance 传入的外部对象。
## 文件组织建议
第一阶段建议把核心集中在 Runtime Core不引入 Command / Query / Event 目录。
```text
My project/Assets/FlowScope/Runtime/
Core/
FlowScope.cs
IArchitectureScope.cs
IScopeInfo.cs
IFeature.cs
Core/Container/
ArchitectureScope.cs
ServiceKey.cs
Registration.cs
RegistrationBuilder.cs
ServiceLifetime.cs
ScopeExceptions.cs
```
测试建议:
```text
My project/Assets/FlowScope/Tests/EditMode/
Core/
ArchitectureScopeRegistrationTests.cs
ArchitectureScopeResolveTests.cs
ArchitectureScopeFeatureTests.cs
ArchitectureScopeDisposeTests.cs
```
如果后续迁移到 package 结构,应保持相同命名空间和目录职责:
```text
Packages/com.flowscope.kernel/Runtime/Core/
Packages/com.flowscope.kernel/Runtime/Core/Container/
Packages/com.flowscope.kernel/Tests/EditMode/Core/
```
命名空间建议:
```text
FlowScope
FlowScope.Container
```
不要继续把新核心放到 `FlowScope.Composition`,避免和旧 `RootContext + FeatureScope + Command/Query` 试验模型混在一起。
## 测试用例清单
### Root / Scope 统一模型
- `Create_ReturnsRootArchitectureScope`
- `RootScope_HasNoParent`
- `ChildScope_UsesSameArchitectureScopeContractAsRoot`
- `ScopePath_BuildsFromRootToChild`
### Register 规则
- `Register_WritesOnlyCurrentScope`
- `Register_OnDisposedScopeThrows`
- `Register_DuplicateKeyInSameScopeThrows`
- `Register_SameKeyInChildOverridesParentWithoutChangingParent`
- `Register_NamedAndUnnamedKeysAreIndependent`
- `RegisterInstance_DefaultDoesNotDisposeExternalInstance`
### Resolve 规则
- `Resolve_FindsCurrentScopeRegistrationFirst`
- `Resolve_FallsBackToParentWhenCurrentMissing`
- `Resolve_ChildRegistrationOverridesParent`
- `Resolve_MissingRegistrationThrows`
- `TryResolve_MissingRegistrationReturnsFalse`
- `TryResolve_ConstructorFailureThrows`
- `Resolve_OnDisposedScopeThrows`
### 生命周期
- `Singleton_IsCachedInRegistrationOwnerScope`
- `Singleton_FromParentIsSharedByChildren`
- `PerScope_IsCachedInResolveStartScope`
- `PerScope_FromParentCreatesDifferentInstancesPerChildScope`
- `Transient_CreatesNewInstanceEveryResolve`
- `Transient_DisposableIsReleasedWhenOwningScopeDisposes`
- `ChildDispose_DoesNotDisposeParentOwnedInstances`
- `ParentDispose_DisposesChildrenAndOwnedInstances`
- `Dispose_IsIdempotent`
- `Dispose_UsesLifoForOwnedInstances`
### CreateScope
- `CreateScope_CreatesChildWithParent`
- `CreateScope_OnDisposedParentThrows`
- `CreateScope_EmptyNameThrows`
- `CreateScope_DuplicateSiblingNameThrows`
- `ChildDispose_RemovesChildFromParent`
### CreateFeature
- `CreateFeature_UsesFeatureTypeNameAsDefaultScopeName`
- `CreateFeature_WithNameUsesExplicitName`
- `CreateFeature_CallsInstallWithCreatedScope`
- `CreateFeature_ReturnsInstalledScope`
- `CreateFeature_InstallCanRegisterServicesToFeatureScope`
- `CreateFeature_InstallFailureDisposesCreatedScope`
- `CreateFeature_InstallFailureRemovesScopeFromParent`
### 默认业务边界
- `FeatureScope_CanResolveRootServices`
- `FeatureScope_CanOwnFeatureServices`
- `TwoFeatureScopes_DoNotSharePerScopeFeatureServices`
- `ChildScope_CanOverrideFeatureServiceForLocalLifetime`
- `FeatureWithoutChildScope_IsEnoughForNormalBusinessServices`
## 当前旧实现迁移 / 替换策略
### 要替换的旧模型
当前 `My project/Assets/FlowScope/Runtime/Composition` 中的试验模型应作为替换对象,而不是继续扩展对象。
旧模型特征:
- `IRootContext``IFeatureScope` 是不同接口。
- `RootContext``FeatureScope` 是不同实现。
- `IFeatureScope` 同时混入 `IArchitectureScope``ICommandContext``IQueryContext`
- `CreateFeatureScope(name)` 只创建 Feature Scope没有统一 Child Scope 模型。
- Command / Query / Architecture roles 与 Scope 基础能力耦合过早。
新模型应改为:
```text
FlowScope.Create()
-> IArchitectureScope Root
Root.CreateFeature<TFeature>()
-> IArchitectureScope Feature
Feature.CreateScope("LocalLifetime")
-> IArchitectureScope Child
```
### 迁移步骤建议
第一阶段迁移只做核心替换,不迁移 Command / Query / Event。
1. 新增 `Core` / `Core.Container` 下的统一 Scope 类型和接口。
2. 新增测试覆盖 Register / Resolve / CreateFeature / Dispose。
3. 将旧 Composition 测试中仍有价值的用例迁移到新 Core 测试:
- Root 服务可被 Feature Resolve。
- Feature 级服务彼此隔离。
- 命名注册可解析。
4. 删除或降级旧 `RootContext` / `FeatureScope` 作为历史试验实现。
5. 暂时不把旧 `ICommandContext` / `IQueryContext` 接回新 Scope。
6. 后续如果要恢复 Command / Query / Event应作为 Scope 上层扩展模块,而不是第一阶段核心职责。
### 兼容策略
第一阶段不建议保留旧 API 兼容层。
原因:
- `CreateFeatureScope(name)` 会把心智继续拉回“Root 特殊、Feature 特殊”的两层模型。
- `IFeatureScope` 会让调用侧继续依赖旧接口名。
- Command / Query 混入旧 FeatureScope 会污染新核心边界。
如果必须短期兼容样例,可用单独 adapter 文件临时桥接,但不得作为新核心主入口:
```text
LegacyCompositionAdapter
CreateFeatureScope(name) -> root.CreateScope(name)
```
该 adapter 应标记为迁移期代码,并在第一阶段验收后移除。
## 第一阶段验收标准
### 文档与边界
1. 新核心 spec 明确以 `ArchitectureScope` 为唯一 Scope 模型。
2. 文档明确旧 `RootContext + FeatureScope` 是替换对象。
3. 文档明确第一阶段不进入 Command / Query / Event、完整 MVC/MVVM、UI Panel 生命周期统一接管。
### API
1. 存在 `FlowScope.Create()` 根入口。
2. 存在统一 `IArchitectureScope`
3. `Root``Feature``Child` 都通过 `IArchitectureScope` 表达。
4. 存在 `IFeature.Install(IArchitectureScope scope)`
5. 存在 `CreateFeature<TFeature>()``CreateFeature<TFeature>(string name)`
6. `CreateFeature<TFeature>()` 默认 Scope 名称为 `typeof(TFeature).Name`
### Register / Resolve
1. 任意 Scope 可以 Register。
2. Register 只写当前 Scope。
3. Resolve 从当前 Scope 开始。
4. Resolve 找不到时向 Parent 查找。
5. 子 Scope 相同 key 覆盖父 Scope。
6. 子 Scope 不能修改父 Scope 注册。
### 生命周期
1. 支持 `Singleton``PerScope``Transient``Instance`
2. 父级 Singleton 被子级共享。
3. 父级 PerScope 注册在不同子 Scope 下生成不同实例。
4. 子 Scope Dispose 不释放父 Scope 对象。
5. 父 Scope Dispose 会释放全部子 Scope。
6. Dispose 幂等。
### 测试
1. EditMode 测试覆盖 Root / Feature / Child 统一模型。
2. EditMode 测试覆盖 Register 只写当前 Scope。
3. EditMode 测试覆盖 Resolve 向父级查找与子级覆盖。
4. EditMode 测试覆盖 CreateFeature 默认名称、自动 Install、失败清理。
5. EditMode 测试覆盖 Dispose 递归释放、子不释放父、幂等。
### 负向验收
第一阶段完成时,不应出现:
- 新核心依赖旧 `RootContext`
- 新核心依赖旧 `FeatureScope`
- 新核心把 Command / Query / Event 作为必需接口。
- 新核心要求每个 UI Panel 都创建 Scope。
- 新核心要求 Feature 内部必须使用 MVC 或 MVVM。
## 后续阶段预留
后续可以在 Scope Tree 稳定后再讨论:
- Command / Query / Event 作为扩展模块如何挂到 Scope。
- Feature 内部 MVC / MVVM 推荐层。
- UI Panel 生命周期是否需要独立模块接管。
- Scope 诊断树、注册来源、Resolve Trace。
- Feature 工厂的依赖注入构造。
- 自动扫描 Feature 的 marker-based 方案。
这些能力不应反向改变第一阶段核心事实:
```text
FlowScope 的第一主角是统一 ArchitectureScope。
```

View File

@@ -0,0 +1,603 @@
# FlowScope Game Core P0 Parallel Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Implement FlowScope Game Core P0 as a minimal Unity-ready vertical slice with stable shared contracts, isolated services, Feature lifecycle, MVVM UI, and a runnable MainMenu sample.
**Architecture:** Work starts with a single contracts branch/worktree that defines shared interfaces and project layout. After contracts are merged, independent worktrees implement Container, data/config/save, resources, UI, audio, GameFlow/Feature, and sample integration in parallel. Final integration happens in a dedicated worktree that owns cross-module fixes and sample validation.
**Tech Stack:** Unity C#, .NET Task/CancellationToken, R3, Addressables, Newtonsoft.Json or System.Text.Json, Unity Test Framework.
---
## Source Documents
- `docs/requirements/p0-requirements-set.md`
- `docs/requirements/p0-container.md`
- `docs/requirements/p0-gameflow.md`
- `docs/requirements/p0-feature.md`
- `docs/requirements/p0-data-r3.md`
- `docs/requirements/p0-configprovider.md`
- `docs/requirements/p0-saveservice.md`
- `docs/requirements/p0-resourceservice.md`
- `docs/requirements/p0-uimanager.md`
- `docs/requirements/p0-audioservice.md`
## Worktree Strategy
Do not open all implementation worktrees before contracts are stable.
1. Create `p0-contracts` first.
2. Merge `p0-contracts`.
3. Create parallel worktrees from the merged contracts branch.
4. Keep each worker inside its owned file set.
5. Merge service worktrees before sample integration.
6. Use a final `p0-integration` worktree to resolve seams and verify the sample.
Recommended worktrees:
| Worktree | Purpose |
|----------|---------|
| `p0-contracts` | Shared asmdefs, interfaces, base types, package skeleton |
| `p0-container` | Container runtime, SourceGen adapter, reflection adapter |
| `p0-data-config-save` | Data/R3 converters, JSON config, local save |
| `p0-resource` | Addressables-backed resource service |
| `p0-ui` | UIManager, UIPanelAttribute, UIPanelBase |
| `p0-audio` | AudioService and audio handles |
| `p0-gameflow-feature` | FeatureContext, GameFlow, FeatureBase |
| `p0-sample-integration` | MainMenu sample scene, sample data/config/UI |
| `p0-integration` | Final merge, compile, test, documentation fixes |
## Proposed Runtime Layout
Create the Core package under the Unity project:
```text
My project/Assets/FlowScope/
├── Runtime/
│ ├── FlowScope.Runtime.asmdef
│ ├── Common/
│ ├── Container/
│ ├── Flow/
│ ├── Data/
│ ├── Config/
│ ├── Save/
│ ├── Resources/
│ ├── UI/
│ └── Audio/
├── Editor/
│ └── FlowScope.Editor.asmdef
├── Tests/
│ ├── EditMode/
│ │ └── FlowScope.Tests.EditMode.asmdef
│ └── PlayMode/
│ └── FlowScope.Tests.PlayMode.asmdef
└── Samples/
└── MainMenuP0/
```
If the repo already introduces a different package layout before implementation starts, update this section first and keep all worker plans aligned.
---
## Task 0: Contracts Worktree
**Worktree:** `p0-contracts`
**Files:**
- Create: `My project/Assets/FlowScope/Runtime/FlowScope.Runtime.asmdef`
- Create: `My project/Assets/FlowScope/Runtime/Common/ResultLog.cs`
- Create: `My project/Assets/FlowScope/Runtime/Container/Container.cs`
- Create: `My project/Assets/FlowScope/Runtime/Container/InjectableAttribute.cs`
- Create: `My project/Assets/FlowScope/Runtime/Flow/FeatureContext.cs`
- Create: `My project/Assets/FlowScope/Runtime/Flow/IFeature.cs`
- Create: `My project/Assets/FlowScope/Runtime/Flow/GameFlowState.cs`
- Create: `My project/Assets/FlowScope/Runtime/Config/IConfigProvider.cs`
- Create: `My project/Assets/FlowScope/Runtime/Config/IConfigRow.cs`
- Create: `My project/Assets/FlowScope/Runtime/Save/ISaveService.cs`
- Create: `My project/Assets/FlowScope/Runtime/Resources/IResourceService.cs`
- Create: `My project/Assets/FlowScope/Runtime/Resources/IResourceHandle.cs`
- Create: `My project/Assets/FlowScope/Runtime/Resources/IResourceGroup.cs`
- Create: `My project/Assets/FlowScope/Runtime/UI/PanelStrategy.cs`
- Create: `My project/Assets/FlowScope/Runtime/UI/UIPanelAttribute.cs`
- Create: `My project/Assets/FlowScope/Runtime/UI/UIPanelBase.cs`
- Create: `My project/Assets/FlowScope/Runtime/Audio/IAudioService.cs`
- Create: `My project/Assets/FlowScope/Runtime/Audio/IAudioHandle.cs`
- [ ] **Step 1: Create the runtime asmdef**
Create `My project/Assets/FlowScope/Runtime/FlowScope.Runtime.asmdef`:
```json
{
"name": "FlowScope.Runtime",
"rootNamespace": "FlowScope",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
```
- [ ] **Step 2: Create Container contract skeleton**
Create `My project/Assets/FlowScope/Runtime/Container/Container.cs` with method signatures from `p0-container.md`:
```csharp
using System;
namespace FlowScope.Container
{
public sealed class Container : IDisposable
{
public void RegisterInstance<T>(T instance) => throw new NotImplementedException();
public void RegisterFactory<T>(Func<Container, T> factory) => throw new NotImplementedException();
public void RegisterType<TInterface, TImplementation>() where TImplementation : TInterface => throw new NotImplementedException();
public void RegisterType<TImplementation>() => throw new NotImplementedException();
public void RegisterAssembly() => throw new NotImplementedException();
public T Resolve<T>() => throw new NotImplementedException();
public bool TryResolve<T>(out T value)
{
value = default;
throw new NotImplementedException();
}
public Container CreateScope() => throw new NotImplementedException();
public void Dispose() => throw new NotImplementedException();
}
}
```
- [ ] **Step 3: Create Feature contracts**
Create `FeatureContext`, `IFeature`, and `GameFlowState` exactly matching `p0-feature.md` and `p0-gameflow.md`.
- [ ] **Step 4: Create service contracts**
Create the config, save, resource, UI, and audio interfaces exactly matching `p0-requirements-set.md`.
- [ ] **Step 5: Compile contracts**
Run Unity compile validation if available. If not available, open the project in Unity and verify there are no C# compile errors.
Expected: contracts compile with `NotImplementedException` bodies where concrete implementations are not yet owned by this task.
- [ ] **Step 6: Commit contracts**
```powershell
git add "My project/Assets/FlowScope/Runtime" docs/requirements
git commit -m "定义 Game Core P0 共享契约"
```
---
## Task A: Container Implementation
**Worktree:** `p0-container`
**Depends on:** `p0-contracts`
**Owned files:**
- Modify: `My project/Assets/FlowScope/Runtime/Container/Container.cs`
- Create: `My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs`
- Create: `My project/Assets/FlowScope/Runtime/Container/ReflectionFactoryBuilder.cs`
- Create: `My project/Assets/FlowScope/Runtime/Container/GeneratedFactories.cs`
- Test: `My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs`
**Do not modify:** Flow, UI, Resource, Save, Config, Audio implementation files.
- [ ] **Step 1: Write Container tests first**
Cover explicit factory, instance registration, child scope lookup, child override, duplicate root registration, dispose order, repeated dispose, and circular dependency.
- [ ] **Step 2: Implement explicit registration and Resolve**
Implement dictionary-backed registrations keyed by `(Type type, object key)` if key support is retained. If key support is deferred, do not add key overloads.
- [ ] **Step 3: Implement scope lookup and disposal**
Child scope lookup checks local registrations first, then parent. Dispose releases local factory-created instances in reverse creation order.
- [ ] **Step 4: Implement reflection adapter**
Only reflection adapter owns constructor inspection. Core Container receives a factory.
- [ ] **Step 5: Stub Source Generator integration**
Create `GeneratedFactories` as the stable handoff point. If real Source Generator is not implemented in this task, tests must prove explicit and reflection paths work, and Generator work must be tracked as a follow-up inside the same worktree before merge.
- [ ] **Step 6: Run Container tests**
Expected: all Container edit mode tests pass.
- [ ] **Step 7: Commit**
```powershell
git add "My project/Assets/FlowScope/Runtime/Container" "My project/Assets/FlowScope/Tests/EditMode/Container"
git commit -m "实现 P0 容器核心能力"
```
---
## Task B: Data, Config, and Save
**Worktree:** `p0-data-config-save`
**Depends on:** `p0-contracts`, `p0-container`
**Owned files:**
- Create: `My project/Assets/FlowScope/Runtime/Data/ReactivePropertyJsonConverter.cs`
- Create: `My project/Assets/FlowScope/Runtime/Config/JsonConfigProvider.cs`
- Create: `My project/Assets/FlowScope/Runtime/Save/JsonSaveSerializer.cs`
- Create: `My project/Assets/FlowScope/Runtime/Save/FileSaveStorage.cs`
- Create: `My project/Assets/FlowScope/Runtime/Save/PlayerPrefsSaveStorage.cs`
- Create: `My project/Assets/FlowScope/Runtime/Save/SaveService.cs`
- Test: `My project/Assets/FlowScope/Tests/EditMode/Data/DataSerializationTests.cs`
- Test: `My project/Assets/FlowScope/Tests/EditMode/Config/JsonConfigProviderTests.cs`
- Test: `My project/Assets/FlowScope/Tests/EditMode/Save/SaveServiceTests.cs`
**Do not modify:** Container internals, UIManager, AudioService, GameFlow.
- [ ] **Step 1: Write Data serialization tests**
Test `ReactiveProperty<int>` serializes as value and deserializes back into a Data instance.
- [ ] **Step 2: Implement reactive JSON converters**
Support P0 primitives: `int`, `float`, `string`, `bool`, and serializable structs.
- [ ] **Step 3: Write Config tests**
Use an in-memory JSON text source where possible. Test normal load, missing id, duplicate id, malformed JSON.
- [ ] **Step 4: Implement `JsonConfigProvider`**
Implement `LoadAllAsync`, `Get<T>`, and `GetAll<T>` for registered config types.
- [ ] **Step 5: Write Save tests**
Test save/load, missing key default, delete, exists, deserialization failure default.
- [ ] **Step 6: Implement SaveService**
Keep storage and serializer separable. Do not add `ListKeys`, cloud save, compression, or migration.
- [ ] **Step 7: Run tests**
Expected: Data, Config, and Save edit mode tests pass.
- [ ] **Step 8: Commit**
```powershell
git add "My project/Assets/FlowScope/Runtime/Data" "My project/Assets/FlowScope/Runtime/Config" "My project/Assets/FlowScope/Runtime/Save" "My project/Assets/FlowScope/Tests/EditMode/Data" "My project/Assets/FlowScope/Tests/EditMode/Config" "My project/Assets/FlowScope/Tests/EditMode/Save"
git commit -m "实现 P0 数据配置和存档"
```
---
## Task C: ResourceService
**Worktree:** `p0-resource`
**Depends on:** `p0-contracts`
**Owned files:**
- Create: `My project/Assets/FlowScope/Runtime/Resources/AddressablesResourceService.cs`
- Create: `My project/Assets/FlowScope/Runtime/Resources/ResourceHandle.cs`
- Create: `My project/Assets/FlowScope/Runtime/Resources/ResourceGroup.cs`
- Create: `My project/Assets/FlowScope/Runtime/Resources/ResourceEntry.cs`
- Test: `My project/Assets/FlowScope/Tests/EditMode/Resources/ResourceGroupTests.cs`
- Test: `My project/Assets/FlowScope/Tests/PlayMode/Resources/AddressablesResourceServiceTests.cs`
**Do not modify:** UIManager or AudioService except through agreed interfaces.
- [ ] **Step 1: Write handle/group tests**
Test group add, group dispose, duplicate add, repeated dispose, disposed handle asset behavior.
- [ ] **Step 2: Implement handle and group**
`ResourceHandle<T>.Asset` returns null after Dispose, matching the P0 document.
- [ ] **Step 3: Write concurrent load test**
Use a fake backend if Addressables is hard to exercise in edit mode. Test that concurrent same-key loads share one backend load.
- [ ] **Step 4: Implement Addressables service**
Default backend is Addressables only. Do not implement Resources, AssetBundle, or YooAsset.
- [ ] **Step 5: Run resource tests**
Expected: edit mode tests pass; play mode Addressables test passes if sample assets are available.
- [ ] **Step 6: Commit**
```powershell
git add "My project/Assets/FlowScope/Runtime/Resources" "My project/Assets/FlowScope/Tests/EditMode/Resources" "My project/Assets/FlowScope/Tests/PlayMode/Resources"
git commit -m "实现 P0 资源加载和引用计数"
```
---
## Task D: UIManager
**Worktree:** `p0-ui`
**Depends on:** `p0-contracts`, `p0-resource`
**Owned files:**
- Modify: `My project/Assets/FlowScope/Runtime/UI/UIPanelAttribute.cs`
- Modify: `My project/Assets/FlowScope/Runtime/UI/UIPanelBase.cs`
- Create: `My project/Assets/FlowScope/Runtime/UI/UIManager.cs`
- Create: `My project/Assets/FlowScope/Runtime/UI/UILayer.cs`
- Create: `My project/Assets/FlowScope/Runtime/UI/UIPanelRecord.cs`
- Test: `My project/Assets/FlowScope/Tests/PlayMode/UI/UIManagerTests.cs`
**Do not modify:** ResourceService internals.
- [ ] **Step 1: Write UI lifecycle tests**
Test layer registration, open, bind, close, unbind, LIFO error, cache reuse, destroy release.
- [ ] **Step 2: Implement `UIPanelAttribute` path support**
Constructor must include `string path = null` and `PanelStrategy? overrideStrategy = null`.
- [ ] **Step 3: Implement `UIPanelBase<TViewModel>`**
Keep only one `CompositeDisposable` property. No duplicate property declarations.
- [ ] **Step 4: Implement layer and stack management**
Close non-top panel throws `InvalidOperationException`.
- [ ] **Step 5: Implement path resolution**
Attribute path wins. Otherwise use `PanelName` without `Panel` suffix: `UI/{Name}/Prefab`.
- [ ] **Step 6: Run UI tests**
Expected: all UI play mode tests pass.
- [ ] **Step 7: Commit**
```powershell
git add "My project/Assets/FlowScope/Runtime/UI" "My project/Assets/FlowScope/Tests/PlayMode/UI"
git commit -m "实现 P0 UI 管理器"
```
---
## Task E: AudioService
**Worktree:** `p0-audio`
**Depends on:** `p0-contracts`, `p0-resource`
**Owned files:**
- Create: `My project/Assets/FlowScope/Runtime/Audio/AudioService.cs`
- Create: `My project/Assets/FlowScope/Runtime/Audio/AudioHandle.cs`
- Create: `My project/Assets/FlowScope/Runtime/Audio/AudioServiceConfig.cs`
- Test: `My project/Assets/FlowScope/Tests/PlayMode/Audio/AudioServiceTests.cs`
**Do not modify:** ResourceService internals.
- [ ] **Step 1: Write audio tests**
Test BGM play/stop, BGM switch, SFX pool cap, mute, volume clamp, handle stop.
- [ ] **Step 2: Implement audio handle**
P0 handle supports `Stop(fadeOut)` and `IsPlaying`. Do not add per-handle `Volume`.
- [ ] **Step 3: Implement BGM channel**
One active BGM. New BGM stops old BGM.
- [ ] **Step 4: Implement SFX pool**
Default pool size is 10. Pool exhaustion behavior follows `AudioServiceConfig`.
- [ ] **Step 5: Implement fade**
Use local coroutine or update driver. Do not introduce a Timer service.
- [ ] **Step 6: Run audio tests**
Expected: all audio play mode tests pass.
- [ ] **Step 7: Commit**
```powershell
git add "My project/Assets/FlowScope/Runtime/Audio" "My project/Assets/FlowScope/Tests/PlayMode/Audio"
git commit -m "实现 P0 音频服务"
```
---
## Task F: GameFlow and Feature
**Worktree:** `p0-gameflow-feature`
**Depends on:** `p0-contracts`, `p0-container`, `p0-resource`
**Owned files:**
- Modify: `My project/Assets/FlowScope/Runtime/Flow/FeatureContext.cs`
- Modify: `My project/Assets/FlowScope/Runtime/Flow/IFeature.cs`
- Create: `My project/Assets/FlowScope/Runtime/Flow/FeatureBase.cs`
- Create: `My project/Assets/FlowScope/Runtime/Flow/GameFlow.cs`
- Test: `My project/Assets/FlowScope/Tests/EditMode/Flow/GameFlowTests.cs`
- Test: `My project/Assets/FlowScope/Tests/EditMode/Flow/FeatureBaseTests.cs`
**Do not modify:** Concrete UI, Audio, Save, Config, or Resource implementations.
- [ ] **Step 1: Write GameFlow state tests**
Test startup, switch, shutdown, illegal calls, NoActiveFeature behavior.
- [ ] **Step 2: Write failure cleanup tests**
Use fake Feature classes that fail in Load/Enter/Exit/Dispose.
- [ ] **Step 3: Implement FeatureContext**
It contains Scope, Resources, Disposables, and CancellationToken.
- [ ] **Step 4: Implement FeatureBase**
FeatureBase stores context, clears disposables on Exit, clears references on Dispose.
- [ ] **Step 5: Implement GameFlow**
GameFlow creates scope/resources/context and owns their final disposal.
- [ ] **Step 6: Run flow tests**
Expected: all flow edit mode tests pass.
- [ ] **Step 7: Commit**
```powershell
git add "My project/Assets/FlowScope/Runtime/Flow" "My project/Assets/FlowScope/Tests/EditMode/Flow"
git commit -m "实现 P0 生命周期编排"
```
---
## Task G: MainMenu P0 Sample
**Worktree:** `p0-sample-integration`
**Depends on:** all service worktrees merged
**Owned files:**
- Create: `My project/Assets/FlowScope/Samples/MainMenuP0/Scripts/GameBootstrap.cs`
- Create: `My project/Assets/FlowScope/Samples/MainMenuP0/Scripts/MainMenuFeature.cs`
- Create: `My project/Assets/FlowScope/Samples/MainMenuP0/Scripts/MainMenuViewModel.cs`
- Create: `My project/Assets/FlowScope/Samples/MainMenuP0/Scripts/PlayerData.cs`
- Create: `My project/Assets/FlowScope/Samples/MainMenuP0/Scripts/MainMenuPanel.cs`
- Create: `My project/Assets/FlowScope/Samples/MainMenuP0/Configs/player_start.json`
- Create: `My project/Assets/FlowScope/Samples/MainMenuP0/Prefabs/MainMenuPanel.prefab`
- Create: `My project/Assets/FlowScope/Samples/MainMenuP0/Scenes/MainMenuP0.unity`
- Test: `My project/Assets/FlowScope/Tests/PlayMode/Samples/MainMenuP0Tests.cs`
- [ ] **Step 1: Create sample data**
`PlayerData` has at least `ReactiveProperty<int> Gold`.
- [ ] **Step 2: Create sample ViewModel**
ViewModel increments Gold through a public command method.
- [ ] **Step 3: Create sample Panel**
Panel binds Gold text and button click manually.
- [ ] **Step 4: Create MainMenuFeature**
Load creates ViewModel. Enter opens `MainMenuPanel`. Exit closes panel.
- [ ] **Step 5: Create GameBootstrap**
Bootstrap registers Container, Config, Save, Resource, UI, Audio, PlayerData, then calls `GameFlow.StartupAsync<MainMenuFeature>`.
- [ ] **Step 6: Write sample play mode test**
Test startup, click increment, UI refresh, shutdown save, restart restore.
- [ ] **Step 7: Run sample**
Expected: sample scene runs in Unity with no console errors.
- [ ] **Step 8: Commit**
```powershell
git add "My project/Assets/FlowScope/Samples/MainMenuP0" "My project/Assets/FlowScope/Tests/PlayMode/Samples"
git commit -m "添加 P0 主菜单纵向切片示例"
```
---
## Task H: Final Integration
**Worktree:** `p0-integration`
**Depends on:** all P0 implementation branches merged or available as PRs
**Owned files:**
- Modify only files needed to resolve integration seams.
- Modify docs only for verified behavior changes.
- [ ] **Step 1: Merge implementation branches one by one**
Recommended order:
```text
p0-contracts
p0-container
p0-data-config-save
p0-resource
p0-ui
p0-audio
p0-gameflow-feature
p0-sample-integration
```
- [ ] **Step 2: Run full compile**
Use Unity compile validation. Expected: zero C# compile errors.
- [ ] **Step 3: Run edit mode tests**
Expected: Container, Data, Config, Save, Resource group, GameFlow tests pass.
- [ ] **Step 4: Run play mode tests**
Expected: UI, Audio, Addressables Resource, MainMenu sample tests pass.
- [ ] **Step 5: Manual sample validation**
Open `MainMenuP0.unity`. Verify:
- UI opens.
- Gold increments on click.
- UI updates immediately.
- Exit saves data.
- Restart restores data.
- Closing Feature releases resources and subscriptions.
- [ ] **Step 6: Update docs**
Update `docs/requirements/p0-requirements-set.md` only if implementation discovered a necessary contract adjustment. Do not silently drift code away from docs.
- [ ] **Step 7: Commit integration**
```powershell
git add "My project/Assets/FlowScope" docs/requirements
git commit -m "集成 Game Core P0 纵向切片"
```
---
## Merge Rules for Agent Workers
- Do not change shared contracts after `p0-contracts` merges unless the integration owner approves the contract change.
- If a worker needs a new method on a shared interface, stop and write a short contract-change note before editing.
- Do not introduce CSV, Luban, Resources backend, AssetBundle backend, cloud save, UI router, or AudioMixer in P0.
- Do not add global EventBus in P0.
- Do not make Feature create root scope; GameFlow owns FeatureContext creation.
- Commit messages must be Chinese.
## Self-Review Checklist
- P0 contracts cover all modules in `p0-requirements-set.md`.
- Every worker has an owned file set.
- Parallel workers do not write the same implementation files.
- Extension-only features are not assigned to P0 workers.
- Final integration owns cross-module validation.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,447 @@
# FlowScope Game Core 设计文档
## 定位
面向休闲/超休闲游戏的 Unity 客户端通用框架。业务逻辑层保持纯 C#,基础设施层直接使用 Unity API。
## 整体架构
```
FlowScope Game Core
├── 基础设施层
│ ├── Container — 自研轻量 DIscope/resolve/dispose无反射
│ └── R3 — ReactiveProperty、事件流、集合订阅
├── 核心服务层(全部接口化,实现可替换)
│ ├── IResourceService — 资源加载/引用计数/分组释放
│ ├── ISaveService — 存档/读档持久化
│ ├── IConfigProvider — 配置表加载/查询
│ ├── IAudioService — BGM/SFX 池化/音量控制
│ ├── INetworkService — 网络请求/推送
│ └── UIManager — MVVM Panel 生命周期/页面栈/层级
├── 数据层按业务域拆分ReactiveProperty 驱动)
│ └── 各游戏按需定义自己的 Data 类
├── 业务特性层(自包含,独立生命周期)
│ └── 各 Feature: Load → Enter → Exit → Dispose
└── 生命周期编排
└── GameFlow — 启动/切换 Feature/关闭的全局时间线
```
## 设计原则
1. 接口化:核心服务全部走接口,实现可替换
2. 显式优于隐式DI 不做反射,手动注册手动传参
3. 按域拆分:数据类按业务域拆分,不建大 Data 类
4. 特性自包含:每个 Feature 自带资源组、订阅、ViewModel切换时干净释放
5. 框架只提供工具:不预设具体 Data 类,游戏自己定义
## 依赖方向(单向,不反向)
```
Feature → Core Services → Interfaces → Infrastructure (Container + R3)
Data Classes纯 C#,无依赖)
```
---
## 基础设施层
### Container — 自研轻量 DI
不做反射、不做构造函数自动注入、不做标签注入。显式注册,显式解析。
```csharp
public class Container : IDisposable
{
void RegisterInstance<T>(T instance);
void RegisterFactory<T>(Func<T> factory);
T Resolve<T>();
bool TryResolve<T>(out T value);
Container CreateScope(); // 子 scope 可访问父级,可覆盖父级
void Dispose(); // 释放 scope 内的 IDisposable
}
```
注册方式:
```csharp
container.RegisterInstance<IConfig>(config);
container.RegisterFactory<ISaveService>(() => new FileSaveService());
```
### R3
直接使用 R3 库提供:
- ReactiveProperty — 可观察属性MVVM 绑定基石
- ReactiveCollection — 可观察列表
- Subject — 事件流
- Observable.Timer / EveryUpdate — 计时和帧调度
- CompositeDisposable — 批量释放订阅
---
## 核心服务层
### IResourceService — 资源管理
```csharp
public interface IResourceService
{
Task<IResourceHandle<T>> LoadAsync<T>(string key) where T : class;
IResourceGroup CreateGroup();
}
public interface IResourceHandle<T> : IDisposable
{
T Asset { get; }
}
public interface IResourceGroup : IDisposable
{
void Add<T>(IResourceHandle<T> handle);
}
```
引用计数机制:同一 key 多次 Load 只加载一次,最后一个 handle Dispose 时才真正卸载。
默认实现AddressablesResourceService。
### ISaveService — 持久化
```csharp
public interface ISaveService
{
Task SaveAsync<T>(string key, T data);
Task<T> LoadAsync<T>(string key, T defaultValue = default);
void Delete(string key);
}
```
默认实现可选 FileSaveServiceJSON 文件)或 PlayerPrefsSaveService。
### IConfigProvider — 配置管理
```csharp
public interface IConfigRow
{
int Id { get; }
}
public interface IConfigProvider
{
Task LoadAllAsync();
T Get<T>(int id) where T : IConfigRow;
IReadOnlyList<T> GetAll<T>() where T : IConfigRow;
}
```
不绑定数据源JSON / CSV / Luban由实现决定。默认 JsonConfigProvider。
### IAudioService — 音频管理
```csharp
public interface IAudioService
{
void PlayBgm(string key, bool loop = true);
void StopBgm(float fadeOut = 0.5f);
void PlaySfx(string key);
void SetBgmVolume(float volume);
void SetSfxVolume(float volume);
void SetMute(bool mute);
}
```
内部AudioSource 对象池BGM 单独一个SFX 池化复用。
### INetworkService — 网络管理
```csharp
public interface INetworkService
{
Task<TResponse> SendAsync<TRequest, TResponse>(string path, TRequest data);
void On<TEvent>(Action<TEvent> handler);
}
```
初期可能用不上,但留好接口。默认 HttpNetworkService。
### UIManager — UI 管理
MVVM 模式的 Panel 生命周期管理。
```csharp
public class UIManager
{
Task OpenAsync<TPanel, TViewModel>(TViewModel viewModel) where TPanel : IPanel;
void Close<TPanel>();
void CloseAll();
}
public interface IPanel
{
void Bind(object viewModel);
void Unbind();
}
```
Panel 生命周期Instantiate → Bind → Show → ... → Hide → Unbind → Destroy。
层级管理Bottom基础层、Middle弹窗层、Top提示层
ViewModel 示例(纯 C#
```csharp
public class ShopViewModel
{
public ReactiveProperty<string> Title { get; } = new("商店");
public ReactiveCollection<ShopItemViewModel> Items { get; } = new();
private readonly PlayerData _player;
private readonly IConfigProvider _config;
public void BuyItem(int itemId)
{
var config = _config.Get<ItemConfig>(itemId);
if (_player.Gold.Value < config.Price) return;
_player.Gold.Value -= config.Price;
}
}
```
Panel 示例Unity MonoBehaviour
```csharp
public class ShopPanel : MonoBehaviour, IPanel
{
[SerializeField] private Text titleText;
[SerializeField] private Transform itemListRoot;
private ShopViewModel _vm;
private CompositeDisposable _disposables = new();
public void Bind(object viewModel)
{
_vm = (ShopViewModel)viewModel;
_vm.Title.Subscribe(t => titleText.text = t).AddTo(_disposables);
_vm.Items.ObserveAdd().Subscribe(e => CreateItemCell(e.Value)).AddTo(_disposables);
}
public void Unbind()
{
_disposables.Clear();
_vm = null;
}
private void OnDestroy() => Unbind();
}
```
---
## 数据层
框架不提供任何具体 Data 类,只提供 ReactiveProperty / ReactiveCollection 工具。
每个游戏按业务域自己定义:
```csharp
// 游戏需要金币?建一个
public class PlayerData
{
public ReactiveProperty<int> Level { get; } = new(1);
public ReactiveProperty<int> Gold { get; } = new(0);
}
// 不需要背包?就不建 InventoryData
// 需要设置?按需加
public class SettingsData
{
public ReactiveProperty<float> BgmVolume { get; } = new(1f);
public ReactiveProperty<string> Language { get; } = new("zh");
}
```
原则:一个 Data 类 = 一个业务域,新业务加新类。
---
## 业务特性层
### IFeature
```csharp
public interface IFeature : IDisposable
{
Task LoadAsync(); // 加载资源、创建 ViewModel
Task EnterAsync(); // 显示 UI、订阅事件
Task ExitAsync(); // 隐藏 UI、取消订阅
void Dispose(); // 释放资源
}
```
每个 Feature 自包含自己的资源组、订阅、ViewModel。切换时干净释放。
Feature 示例:
```csharp
public class ShopFeature : IFeature
{
private readonly IResourceService _resource;
private readonly UIManager _uiManager;
private readonly PlayerData _player;
private readonly IConfigProvider _config;
private IResourceGroup _resources;
private CompositeDisposable _disposables;
private ShopViewModel _viewModel;
public async Task LoadAsync()
{
_resources = _resource.CreateGroup();
var bgm = await _resource.LoadHandleAsync<AudioClip>("audio/bgm_shop");
_resources.Add(bgm);
_viewModel = new ShopViewModel(_player, _config);
}
public async Task EnterAsync()
{
_disposables = new CompositeDisposable();
await _uiManager.OpenAsync<ShopPanel, ShopViewModel>(_viewModel);
}
public async Task ExitAsync()
{
_uiManager.Close<ShopPanel>();
_disposables?.Dispose();
}
public void Dispose()
{
_resources?.Dispose();
_viewModel = null;
}
}
```
---
## 生命周期编排
### GameFlow
```csharp
public class GameFlow
{
private readonly Container _container;
private IFeature _currentFeature;
public async Task StartupAsync()
{
var config = _container.Resolve<IConfigProvider>();
await config.LoadAllAsync();
var save = _container.Resolve<ISaveService>();
var player = await save.LoadAsync<PlayerData>("player", new PlayerData());
_container.RegisterInstance(player);
await SwitchToAsync<MainMenuFeature>();
}
public async Task SwitchToAsync<TFeature>() where TFeature : IFeature, new()
{
if (_currentFeature != null)
{
await _currentFeature.ExitAsync();
_currentFeature.Dispose();
}
var feature = new TFeature();
await feature.LoadAsync();
await feature.EnterAsync();
_currentFeature = feature;
}
public async Task ShutdownAsync()
{
if (_currentFeature != null)
{
await _currentFeature.ExitAsync();
_currentFeature.Dispose();
}
var save = _container.Resolve<ISaveService>();
await save.SaveAsync("player", _container.Resolve<PlayerData>());
}
}
```
### 启动入口
```csharp
public class GameBootstrap : MonoBehaviour
{
private void Awake()
{
var container = new Container();
// 核心服务
container.RegisterInstance<IResourceService>(new AddressablesResourceService());
container.RegisterInstance<ISaveService>(new FileSaveService());
container.RegisterInstance<IConfigProvider>(new JsonConfigProvider());
container.RegisterInstance<IAudioService>(new UnityAudioService());
container.RegisterInstance<UIManager>(new UIManager());
// 游戏数据
container.RegisterInstance(new PlayerData());
container.RegisterInstance(new SettingsData());
var flow = new GameFlow(container);
flow.StartupAsync().Forget();
}
}
```
---
## 模块总表
| 模块 | 职责 | 类型 |
|------|------|------|
| Container | 轻量 DIscope/resolve/dispose | 自研 |
| R3 | ReactiveProperty、事件流、调度 | 第三方库 |
| IResourceService | 资源加载、引用计数、分组释放 | 接口 |
| ISaveService | 存档/读档持久化 | 接口 |
| IConfigProvider | 配置表加载/查询 | 接口 |
| IAudioService | BGM/SFX 池化、音量控制 | 接口 |
| INetworkService | 网络请求/推送 | 接口 |
| UIManager | MVVM Panel 生命周期、页面栈、层级 | 类 |
| Data 类 | 按业务域拆分的运行时状态 | 游戏自定义 |
| IFeature | 自包含特性模块,独立生命周期 | 接口 |
| GameFlow | 全局启动/切换/关闭时间线 | 类 |
## 技术选型
| 选型 | 决定 | 理由 |
|------|------|------|
| DI 容器 | 自研 | 休闲游戏只需显式注册/解析/scope不需要反射注入 |
| 响应式 | R3 | 成熟的 Unity 响应式库,自带 ObservableProperty/Collection |
| 异步 | C# Task + async/await | 休闲游戏的 GC 开销可忽略,不需要 UniTask 的零 GC |
| 资源加载 | Addressables | Unity 标准,通过 IResourceService 接口可替换 |
| 配置数据 | JSON默认 | 通过 IConfigProvider 接口可换 Luban |
| UI | uGUI + MVVM | BindableProperty 驱动ViewModel 纯 C# |
## 不做的事
- 不做反射式 DI / 自动注入
- 不做完整的 ORM
- 不做 UI 自动绑定
- 不做热更新框架
- 不做编辑器工具
- 不预设具体业务 Data 类
- 不依赖 UniTaskC# Task 够用)

Some files were not shown because too many files have changed in this diff Show More