mirror of
https://github.com/motajs/template.git
synced 2026-10-29 12:20:17 +08:00
docs(10-38): 新增 CoreState 组装原理页
This commit is contained in:
parent
6369f111e9
commit
44f1cd6526
193
docs/advanced/iii/core-state.md
Normal file
193
docs/advanced/iii/core-state.md
Normal file
@ -0,0 +1,193 @@
|
||||
# CoreState 组装与初始化
|
||||
|
||||
数据端主对象 `CoreState` 是数据端的组合根。构造它时,所有子系统按层装配好、插件注册并排序、装配期回调同步执行;调用 `load()` 时,加载按固定次序推进。存档对象、存档执行器与加载器也都挂在它身上。
|
||||
|
||||
本篇只讲数据端的装配与生命周期。数据端插件怎么写见[插件系统](../i/plugin-system.md),可存档内容的形状与压缩见[可存档内容](../i/saveable-content.md)。
|
||||
|
||||
## 系统模型
|
||||
|
||||
组合根存在的理由只有三条:读者拿到的所有入口都从它出发;层与层之间由它注入,不需要各自寻找依赖;生命周期只有一套,构造、加载、存档都按同一顺序推进。
|
||||
|
||||
| 层 | 放什么 | 一句话职责 |
|
||||
| ---------------- | ------------------------------------------------ | -------------------------------------- |
|
||||
| Layer 0 公共数据 | 朝向、图块 / 道具 / 地图 / 事件 / 怪物仓库、录像 | 不依赖其它层,提供工具性接口与原始数据 |
|
||||
| Layer 1 状态基座 | 标记、地图、勇士、怪物管理 | 保存需要进存档的运行时状态 |
|
||||
| Layer 2 系统层 | 怪物上下文、战斗、事件、寻路 | 在状态之上执行游戏逻辑 |
|
||||
| Layer 3 顶层 | 加载管理器、加载器 | 面向启动流程的入口 |
|
||||
|
||||
```text
|
||||
CoreState
|
||||
├─ Layer 0 公共数据:朝向、仓库、录像
|
||||
├─ Layer 1 状态基座:标记、地图、勇士、怪物管理
|
||||
├─ Layer 2 系统层:怪物上下文、战斗、事件、寻路
|
||||
├─ Layer 3 顶层:加载管理器、加载器
|
||||
├─ 插件注册表:id → 插件,按依赖排出固定顺序
|
||||
└─ 存档:可存档对象映射 + 执行器绑定
|
||||
```
|
||||
|
||||
层号只表示装配先后与依赖方向:上层可以拿下层,下层不反向依赖上层。需要回指主对象的子系统在构造时就会拿到它,这一点在后面的装配顺序里展开。
|
||||
|
||||
## 入口的职责
|
||||
|
||||
启动数据端只需要两个函数和一个主插件:
|
||||
|
||||
- 数据端入口只重新导出创建函数。业务代码从统一入口导入 `createCoreState`,不需要直接接触构造器。
|
||||
- 配置入口 `createCoreStateConfig(starter)` 组装四样东西:加载 starter、核心配置地址、插件列表与事件注册。插件列表把主插件放在最前面,再展开插件列表文件里的业务插件。
|
||||
- 创建函数 `createCoreState(starter)` 用上面的配置构造实例,返回尚未异步初始化的主对象;加载要由调用方推进,见[加载管线](#加载管线)。
|
||||
|
||||
主插件 `MainDataPlugin` 承担数据端的内置装配,在 `awake` 里依次做:
|
||||
|
||||
- 注册四方向与八方向朝向处理器;
|
||||
- 为寻路注入通行谓词,把图层先置空,并把移动器指向勇士;
|
||||
- 为勇士换上默认的顶层移动实现;
|
||||
- 添加默认加载钩子;
|
||||
- 初始化敌人、录像与钩子;
|
||||
- 注册五个可存档对象。
|
||||
|
||||
这五个存档 id 与对象一一对应:
|
||||
|
||||
| 存档 id | 对象 |
|
||||
| ---------------- | -------------------- |
|
||||
| `@system/hero` | `state.hero` |
|
||||
| `@system/flags` | `state.flags` |
|
||||
| `@system/maps` | `state.maps` |
|
||||
| `@system/enemy` | `state.enemyManager` |
|
||||
| `@system/replay` | `state.replaySystem` |
|
||||
|
||||
存档 id 与插件 id 是两套命名。注册规则、重复 id 的处理与执行器绑定见[可存档内容](../i/saveable-content.md#注册到存档系统)。
|
||||
|
||||
主插件的 `afterLoad` 在加载完成后才执行:它从加载器取核心配置,再用配置里的初始属性与坐标创建勇士的属性与位置。属性与位置不能放进 `awake`,因为那时核心配置还没读出来。
|
||||
|
||||
## 加载管线
|
||||
|
||||
`state.load()` 是一个异步迭代器,按固定四步推进:
|
||||
|
||||
```text
|
||||
load()
|
||||
├─ 1. 插件 initialize 异步,加载开始前
|
||||
├─ 2. loader.start() 异步迭代,真正加载数据
|
||||
├─ 3. 插件 afterLoad 异步,加载完成后
|
||||
└─ 4. 检查插件状态 未到 Loaded 的插件逐个告警
|
||||
```
|
||||
|
||||
`initialize` 放在加载前,是因为此时还可以往加载器里追加配置;插件要在加载前完成的异步准备也放在这里。`loader.start()` 依次读取核心配置、额外配置与自定义任务,并产出加载进度。`afterLoad` 放在加载后,因为此时核心配置、地图与其它内容才可读。最后一步只做检查:正常走完三个阶段的插件状态都是 `Loaded`,不是的会被点名。
|
||||
|
||||
`load()` 与 `loader.start()` 都是异步迭代器:拿到迭代器不等于加载开始,必须把迭代推进完。`load()` 只负责初次加载,玩家读档不会重跑这条管线。
|
||||
|
||||
## 存档挂载
|
||||
|
||||
存档系统只认识两样东西:注册过的可存档对象,以及绑定在这些对象上的执行器。两者都挂在主对象上。
|
||||
|
||||
- 注册:`state.addSaveableContent(id, content)`。id 在整份存档里唯一;重复注册会告警,并且只有第一个注册的对象生效。
|
||||
- 绑定:`state.bindSaveableExecuter(content 或 id, executor)`。传对象时,对象必须已经注册,否则告警并忽略;传 id 时,找不到对应对象会静默返回。
|
||||
- 读档:主对象先清掉残留的录像禁用标记,再逐个对象执行 `loadState`。每个对象的 `loadState` 都被录像禁用与恢复包裹,读档本身不会记进录像;执行器在这层包裹之外运行,因为它可能需要做出需要记录的行为。
|
||||
|
||||
下面这个函数注册一个计数对象并绑定执行器:
|
||||
|
||||
```ts
|
||||
// 存档形状:只放重建必需的字段
|
||||
interface ICounterSave {
|
||||
readonly count: number;
|
||||
}
|
||||
|
||||
// 可存档对象:负责在活对象与存档数据之间转换
|
||||
class CounterSaveable implements ISaveableContent<ICounterSave> {
|
||||
private count = 0;
|
||||
|
||||
add(delta: number): void {
|
||||
this.count += delta;
|
||||
}
|
||||
|
||||
// 返回独立的新对象,不能返回 this
|
||||
saveState(): ICounterSave {
|
||||
return { count: this.count };
|
||||
}
|
||||
|
||||
// 按存档覆盖运行时状态,不保留旧值
|
||||
loadState(state: ICounterSave): void {
|
||||
this.count = state.count;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行器:当前对象 loadState 返回后立即运行
|
||||
class CounterExecutor implements ISaveableExecutor<ICounterSave> {
|
||||
afterLoad(data: ICounterSave, state: ICoreState): void {
|
||||
// 此时只保证当前对象已经恢复,不要假设其它对象也恢复完毕
|
||||
console.log('计数已恢复:', data.count, state);
|
||||
}
|
||||
}
|
||||
|
||||
// 注册并绑定:通常写在插件的 awake 里
|
||||
export function registerCounter(state: ICoreState): void {
|
||||
const counter = new CounterSaveable();
|
||||
// 存档 id 必须唯一;重复注册会告警并忽略后一个
|
||||
state.addSaveableContent('@plugin/counter', counter);
|
||||
// 传对象或注册 id 都可以;对象要先注册,再绑定
|
||||
state.bindSaveableExecuter(counter, new CounterExecutor());
|
||||
}
|
||||
```
|
||||
|
||||
## 在游戏内另建一个数据端实例
|
||||
|
||||
构造器不依赖全局单例。只要传一份完整配置,就能在游戏内或脚本里再建一个独立的数据端实例,与主实例并行运行;回放、结果对比、离线校验都可以这样做。
|
||||
|
||||
配置有四个必填字段:加载 starter、核心配置地址、插件列表与事件注册。缺任何一项,构造期就会失败;也不能只给加载 starter 与核心配置地址。
|
||||
|
||||
```ts
|
||||
// 再建一个数据端实例:与主实例完全独立
|
||||
export async function createSecondaryState(
|
||||
loadStarter: ILoadTaskStarter,
|
||||
coreURL: string,
|
||||
dataPlugins: ICoreStateConfig['dataPlugins'],
|
||||
events: ICoreStateConfig['events']
|
||||
): Promise<ICoreState> {
|
||||
// 四个字段都必填:构造期就要用它们装配加载器与插件
|
||||
const config: ICoreStateConfig = {
|
||||
loadStarter,
|
||||
coreURL,
|
||||
dataPlugins,
|
||||
events
|
||||
};
|
||||
|
||||
// 新实例不共享主实例的勇士、地图、事件仓库或存档
|
||||
const state = new CoreState(config);
|
||||
|
||||
// load() 是异步迭代器:必须消费它,加载才真正推进
|
||||
for await (const _loadedBytes of state.load()) {
|
||||
// 这里可以读取加载进度,也可以忽略
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
```
|
||||
|
||||
两个实例各自持有自己的子系统与存档映射,互不影响。要让第二个实例与主实例行为一致,就传同一组插件列表与事件注册,并确保主插件也在列表里;需要另一套规则时,换一组插件与事件即可。
|
||||
|
||||
## 内部实现:装配顺序
|
||||
|
||||
此章节讲解一下 CoreState 的装配顺序的内部实现。
|
||||
|
||||
构造器按四层顺序创建全部子系统,再处理插件。层与层之间不是随便排列的:每一层只使用已经装好的下层,并把自己暴露给上层。
|
||||
|
||||
| 层 | 装入的子系统 | 为什么在这一层 |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
|
||||
| L0 公共数据 | 朝向绑定器 `roleFace`、朝向管理器 `faceManager`、图块仓库 `tileStore`、道具仓库 `itemStore`、地图仓库 `mapStore`、事件仓库 `eventStore`、怪物仓库 `enemyStore`、录像系统 `replaySystem` | 不依赖其它子系统,先建好供所有层使用 |
|
||||
| L1 状态基座 | 标记系统 `flags`、地图状态 `maps`、勇士状态 `hero`、怪物管理 `enemyManager` | 保存需要进存档的运行时状态 |
|
||||
| L2 系统层 | 怪物上下文 `enemyContext`、战斗流程 `combat`、事件系统 `eventSystem`、寻路系统 `pathfinding` | 在状态之上执行逻辑,构造时需要拿到主对象 |
|
||||
| L3 顶层 | 加载管理器 `loadManager`(`LoadManager`)、数据加载器 `loader` | 面向启动流程;加载器随后登记核心配置地址 |
|
||||
|
||||
L0 内部也有先后:道具仓库与怪物仓库依赖图块仓库,所以图块仓库先建。L1 与 L2 里有几个子系统需要回指主对象,构造时就把当前实例传进去:地图、勇士、怪物上下文、战斗、事件系统与寻路都属于这一类。L3 的加载器不反向引用主对象,它只拿加载管理器与 starter,再把核心配置登记为自己的第一份配置。
|
||||
|
||||
四层装完后才处理插件:先遍历插件列表建立注册表,重复 id 在这里告警并跳过;再按每个插件声明的 `dataDeps` 建依赖表,调用一次 `orderPlugins` 得到固定顺序;最后同步执行 `awake`,把已排好序的插件逐个装配。装配期回调结束后,构造器返回,异步初始化与加载还没有开始。
|
||||
|
||||
## 边界与注意
|
||||
|
||||
- **加载必须被迭代推进**:`createCoreState` 只构造实例;`load()` 是异步迭代器,不消费它,加载不会开始,插件也不会进入 `Loaded`。
|
||||
- **配置四个字段都必填**:加载 starter、核心配置地址、插件列表、事件注册缺一不可。
|
||||
- **重复可存档 id 只保留第一个**:后一个注册会告警并被忽略,不是覆盖。
|
||||
- **执行器绑未注册对象会告警**:传对象前先调用 `addSaveableContent`;传 id 时找不到对象则静默返回。
|
||||
- **读档的键要对得上**:存档缺少某个已注册 id 会告警并跳过;存档多出未注册 id 会在读完时告警。
|
||||
- **插件未到完成态会被告警**:`load()` 结束时会逐个检查,状态不是 `Loaded` 的插件都会被点名。
|
||||
- **读档期间录像被临时禁用**:每个对象的 `loadState` 都被禁用与恢复包裹,执行器不在包裹内。
|
||||
- **不要重排装配顺序**:层与层之间的依赖是固定的,重排会让需要回指主对象的子系统拿不到已装配的下层。
|
||||
- **不要手动构造内部子系统**:需要新能力时新增插件,或通过公开入口扩展;自己创建内部对象会绕过存档、录像与生命周期管理。
|
||||
Loading…
Reference in New Issue
Block a user