1092 lines
32 KiB
HTML
1092 lines
32 KiB
HTML
<!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 里。项目写一份 GameComposition,Bootstrap 只负责构建 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<MainMenuFeature>(
|
||
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<IPlayerData>(new PlayerData());
|
||
app.Services.RegisterFactory<IFishingApi>(
|
||
s => 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<T>(T instance);
|
||
void RegisterFactory<T>(Func<IServiceProvider, T> 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<FishingFeature>(
|
||
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<out TContext>
|
||
where TContext : IFeatureContext
|
||
{
|
||
TContext Context { get; }
|
||
}
|
||
|
||
public interface ICanUseResources :
|
||
IHasFeatureContext<IFeatureContext> {}
|
||
|
||
public interface ICanRequestNavigation :
|
||
IHasFeatureContext<INavigableFeatureContext> {}</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<IResourceHandle<T>>
|
||
LoadOwnedAsync<T>(
|
||
this ICanUseResources self,
|
||
string key)
|
||
where T : class
|
||
{
|
||
return self.Context.Resources.LoadAsync<T>(
|
||
key,
|
||
self.Context.CancellationToken);
|
||
}
|
||
|
||
public static ValueTask SwitchToAsync<TFeature>(
|
||
this ICanRequestNavigation self,
|
||
FeatureSwitchRequest request)
|
||
where TFeature : IFeature
|
||
{
|
||
return self.Context.Navigator.SwitchAsync<TFeature>(
|
||
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<TFeature>(
|
||
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<IResourceService>()
|
||
.CreateGroup();
|
||
|
||
var context = new FeatureContext(
|
||
scope, lifetime, resources, ct, request.Args);
|
||
|
||
var feature = scope.GetRequiredService<TFeature>();
|
||
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<IFeatureContext>.Context
|
||
=> Context;
|
||
|
||
public async ValueTask LoadAsync(IFeatureContext context)
|
||
{
|
||
Context = (INavigableFeatureContext)context;
|
||
|
||
var panel = await this.LoadOwnedAsync<GameObject>(
|
||
"UI/MainMenuPanel");
|
||
|
||
Context.Lifetime.Add(panel);
|
||
}
|
||
|
||
public ValueTask EnterAsync(IFeatureContext context)
|
||
=> ValueTask.CompletedTask;
|
||
|
||
public ValueTask ExitAsync(IFeatureContext context)
|
||
=> 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<Type> Dependencies);
|
||
|
||
public sealed record RegistrationReport(
|
||
IReadOnlyList<ServiceRegistration> Services,
|
||
IReadOnlyList<string> 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 + FeatureHost;GameComposition 作为推荐,不作为强制。</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>
|