Files
FlowScope/docs/guides/core-contracts-five-rules-preview.html
2026-06-18 15:11:42 +08:00

1092 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FlowScope Core 五条约束预览</title>
<style>
:root {
--paper: #f6f0df;
--ink: #17211f;
--muted: #66716b;
--line: #d5c9ad;
--panel: #fffaf0;
--green: #1f6a58;
--red: #b54135;
--blue: #275f9f;
--gold: #a36e12;
--violet: #69548e;
--code: #151b1a;
--code-line: #2a3432;
--shadow: 0 18px 44px rgba(31, 28, 21, .14);
--sans: "Segoe UI", "Microsoft YaHei", sans-serif;
--serif: Georgia, "Times New Roman", serif;
--mono: "Cascadia Mono", Consolas, monospace;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--ink);
font-family: var(--sans);
background:
linear-gradient(90deg, rgba(23, 33, 31, .04) 1px, transparent 1px) 0 0 / 34px 34px,
linear-gradient(rgba(23, 33, 31, .04) 1px, transparent 1px) 0 0 / 34px 34px,
var(--paper);
}
.page {
width: min(1240px, calc(100% - 32px));
margin: 0 auto;
padding: 28px 0 64px;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
padding-bottom: 18px;
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: 13px;
}
.nav {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.nav a {
color: var(--ink);
text-decoration: none;
border: 1px solid var(--line);
background: rgba(255, 250, 240, .78);
padding: 7px 10px;
}
.hero {
display: grid;
grid-template-columns: 1fr .9fr;
gap: 34px;
align-items: center;
min-height: 620px;
padding: 54px 0 36px;
}
.kicker {
margin: 0 0 12px;
color: var(--red);
font-weight: 900;
letter-spacing: .08em;
text-transform: uppercase;
font-size: 13px;
}
h1 {
margin: 0;
font-family: var(--serif);
font-size: clamp(46px, 7vw, 96px);
line-height: .92;
letter-spacing: 0;
}
.lead {
max-width: 780px;
margin: 26px 0 0;
color: #34413d;
font-size: 19px;
line-height: 1.75;
}
.five-board {
display: grid;
gap: 12px;
padding: 18px;
border: 1px solid var(--line);
background: var(--panel);
box-shadow: var(--shadow);
}
.rule-tile {
display: grid;
grid-template-columns: 42px 1fr;
gap: 12px;
align-items: start;
padding: 14px;
border: 2px solid var(--ink);
background: #fffdf8;
box-shadow: 6px 6px 0 rgba(23, 33, 31, .1);
}
.rule-tile span {
display: grid;
place-items: center;
width: 42px;
height: 42px;
color: #fffaf0;
background: var(--ink);
font-family: var(--mono);
font-weight: 900;
}
.rule-tile strong {
display: block;
margin-bottom: 5px;
font-size: 18px;
}
.rule-tile p {
margin: 0;
color: #47524e;
line-height: 1.55;
font-size: 14px;
}
section {
padding: 38px 0;
border-top: 1px solid var(--line);
}
.section-head {
display: flex;
align-items: end;
justify-content: space-between;
gap: 20px;
margin-bottom: 20px;
}
h2 {
margin: 0;
font-family: var(--serif);
font-size: clamp(32px, 4.4vw, 58px);
line-height: 1;
letter-spacing: 0;
}
.note {
max-width: 620px;
margin: 0;
color: var(--muted);
line-height: 1.65;
font-size: 15px;
}
.grid-5 {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 14px;
}
.principle {
min-height: 260px;
padding: 16px;
background: var(--panel);
border: 1px solid var(--line);
box-shadow: 0 10px 28px rgba(31, 28, 21, .08);
}
.principle:nth-child(1) { border-top: 8px solid var(--green); }
.principle:nth-child(2) { border-top: 8px solid var(--blue); }
.principle:nth-child(3) { border-top: 8px solid var(--red); }
.principle:nth-child(4) { border-top: 8px solid var(--gold); }
.principle:nth-child(5) { border-top: 8px solid var(--violet); }
.principle h3 {
margin: 0 0 10px;
font-family: var(--serif);
font-size: 25px;
line-height: 1.05;
}
.principle p {
margin: 0;
color: #3c4844;
line-height: 1.62;
font-size: 14px;
}
.flow {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 10px;
align-items: stretch;
margin-top: 18px;
}
.flow-step {
position: relative;
padding: 16px 14px;
border: 2px solid var(--ink);
background: #fffdf8;
min-height: 150px;
}
.flow-step::after {
content: "→";
position: absolute;
right: -20px;
top: 50%;
transform: translateY(-50%);
font-family: var(--mono);
font-size: 22px;
color: var(--muted);
z-index: 1;
}
.flow-step:last-child::after {
display: none;
}
.flow-step small {
display: block;
color: var(--muted);
font-family: var(--mono);
font-size: 12px;
margin-bottom: 8px;
}
.flow-step strong {
display: block;
margin-bottom: 8px;
font-size: 18px;
}
.flow-step p {
margin: 0;
color: #3c4844;
line-height: 1.55;
font-size: 13px;
}
.code-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
align-items: stretch;
}
.code-card {
border: 1px solid var(--line);
background: var(--panel);
box-shadow: var(--shadow);
overflow: hidden;
}
.code-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: #eee1c5;
border-bottom: 1px solid var(--line);
}
.code-head strong {
font-size: 15px;
}
.code-head span {
color: var(--muted);
font-family: var(--mono);
font-size: 12px;
}
pre {
margin: 0;
padding: 18px;
background: var(--code);
color: #f7f0dd;
overflow-x: auto;
font-family: var(--mono);
font-size: 13px;
line-height: 1.68;
tab-size: 2;
}
.matrix {
overflow-x: auto;
border: 1px solid var(--line);
background: var(--panel);
box-shadow: var(--shadow);
}
table {
width: 100%;
min-width: 980px;
border-collapse: collapse;
}
th,
td {
padding: 14px 15px;
border-right: 1px solid var(--line);
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
line-height: 1.55;
}
th {
background: #eee1c5;
font-size: 14px;
}
td:first-child {
font-family: var(--mono);
font-size: 13px;
white-space: nowrap;
}
tr:last-child td {
border-bottom: 0;
}
.callout {
margin-top: 18px;
padding: 18px;
border-left: 8px solid var(--red);
background: var(--panel);
box-shadow: 0 10px 28px rgba(31, 28, 21, .08);
color: #38423f;
line-height: 1.7;
}
.footer {
color: var(--muted);
font-size: 13px;
line-height: 1.6;
padding-top: 28px;
}
@media (max-width: 980px) {
.hero,
.code-grid,
.flow,
.grid-5 {
grid-template-columns: 1fr;
}
.flow-step::after {
content: "↓";
right: 50%;
top: auto;
bottom: -25px;
transform: translateX(50%);
}
.section-head {
display: block;
}
.note {
margin-top: 12px;
}
}
@media (max-width: 560px) {
.page {
width: min(100% - 22px, 1240px);
}
.topbar {
align-items: flex-start;
flex-direction: column;
}
}
</style>
</head>
<body>
<main class="page">
<header class="topbar">
<div>FlowScope Kernel Contract Preview</div>
<nav class="nav" aria-label="页面导航">
<a href="#idea">核心思想</a>
<a href="#rules">五条约束</a>
<a href="#usage">消费侧</a>
<a href="#contracts">代码预览</a>
<a href="#diagnostics">诊断图</a>
<a href="#limits">边界</a>
</nav>
</header>
<section class="hero" aria-labelledby="hero-title">
<div>
<p class="kicker">五件事,五种责任</p>
<h1 id="hero-title">自动注册不是黑箱,接口也不是装饰。</h1>
<p class="lead">
新版 Core 的关键不是“多加几个抽象”,而是把 Unity 项目最容易失控的五件事拆成五种机制:
自动注册只减少装配代码构造注入暴露依赖能力接口限制职责Scope 管生命周期,
诊断图让自动化可见。
</p>
</div>
<aside class="five-board" aria-label="五条设计约束">
<div class="rule-tile">
<span>01</span>
<div>
<strong>自动注册解决少写装配代码</strong>
<p>只在模块内部自动化,不做全项目黑箱扫描。</p>
</div>
</div>
<div class="rule-tile">
<span>02</span>
<div>
<strong>构造注入解决依赖清晰</strong>
<p>业务类真实依赖必须出现在构造函数里。</p>
</div>
</div>
<div class="rule-tile">
<span>03</span>
<div>
<strong>能力接口解决职责权限</strong>
<p>像 QFramework 一样,用接口限制“对象能做什么”。</p>
</div>
</div>
<div class="rule-tile">
<span>04</span>
<div>
<strong>Scope 解决生命周期</strong>
<p>Root、Stage、Feature 各自创建和释放自己的对象。</p>
</div>
</div>
<div class="rule-tile">
<span>05</span>
<div>
<strong>诊断图解决自动注册不黑箱</strong>
<p>启动时输出注册表、依赖图、生命周期归属。</p>
</div>
</div>
</aside>
</section>
<section id="idea">
<div class="section-head">
<h2>核心思想</h2>
<p class="note">最新版不应该只是“一堆模块手动装起来”。它的全局概念是消费侧的 GameComposition框架只把这份配方变成可运行、可诊断、可释放的 FlowScopeRuntime。</p>
</div>
<div class="matrix">
<table>
<thead>
<tr>
<th>概念</th>
<th>是谁拥有</th>
<th>负责什么</th>
<th>不负责什么</th>
</tr>
</thead>
<tbody>
<tr>
<td>GameComposition</td>
<td>消费侧</td>
<td>声明项目用了哪些模块、项目服务、首个 Feature、启动前置条件。</td>
<td>不执行 Feature 生命周期,不释放资源。</td>
</tr>
<tr>
<td>CompositionBuilder</td>
<td>框架提供,消费侧调用</td>
<td>收集模块安装、服务注册、自动扫描规则和诊断信息。</td>
<td>不决定项目业务启动顺序。</td>
</tr>
<tr>
<td>FlowScopeRuntime</td>
<td>框架创建,消费侧持有</td>
<td>持有 RootScope、FeatureHost、Diagnostics作为运行时总句柄。</td>
<td>不变成全能 GContext不允许业务随便从它 Resolve 一切。</td>
</tr>
<tr>
<td>FeatureHost</td>
<td>框架</td>
<td>执行 Feature 切换、Scope 创建、生命周期和失败清理。</td>
<td>不写业务逻辑,不决定首个 Feature 是谁。</td>
</tr>
</tbody>
</table>
</div>
<div class="callout">
所以它不是回到 AppKernel 接管一切,也不是散装 Bootstrap。消费侧用一个 CompositionRoot 声明全局结构;
框架把这份声明转成 Runtime并保证依赖、权限、生命周期和诊断。
</div>
</section>
<section id="rules">
<div class="section-head">
<h2>五条约束</h2>
<p class="note">它们不是并列名词,而是一条链。少写注册不等于隐藏依赖;自动化越多,诊断越要硬。</p>
</div>
<div class="grid-5">
<article class="principle">
<h3>自动注册</h3>
<p>目标是少写重复装配代码。它只能在模块边界内工作,必须有 Marker、生命周期声明和诊断输出。</p>
</article>
<article class="principle">
<h3>构造注入</h3>
<p>目标是让依赖可读、可测、可替换。不要把自动注册变成业务代码到处 Resolve。</p>
</article>
<article class="principle">
<h3>能力接口</h3>
<p>目标是限制职责权限。对象实现什么能力,才获得什么扩展方法;接口必须能复用。</p>
</article>
<article class="principle">
<h3>Scope</h3>
<p>目标是明确谁拥有对象、谁释放对象。Feature 临时对象不能留在 Root。</p>
</article>
<article class="principle">
<h3>诊断图</h3>
<p>目标是让自动注册可审查。启动时能看见每个服务来自哪个模块、依赖谁、生命周期是什么。</p>
</article>
</div>
<div class="flow" aria-label="机制链路">
<div class="flow-step">
<small>install</small>
<strong>显式安装模块</strong>
<p>消费侧知道装了哪些模块,不让框架猜启动顺序。</p>
</div>
<div class="flow-step">
<small>scan</small>
<strong>模块内自动注册</strong>
<p>扫描本模块 Assembly 和 Marker减少重复注册。</p>
</div>
<div class="flow-step">
<small>inject</small>
<strong>构造函数声明依赖</strong>
<p>类依赖在类型定义处暴露,不靠内部 Resolve。</p>
</div>
<div class="flow-step">
<small>scope</small>
<strong>进入 FeatureScope</strong>
<p>资源、订阅、临时状态进入本次 Feature 生命周期。</p>
</div>
<div class="flow-step">
<small>report</small>
<strong>输出诊断图</strong>
<p>自动化结果可见,启动失败能定位到模块和依赖链。</p>
</div>
</div>
</section>
<section id="usage">
<div class="section-head">
<h2>消费侧用法</h2>
<p class="note">消费侧仍然保留 CompositionRoot但不再把注册散落在 MonoBehaviour 里。项目写一份 GameCompositionBootstrap 只负责构建 Runtime 和进入首个 Feature。</p>
</div>
<div class="code-grid">
<article class="code-card">
<div class="code-head">
<strong>GameBootstrap</strong>
<span>干净入口</span>
</div>
<pre><code>public sealed class GameBootstrap : MonoBehaviour
{
private FlowScopeRuntime _runtime;
private async void Start()
{
var composition = new FishingGameComposition(
addressablesBackend,
uiRoot,
savePath);
_runtime = FlowScopeRuntime.Build(composition);
await _runtime.Features.SwitchAsync&lt;MainMenuFeature&gt;(
FeatureSwitchRequest.Replace(),
destroyCancellationToken);
}
private async void OnDestroy()
{
if (_runtime != null)
await _runtime.DisposeAsync();
}
}</code></pre>
</article>
<article class="code-card">
<div class="code-head">
<strong>FishingGameComposition</strong>
<span>消费侧全局配方</span>
</div>
<pre><code>public sealed class FishingGameComposition : IGameComposition
{
private readonly IResourceBackend _resourceBackend;
private readonly Transform _uiRoot;
private readonly string _savePath;
public FishingGameComposition(
IResourceBackend resourceBackend,
Transform uiRoot,
string savePath)
{
_resourceBackend = resourceBackend;
_uiRoot = uiRoot;
_savePath = savePath;
}
public void Configure(ICompositionBuilder app)
{
app.UseRoot(new ServiceContainer());
app.Install(new ResourceModule(_resourceBackend));
app.Install(new UIModule(_uiRoot));
app.Install(new SaveModule(_savePath));
app.Install(new FishingModule());
app.Services.RegisterInstance&lt;IPlayerData&gt;(new PlayerData());
app.Services.RegisterFactory&lt;IFishingApi&gt;(
s =&gt; new FishingApi());
app.Features.UseDefaultSwitchPolicy(FeatureSwitchPolicy.ReplaceOnly);
}
}</code></pre>
</article>
</div>
</section>
<section id="contracts">
<div class="section-head">
<h2>代码实现预览</h2>
<p class="note">下面不是完整实现,而是接口和关键路径预览。重点是职责分离:自动注册、依赖、权限、生命周期、诊断各管一件事。</p>
</div>
<div class="code-grid">
<article class="code-card">
<div class="code-head">
<strong>组合根契约</strong>
<span>全局概念,不接管启动</span>
</div>
<pre><code>public interface IGameComposition
{
void Configure(ICompositionBuilder app);
}
public interface ICompositionBuilder
{
IServiceRegistry Services { get; }
IFeatureRegistry Features { get; }
IDiagnosticOptions Diagnostics { get; }
void UseRoot(IServiceScope root);
void Install(IModule module);
}
public sealed class FlowScopeRuntime : IAsyncDisposable
{
public IServiceScope Root { get; }
public IFeatureHost Features { get; }
public IDependencyDiagnostics Diagnostics { get; }
public static FlowScopeRuntime Build(IGameComposition composition)
{
var builder = new CompositionBuilder();
composition.Configure(builder);
return builder.BuildRuntime();
}
}</code></pre>
</article>
<article class="code-card">
<div class="code-head">
<strong>自动注册契约</strong>
<span>少写装配代码</span>
</div>
<pre><code>public enum ServiceLifetime
{
Root,
Stage,
Feature
}
[AttributeUsage(AttributeTargets.Class)]
public sealed class FeatureServiceAttribute : Attribute
{
public ServiceLifetime Lifetime { get; }
public FeatureServiceAttribute(
ServiceLifetime lifetime = ServiceLifetime.Feature)
{
Lifetime = lifetime;
}
}
public interface IModule
{
void Install(IServiceRegistry services);
}
public interface IServiceRegistry
{
void RegisterInstance&lt;T&gt;(T instance);
void RegisterFactory&lt;T&gt;(Func&lt;IServiceProvider, T&gt; factory);
IModuleScanBuilder Scan(Assembly assembly);
}</code></pre>
</article>
<article class="code-card">
<div class="code-head">
<strong>构造注入</strong>
<span>依赖清晰</span>
</div>
<pre><code>public sealed class StartFishingUseCase
{
private readonly IFishingSession _session;
private readonly IPlayerWallet _wallet;
private readonly IFeatureNavigator _navigator;
public StartFishingUseCase(
IFishingSession session,
IPlayerWallet wallet,
IFeatureNavigator navigator)
{
_session = session;
_wallet = wallet;
_navigator = navigator;
}
public async ValueTask ExecuteAsync(CancellationToken ct)
{
if (!_wallet.CanPay(_session.EntryCost))
return;
await _navigator.SwitchAsync&lt;FishingFeature&gt;(
FeatureSwitchRequest.Replace(), ct);
}
}</code></pre>
</article>
<article class="code-card">
<div class="code-head">
<strong>能力接口</strong>
<span>职责权限</span>
</div>
<pre><code>public interface IFeatureContext
{
IServiceProvider Services { get; }
IFeatureLifetime Lifetime { get; }
IResourceGroup Resources { get; }
CancellationToken CancellationToken { get; }
}
public interface INavigableFeatureContext : IFeatureContext
{
IFeatureNavigator Navigator { get; }
}
public interface IHasFeatureContext&lt;out TContext&gt;
where TContext : IFeatureContext
{
TContext Context { get; }
}
public interface ICanUseResources :
IHasFeatureContext&lt;IFeatureContext&gt; {}
public interface ICanRequestNavigation :
IHasFeatureContext&lt;INavigableFeatureContext&gt; {}</code></pre>
</article>
<article class="code-card">
<div class="code-head">
<strong>能力扩展方法</strong>
<span>接口必须能复用</span>
</div>
<pre><code>public static class FeatureCapabilityExtensions
{
public static ValueTask&lt;IResourceHandle&lt;T&gt;&gt;
LoadOwnedAsync&lt;T&gt;(
this ICanUseResources self,
string key)
where T : class
{
return self.Context.Resources.LoadAsync&lt;T&gt;(
key,
self.Context.CancellationToken);
}
public static ValueTask SwitchToAsync&lt;TFeature&gt;(
this ICanRequestNavigation self,
FeatureSwitchRequest request)
where TFeature : IFeature
{
return self.Context.Navigator.SwitchAsync&lt;TFeature&gt;(
request,
self.Context.CancellationToken);
}
}</code></pre>
</article>
<article class="code-card">
<div class="code-head">
<strong>FeatureHost</strong>
<span>Scope 管生命周期</span>
</div>
<pre><code>public sealed class FeatureHost : IFeatureHost
{
private readonly IServiceScope _root;
private ActiveFeature _active;
public async ValueTask SwitchAsync&lt;TFeature&gt;(
FeatureSwitchRequest request,
CancellationToken ct)
where TFeature : IFeature
{
if (_active != null)
await _active.ExitAndDisposeAsync(ct);
var scope = _root.CreateScope();
var lifetime = new FeatureLifetime();
var resources = scope.GetRequiredService&lt;IResourceService&gt;()
.CreateGroup();
var context = new FeatureContext(
scope, lifetime, resources, ct, request.Args);
var feature = scope.GetRequiredService&lt;TFeature&gt;();
await feature.LoadAsync(context);
await feature.EnterAsync(context);
_active = new ActiveFeature(feature, context, scope);
}
}</code></pre>
</article>
<article class="code-card">
<div class="code-head">
<strong>Feature 示例</strong>
<span>能力接口实际用法</span>
</div>
<pre><code>public sealed class MainMenuFeature :
IFeature,
ICanUseResources,
ICanRequestNavigation
{
public INavigableFeatureContext Context { get; private set; }
IFeatureContext IHasFeatureContext&lt;IFeatureContext&gt;.Context
=&gt; Context;
public async ValueTask LoadAsync(IFeatureContext context)
{
Context = (INavigableFeatureContext)context;
var panel = await this.LoadOwnedAsync&lt;GameObject&gt;(
"UI/MainMenuPanel");
Context.Lifetime.Add(panel);
}
public ValueTask EnterAsync(IFeatureContext context)
=&gt; ValueTask.CompletedTask;
public ValueTask ExitAsync(IFeatureContext context)
=&gt; ValueTask.CompletedTask;
}</code></pre>
</article>
</div>
<div class="callout">
这里的能力接口只建议给 Feature、GameAct 这类框架生命周期对象使用。
UseCase、Command、ViewModel 不应该为了拿框架能力而实现一堆接口,它们更适合构造注入业务 Port。
</div>
</section>
<section id="diagnostics">
<div class="section-head">
<h2>诊断图</h2>
<p class="note">自动注册必须能回答三个问题:谁注册了它,它依赖谁,它属于哪个生命周期。</p>
</div>
<div class="code-grid">
<article class="code-card">
<div class="code-head">
<strong>诊断接口</strong>
<span>自动注册不黑箱</span>
</div>
<pre><code>public interface IDependencyDiagnostics
{
RegistrationReport BuildRegistrationReport();
DependencyGraph BuildDependencyGraph(Type rootType);
}
public sealed record ServiceRegistration(
Type ServiceType,
Type ImplementationType,
ServiceLifetime Lifetime,
string ModuleName,
IReadOnlyList&lt;Type&gt; Dependencies);
public sealed record RegistrationReport(
IReadOnlyList&lt;ServiceRegistration&gt; Services,
IReadOnlyList&lt;string&gt; Warnings);</code></pre>
</article>
<article class="code-card">
<div class="code-head">
<strong>输出示例</strong>
<span>给团队看的报告</span>
</div>
<pre><code>FishingModule
IFishingSession -> FishingSession [Feature]
depends on:
IPlayerData
IFishingApi
IFishingEconomy -> FishingEconomy [Feature]
depends on:
IPlayerWallet
Tables
MainMenuFeature
depends on:
IUIService
IResourceService
IFeatureNavigator
Warnings
EventScratchPanel resolves PlayerData from RootScope.
ScratchTicketManager registered as Root but implements IDisposable.</code></pre>
</article>
</div>
</section>
<section id="limits">
<div class="section-head">
<h2>边界规则</h2>
<p class="note">这套设计不是“自动注册万能”,也不是“能力接口万能”。每种机制只解决一个问题。</p>
</div>
<div class="matrix">
<table>
<thead>
<tr>
<th>机制</th>
<th>负责</th>
<th>不负责</th>
<th>容易误用</th>
</tr>
</thead>
<tbody>
<tr>
<td>自动注册</td>
<td>减少重复装配代码。</td>
<td>不负责说明业务依赖。</td>
<td>扫全项目、无 Marker、无生命周期、无诊断。</td>
</tr>
<tr>
<td>构造注入</td>
<td>暴露类的真实依赖。</td>
<td>不负责减少注册代码。</td>
<td>构造函数塞进万能 Context 或 IServiceProvider。</td>
</tr>
<tr>
<td>能力接口</td>
<td>限制对象能做什么。</td>
<td>不负责业务依赖注入。</td>
<td>让所有业务对象都实现框架接口。</td>
</tr>
<tr>
<td>Scope</td>
<td>创建、隔离、释放生命周期对象。</td>
<td>不负责业务分层。</td>
<td>Feature 临时对象注册到 Root。</td>
</tr>
<tr>
<td>诊断图</td>
<td>让自动注册结果可见。</td>
<td>不替代架构边界。</td>
<td>只在出错时打印,不在启动报告里展示。</td>
</tr>
</tbody>
</table>
</div>
<div class="matrix" style="margin-top:18px">
<table>
<thead>
<tr>
<th>方案</th>
<th>消费侧感觉</th>
<th>好处</th>
<th>缺点</th>
</tr>
</thead>
<tbody>
<tr>
<td>旧 GContext</td>
<td>全局入口很方便,哪里都能 Resolve 和 Publish。</td>
<td>接入快,适合业务快速堆功能。</td>
<td>依赖隐藏、生命周期不清、事件命令混用、自动发现难追踪。</td>
</tr>
<tr>
<td>散装 Bootstrap</td>
<td>消费侧能控制所有细节,但代码像一堆 new 和 Register。</td>
<td>启动顺序清楚,没有框架抢主控权。</td>
<td>缺少全局配方概念,项目越大越像脚本清单。</td>
</tr>
<tr>
<td>新版 GameComposition</td>
<td>消费侧声明一份项目配方Bootstrap 只 Build Runtime。</td>
<td>既有全局概念,又不接管 App 启动;模块显式,模块内自动;可输出诊断图。</td>
<td>需要维护 Composition 规范;小项目会觉得比直接 new 多一层Builder API 设计不好会变成新 DSL 负担。</td>
</tr>
</tbody>
</table>
</div>
<div class="matrix" style="margin-top:18px">
<table>
<thead>
<tr>
<th>GameComposition 成本</th>
<th>具体是什么意思</th>
<th>什么时候会出问题</th>
<th>规避方式</th>
</tr>
</thead>
<tbody>
<tr>
<td>维护 Composition 规范</td>
<td>团队必须约定哪些东西写在 Composition哪些东西写在 Module哪些东西留在 Feature 内部。</td>
<td>如果所有人都往 Composition 塞注册、初始化、业务判断,它会变成新的总控脚本。</td>
<td>Composition 只声明结构:安装模块、注册项目级服务、选择策略;业务初始化放 Feature 或项目 Bootstrap 步骤。</td>
</tr>
<tr>
<td>小项目多一层</td>
<td>只有一个场景、几个服务、没有复杂 Feature 切换时GameComposition 会显得比直接 new 多绕一步。</td>
<td>原型项目、Game Jam、小 Demo、一次性工具页面会被框架流程拖慢。</td>
<td>提供 SimpleBootstrap 模式:允许直接 new ServiceContainer + FeatureHostGameComposition 作为推荐,不作为强制。</td>
</tr>
<tr>
<td>Builder API 变 DSL 负担</td>
<td>如果 Builder 做太多语法糖,开发者需要学习一套框架专用语言,而不是普通 C#。</td>
<td>出现大量 `UseX().WithY().ForZ().When(...)` 链式调用,真实执行顺序反而不清楚。</td>
<td>Builder 只保留少量动词:`UseRoot`、`Install`、`Register`、`UsePolicy`、`EnableDiagnostics`。复杂逻辑回到普通 C#。</td>
</tr>
</tbody>
</table>
</div>
<div class="callout">
判断 GameComposition 是否值得引入,看项目是不是已经有跨模块服务、多个 Feature、资源释放、UI 生命周期和启动诊断需求。
如果只是一个小 Demo直接 Bootstrap 更合适;如果是长期 Unity 项目Composition 能把“项目全局结构”从散落代码里抽出来。
</div>
</section>
<footer class="footer">
<strong>结论:</strong>
模块显式、模块内自动;依赖构造注入;职责用能力接口限制;对象归 Scope 管;
自动化结果必须输出诊断图。这五件事分清以后,框架才不会退化成新的 GContext。
</footer>
</main>
</body>
</html>