1349 lines
45 KiB
HTML
1349 lines
45 KiB
HTML
<!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 启动;消费侧保留 CompositionRoot,Kernel 只提供机制。</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<T>、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<IPlayerData>(new PlayerData());
|
||
_root.RegisterFactory<IFishingApi>(s => new FishingApi());
|
||
|
||
// 3. 框架只提供 FeatureHost 机制
|
||
_host = FlowScopeKernel.CreateFeatureHost(_root);
|
||
|
||
await _host.SwitchAsync<MainMenuFeature>(
|
||
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 收为 RootScope;Act 临时依赖进入 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 创建 ActScope;AGameAct 只把资源和订阅挂进自己的 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 驱动 UI,Feature 间用 Data 而不是互调内部对象。</td>
|
||
<td>跨 Act 持久状态放 Stage/Data;Act 内临时状态放 ActScope;UI 通过 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 ActScope,ActScope 释放资源组、订阅、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<out TContext>
|
||
where TContext : IFeatureContext
|
||
{
|
||
TContext Context { get; }
|
||
}
|
||
|
||
public interface ICanGetService :
|
||
IHasFeatureContext<IFeatureContext> {}
|
||
|
||
public interface ICanUseLifetime :
|
||
IHasFeatureContext<IFeatureContext> {}
|
||
|
||
public interface ICanUseResources :
|
||
IHasFeatureContext<IFeatureContext> {}
|
||
|
||
public interface ICanRequestNavigation :
|
||
IHasFeatureContext<INavigableFeatureContext> {}
|
||
|
||
public static class FeatureCapabilityExtensions
|
||
{
|
||
public static T GetService<T>(this ICanGetService self)
|
||
=> self.Context.Services.GetRequiredService<T>();
|
||
|
||
public static void OnExitDispose(
|
||
this ICanUseLifetime self, IDisposable disposable)
|
||
=> self.Context.Lifetime.Add(disposable);
|
||
|
||
public static ValueTask<IResourceHandle<T>> LoadOwnedAsync<T>(
|
||
this ICanUseResources self, string key)
|
||
where T : class
|
||
=> self.Context.Resources.LoadAsync<T>(
|
||
key, self.Context.CancellationToken);
|
||
|
||
public static ValueTask SwitchToAsync<TFeature>(
|
||
this ICanRequestNavigation self,
|
||
FeatureSwitchRequest request)
|
||
where TFeature : IFeature
|
||
=> self.Context.Navigator.SwitchAsync<TFeature>(
|
||
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 等价 RootScope,FishingStage 等价 StageHost,AGameAct 等价 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 等玩法级对象进入 ActScope,ExitAsync 统一释放。</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>
|