Files
FlowScope/docs/guides/current-project-core-evolution-summary.html
2026-06-18 15:11:42 +08:00

1349 lines
45 KiB
HTML
Raw Permalink 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>当前项目 Core 演进总结</title>
<style>
:root {
--paper: #f7f3e8;
--ink: #1d2524;
--muted: #66706e;
--line: #d9d0bd;
--panel: #fffaf0;
--panel-strong: #efe5d0;
--root: #174c45;
--stage: #b23a31;
--act: #2f5e9e;
--event: #7a5a13;
--shadow: 0 18px 50px rgba(35, 31, 24, .13);
--mono: "Cascadia Mono", "Consolas", monospace;
--serif: "Georgia", "Times New Roman", serif;
--sans: "Segoe UI", "Microsoft YaHei", sans-serif;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
color: var(--ink);
font-family: var(--sans);
background:
linear-gradient(90deg, rgba(29, 37, 36, .035) 1px, transparent 1px) 0 0 / 32px 32px,
linear-gradient(rgba(29, 37, 36, .035) 1px, transparent 1px) 0 0 / 32px 32px,
var(--paper);
}
a {
color: inherit;
text-decoration: none;
}
.page {
width: min(1180px, calc(100% - 32px));
margin: 0 auto;
padding: 28px 0 56px;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
padding: 12px 0 24px;
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: 13px;
}
.nav {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.nav a {
border: 1px solid var(--line);
padding: 7px 10px;
background: rgba(255, 250, 240, .74);
}
.hero {
display: grid;
grid-template-columns: 1.05fr .95fr;
gap: 34px;
align-items: stretch;
padding: 48px 0 36px;
}
.hero-copy {
min-width: 0;
}
.kicker {
margin: 0 0 12px;
font-size: 13px;
color: var(--stage);
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
}
h1 {
margin: 0;
font-family: var(--serif);
font-size: clamp(42px, 7vw, 88px);
line-height: .94;
letter-spacing: 0;
max-width: 820px;
}
.lead {
margin: 24px 0 0;
max-width: 720px;
font-size: 19px;
line-height: 1.75;
color: #34403e;
}
.hero-board {
position: relative;
min-height: 430px;
padding: 22px;
border: 1px solid var(--line);
background: var(--panel);
box-shadow: var(--shadow);
overflow: hidden;
}
.hero-board::before {
content: "";
position: absolute;
inset: 14px;
border: 1px dashed rgba(29, 37, 36, .22);
pointer-events: none;
}
.rename-lane {
position: relative;
display: grid;
gap: 16px;
z-index: 1;
}
.rename-card {
border: 2px solid var(--ink);
background: #fffdf7;
padding: 16px;
box-shadow: 7px 7px 0 rgba(29, 37, 36, .12);
}
.rename-card small {
display: block;
color: var(--muted);
font-weight: 700;
margin-bottom: 8px;
}
.rename-card strong {
display: block;
font-size: 24px;
line-height: 1.2;
font-family: var(--serif);
}
.rename-card p {
margin: 9px 0 0;
color: #3d4846;
line-height: 1.55;
font-size: 14px;
}
.arrow {
align-self: center;
justify-self: center;
color: var(--muted);
font-family: var(--mono);
font-size: 22px;
}
section {
padding: 34px 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(30px, 4vw, 52px);
letter-spacing: 0;
line-height: 1;
}
.note {
max-width: 540px;
margin: 0;
color: var(--muted);
line-height: 1.65;
font-size: 15px;
}
.grid-3 {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
}
.grid-5 {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 14px;
}
.source-card {
min-height: 285px;
padding: 16px;
border: 1px solid var(--line);
background: var(--panel);
box-shadow: 0 10px 28px rgba(35, 31, 24, .08);
}
.source-card h3 {
margin: 0 0 10px;
font-family: var(--serif);
font-size: 24px;
line-height: 1.08;
}
.source-card .take {
margin: 0 0 10px;
color: var(--root);
font-weight: 800;
line-height: 1.45;
}
.source-card p {
margin: 0;
color: #3d4846;
line-height: 1.58;
font-size: 14px;
}
.source-card .avoid {
margin-top: 12px;
color: var(--stage);
font-size: 13px;
line-height: 1.55;
}
.scope-card {
display: grid;
grid-template-rows: auto auto 1fr;
min-height: 330px;
padding: 18px;
background: var(--panel);
border: 1px solid var(--line);
box-shadow: 0 10px 28px rgba(35, 31, 24, .08);
}
.scope-card.root {
border-top: 8px solid var(--root);
}
.scope-card.stage {
border-top: 8px solid var(--stage);
}
.scope-card.act {
border-top: 8px solid var(--act);
}
.scope-card h3 {
margin: 0;
font-family: var(--serif);
font-size: 28px;
letter-spacing: 0;
}
.scope-card .alias {
margin: 8px 0 16px;
color: var(--muted);
font-family: var(--mono);
font-size: 13px;
}
.scope-card ul {
margin: 0;
padding-left: 20px;
line-height: 1.72;
color: #34403e;
}
.matrix {
overflow-x: auto;
border: 1px solid var(--line);
background: var(--panel);
box-shadow: var(--shadow);
}
table {
width: 100%;
min-width: 920px;
border-collapse: collapse;
}
th,
td {
border-bottom: 1px solid var(--line);
border-right: 1px solid var(--line);
padding: 15px 16px;
text-align: left;
vertical-align: top;
line-height: 1.55;
}
th {
background: var(--panel-strong);
font-size: 14px;
color: #2d3836;
}
td:first-child {
font-family: var(--mono);
font-size: 13px;
color: #26302f;
white-space: nowrap;
}
tr:last-child td {
border-bottom: 0;
}
.compare {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
}
.version-card {
position: relative;
min-height: 430px;
padding: 20px;
border: 1px solid var(--line);
background: var(--panel);
box-shadow: var(--shadow);
overflow: hidden;
}
.version-card::before {
content: attr(data-version);
position: absolute;
right: 14px;
top: 12px;
color: rgba(29, 37, 36, .12);
font-family: var(--serif);
font-size: 84px;
font-weight: 800;
line-height: 1;
pointer-events: none;
}
.version-card h3 {
position: relative;
margin: 0 0 14px;
font-family: var(--serif);
font-size: 34px;
line-height: 1.05;
}
.version-card p {
position: relative;
margin: 0 0 14px;
color: #34403e;
line-height: 1.65;
}
.version-card ul {
position: relative;
margin: 0;
padding-left: 20px;
line-height: 1.7;
}
.tag-row {
position: relative;
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 18px;
}
.tag {
border: 1px solid var(--line);
background: #fffdf7;
padding: 6px 8px;
font-family: var(--mono);
font-size: 12px;
color: #394341;
}
.score {
font-family: var(--mono);
font-weight: 800;
white-space: nowrap;
}
.score.good {
color: var(--root);
}
.score.mid {
color: var(--event);
}
.score.weak {
color: var(--stage);
}
.blueprint {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
align-items: stretch;
}
.diagram {
padding: 20px;
border: 1px solid var(--line);
background: #fffdf7;
box-shadow: var(--shadow);
}
.stack {
display: grid;
gap: 14px;
}
.layer {
padding: 16px;
border: 2px solid var(--ink);
background: var(--panel);
position: relative;
}
.layer::after {
content: attr(data-label);
position: absolute;
right: 12px;
top: 10px;
color: var(--muted);
font-family: var(--mono);
font-size: 12px;
}
.layer strong {
display: block;
font-size: 20px;
margin-bottom: 8px;
}
.layer p {
margin: 0;
line-height: 1.6;
color: #3d4846;
}
.contract {
margin: 0;
padding: 20px;
overflow-x: auto;
color: #f4efe4;
background: #17201f;
border: 1px solid #17201f;
font-family: var(--mono);
font-size: 13px;
line-height: 1.7;
tab-size: 2;
}
.roadmap {
display: grid;
gap: 14px;
counter-reset: step;
}
.step {
display: grid;
grid-template-columns: 64px 1fr;
gap: 16px;
align-items: start;
padding: 18px;
border: 1px solid var(--line);
background: var(--panel);
}
.step::before {
counter-increment: step;
content: counter(step, decimal-leading-zero);
display: grid;
place-items: center;
width: 48px;
height: 48px;
color: #fffaf0;
background: var(--ink);
font-family: var(--mono);
font-weight: 800;
}
.step h3 {
margin: 0 0 8px;
font-size: 20px;
}
.step p {
margin: 0;
color: #3d4846;
line-height: 1.65;
}
.warning {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
}
.do,
.dont {
padding: 20px;
border: 1px solid var(--line);
background: var(--panel);
}
.do {
border-left: 8px solid var(--root);
}
.dont {
border-left: 8px solid var(--stage);
}
.do h3,
.dont h3 {
margin: 0 0 12px;
font-family: var(--serif);
font-size: 28px;
}
.do ul,
.dont ul {
margin: 0;
padding-left: 20px;
line-height: 1.75;
}
.footer {
padding: 28px 0 0;
color: var(--muted);
font-size: 13px;
line-height: 1.6;
}
@media (max-width: 900px) {
.hero,
.blueprint,
.compare,
.warning {
grid-template-columns: 1fr;
}
.grid-5 {
grid-template-columns: 1fr;
}
.grid-3 {
grid-template-columns: 1fr;
}
.section-head {
display: block;
}
.note {
margin-top: 12px;
}
}
@media (max-width: 560px) {
.page {
width: min(100% - 22px, 1180px);
padding-top: 16px;
}
.topbar {
align-items: flex-start;
flex-direction: column;
}
.hero {
padding-top: 32px;
}
.hero-board {
min-height: 0;
}
.step {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<main class="page">
<header class="topbar">
<div>FlowScope / 当前商业项目 Core 对照总结</div>
<nav class="nav" aria-label="页面导航">
<a href="#target">收口目标</a>
<a href="#references">参考来源</a>
<a href="#newcore">新框架</a>
<a href="#consumer">消费侧用法</a>
<a href="#p0">P0 对比</a>
<a href="#five">五个失控点</a>
<a href="#contracts">接口结构</a>
<a href="#migration">迁移步骤</a>
<a href="#guardrails">边界规则</a>
</nav>
</header>
<section class="hero" aria-labelledby="hero-title">
<div class="hero-copy">
<p class="kicker">可渐进收口,也允许完全重来</p>
<h1 id="hero-title">集百家之长,重组一套清晰的游戏 Core。</h1>
<p class="lead">
当前项目已经有事实上的运行时结构:<strong>GContext</strong> 接全局系统,
<strong>FishingStage</strong> 管捕鱼会话,<strong>AGameAct</strong> 管具体玩法。
如果基于现有项目演进,就把这些概念收口成接口边界;如果新框架允许重来,
就直接吸收 QFramework、Loxodon、uFrame、当前项目和新版 P0 的优点,重新设计职责分层。
</p>
</div>
<aside class="hero-board" aria-label="命名收口示意">
<div class="rename-lane">
<div class="rename-card">
<small>现在</small>
<strong>GContext</strong>
<p>全局 DI 容器 + 事件总线 + 大量服务入口。</p>
</div>
<div class="arrow">↓ rename / narrow</div>
<div class="rename-card">
<small>演进或重建后</small>
<strong>CompositionRoot + RootScope</strong>
<p>消费侧自己组装启动流程;框架只提供 Scope、FeatureHost、Lifetime 和 Navigation 契约。</p>
</div>
<div class="arrow">↓ host</div>
<div class="rename-card">
<small>运行单元</small>
<strong>StageScope → ActScope</strong>
<p>FishingStage 拥有会话级依赖AGameAct 拥有玩法级资源、订阅、UI 和临时状态。</p>
</div>
</div>
</aside>
</section>
<section id="target">
<div class="section-head">
<h2>最终收成什么样</h2>
<p class="note">核心不是引入新名词,而是把“谁拥有谁、谁释放谁、谁可以发起切换”固定下来。</p>
</div>
<div class="grid-3">
<article class="scope-card root">
<h3>RootScope</h3>
<div class="alias">来自GContext.container</div>
<ul>
<li>应用级单例服务</li>
<li>资源、配置、UI、声音、SDK、StageService</li>
<li>只在应用启动和关闭时创建/释放</li>
<li>不放 Act 临时数据,不放活动运行态</li>
</ul>
</article>
<article class="scope-card stage">
<h3>StageScope</h3>
<div class="alias">来自FishingStage</div>
<ul>
<li>捕鱼会话级状态和服务</li>
<li>奖励队列、活动总控、地图会话、StageData</li>
<li>负责创建和销毁 ActScope</li>
<li>接收 typed navigation command不裸听所有全局事件</li>
</ul>
</article>
<article class="scope-card act">
<h3>ActScope</h3>
<div class="alias">来自AGameAct</div>
<ul>
<li>具体玩法的 UI、资源、订阅和临时状态</li>
<li>FishingAct、BuildAct、活动 Act 都是同一模型</li>
<li>进入时注册,退出时自动释放</li>
<li>不把自己的临时数据留在 RootScope</li>
</ul>
</article>
</div>
</section>
<section id="references">
<div class="section-head">
<h2>新框架参考了什么</h2>
<p class="note">这不是照搬某一个框架,而是把每个框架最能解决问题的部分拆出来,再避开它们在大型 Unity 项目里容易失控的部分。</p>
</div>
<div class="grid-5">
<article class="source-card">
<h3>QFramework</h3>
<p class="take">吸收:状态变更有入口,业务行为命令化。</p>
<p>它提醒我们 UI 不应该到处直接改长期状态,写操作应通过 Command、UseCase 或 Service Method 进入。</p>
<p class="avoid">避免:静态 Architecture 入口和单容器 Service Locator。</p>
</article>
<article class="source-card">
<h3>Loxodon</h3>
<p class="take">吸收ServiceBundle 和 UI/Binding 管线拆分。</p>
<p>它提醒我们模块装配要成组出现UI 绑定、Window、Messenger、Prefs 应该是外围模块,不是 Kernel。</p>
<p class="avoid">避免ApplicationContext 变成全能上下文。</p>
</article>
<article class="source-card">
<h3>uFrame</h3>
<p class="take">吸收可观察状态、命令流、Bind/Unbind 生命周期。</p>
<p>它提醒我们 UI 订阅必须有明确释放作用域View 只响应 ViewModel不直接操纵业务对象。</p>
<p class="avoid">避免:全局容器创建 ViewModel、命名约定查找 prefab。</p>
</article>
<article class="source-card">
<h3>当前项目</h3>
<p class="take">吸收:真实商业项目里的 Stage/Act 玩法生命周期。</p>
<p>GContext、FishingStage、AGameAct 证明项目真正需要的是主游戏会话和玩法模式切换,而不是单纯 MVVM。</p>
<p class="avoid">避免全局事件承载命令Act 临时状态留在 Root。</p>
</article>
<article class="source-card">
<h3>新版 P0</h3>
<p class="take">吸收Container Scope、GameFlow、FeatureContext、ResourceGroup。</p>
<p>它把五个失控点统一收束到一条生命周期链:创建 scope进入 Feature退出时释放资源和订阅。</p>
<p class="avoid">避免:旧版 P0 那种横向大而全 Core。</p>
</article>
</div>
<div class="matrix" style="margin-top:18px">
<table>
<thead>
<tr>
<th>对比对象</th>
<th>它主要解决的问题</th>
<th>对五个失控点的覆盖</th>
<th>新框架吸收什么</th>
<th>新框架不吸收什么</th>
</tr>
</thead>
<tbody>
<tr>
<td>QFramework</td>
<td>业务分层、Command/Query、Model/System、事件和可绑定状态。</td>
<td><span class="score mid"></span>:能约束状态变更和依赖方向,但不负责 Feature 生命周期和资源释放。</td>
<td>状态变更入口、上层驱动下层、Command/UseCase 思想。</td>
<td>静态 Architecture、单容器、把业务分层强塞进 Kernel。</td>
</tr>
<tr>
<td>Loxodon</td>
<td>Unity MVVM、DataBinding、Window、ServiceBundle、Messenger。</td>
<td><span class="score mid"></span>:能解决 UI 状态同步和模块装配,但不解决玩法切换和 Feature 生命周期。</td>
<td>ServiceBundle 装配、Binding 管线拆分、Window 状态管理。</td>
<td>把 Prefs、Messenger、Binding、UI 全塞进 Core。</td>
</tr>
<tr>
<td>uFrame</td>
<td>ViewModel 可观察状态、Signal 命令流、View Bind/Unbind。</td>
<td><span class="score weak">局部</span>UI 生命周期有启发,但全局 Kernel 和命名约定会放大失控。</td>
<td>BindingScope、UI Intent、View 订阅 ViewModel 的方向。</td>
<td>全局容器创建 ViewModel、Resources.Load 约定、ViewModel 框架对象化。</td>
</tr>
<tr>
<td>当前项目</td>
<td>真实商业项目里的全局服务接入、捕鱼 Stage、Act 玩法切换。</td>
<td><span class="score mid"></span>:解决了能跑和能切,但依赖、事件、释放边界还偏软。</td>
<td>GContext 的真实服务覆盖、FishingStage/AGameAct 的玩法生命周期经验。</td>
<td>全局 Publish 命令、全局 Resolve 一切、字符串 actId 作为长期协议。</td>
</tr>
<tr>
<td>新版 P0</td>
<td>最小纵向闭环启动、Feature、UI、Data、资源、保存、退出。</td>
<td><span class="score good"></span>:用 Scope、FeatureContext、ResourceGroup、Disposables 直接对准五个失控点。</td>
<td>作为新框架主骨架RootScope、FeatureHost、FeatureContext、Lifetime、Navigation、CoreModules。</td>
<td>旧版 P0 的大而全模块铺开、P3 生态能力提前进入核心。</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="newcore">
<div class="section-head">
<h2>最新框架方向</h2>
<p class="note">如果允许完全重来,目标不是复刻当前项目,也不是让框架接管 App 启动;消费侧保留 CompositionRootKernel 只提供机制。</p>
</div>
<div class="matrix">
<table>
<thead>
<tr>
<th>层级</th>
<th>核心职责</th>
<th>解决的问题</th>
<th>主要参考</th>
<th>不负责什么</th>
</tr>
</thead>
<tbody>
<tr>
<td>CompositionRoot</td>
<td>消费侧代码,负责创建 RootScope、注册项目服务、安装可选模块、选择初始 Feature。</td>
<td>解决不同项目启动流程差异巨大,不让框架硬编码 SDK、热更、登录、隐私弹窗等顺序。</td>
<td>真实商业项目启动经验、Loxodon ServiceBundle 的模块安装思想。</td>
<td>它不是 Kernel 类型;框架只提供 helper不抢启动主控权。</td>
</tr>
<tr>
<td>RootScope</td>
<td>全局服务容器Config、Save、Resource、UI、Audio、SDK Adapter由消费侧创建和注册。</td>
<td>解决依赖到处找,但同时给依赖加生命周期边界。</td>
<td>GContext、QFramework IOC、Loxodon ServiceContainer、新版 P0 Container。</td>
<td>不放 Act 临时数据,不承载活动运行态。</td>
</tr>
<tr>
<td>FeatureHost / GameFlow</td>
<td>创建 FeatureScope串行执行 Load、Enter、Exit、Dispose。</td>
<td>解决玩法/页面流切换黑箱、失败清理、重复调用。</td>
<td>新版 P0 GameFlow、当前项目 FishingStage。</td>
<td>不关心 Feature 内部 UI 怎么刷新,不替 Feature 写业务。</td>
</tr>
<tr>
<td>Feature / GameAct</td>
<td>一个可进入、可退出、可释放的业务运行单元。</td>
<td>解决玩法资源、订阅、UI、临时状态归属不清。</td>
<td>当前项目 AGameAct、新版 P0 IFeature、uFrame Bind/Unbind。</td>
<td>不直接调用其他 Feature 内部对象。</td>
</tr>
<tr>
<td>FeatureContext</td>
<td>由多个能力接口组合出来:服务解析、资源组、生命周期、取消信号、启动参数。</td>
<td>解决资源和订阅释放不彻底,异步取消无统一入口。</td>
<td>新版 P0 FeatureContext、当前项目 CompositeDisposable/Addressables handle 经验。</td>
<td>不变成万能业务上下文,不默认暴露所有全局能力。</td>
</tr>
<tr>
<td>Navigation</td>
<td>提供 typed SwitchRequest / SwitchCommand / Navigator。</td>
<td>解决字符串 actId、全局 Publish 切 Act、来源不可追踪。</td>
<td>QFramework Command、当前项目 UnloadActToNextAct、新版 P0 SwitchToAsync。</td>
<td>不做复杂路由生态P0 只保证切换顺序和失败清理。</td>
</tr>
<tr>
<td>State / Data</td>
<td>纯 C# Data + Observable State + ViewModel/UseCase 写入入口。</td>
<td>解决 UI 随手改状态、状态事务边界不清。</td>
<td>QFramework Model/Command、uFrame P&lt;T&gt;、Loxodon ViewModel、新版 P0 R3 Data。</td>
<td>不把响应式状态塞进 Kernel不要求所有项目用同一套业务分层。</td>
</tr>
<tr>
<td>CoreModules</td>
<td>Resource、UI、Save、Config、Audio、Events 等可选模块。</td>
<td>解决真实项目必需能力,但避免 Kernel 变厚。</td>
<td>Loxodon 外围模块、新版 P0 模块清单、当前项目真实服务覆盖。</td>
<td>不强制所有项目一次性接入,不把热更/云存档/完整路由放进 P0。</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="p0">
<div class="section-head">
<h2>两版 P0 Core 对比</h2>
<p class="note">前一版在定义“游戏 Core 应该有什么”,后一版在定义“最小可运行闭环必须怎么跑”。差异不只是模块多少,而是所有权从模糊变清楚。</p>
</div>
<div class="compare">
<article class="version-card" data-version="OLD">
<h3>旧版 P0通用 Core 模板</h3>
<p>
旧版把 Core 看成一套横向基础设施合集Game Loop、State Machine、Input、Scene、Event、
Data、Audio、UI、Save、Config、Time、Asset Loader 都在 Core 里。
</p>
<ul>
<li>解决的是“一个通用游戏框架应该覆盖哪些系统”。</li>
<li>优点是视野完整,能帮团队盘点 Unity 项目常见模块。</li>
<li>问题是 P0 太宽,容易把 Event、Scene、Data、UI、Save 都做成全局系统。</li>
<li>它更像框架蓝图,不像可立即验收的最小交付。</li>
</ul>
<div class="tag-row" aria-label="旧版关键词">
<span class="tag">Game Loop</span>
<span class="tag">State Machine</span>
<span class="tag">Event System</span>
<span class="tag">Scene Manager</span>
<span class="tag">Asset Loader</span>
</div>
</article>
<article class="version-card" data-version="NEW">
<h3>新版 P0最小纵向切片</h3>
<p>
新版把 Core 收敛为能跑通一个真实休闲游戏闭环的最小结构:
Container、GameFlow、Feature、FeatureContext、ResourceGroup、UIManager、Save、Config、Audio。
</p>
<ul>
<li>解决的是“启动、进入 Feature、打开 UI、响应数据、切换/退出并释放”。</li>
<li>优点是所有权清楚GameFlow 管编排Feature 管业务Context 管资源和订阅。</li>
<li>明确不做全局 EventBus、完整 UI 路由、多资源后端、复杂存档生态。</li>
<li>它更像可测试、可落地、可迁移到当前项目的核心运行时。</li>
</ul>
<div class="tag-row" aria-label="新版关键词">
<span class="tag">Container Scope</span>
<span class="tag">GameFlow</span>
<span class="tag">FeatureContext</span>
<span class="tag">ResourceGroup</span>
<span class="tag">R3 Data</span>
</div>
</article>
</div>
</section>
<section id="consumer">
<div class="section-head">
<h2>消费侧怎么用</h2>
<p class="note">框架不接管 App 启动。项目自己的 Bootstrap 决定 SDK、热更、登录、配置、首个 Feature 的顺序FlowScope 只提供可组合的 Kernel 能力。</p>
</div>
<div class="blueprint">
<div class="diagram" aria-label="消费侧组装流程">
<div class="stack">
<div class="layer" data-label="project">
<strong>GameBootstrap</strong>
<p>项目自己的入口。按项目需求初始化 SDK、热更、隐私、登录和配置。</p>
</div>
<div class="layer" data-label="compose">
<strong>RootScope + Modules</strong>
<p>消费侧创建 RootScope安装 Resource/UI/Save/Audio 等模块和项目服务。</p>
</div>
<div class="layer" data-label="kernel">
<strong>FeatureHost</strong>
<p>框架负责 Feature 切换、FeatureScope 创建、生命周期和失败清理。</p>
</div>
<div class="layer" data-label="game">
<strong>MainMenuFeature</strong>
<p>业务 Feature 只消费自己声明的能力接口,不直接触碰全局大上下文。</p>
</div>
</div>
</div>
<pre class="contract"><code>public sealed class GameBootstrap : MonoBehaviour
{
private IFeatureHost _host;
private IServiceScope _root;
private async void Start()
{
_root = new ServiceContainer();
// 1. 消费侧决定安装哪些模块
new ResourceModule(addressablesBackend).Install(_root);
new UIModule(uiRoot).Install(_root);
new SaveModule(savePath).Install(_root);
// 2. 消费侧注册项目服务和数据
_root.RegisterInstance&lt;IPlayerData&gt;(new PlayerData());
_root.RegisterFactory&lt;IFishingApi&gt;(s =&gt; new FishingApi());
// 3. 框架只提供 FeatureHost 机制
_host = FlowScopeKernel.CreateFeatureHost(_root);
await _host.SwitchAsync&lt;MainMenuFeature&gt;(
FeatureSwitchRequest.Replace(), destroyCancellationToken);
}
}</code></pre>
</div>
<div class="matrix" style="margin-top:18px">
<table>
<thead>
<tr>
<th>装配方式</th>
<th>好处</th>
<th>缺点</th>
<th>建议用法</th>
</tr>
</thead>
<tbody>
<tr>
<td>强类型显式装配</td>
<td>启动依赖一眼可见重构安全IL2CPP/AOT 风险低;模块顺序由项目掌控;测试时容易替换服务。</td>
<td>样板代码多;消费侧要懂模块顺序;模块多时 Bootstrap 会变长;不适合所有服务都手写注册。</td>
<td>用于核心模块、项目级服务、启动关键路径,例如 Resource、UI、Save、Audio、账号服务。</td>
</tr>
<tr>
<td>约定/扫描装配</td>
<td>接入快业务类少写注册适合大量同类对象比如配置行、Panel、简单 UseCase。</td>
<td>依赖来源不明显运行期错误更晚暴露AOT/裁剪要额外处理;调试链路更隐蔽。</td>
<td>作为可选插件,不进入 Kernel 主路径;必须有日志、诊断和关闭开关。</td>
</tr>
<tr>
<td>混合装配</td>
<td>核心路径显式,重复注册自动化;兼顾可控性和开发效率。</td>
<td>需要文档说明哪些必须显式,哪些可以自动;否则团队会混用失控。</td>
<td>推荐默认CompositionRoot 显式安装模块,模块内部可以用 Source Generator 或扫描补注册。</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="five">
<div class="section-head">
<h2>五个最容易失控点</h2>
<p class="note">这里把当前项目、旧版 P0、新版 P0 放到同一张表里。判断标准不是“功能多不多”,而是能不能让责任和释放边界变硬。</p>
</div>
<div class="matrix">
<table>
<thead>
<tr>
<th>Unity 项目失控点</th>
<th>当前项目现状</th>
<th>旧版 P0 能否解决</th>
<th>新版 P0 能否解决</th>
<th>落到当前项目应收成什么</th>
</tr>
</thead>
<tbody>
<tr>
<td>依赖到处拿</td>
<td>GContext 能接起来,但容易变成全局 Service Locator。</td>
<td><span class="score mid">部分</span>:有模块清单,但缺少 Root/Feature scope 规则。</td>
<td><span class="score good"></span>Container + 父子 Scope + 构造注入让依赖边界更显式。</td>
<td>GContext 收为 RootScopeAct 临时依赖进入 ActScope。</td>
</tr>
<tr>
<td>生命周期没人负责</td>
<td>FishingStage 和 AGameAct 有 Start/Stop但资源、订阅、数据释放靠人工约定。</td>
<td><span class="score weak"></span>:模块都有验收,但没有统一 FeatureContext 所有权。</td>
<td><span class="score good"></span>GameFlow 创建 scope、resources、disposables并在切换/关闭时释放。</td>
<td>FishingStage 创建 ActScopeAGameAct 只把资源和订阅挂进自己的 context。</td>
</tr>
<tr>
<td>流程切换变黑箱</td>
<td>UnloadActToNextAct 是全局事件 + 字符串 actId来源和并发不够清楚。</td>
<td><span class="score mid">部分</span>State Machine / Scene Manager 提到切换,但偏通用。</td>
<td><span class="score good"></span>GameFlow.SwitchToAsync 定义状态机、失败清理和重复调用行为。</td>
<td>用 IActNavigator / SwitchActCommand 收口 Act 切换,旧事件只做兼容转发。</td>
</tr>
<tr>
<td>状态被 UI 随手改</td>
<td>大量 Manager、DataCenter、Panel 可通过全局入口互相影响,状态变更入口不统一。</td>
<td><span class="score mid">部分</span>Data Manager + Event System 有方向,但容易继续全局化。</td>
<td><span class="score good"></span>R3 Data 纯 C#ViewModel 驱动 UIFeature 间用 Data 而不是互调内部对象。</td>
<td>跨 Act 持久状态放 Stage/DataAct 内临时状态放 ActScopeUI 通过 ViewModel/UseCase 改状态。</td>
</tr>
<tr>
<td>资源和 UI 释放不彻底</td>
<td>有 Addressables handle、panel 清理、CompositeDisposable但分散在 Stage、Act、Panel、Manager。</td>
<td><span class="score mid">部分</span>Asset Loader / UI Framework 提到引用计数和面板栈,但没有统一挂载点。</td>
<td><span class="score good"></span>IResourceGroup + UIPanel Unbind + FeatureContext.Disposables 形成释放链。</td>
<td>AGameAct 退出时统一 Dispose ActScopeActScope 释放资源组、订阅、Panel 绑定。</td>
</tr>
</tbody>
</table>
</div>
</section>
<section>
<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>GContext</td>
<td>RootScope / RootContext</td>
<td>全局服务解析、应用级事件、应用级生命周期</td>
<td>Act 临时数据、玩法内状态、命令式流程控制</td>
</tr>
<tr>
<td>FishingStage</td>
<td>FishingStageHost</td>
<td>捕鱼会话启动、Stage 级服务、Act 切换编排</td>
<td>具体玩法 UI 细节、具体活动资源、散落的数据初始化</td>
</tr>
<tr>
<td>AGameAct</td>
<td>GameAct / FeatureInstance</td>
<td>玩法进入退出、资源句柄、UI 面板、事件订阅、临时状态</td>
<td>全局服务注册、跨 Act 调度、其他玩法状态修改</td>
</tr>
<tr>
<td>UnloadActToNextAct</td>
<td>IActNavigator / SwitchActCommand</td>
<td>表达“我要切到哪个 Act”这个意图</td>
<td>不要再作为任意地方都能 Publish 的全局命令事件</td>
</tr>
<tr>
<td>ActivityResolver</td>
<td>ActivityScope / ActivityFacade</td>
<td>活动管理器访问、活动数据加载、活动有效性判断</td>
<td>不要让面板和 Act 到处直接穿透全局容器</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="contracts">
<div class="section-head">
<h2>能力接口组合</h2>
<p class="note">参考 QFramework 的能力接口思想,但接口必须能复用。这里的接口不是空标签,而是挂在 Context 锚点上,通过扩展方法提供能力。</p>
</div>
<div class="blueprint">
<div class="diagram" aria-label="目标结构图">
<div class="stack">
<div class="layer" data-label="global">
<strong>IHasFeatureContext</strong>
<p>所有能力接口共享的锚点,避免每个接口都重复暴露一堆属性。</p>
</div>
<div class="layer" data-label="stage">
<strong>ICanGetService / ICanUseLifetime</strong>
<p>通过扩展方法复用服务解析和退出释放能力。</p>
</div>
<div class="layer" data-label="navigation">
<strong>ICanRequestNavigation</strong>
<p>只有拿到导航能力的对象,才能请求 Feature 切换。</p>
</div>
<div class="layer" data-label="feature">
<strong>Feature / UseCase / ViewModel</strong>
<p>按需要实现能力接口;普通对象优先构造注入具体 Port。</p>
</div>
</div>
</div>
<pre class="contract"><code>public interface IFeatureContext
{
IServiceProvider Services { get; }
IFeatureLifetime Lifetime { get; }
IResourceGroup Resources { get; }
CancellationToken CancellationToken { get; }
FeatureArgs Args { get; }
}
public interface INavigableFeatureContext : IFeatureContext
{
IFeatureNavigator Navigator { get; }
}
public interface IHasFeatureContext&lt;out TContext&gt;
where TContext : IFeatureContext
{
TContext Context { get; }
}
public interface ICanGetService :
IHasFeatureContext&lt;IFeatureContext&gt; {}
public interface ICanUseLifetime :
IHasFeatureContext&lt;IFeatureContext&gt; {}
public interface ICanUseResources :
IHasFeatureContext&lt;IFeatureContext&gt; {}
public interface ICanRequestNavigation :
IHasFeatureContext&lt;INavigableFeatureContext&gt; {}
public static class FeatureCapabilityExtensions
{
public static T GetService&lt;T&gt;(this ICanGetService self)
=&gt; self.Context.Services.GetRequiredService&lt;T&gt;();
public static void OnExitDispose(
this ICanUseLifetime self, IDisposable disposable)
=&gt; self.Context.Lifetime.Add(disposable);
public static ValueTask&lt;IResourceHandle&lt;T&gt;&gt; LoadOwnedAsync&lt;T&gt;(
this ICanUseResources self, string key)
where T : class
=&gt; 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
=&gt; self.Context.Navigator.SwitchAsync&lt;TFeature&gt;(
request, self.Context.CancellationToken);
}
public interface IFeature
{
ValueTask LoadAsync(IFeatureContext context);
ValueTask EnterAsync(IFeatureContext context);
ValueTask ExitAsync(IFeatureContext context);
}</code></pre>
</div>
<div class="matrix" style="margin-top:18px">
<table>
<thead>
<tr>
<th>使用位置</th>
<th>推荐方式</th>
<th>原因</th>
</tr>
</thead>
<tbody>
<tr>
<td>Feature / GameAct</td>
<td>可以实现 `ICanGetService`、`ICanUseResources`、`ICanUseLifetime`。</td>
<td>Feature 本来就是框架生命周期对象,使用能力接口可以复用扩展方法。</td>
</tr>
<tr>
<td>需要切换流程的 Feature</td>
<td>额外实现 `ICanRequestNavigation`,并接收 `INavigableFeatureContext`。</td>
<td>导航是高权限能力,不应默认给所有对象。</td>
</tr>
<tr>
<td>UseCase / Command</td>
<td>优先构造注入业务 Port例如 `IPlayerWallet`、`IFishingSession`、`IFeatureNavigator`。</td>
<td>业务对象不应该为了拿框架能力而继承一堆接口;它应该声明自己的真实依赖。</td>
</tr>
<tr>
<td>ViewModel / Presenter</td>
<td>优先只拿只读状态和 UI Intent 输出口。</td>
<td>避免 ViewModel 直接 Resolve 服务或直接改全局 Data。</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="migration">
<div class="section-head">
<h2>两条落地路径</h2>
<p class="note">旧项目可以渐进收口,新框架可以完全重来。两条路共享同一套目标结构,区别只是先做适配还是直接按新接口实现。</p>
</div>
<div class="roadmap">
<article class="step">
<div>
<h3>旧项目路径:先命名</h3>
<p>明确 GContext 等价 RootScopeFishingStage 等价 StageHostAGameAct 等价 FeatureInstance。团队先用同一套语言讨论。</p>
</div>
</article>
<article class="step">
<div>
<h3>旧项目路径:加适配器</h3>
<p>在现有 GContext 外包一层 IRootScope在 FishingStage 外包一层 IFishingStageHost让旧代码继续 Resolve 和 Publish。</p>
</div>
</article>
<article class="step">
<div>
<h3>收导航:替换 UnloadActToNextAct</h3>
<p>新增 IActNavigator把切 Act 的来源集中到 SwitchAsync。旧事件先转发到 Navigator后续新代码不再直接 Publish 切换事件。</p>
</div>
</article>
<article class="step">
<div>
<h3>建 ActScope临时状态不进 Root</h3>
<p>FishingData、CollectingData、活动 Panel 订阅、Addressables handle 等玩法级对象进入 ActScopeExitAsync 统一释放。</p>
</div>
</article>
<article class="step">
<div>
<h3>新框架路径:直接按边界实现</h3>
<p>从 RootScope、FeatureHost、FeatureContext、Lifetime、Navigation、CoreModules 开始,不背旧命名和旧全局事件,只保留当前项目验证过的业务需求。</p>
</div>
</article>
<article class="step">
<div>
<h3>共同目标:拆 Bundle</h3>
<p>把 SubBoostrap 里的服务注册逐步拆成 ResourceBundle、UIBundle、ActivityBundle、FishingDataBundle形成 Start/Stop 边界。</p>
</div>
</article>
</div>
</section>
<section id="guardrails">
<div class="section-head">
<h2>边界规则</h2>
<p class="note">判断一次改动是否在正确方向上,就看它有没有让依赖、生命周期、切换入口更显式。</p>
</div>
<div class="warning">
<article class="do">
<h3>要做</h3>
<ul>
<li>Root 只放全局服务。</li>
<li>Stage 只管会话和 Act 编排。</li>
<li>Act 拥有自己的资源、UI、订阅和临时状态。</li>
<li>切 Act 走 typed command / navigator。</li>
<li>服务注册按 Bundle 成组出现。</li>
</ul>
</article>
<article class="dont">
<h3>不要做</h3>
<ul>
<li>不要把所有东西继续塞进 GContext。</li>
<li>不要让任意 UI 直接 Publish 切 Act。</li>
<li>不要让 Act 临时数据常驻全局容器。</li>
<li>不要用字符串 actId 作为长期主协议。</li>
<li>不要在没有职责边界和验收切片前盲目重写。</li>
</ul>
</article>
</div>
</section>
<footer class="footer">
<strong>一句话:</strong>
当前项目已经证明 Root / Stage / Act 是真实需求。旧项目可以渐进收口,新框架可以完全重来;
共同目标都是形成“有 Scope、有 Navigator、有 Bundle、有释放边界的运行时结构”。
</footer>
</main>
</body>
</html>