mirror of
https://github.com/motajs/template.git
synced 2026-10-29 12:20:17 +08:00
docs(10-31): 修复 Mover/FaceHandler 读者问题(第1轮)
This commit is contained in:
parent
f317446aa7
commit
b243c51479
77
.planning/phases/10-docs-restructure/10-31-READER-REVIEW.md
Normal file
77
.planning/phases/10-docs-restructure/10-31-READER-REVIEW.md
Normal file
@ -0,0 +1,77 @@
|
||||
# 高级 I 补写(Mover / FaceHandler)读者审阅
|
||||
|
||||
## 读者人格(逐字,沿用高级 I)
|
||||
|
||||
> 假设你是一位对代码有一定了解、了解基本语法(变量、函数、类、import 等),但代码能力一般、不熟悉引擎内部架构的游戏作者。你已经读完中级教程,正在阅读高级教程 I,想判断自己能不能照着文档完成插件等扩展。
|
||||
|
||||
## 第 1 轮 · 问题清单
|
||||
|
||||
### 阻断性(4)
|
||||
|
||||
| # | 问题 | 定位 |
|
||||
|---|------|------|
|
||||
| B-01 | `start()` 返回 `null` 时,文档让读者「等 `controller.onEnd` 或调用 `stop()`」,但此时根本没有 controller(返回的就是 null),`IHeroMover` 也没有 `stop()`。应改为:保存上一次 `start()` 返回的 controller,等它的 `onEnd` 或对它 `stop()` | `mover.md`「边界与注意」 |
|
||||
| B-02 | 勇士扩展点 `useTopImplementation` 只有一句,没有 `IHeroMoveTopImpl` 契约、示例,也没说与内置 `DefaultHeroMoveTopImpl` 的关系;而 `ObjectMover` 模板会丢掉勇士的通行/踩踏/撞击/自动存档。照文档只能在「整类重写丢功能」和「不改」之间选 | `mover.md`「拿到现成的移动器」「实现自己的移动器」 |
|
||||
| B-03 | `MapTileBase.setFaceDirection` 的降级回退与源码不一致:文档说折一次再查、查到就换图;源码降级分支只 `return next.identifier`,没有 `this.set(...)`(`packages-user/data-base/src/map/tile.ts:56-71`) | `face-handler.md`「它们怎么串起来」 |
|
||||
| B-04 | `Dir4FaceHandler.dirOf` 实际遍历 `DIR8_MOVEMENTS`,`dir4.dirOf(1,1)` 返回 `RightDown`;与「四方向只认上下左右」不一致(源码 bug 或文档需改) | `face-handler.md`「IFaceHandler 能做什么」「两个内置处理器」 |
|
||||
|
||||
### 非阻断(摘要)
|
||||
|
||||
- `config()` / `speed()` / `animDir()` 的持久性:mover 上不重置,会带到后续移动(文档需说明)。
|
||||
- `mover.push(steps[])` 是数组,`controller.push(...steps)` 是可变参数,易写错。
|
||||
- 执行中异常路径:步进方法/钩子抛错时 `onEnd` 不兑现、`moving` 恒真,后续 `start()` 恒返回 null(文档需提示)。
|
||||
- 「怎么验证」的 `step(Right,2).face(Down).step(Down)` 是 4 个 step,不是 3 个。
|
||||
- `faceManager.get(FaceGroup.Dir8)` 不传泛型时返回 `IFaceHandler<number>`,静态类型非 `FaceDirection`。
|
||||
- 朝向图块的示例数字 1/2 来源与地图编号冲突未说明。
|
||||
- 重复 `malloc` 时子图块旧 `faceMap` 仍可查到旧映射。
|
||||
- `mapMovement` 含 `Unknown` 零偏移;直接遍历会把对象原地走一步(寻路内部会跳过)。
|
||||
- `Dir8FaceHandler.degrade(999)` 实际返回 999,不是 `Unknown`。
|
||||
- `mover.ts` 的 `IObjectMover.forward` 注释「沿当前移动方向」与文档「沿面朝朝向」的歧义(按 D-76,实现正确,文档需说明)。
|
||||
- `index.md`:「同时覆盖数据端与渲染端」与「暂时只能写数据端」张力;「读完这一层」未覆盖 Mover/FaceHandler;阅读路径未列入。
|
||||
- 自定义 mover/handler 接进插件/真实对象的目录、生命周期、持有者、存档、替换现有 mover 未说明(最大非阻断缺口)。
|
||||
- `HeroMover` 开头说「负责边界/通行/踩踏/撞击」,后文又说「通行判断不在移动器里」,需一句「自己不判定,只转发给 topImpl」。
|
||||
|
||||
## 迭代记录
|
||||
|
||||
## 第 1 轮 · 修复记录
|
||||
|
||||
仅改 `docs/advanced/mover.md`、`docs/advanced/face-handler.md`、`docs/advanced/index.md` 与本记录;未改生产代码与配置。
|
||||
|
||||
### 阻断项
|
||||
|
||||
| # | 处置 | 说明 |
|
||||
|---|------|------|
|
||||
| B-01 | 已修正 | `mover.md`「边界与注意」:`start()` 返回 `null` 时没有新控制器,只能用上一次 `start()` 保存的 `controller` 等 `onEnd` 或对它 `stop()`;明确 `IHeroMover` 没有 `stop()`。 |
|
||||
| B-02 | 已修正 | `mover.md` 新增「勇士的顶层实现」:`IHeroMoveTopImpl` 六个方法、`IHeroMoveTopHandler` 字段、与 `DefaultHeroMoveTopImpl` 的关系与装配位置,并给出包装默认实现的完整类示例;说明 `ObjectMover` 只算落点,勇士的边界/通行/踩踏/撞击与配置不在其中。 |
|
||||
| B-03 | 已按注释修正并报告 | `face-handler.md` 按 `setFaceDirection` 方法注释描述「折一次再查、查到就换图」的语义,并标注实现差异:降级分支只 `return next.identifier`,未 `this.set(...)` 写回。按 D-72 以注释为准,未按实现改写注释语义。 |
|
||||
| B-04 | 已按通用语义修正并报告 | `face-handler.md` 不再断言 `Dir4` 只返回上下左右;在 `dirOf` 段说明内置 `Dir4FaceHandler.dirOf` 遍历八方向表,`dirOf(1, 1)` 返回 `RightDown`,属实现与直觉/文档描述不一致;接口注释只承诺多解时返回其一。 |
|
||||
|
||||
### 非阻断项
|
||||
|
||||
| 项 | 处置 |
|
||||
|----|------|
|
||||
| `config()` / `speed()` / `animDir()` 持久性 | `mover.md`「移动器状态」补充:状态不随 `start()` 重置,会带入下一次;需要默认值要显式重设。 |
|
||||
| `mover.push(steps[])` 与 `controller.push(...steps)` | `mover.md`「排一个移动计划」补充两者形状差异。 |
|
||||
| 异常路径 | `mover.md`「执行与等待」与「边界与注意」补充:抛错时 `onEnd` 不兑现、`moving` 恒真、后续 `start()` 恒 `null`。 |
|
||||
| 验证步数 | `mover.md`「怎么验证」改为四步计划,预期写明 `Dir`、`Dir`、`Face`、`Dir` 四个步骤。 |
|
||||
| `faceManager.get` 泛型 | `face-handler.md` 说明不传泛型返回 `IFaceHandler<number>`,示例改用 `get<FaceDirection>`。 |
|
||||
| 朝向图块编号来源 | `face-handler.md` 说明 `1` / `2` 为占位,`malloc` / `bind` 会覆盖既有映射,应取真实图块数字。 |
|
||||
| 重复 `malloc` | `face-handler.md`「边界与注意」补充子图块仍持有旧 `faceMap`,需重新 `bind`。 |
|
||||
| `mapMovement` 含 `Unknown` 零偏移 | `face-handler.md` 说明自行遍历要跳过零偏移,寻路构图已跳过。 |
|
||||
| `Dir8FaceHandler.degrade(999)` | `face-handler.md` 报告实现返回 `999`,与接口注释「无效值应返回 `Unknown`」不一致,提示不要把该值当合法方向。 |
|
||||
| D-76 朝向语义 | `mover.md`「移动器状态」补充:源码注释的「当前移动方向」指面朝朝向,`onSetMoveDir` 的「朝向」指移动朝向,实现正确。 |
|
||||
| `index.md` 张力与覆盖 | 统一为「目标双端覆盖、当前只写数据端」;阅读顺序说明 Mover / FaceHandler 为按需参考;阅读路径与「读完这一层」补入两篇能力。 |
|
||||
| 接入与替换 | `mover.md` 新增「接进插件与真实对象」:`awake` / `afterLoad` 装配、处理器注册、勇士 `useTopImplementation`、动态图块 mover 只读、生命周期与存档。 |
|
||||
| `HeroMover` 不判定通行 | 开头与「示例」段改为:`HeroMover` 只登记录像、写坐标、跑流程,通行/边界/踩踏/撞击转发给 `IHeroMoveTopImpl`。 |
|
||||
|
||||
### 报告给用户的源码/注释不一致
|
||||
|
||||
1. `MapTileBase.setFaceDirection`(`packages-user/data-base/src/map/tile.ts:55-72`):注释描述降级后查到映射就换图,实现降级分支只返回 `next.identifier`,没有 `this.set(...)`。
|
||||
2. `Dir4FaceHandler.dirOf`(`packages-user/data-common/src/common/faceManager.ts:202-207`):遍历 `DIR8_MOVEMENTS`,`dirOf(1, 1)` 返回 `RightDown`;接口注释未限制只返回本组方向。
|
||||
3. `Dir8FaceHandler.degrade`(`packages-user/data-common/src/common/faceManager.ts:95-97`):原样返回入参,`degrade(999)` 为 `999`;`IFaceHandler.degrade` 注释约定无法降级时返回 `Unknown`。
|
||||
|
||||
### 本轮构建
|
||||
|
||||
- `pnpm exec prettier --write docs/advanced/mover.md docs/advanced/face-handler.md docs/advanced/index.md`:通过(mover 有格式变动,其余 unchanged)。
|
||||
- `pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build`:`build complete in 15.06s`,无 dead link 报错。
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
|
||||
## 拿到朝向处理器
|
||||
|
||||
`state.faceManager` 是朝向处理器的注册表,有两种取法:按数字分组取,或按字符串 id 取。两者找不到时都返回 `null`:
|
||||
`state.faceManager` 是朝向处理器的注册表,有两种取法:按数字分组取,或按字符串 id 取。两者找不到时都返回 `null`。`get` 的泛型决定返回的 `IFaceHandler<T>` 里 `T` 是什么:不写泛型时 `T` 退化为 `number`,`degrade`、`opposite`、`next` 的静态类型都会是 `number`;需要 `FaceDirection` 的强类型时写 `get<FaceDirection>(...)`。
|
||||
|
||||
```ts
|
||||
import { FaceDirection, FaceGroup } from '@user/data-common';
|
||||
@ -32,7 +32,7 @@ import { ICoreState } from '@user/data-state';
|
||||
|
||||
/** 取内置的八方向处理器,并算一步向上的偏移。 */
|
||||
function stepUp(state: ICoreState): void {
|
||||
const dir8 = state.faceManager.get(FaceGroup.Dir8);
|
||||
const dir8 = state.faceManager.get<FaceDirection>(FaceGroup.Dir8);
|
||||
if (!dir8) {
|
||||
// 启动期没有注册时才会走到这里
|
||||
console.log('八方向处理器未注册');
|
||||
@ -83,9 +83,9 @@ state.faceManager.registerById('dir8', dir8);
|
||||
|
||||
`opposite` 与 `next` 都在本组内求解:先 `degrade`,再查本组的映射表,因此传入一个本组不认的方向时,结果取决于 `degrade` 的归并。`next` 的第二个参数是 `anticlockwise`,省略时为 `false`,即顺时针。
|
||||
|
||||
`dirOf` 走的是相反方向:给你一个偏移量,问它是哪个方向。它不接收方向,也不会先 `degrade`;偏移量与表中某一项完全相等时才命中,多个方向共用同一偏移时只返回其中一个。
|
||||
`dirOf` 走的是相反方向:给你一个偏移量,问它是哪个方向。它不接收方向,也不会先 `degrade`;偏移量与表中某一项完全相等时才命中,多个方向共用同一偏移时只返回其中一个。它反查的是处理器内部的偏移表,命中顺序就是表的条目顺序。内置 `Dir4FaceHandler.dirOf` 实际遍历的是八方向偏移表,所以 `dir4.dirOf(1, 1)` 会返回 `RightDown`,而不是 `Unknown`;这与「四方向只认上下左右」的直觉不一致,属于实现与文档描述不一致的地方(按接口注释,`dirOf` 只承诺「多个符合时返回其中一个」,并不承诺只返回本组方向)。需要严格四方向结果时,用 `mapDirection()` 的成员集合过滤返回值。
|
||||
|
||||
`mapDirection` 与 `mapMovement` 用于遍历。两者的结果都包含 `Unknown`,`mapMovement` 里 `Unknown` 对应的偏移是 `{ x: 0, y: 0 }`。寻路就是按 `mapMovement` 的条目扩展邻域的,所以处理器认哪些方向,直接决定图的分支数。
|
||||
`mapDirection` 与 `mapMovement` 用于遍历。两者的结果都包含 `Unknown`,`mapMovement` 里 `Unknown` 对应的偏移是 `{ x: 0, y: 0 }`。寻路就是按 `mapMovement` 的条目扩展邻域的,所以处理器认哪些方向,直接决定图的分支数。自己按 `mapMovement` 扩展邻域时,要跳过零偏移条目,否则对象会原地走一步;寻路的构图循环就是这样处理的。
|
||||
|
||||
## 两个内置处理器
|
||||
|
||||
@ -96,9 +96,9 @@ state.faceManager.registerById('dir8', dir8);
|
||||
| `Dir8FaceHandler` | 八个方向 | 原样返回,不做归并 | 八方向移动、勇士与动态图块的默认 |
|
||||
| `Dir4FaceHandler` | 上下左右四个方向 | 四个正方向原样;四个斜向折到左右;其余为 `Unknown` | 只走四方向的地图与寻路 |
|
||||
|
||||
`Dir8FaceHandler` 的 `degrade` 把八个方向原样保留;按接口约定,无法合理降级的方向(包括 `Unknown`)应返回 `Unknown`。查表失败时 `movement` 退回零偏移,`opposite` 与 `next` 退回 `Unknown`。
|
||||
`Dir8FaceHandler` 的 `degrade` 把八个方向原样保留;按接口注释的约定,无法合理降级的方向(包括 `Unknown`)应返回 `Unknown`。当前实现是把入参原样返回,所以 `degrade(999)` 得到 `999`——这是实现与注释不一致的地方,不要把这种返回值当作合法方向使用。查表失败时 `movement` 退回零偏移,`opposite` 与 `next` 退回 `Unknown`。
|
||||
|
||||
`Dir4FaceHandler` 的归并需要记住一条:斜向折到左右,不是折到上下。
|
||||
`Dir4FaceHandler` 的归并需要记住一条:斜向折到左右,不是折到上下。上表说的是 `degrade` 与位移的方向集合;不要据此认为 `dirOf` 也只返回四个方向,原因见前文。
|
||||
|
||||
| 输入 | `degrade` 结果 |
|
||||
| ------------ | -------------- |
|
||||
@ -127,7 +127,7 @@ function targetOf(
|
||||
state: ICoreState,
|
||||
direction: FaceDirection
|
||||
): { x: number; y: number } | null {
|
||||
const handler = state.faceManager.get(FaceGroup.Dir8);
|
||||
const handler = state.faceManager.get<FaceDirection>(FaceGroup.Dir8);
|
||||
if (!handler) return null;
|
||||
|
||||
const loc = state.hero.getLocation();
|
||||
@ -146,7 +146,7 @@ function oppositeOf(
|
||||
state: ICoreState,
|
||||
direction: FaceDirection
|
||||
): FaceDirection | null {
|
||||
const handler = state.faceManager.get(FaceGroup.Dir8);
|
||||
const handler = state.faceManager.get<FaceDirection>(FaceGroup.Dir8);
|
||||
if (!handler) return null;
|
||||
|
||||
// [需改] 把 direction 换成你要取反的方向
|
||||
@ -155,12 +155,12 @@ function oppositeOf(
|
||||
```
|
||||
|
||||
```ts
|
||||
import { FaceGroup } from '@user/data-common';
|
||||
import { FaceDirection, FaceGroup } from '@user/data-common';
|
||||
import { ICoreState } from '@user/data-state';
|
||||
|
||||
/** 列出四方向处理器认的所有方向与偏移,包含 Unknown。 */
|
||||
function listDir4Steps(state: ICoreState): void {
|
||||
const handler = state.faceManager.get(FaceGroup.Dir4);
|
||||
const handler = state.faceManager.get<FaceDirection>(FaceGroup.Dir4);
|
||||
if (!handler) return;
|
||||
|
||||
for (const [dir, offset] of handler.mapMovement()) {
|
||||
@ -364,7 +364,11 @@ function bindFacingTiles(binder: IRoleFaceBinder): void {
|
||||
|
||||
`bind` 有两个前置条件,任一不满足都会记一条错误日志并放弃绑定:`main` 没有 `malloc` 过,或绑定的朝向与主朝向相同。因此建组的顺序必须是先 `malloc` 主图块,再 `bind` 其余朝向。`getFaceOf` 与 `getMainFace` 找不到时返回 `null`;`getFaceDirection` 找不到时返回 `Unknown`,而不是 `null`。
|
||||
|
||||
地图图层默认使用 `state.roleFace` 作为自己的朝向绑定器,所以图块转向时查的就是这份映射。`MapTileBase.setFaceDirection` 先按原方向查 `getFaceOf`,查不到时再用 `Dir4` 处理器的 `degrade` 折一次再查,仍然查不到就保持原图块数字。这就是两层结构在实现里的交汇点:朝向映射决定换哪张图,处理器决定斜向该往哪折。
|
||||
示例里的 `1` 与 `2` 只是占位,换图前先确认这两个图块数字在当前地图里没有被别的朝向组占用。`malloc` 会覆盖同一个图块数字原有的组,`bind` 会覆盖该组同一朝向的旧映射,所以要从地图的图块数据里取真实数字,不要照抄示例。
|
||||
|
||||
地图图层默认使用 `state.roleFace` 作为自己的朝向绑定器,所以图块转向时查的就是这份映射。`MapTileBase.setFaceDirection` 先按原方向查 `getFaceOf`,查到就换图并返回新数字;查不到时,按方法注释的约定,再用 `Dir4` 处理器的 `degrade` 折一次、重新查一次,查到就换成折后的图块并返回,仍查不到则保持原图块数字。这就是两层结构在实现里的交汇点:朝向映射决定换哪张图,处理器决定斜向该往哪折。
|
||||
|
||||
<strong>实现差异</strong>:当前 `setFaceDirection` 的降级分支只 `return next.identifier`,没有调用 `this.set(...)` 写回图块,所以折一次查到映射时,返回值变了但图块本身没换。本文按方法注释描述语义;这个不一致尚未修正,使用降级路径时不要假定图块已经换掉。
|
||||
|
||||
## 它们怎么串起来
|
||||
|
||||
@ -395,8 +399,9 @@ function bindFacingTiles(binder: IRoleFaceBinder): void {
|
||||
- <strong>dirOf 可能有多解</strong>:偏移量相同、方向不同的情况只返回其中一个,不要用它反推「唯一方向」,也不要用它校验位移是否合法。
|
||||
- <strong>Unknown 的偏移是零</strong>:`movement` 与 `move` 对 `Unknown` 返回 `{ x: 0, y: 0 }`,`opposite` 与 `next` 返回 `Unknown`;把它当普通方向做位置运算,对象会原地不动。
|
||||
- <strong>bind 前必须 malloc</strong>:主图块没有分配朝向时,`bind` 会记错误并放弃;绑定的朝向与主朝向相同也会被拒绝。
|
||||
- <strong>重复 malloc 会覆盖</strong>:同一个图块数字再次 `malloc` 会重建该组的映射,之前 `bind` 进去的其它朝向不会被保留。
|
||||
- <strong>重复 malloc 会覆盖</strong>:同一个图块数字再次 `malloc` 会重建该组的映射,之前 `bind` 进去的其它朝向不会被保留;已经 `bind` 过的子图块仍持有旧映射表,`getFaceOf` 可能查到旧结果。重建整组后要重新 `bind` 所有子图块。
|
||||
- <strong>处理器是共享对象</strong>:不要在实例上保存「当前处理的是哪个对象」之类的状态;多个对象共用同一个处理器时,状态会互相覆盖。
|
||||
- <strong>处理器不随对象销毁</strong>:处理器是无状态的共享对象,注册一次即可,也没有卸载流程;真正持有它的是移动器、寻路 `finder` 与图块层,换处理器要改使用方的引用,而不是重新 `register`。
|
||||
- <strong>不同分组互不影响</strong>:`Dir4`、`Dir8` 与自定义分组各有一份处理器,换掉其中一个不会改动其余分组。
|
||||
- <strong>注册不替换已有实例</strong>:注册只影响之后取用它的代码;已经构造好的移动器仍持有构造时传入的处理器。
|
||||
- <strong>getMainFace 返回的是朝向</strong>:返回的 `IFaceData` 里,`identifier` 仍是你传入的图块数字,`face` 是它所属组的主朝向,不是主图块的数字。
|
||||
@ -411,8 +416,8 @@ import { ICoreState } from '@user/data-state';
|
||||
|
||||
/** 打印内置处理器的关键结果,确认方向换算符合预期。 */
|
||||
function verifyFaceHandler(state: ICoreState): void {
|
||||
const dir4 = state.faceManager.get(FaceGroup.Dir4);
|
||||
const dir8 = state.faceManager.get(FaceGroup.Dir8);
|
||||
const dir4 = state.faceManager.get<FaceDirection>(FaceGroup.Dir4);
|
||||
const dir8 = state.faceManager.get<FaceDirection>(FaceGroup.Dir8);
|
||||
if (!dir4 || !dir8) {
|
||||
console.log('处理器未注册,检查 initialize.ts 的注册');
|
||||
return;
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
- 你要理解扩展机制本身,知道声明式接口、插件、存档与录像之间怎么配合;
|
||||
- 你愿意读一点源码,但不需要成为引擎的编写者。
|
||||
|
||||
它不要求你改造引擎内部。高级教程同时覆盖数据端与渲染端;渲染端尚未完工,暂时只能写数据端的内容,所以高级 I 的例子都落在数据端:你写的内容、你注册的对象、你声明的规则。
|
||||
它不要求你改造引擎内部。高级教程的目标是同时覆盖数据端与渲染端;渲染端尚未完工,当前只写数据端内容,渲染端部分只用占位标记指出归属。所以高级 I 的例子都落在数据端:你写的内容、你注册的对象、你声明的规则。
|
||||
|
||||
## 高级教程的三层
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
| 8 | 移动 Mover | 能排出一次移动、等它走完,并在需要时自己实现移动器 |
|
||||
| 9 | FaceHandler 系列 | 能选对朝向处理器、算出目标格与反方向,并把朝向映射到图块数字 |
|
||||
|
||||
推荐阅读顺序:声明式 vs 命令式 → 插件系统 → 可存档内容 → 录像一致性 → data-common 的 Store → 钩子深入。移动 Mover 与 FaceHandler 系列偏参考性质,可等用到时再读。
|
||||
推荐阅读顺序:声明式 vs 命令式 → 插件系统 → 可存档内容 → 录像一致性 → data-common 的 Store → 钩子深入。移动 Mover 与 FaceHandler 系列偏参考性质,等要写移动或朝向逻辑时再读;它们各自独立,不依赖前面的顺序。
|
||||
|
||||
插件系统是这一层的核心,可存档内容、录像一致性、data-common 的 Store 与钩子深入都建立在它提供的扩展位置上。也可以按需求跳读,但建议至少先读前两篇:它们决定你是在「声明一条规则」,还是在「写一段一次性脚本」。
|
||||
|
||||
@ -53,7 +53,7 @@
|
||||
按目标不同,可以走两条路径:
|
||||
|
||||
- <strong>做扩展系统</strong>:声明式 → 插件 → 可存档内容 → 录像一致性。适合新增任务、商店、计数器这类有自己状态与生命周期的功能。读完能独立装配一个插件,并让它的数据跟着存档走。
|
||||
- <strong>改内容或机制</strong>:声明式 → data-common 的 Store → 钩子深入。适合在现有机制上加行为、按接口读取内容定义。读完能分清哪些只需要读、哪些由数据端登记,也能把钩子限定到目标对象上。
|
||||
- <strong>改内容或机制</strong>:声明式 → data-common 的 Store → 钩子深入,按需接上移动 Mover 或 FaceHandler 系列。适合在现有机制上加行为、按接口读取内容定义。读完能分清哪些只需要读、哪些由数据端登记,也能把钩子限定到目标对象上;要改移动或朝向规则时,知道该换哪个扩展点。
|
||||
|
||||
两条路径都建议先读声明式篇。它决定你后面是在「加一条规则」,还是「写一段一次性脚本」;这个判断错了,插件、存档与录像都会跟着别扭。
|
||||
|
||||
@ -83,6 +83,8 @@
|
||||
- 能拿到一个需求,判断它该做成插件,还是按现有接口读取即可。
|
||||
- 能说出一个可存档对象必须满足的三个约束:只存重建必需的数据、`saveState` 返回独立对象(引擎不替你深拷贝)、`loadState` 先清空再重建。
|
||||
- 能说明为什么建议一个对象一个钩子实例,并在回调拿到多个对象时用对象引用筛出目标。
|
||||
- 能排出一次移动、等到它结束,并在需要时实现自己的移动器或勇士顶层实现。
|
||||
- 能选对朝向处理器、算出目标格与反方向,并把朝向映射到图块数字。
|
||||
|
||||
## 继续阅读
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
对象移动是数据端最常见的动作之一:勇士走一步、动态图块被推走、NPC 沿路径巡逻。移动器把「想怎么走」和「实际怎么走」拆开:先用链式调用排出一串移动步骤,再调用 `start()` 按顺序执行;每一步落在哪一格,由移动器的子类决定。
|
||||
|
||||
这样拆的好处是同一套执行流程可以服务不同对象。勇士的 `HeroMover` 负责边界、通行、踩踏与撞击,动态图块的 `DynamicTileMover` 只把方向换成落点。读这篇不需要先读它们的源码:先会用现成移动器,再看怎么扩展。
|
||||
这样拆的好处是同一套执行流程可以服务不同对象。勇士的 `HeroMover` 自己不判定能不能走,它把边界、通行、踩踏与撞击转发给顶层实现对象 `IHeroMoveTopImpl`;动态图块的 `DynamicTileMover` 只把方向换成落点。读这篇不需要先读它们的源码:先会用现成移动器,再看怎么扩展。
|
||||
|
||||
数据端只维护坐标与状态,画面上的位移、动画与速度表现由渲染端读取这些状态后完成。渲染端内容用 `渲染端 · 移动表现` 占位。
|
||||
|
||||
@ -39,7 +39,89 @@
|
||||
| `autoSave` | 是否在特定时机触发自动存档 |
|
||||
| `allowOutBound` | 是否允许勇士到达地图之外 |
|
||||
|
||||
`config` 只覆盖传入的字段,没传的保持原值;`getConfig` 返回当前配置的只读快照。`useTopImplementation` 用来替换勇士的顶层判定实现,通行、边界、踩踏与撞击都由它负责。
|
||||
`config` 只覆盖传入的字段,没传的保持原值;`getConfig` 返回当前配置的只读快照。
|
||||
|
||||
### 勇士的顶层实现
|
||||
|
||||
`useTopImplementation` 替换的是勇士这一层的行为,不是移动器本身。`IHeroMover` 没有 `stop()`,停止只发生在 `start()` 返回的控制器上。
|
||||
|
||||
`HeroMover` 在每一步把信息打包成 `IHeroMoveTopHandler`,交给 `IHeroMoveTopImpl` 判断:
|
||||
|
||||
| 方法 | 返回 | 作用 |
|
||||
| ------------------------ | ---------------- | ------------------------------------------------------ |
|
||||
| `predicate()` | `IPassPredicate` | 返回通行性谓词,`HeroMover` 用它判断能否进入、是否撞击 |
|
||||
| `inBound(x, y, floorId)` | `boolean` | 目标格是否在地图范围内 |
|
||||
| `enter(handler)` | `Promise<void>` | 正常进入目标格后触发 |
|
||||
| `leave(handler)` | `Promise<void>` | 离开当前格时触发 |
|
||||
| `hit(handler)` | `Promise<void>` | 撞到不可进入的目标格时触发 |
|
||||
| `cannotEnter(handler)` | `Promise<void>` | 被判定为无法进入时触发 |
|
||||
|
||||
`handler` 里带着 `currLoc`、`nextLoc`、`direction`、`floorId`、`face` 与 `state`,够你在判定里读位置、朝向与状态。引擎内置的 `DefaultHeroMoveTopImpl`(`src/data/hero/moverImpl.ts`)就是默认实现:它用通行谓词与事件系统完成边界判断、进出与撞击触发器。`MainDataPlugin` 在 `awake` 里把它装上:
|
||||
|
||||
```ts
|
||||
// src/data/initialize.ts 的 awake 中
|
||||
const heroMoveTopImpl = new DefaultHeroMoveTopImpl(state);
|
||||
state.hero.location.mover.useTopImplementation(heroMoveTopImpl);
|
||||
```
|
||||
|
||||
想保留默认行为、只改其中一环时,包一层并转发给默认实现,比从头写省事:
|
||||
|
||||
```ts
|
||||
// src/data/hero/moverTop.ts
|
||||
import {
|
||||
IHeroMoveTopHandler,
|
||||
IHeroMoveTopImpl,
|
||||
IPassPredicate
|
||||
} from '@user/data-base';
|
||||
import { IStateSystem } from '@user/data-system';
|
||||
import { DefaultHeroMoveTopImpl } from './moverImpl';
|
||||
|
||||
/** 保留默认的边界、通行与事件,只给撞击加一条日志。 */
|
||||
export class MyHeroMoveTopImpl implements IHeroMoveTopImpl {
|
||||
// 默认实现负责边界、通行谓词与四类事件派发
|
||||
private readonly base: DefaultHeroMoveTopImpl;
|
||||
|
||||
constructor(state: IStateSystem) {
|
||||
this.base = new DefaultHeroMoveTopImpl(state);
|
||||
}
|
||||
|
||||
predicate(): IPassPredicate {
|
||||
return this.base.predicate();
|
||||
}
|
||||
|
||||
inBound(x: number, y: number, floorId: string | undefined): boolean {
|
||||
return this.base.inBound(x, y, floorId);
|
||||
}
|
||||
|
||||
enter(handler: IHeroMoveTopHandler): Promise<void> {
|
||||
return this.base.enter(handler);
|
||||
}
|
||||
|
||||
leave(handler: IHeroMoveTopHandler): Promise<void> {
|
||||
return this.base.leave(handler);
|
||||
}
|
||||
|
||||
hit(handler: IHeroMoveTopHandler): Promise<void> {
|
||||
// [需改] 撞击时先做自己的事,再交给默认实现触发原有触发器
|
||||
console.log('撞到', handler.nextLoc);
|
||||
return this.base.hit(handler);
|
||||
}
|
||||
|
||||
cannotEnter(handler: IHeroMoveTopHandler): Promise<void> {
|
||||
// 默认实现的 cannotEnter 是空实现,没有参数
|
||||
return this.base.cannotEnter();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
装配时把它换进勇士移动器:
|
||||
|
||||
```ts
|
||||
// src/data/initialize.ts 的 awake 中,替换掉默认实现
|
||||
state.hero.location.mover.useTopImplementation(new MyHeroMoveTopImpl(state));
|
||||
```
|
||||
|
||||
这就是勇士的扩展点:`HeroMover` 负责录像登记、坐标写入与步进流程,`IHeroMoveTopImpl` 负责「这一步能不能走、撞到什么、进出触发什么」。给勇士扩展时,优先实现 `IHeroMoveTopImpl` 并通过 `useTopImplementation` 装上;不要为了改通行规则去重写整个移动器,更不要从 `ObjectMover` 起手——`ObjectMover` 只算落点,边界、通行、踩踏、撞击和 `HeroMover` 的配置(含 `autoSave` 开关)都不在里面,勇士会失去这些行为。
|
||||
|
||||
勇士位置对象的接口见[勇士位置](../intermediate/hero-location.md);动态图块的创建、转换与移动见[静态图块与动态图块](./ii/static-vs-dynamic-tiles.md)。
|
||||
|
||||
@ -69,7 +151,7 @@
|
||||
|
||||
`speed` 与 `animDir` 只是写移动器状态,本身不产生位移。`speed` 的单位是毫秒每格,初始 `currentSpeed` 为 `100`;`animDir` 设置 `ObjectAnimDirection.Forward` 或 `Backward`。真正让速度与动画方向「看得见」的是渲染端。
|
||||
|
||||
`push` 接收的是步骤对象数组,适合把循环生成或从别处收集来的步骤一次加入。`clear` 只清空尚未执行的队列,不影响正在执行的那一次。
|
||||
`mover.push(steps)` 接收的是步骤对象数组,适合把循环生成或从别处收集来的步骤一次加入;`controller.push(...steps)` 是可变参数,直接把步骤对象逐个传入。两者名字相同、形状不同,别把数组塞给控制器。`clear` 只清空尚未执行的队列,不影响正在执行的那一次。
|
||||
|
||||
## 执行与等待
|
||||
|
||||
@ -105,7 +187,7 @@ async function walkRightThenDown(state: ICoreState): Promise<void> {
|
||||
}
|
||||
```
|
||||
|
||||
`onEnd` 是整次执行结束时兑现的 Promise,`done` 是同一时点的布尔查询。等待优先用 `await controller.onEnd`;`done` 适合在同步代码里快速判断。
|
||||
`onEnd` 是整次执行结束时兑现的 Promise,`done` 是同一时点的布尔查询。等待优先用 `await controller.onEnd`;`done` 适合在同步代码里快速判断。`onEnd` 只在整次执行正常跑完时才兑现,任何一步或任一钩子抛错都会让它永远悬着,见「边界与注意」。
|
||||
|
||||
`start()` 会做三件事:把当前队列切片成本次执行的局部队列、清空移动器内部的待执行队列、把 `moveDirection` 重置为 `Unknown`。因此执行期间再用移动器规划,写的是「下一次」的队列,不是本次。
|
||||
|
||||
@ -196,6 +278,10 @@ async function stopBeforeFirstStep(state: ICoreState): Promise<void> {
|
||||
|
||||
<strong>面朝朝向(`faceDirection`)</strong>指对象「脸朝哪」,<strong>移动朝向(`moveDirection`)</strong>指「这一步往哪走」,两者是不同的概念,不要混用。`step` 会让两者一起更新;`stepFace` 与 `backward` 会让两者不同,例如向右走但脸朝上,或者朝下后退时移动朝向是上。`forward()` 沿的是面朝朝向,而不是移动朝向。
|
||||
|
||||
`IObjectMover.forward` 的源码注释写的是「沿当前移动方向」,这里指的是**面朝朝向**:`forward` 读的是 `faceDirection`,不是 `moveDirection`。同理,`onSetMoveDir` 注释里的「设置朝向」指设置**移动朝向**。源码实现与这个理解一致,不要把它当成 bug 去改。
|
||||
|
||||
`speed` 与 `animDir` 往队列里放的是步骤,真正写入 `currentSpeed`、`currAnimDir` 是在这一步执行时;执行后状态留在移动器上,`start()` 不会恢复默认值,所以会带到下一次移动。勇士的 `config` 也是立即写入且不重置。想每次都从默认值开始,就在排计划时显式写 `speed(100)`、`animDir(ObjectAnimDirection.Forward)` 与对应的 `config`。
|
||||
|
||||
`setPos`、`setFaceDir`、`setMoveDir` 与链式规划不同:它们立即生效、不排队。`setPos` 直接写对象坐标,`setFaceDir` 直接改面朝朝向,`setMoveDir` 直接改移动朝向。三者分别触发 `onSetPos`、`onSetFaceDir`、`onSetMoveDir` 钩子。
|
||||
|
||||
移动过程中的落点写入由基类直接调用 `tile.setPos`,不经过移动器的 `setPos`,所以不会触发 `onSetPos`。`onSetPos` 只对应你显式调用移动器 `setPos` 的那一次。
|
||||
@ -447,7 +533,7 @@ export class SimpleMover extends ObjectMover<SimpleTile> {
|
||||
|
||||
这段示例把「方向 → 偏移 → 落点」串了起来:`faceHandler.movement(this.moveDirection)` 给出单格的坐标偏移,加到 `tile.x`、`tile.y` 上就是落点;基类拿到落点后负责写入坐标。
|
||||
|
||||
通行判断不在移动器里。`SimpleMover` 不检查目标格能不能进、有没有墙,也不触发踩踏事件。它只回答「这一步落到哪」。能不能走属于调用方的规则,`HeroMover` 就是把这件事交给顶层实现对象的。
|
||||
通行判断不在移动器基类里。`SimpleMover` 不检查目标格能不能进、有没有墙,也不触发踩踏事件,它只回答「这一步落到哪」。`HeroMover` 也一样:它登记录像、写坐标、跑流程,但自己不判定通行,而是把判定与进出/撞击事件转发给顶层实现对象(见前文「勇士的顶层实现」)。
|
||||
|
||||
把对象与移动器接起来:
|
||||
|
||||
@ -471,6 +557,22 @@ async function runSimpleMover(): Promise<void> {
|
||||
|
||||
`faceHandler` 决定方向体系。四方向对象要配四方向处理器,八方向对象配八方向处理器;混用会让偏移与朝向对不上。朝向处理器本身的接口与自定义方式见[FaceHandler 系列](./face-handler.md)。
|
||||
|
||||
## 接进插件与真实对象
|
||||
|
||||
自定义移动器与朝向处理器都要在数据端插件的 `awake` 里装配,依赖加载后配置或已创建对象的放到 `afterLoad`。
|
||||
|
||||
- <strong>处理器</strong>:`state.faceManager.register(...)` 放进 `awake`,见[FaceHandler 系列](./face-handler.md#实现自己的朝向处理器)。
|
||||
- <strong>勇士的规则</strong>:`state.hero.location.mover.useTopImplementation(...)` 是公开的替换点;移动器实例本身由 `createLocation` 创建,没有公开 setter。
|
||||
- <strong>动态图块</strong>:`mover` 在 `DynamicTile` 构造时随对象创建,只读;需要整套换掉时,在自己的对象创建流程里构造替代实例,而不是事后改字段。
|
||||
- <strong>生命周期</strong>:移动器与对象同生命周期,不需要卸载;钩子用 `unload()` 移除。移动器上的状态不进入存档,位置由 `IHeroLocationSave` 保存;自定义的额外状态要做成可存档内容,见[可存档内容](./saveable-content.md)。
|
||||
|
||||
```ts
|
||||
// src/data/plugin/my-plugin/plugin.ts 的 awake 中
|
||||
state.hero.location.mover.useTopImplementation(new MyHeroMoveTopImpl(state));
|
||||
```
|
||||
|
||||
这段放 `awake` 还是 `afterLoad` 取决于依赖:`DefaultHeroMoveTopImpl` 只用到 `state`,`awake` 就够;如果装配需要加载后的配置或已创建的对象,放到 `afterLoad`。
|
||||
|
||||
## 与录像、存档的关系
|
||||
|
||||
`HeroMover` 在 `onStepStart`、`onStepEnd`、`onStepSettled` 上标了 `@shouldReplay`;`DynamicTileMover` 标在 `onStepEnd`。这些方法会改变状态,所以移动会进入录像检查。自定义移动器同理:把 `@shouldReplay` 标在真正改状态的步进方法上,通常是 `onStepEnd` 或 `onStepSettled`,不要标在只读查询上。
|
||||
@ -512,11 +614,12 @@ async function runSimpleMover(): Promise<void> {
|
||||
|
||||
## 边界与注意
|
||||
|
||||
- <strong>start 返回 null</strong>:移动器已经在移动时不会接管。先等 `controller.onEnd`,或调用 `stop()` 后再排新计划。
|
||||
- <strong>start 返回 null</strong>:移动器已经在移动时不会接管,此时也没有新的控制器可操作。要等或要停,只能用上一次 `start()` 保存的 `controller`:`await controller.onEnd`,或调用 `controller.stop()`。`IHeroMover` 没有 `stop()`,不要在移动器上找。
|
||||
- <strong>moveDirection 被重置</strong>:`start()` 把移动朝向(`moveDirection`)设为 `Unknown`,面朝朝向(`faceDirection`)不受影响;`forward()` 用的是面朝朝向,不依赖这次重置。
|
||||
- <strong>执行期间链式方法无效</strong>:`mover.step`、`mover.face`、`mover.speed` 等写的是下一次的队列;执行中追加只能用 `controller.push` 或 `controller.insert`。
|
||||
- <strong>stop 不立刻中断</strong>:当前步会走完。第一步还没开始时调用,可能一步都不走。
|
||||
- <strong>不要在步进方法里 await stop</strong>:`stop()` 返回的 Promise 要等当前步结束才兑现,在步进方法或步进钩子里 `await` 会卡死整次移动。
|
||||
- <strong>步进方法或钩子抛错会卡住移动器</strong>:`ObjectMover` 没有为执行 Promise 兜底;任何一步或任一钩子抛错,`onEnd` 都不再兑现、`done` 保持假、`moving` 保持真,之后的 `start()` 会一直返回 `null`。自定义步进方法要把预期内的失败转成结果码,而不是抛错。
|
||||
- <strong>setPos 不触发步进钩子</strong>:移动器的 `setPos` 只触发 `onSetPos`,不触发 `onStepStart` / `onStepEnd`;移动过程中的落点写入也不会触发 `onSetPos`。
|
||||
- <strong>speed 与 animDir 只是状态</strong>:它们不产生位移。没有消费方读取时,设置速度与动画方向不会产生可见效果。
|
||||
- <strong>faceHandler 必须与对象方向体系一致</strong>:四方向对象配八方向处理器,或反过来,都会让偏移与朝向对不上。
|
||||
@ -526,7 +629,7 @@ async function runSimpleMover(): Promise<void> {
|
||||
|
||||
## 怎么验证
|
||||
|
||||
先排一个固定的三步计划,观察坐标、`done` 与钩子顺序:
|
||||
先排一个固定的四步计划:向右两格、转向、向下一格,观察坐标、`done` 与钩子顺序:
|
||||
|
||||
```ts
|
||||
import { FaceDirection, IObjectMoverHooks } from '@user/data-common';
|
||||
@ -580,7 +683,7 @@ async function verifyPlan(state: ICoreState): Promise<void> {
|
||||
}
|
||||
```
|
||||
|
||||
预期:`done` 从假变真;坐标先水平移动两格,再向下移动一格;钩子顺序与表中一致,且每一步的 `onStepStart` 都在对应的 `onStepEnd` 之前。
|
||||
预期:`done` 从假变真;整次共四个步骤(`Dir`、`Dir`、`Face`、`Dir`),坐标先水平移动两格,再向下移动一格;钩子顺序与表中一致,且每一步的 `onStepStart` 都在对应的 `onStepEnd` 之前。
|
||||
|
||||
再验证执行中追加:调用 `start()` 后立刻 `controller.push` 一个 `Dir` 步骤,观察最终坐标是否多走了这一步。如果改用 `mover.step` 追加,最终坐标不会包含它,因为那是下一次执行的队列。
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user