From f4edda66c202a7e40de9e42dab6c1027bae55165 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sat, 24 Oct 2026 19:55:22 +0800 Subject: [PATCH] =?UTF-8?q?docs(10-32):=20=E8=A1=A5=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=B9=B6=E6=B8=85=E7=90=86=E5=85=83=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=EF=BC=88D-79..D-82=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mover/face-handler 首节改为系统模型,先讲概念与数据流再讲接口 - 删除使用概率、源码叙述与实现/注释差异说明 - 朝向处理器自定义示例改为六边形布局,保留单节结构 --- .../10-docs-restructure/10-32-USER-REVIEW.md | 19 +++ docs/advanced/face-handler.md | 126 +++++++++--------- docs/advanced/mover.md | 24 ++-- 3 files changed, 101 insertions(+), 68 deletions(-) diff --git a/.planning/phases/10-docs-restructure/10-32-USER-REVIEW.md b/.planning/phases/10-docs-restructure/10-32-USER-REVIEW.md index 0f237e4..b9cf5ac 100644 --- a/.planning/phases/10-docs-restructure/10-32-USER-REVIEW.md +++ b/.planning/phases/10-docs-restructure/10-32-USER-REVIEW.md @@ -42,3 +42,22 @@ **验证:** `prettier --write` 两篇通过;`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` 构建通过,无死链;CRLF 保持。 **自评(按 D-77 / D-78):** 高概率章节(拿移动器、排计划、开始与等待、执行中控制、常用方法、用在哪里)占正文约一半且全部前置;自定义仅末尾 2 节 / 1 节。仍属低概率但保留的内容:`mover.md` 的自定义模板与勇士顶层实现示例(读者确需扩展时才有用),`face-handler.md` 的 `IRoleFaceBinder` 细节(图块换向时才会用到)。\*\* + +## 第 2 轮(2026-10-24) + +**用户意见:** 两篇文档应先讲系统模型,再讲接口与用法(D-82);删除正文中的使用概率元信息(D-79)、源码叙述与实现 / 注释差异说明(D-80),以及把用户问题写进文档的框架(D-81)。朝向组的自定义示例应体现非方格布局(六边形),而不是任意示例;自定义仍只保留一节。 + +| 条目 | 落点 | 处置 | 状态 | +| ---- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------- | +| M1 | `mover.md` 首节 | 新增「移动器模型」:可移动对象、移动计划、执行与控制器、面朝 / 移动两个方向;接口分工表并入模型 | 状态:已处理 | +| M2 | `mover.md` 全文 | 删除「使用概率很低」、`forward` 的源码注释说明、默认实现与装配处的源码路径 | 状态:已处理 | +| F1 | `face-handler.md` 首节 | 把「三个概念」改为「朝向模型」,补齐 `IFaceDescriptor` 与 `IRoleFaceBinder`,先讲移动与显示两条数据流 | 状态:已处理 | +| F2 | `face-handler.md` 全文 | 删除「使用概率很低」、`dirOf` 八方向遍历、降级分支未写回图块、`Dir8FaceHandler.degrade` 实现差异、分组来源的源码路径 | 状态:已处理 | +| F3 | `face-handler.md` 自定义一节 | 示例改为六边形布局的 `HexFaceHandler`(六个邻接方向);保留四条契约与一个注册示例,仍为一节 | 状态:已处理 | + +### 执行记录 + +- 仅修改 `docs/advanced/mover.md`、`docs/advanced/face-handler.md`;未改代码、配置、状态文件,未写 SUMMARY。 +- `mover.md`:新增首节「移动器模型」;删除正文中的使用概率与源码 / 实现差异叙述;保留 D-76 面朝朝向与移动朝向的语义区分。 +- `face-handler.md`:首节改为「朝向模型」,补齐五个组件与两条链路;删除源码级与实现 / 注释差异说明;自定义一节改为六边形布局示例(`HexFaceHandler`,六个邻接方向),注册改用字符串 id `hex`;验证段同步改为六方向检查。 +- 验证:`prettier --write` 两篇 unchanged;`pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build` 输出 `build complete in 17.97s`,无 dead link;CRLF 与单 H1 保持。 diff --git a/docs/advanced/face-handler.md b/docs/advanced/face-handler.md index 38f9ca5..3fc6c44 100644 --- a/docs/advanced/face-handler.md +++ b/docs/advanced/face-handler.md @@ -8,13 +8,21 @@ -## 三个概念 +## 朝向模型 -| 概念 | 是什么 | 用在哪 | -| --------------- | -------------------------------------------------------------- | ----------------------------------------- | -| `FaceDirection` | 方向值,`const enum`,含 `Unknown` 在内共九个成员 | 移动方向、朝向、寻路步骤里的 `dir` | -| `FaceGroup` | 方向组的编号,内置 `Dir4` 与 `Dir8`,表示「这一组认哪些方向」 | 从 `state.faceManager` 取处理器时作为 key | -| `IFaceHandler` | 朝向处理器,把任意方向换算成本组支持的方向、偏移、反方向与旋转 | 移动器构造、寻路邻域、图块转向 | +朝向在数据端分两层:**逻辑朝向**决定对象怎么走、怎么转向;**图块数字**决定画哪张图。两层之间由五个组件连接: + +| 组件 | 是什么 | 负责 | +| ----------------- | -------------------------------------------------------------- | ----------------------------------------- | +| `FaceDirection` | 方向值,`const enum`,含 `Unknown` 在内共九个成员 | 移动方向、朝向、寻路步骤里的 `dir` | +| `FaceGroup` | 方向组的编号,内置 `Dir4` 与 `Dir8`,表示「这一组认哪些方向」 | 从 `state.faceManager` 取处理器时作为 key | +| `IFaceHandler` | 朝向处理器,把任意方向换算成本组支持的方向、偏移、反方向与旋转 | 移动器构造、寻路邻域、图块转向 | +| `IFaceDescriptor` | 只读的 `{ x, y }` 增量,表示朝某方向走一步坐标加多少 | 处理器返回位移时使用 | +| `IRoleFaceBinder` | 朝向图块绑定器 | 把朝向映射成图块数字 | + +一条链路服务于移动:移动器或寻路拿到一个 `IFaceHandler`,把 `FaceDirection` 换成坐标偏移,用它算落点、扩展邻域。另一条链路服务于显示:`IRoleFaceBinder` 按「当前图块数字 + 目标朝向」查出该朝向对应的图块数字,换向时换图。 + +`IFaceHandler` 与 `IRoleFaceBinder` 都是共享对象:处理器不保存「当前处理的是哪个对象」的状态,绑定器保存的是图块数字之间的映射。两者都不直接绘制,渲染端读取结果后完成显示。 `FaceDirection` 的成员依次是 `Unknown`、`Left`、`Up`、`Right`、`Down`,再加上四个斜向 `LeftUp`、`RightUp`、`LeftDown`、`RightDown`。它是 `const enum`,数值由声明顺序决定;不要按数值硬编码,一律写成员名。`Unknown` 表示没有方向,它也会出现在处理器的输入与输出里。 @@ -49,7 +57,7 @@ function stepUp(state: ICoreState): void { `get` 的泛型决定返回的 `IFaceHandler` 里 `T` 是什么:不写泛型时 `T` 退化为 `number`,`degrade`、`opposite`、`next` 的静态类型都会是 `number`;需要 `FaceDirection` 的强类型时写 `get(...)`。 -内置处理器在启动期注册(`src/data/initialize.ts` 的 `awake` 中),四方向与八方向各注册了数字 key 与字符串 id。启动之后按约定只读使用:运行期再注册只会影响之后取用它的地方,不会替换已经构造好的对象里保存的处理器。 +内置处理器在启动期注册,四方向与八方向各注册了数字 key 与字符串 id。启动之后按约定只读使用:运行期再注册只会影响之后取用它的地方,不会替换已经构造好的对象里保存的处理器。 同一个角色的图块数字映射由 `state.roleFace` 负责,见下文「朝向图块映射」。 @@ -91,7 +99,7 @@ function targetOf( `move` 适合一次算多格,例如 `handler.move(FaceDirection.Left, 3)` 得到 `{ x: -3, y: 0 }`;传负数就是反方向。`opposite` 与 `next` 都在本组内求解:先 `degrade`,再查本组的映射表,所以传入本组不认的方向时,结果取决于 `degrade` 的归并。`next` 的第二个参数是 `anticlockwise`,省略时为 `false`,即顺时针。 -`dirOf` 走的是相反方向:给你一个偏移量,问它是哪个方向。它不接收方向,也不会先 `degrade`;偏移量与表中某一项完全相等时才命中,多个方向共用同一偏移时只返回其中一个,命中顺序就是表的条目顺序。内置 `Dir4FaceHandler.dirOf` 实际遍历的是八方向偏移表,所以 `dir4.dirOf(1, 1)` 会返回 `RightDown`,而不是 `Unknown`。这是内置实现的特例,不要照抄到自定义处理器里,也不要拿它做四方向判定;需要严格四方向结果时,把返回值再用 `degrade` 折一次。 +`dirOf` 走的是相反方向:给你一个偏移量,问它是哪个方向。它不接收方向,也不会先 `degrade`;偏移量与表中某一项完全相等时才命中,多个方向共用同一偏移时只返回其中一个。不要用它反推「唯一方向」,也不要用它校验位移是否合法。 `mapDirection` 与 `mapMovement` 用于遍历,两者的结果都包含 `Unknown`,`mapMovement` 里 `Unknown` 对应的偏移是 `{ x: 0, y: 0 }`。自己按 `mapMovement` 扩展邻域时,要跳过零偏移条目,否则对象会原地走一步;寻路的构图循环就是这样处理的。 @@ -103,7 +111,7 @@ function targetOf( 寻路复用同一套方向规则。构建有向图时,它按 `mapMovement()` 的条目向相邻格扩展,所以处理器直接决定「能往哪几个方向走」:四方向处理器只扩展上下左右,八方向处理器会扩展到斜向。注入方式是 `finder.useFaceHandler(handler)`;没有注入时构图会失败(找不到图层、谓词或处理器都会失败),详见[自动寻路](./ii/pathfinding.md)。 -因为「方向即偏移」被多处共用,改处理器的行为会同时影响移动与寻路:把四方向处理器的斜向归并改成折到上下,移动器的斜向输入和寻路的斜向分支会一起改变。只希望其中一处变化时,不要直接换掉共享的处理器,而是给那处单独注入一个处理器实例。 +因为「方向即偏移」被多处共用,改处理器的行为会同时影响移动与寻路:调整四方向处理器的斜向归并,移动器的斜向输入和寻路的斜向分支会一起改变。只希望其中一处变化时,不要直接换掉共享的处理器,而是给那处单独注入一个处理器实例。 ### 图块朝向 @@ -113,7 +121,7 @@ function targetOf( 2. 查不到时用 `Dir4` 处理器的 `degrade` 折一次,再查一次; 3. 仍查不到则保持原图块数字。 -第 2 步在数据端只返回折后查到的图块数字,没有把图块本身换掉。按方法注释的语义,降级路径用于「斜向折成四方向再取图」,但当前实现不会写回图块,所以使用降级路径时不要假定图块已经换掉。 +第 2 步把方向降级为四方向后再查一次图,查到就换图并返回新的图块数字。 ### 朝向图块映射 @@ -156,7 +164,7 @@ function bindFacingTiles(state: ICoreState): void { ## 两个内置处理器 -内置实现都在 `@user/data-common` 的 `faceManager.ts` 里。它们都实现 `IFaceHandler`,区别只在认哪些方向、以及斜向怎么归并: +内置的 `Dir8FaceHandler` 与 `Dir4FaceHandler` 都实现 `IFaceHandler`,区别只在认哪些方向、以及斜向怎么归并: | 处理器 | 支持方向 | `degrade` 行为 | 典型用途 | | ----------------- | ---------------- | -------------------------------------------------- | -------------------------------- | @@ -173,17 +181,15 @@ function bindFacingTiles(state: ICoreState): void { | `RightDown` | `Right` | | 其余非正方向 | `Unknown` | -这一点决定了四方向体系里的实际手感:斜向输入不会让对象斜着走,也不会变成上下位移,而是变成横向一步。四方向地图上如果希望斜向折到上下,就需要自己写一个处理器(见下一节)。 +这一点决定了四方向体系里的实际手感:斜向输入不会让对象斜着走,也不会变成上下位移,而是变成横向一步。需要别的归并策略,或方向体系根本不是方格(例如六边形布局)时,才需要自己写处理器(见下一节)。 两个处理器在 `Unknown` 上的行为一致:`movement` 与 `move` 返回零偏移,`opposite` 与 `next` 返回 `Unknown`,`mapDirection` 与 `mapMovement` 会把它列出来。查表失败时 `movement` 同样退回零偏移。 -**实现差异**:按接口注释的约定,无法合理降级的方向(包括 `Unknown`)应返回 `Unknown`;但当前 `Dir8FaceHandler.degrade` 把入参原样返回,所以传入非法值会得到一个非法方向。以注释为准,不要把这种返回值当作合法方向使用。 - -勇士与动态图块默认都走 `Dir8`:勇士的分组来自 `src/data/constant.ts` 的 `HERO_FACE`,由 `MainDataPlugin` 构造时传入;动态图块的分组来自 `data-base/src/shared.ts` 的 `DYNAMIC_MOVER_FACE`(同文件的 `HERO_MOVER_FACE` 也定义为 `Dir8`)。需要四方向时,改的是这些分组来源,而不是处理器本身的实现。 +勇士与动态图块默认都走 `Dir8`,分组分别来自 `HERO_FACE` 与 `DYNAMIC_MOVER_FACE`。需要四方向时,改的是分组来源。 ## 实现自己的朝向处理器 -这一节的使用概率很低。需要自定义的场合有两类:仍然走四方向,但希望斜向换一种归并;或者方向数不是 4 与 8,需要一套自己的方向规则。两者都通过实现 `IFaceHandler` 完成。 +需要自定义处理器的场合通常不是「换一种斜向归并」,而是**坐标系本身不是方格**。例如六边形地图有六个邻接方向,既不是四方向也不是八方向;这类布局需要一套自己的方向规则,通过实现 `IFaceHandler` 提供。 实现时把「降级表」「运动表」「旋转顺序」放在模块顶层做共享常量,类只负责按契约查表。处理器是共享对象,实例上不保存任何「当前对象」的状态。几条契约要逐条满足: @@ -192,52 +198,53 @@ function bindFacingTiles(state: ICoreState): void { - `mapMovement` 必须包含 `Unknown` 及其零偏移,调用方靠这张表把每个方向都映射到偏移。 - `dirOf` 匹配不到时返回 `Unknown`,不要抛错;只遍历本组自己的偏移表。 -下面这个模板仍是四方向,但把斜向折到上下,用来演示 `degrade` 的策略作用。`[需改]` 标出需要替换的部分: +下面以六边形布局为例:只认左右与四个斜向,上下不是邻接方向。偏移量按示例的行列坐标给出,换坐标系时替换成自己的增量。`[需改]` 标出需要按布局替换的部分: ```ts import { IFaceDescriptor, IFaceHandler } from '@motajs/common'; import { FaceDirection } from '@user/data-common'; -// [需改] 降级表:本组认哪些方向、其余方向怎么归并 -const DEGRADE: ReadonlyMap = new Map([ +// [需改] 降级表:六边形只认六个邻接方向,其余方向无法降级 +const HEX_DEGRADE: ReadonlyMap = new Map([ [FaceDirection.Left, FaceDirection.Left], - [FaceDirection.Up, FaceDirection.Up], [FaceDirection.Right, FaceDirection.Right], - [FaceDirection.Down, FaceDirection.Down], - // 斜向折到上下,这是与内置 Dir4FaceHandler 的唯一区别 - [FaceDirection.LeftUp, FaceDirection.Up], - [FaceDirection.RightUp, FaceDirection.Up], - [FaceDirection.LeftDown, FaceDirection.Down], - [FaceDirection.RightDown, FaceDirection.Down] + [FaceDirection.LeftUp, FaceDirection.LeftUp], + [FaceDirection.RightUp, FaceDirection.RightUp], + [FaceDirection.LeftDown, FaceDirection.LeftDown], + [FaceDirection.RightDown, FaceDirection.RightDown] ]); -// [需改] 运动表:本组每个方向的单步偏移,必须包含 Unknown 的零偏移 -const MOVEMENTS: ReadonlyMap = new Map([ +// [需改] 运动表:六个方向的单步偏移,必须包含 Unknown 的零偏移 +const HEX_MOVEMENTS: ReadonlyMap = new Map([ [FaceDirection.Unknown, { x: 0, y: 0 }], [FaceDirection.Left, { x: -1, y: 0 }], - [FaceDirection.Up, { x: 0, y: -1 }], [FaceDirection.Right, { x: 1, y: 0 }], - [FaceDirection.Down, { x: 0, y: 1 }] + [FaceDirection.LeftUp, { x: -1, y: -1 }], + [FaceDirection.RightUp, { x: 1, y: -1 }], + [FaceDirection.LeftDown, { x: -1, y: 1 }], + [FaceDirection.RightDown, { x: 1, y: 1 }] ]); // 顺时针顺序,不含 Unknown;逆时针与反方向都在这张表上取 -const CW: readonly FaceDirection[] = [ - FaceDirection.Up, +const HEX_CW: readonly FaceDirection[] = [ FaceDirection.Right, - FaceDirection.Down, - FaceDirection.Left + FaceDirection.RightDown, + FaceDirection.LeftDown, + FaceDirection.Left, + FaceDirection.LeftUp, + FaceDirection.RightUp ]; -/** 四方向处理器:斜向一律折到上下。 */ -export class VerticalDir4FaceHandler implements IFaceHandler { +/** 六边形布局的朝向处理器:只认六个邻接方向。 */ +export class HexFaceHandler implements IFaceHandler { degrade(dir: number): FaceDirection { - // 表中没有的方向(含 Unknown 与任意非法值)一律视为无法降级 - return DEGRADE.get(dir) ?? FaceDirection.Unknown; + // 不在六个方向里的输入(含 Up、Down、Unknown 与非法值)都无法降级 + return HEX_DEGRADE.get(dir as FaceDirection) ?? FaceDirection.Unknown; } movement(dir: number): IFaceDescriptor { // 先降级,再查表 - return MOVEMENTS.get(this.degrade(dir)) ?? { x: 0, y: 0 }; + return HEX_MOVEMENTS.get(this.degrade(dir)) ?? { x: 0, y: 0 }; } move(dir: number, count: number): IFaceDescriptor { @@ -247,55 +254,54 @@ export class VerticalDir4FaceHandler implements IFaceHandler { } opposite(dir: number): FaceDirection { - const i = CW.indexOf(this.degrade(dir)); - return i < 0 ? FaceDirection.Unknown : CW[(i + 2) % CW.length]; + // 六个方向里相隔三个位置的方向就是反方向 + const i = HEX_CW.indexOf(this.degrade(dir)); + return i < 0 ? FaceDirection.Unknown : HEX_CW[(i + 3) % HEX_CW.length]; } next(dir: number, anticlockwise: boolean = false): FaceDirection { - const i = CW.indexOf(this.degrade(dir)); + const i = HEX_CW.indexOf(this.degrade(dir)); if (i < 0) return FaceDirection.Unknown; // 顺时针 +1,逆时针 -1,转满一圈回到起点 const step = anticlockwise ? -1 : 1; - return CW[(i + step + CW.length) % CW.length]; + return HEX_CW[(i + step + HEX_CW.length) % HEX_CW.length]; } dirOf(dx: number, dy: number): FaceDirection { - // 只认本组的单步偏移;斜向偏移没有对应方向 - for (const [dir, { x, y }] of MOVEMENTS) { + // 只认本组的单步偏移;其它偏移没有对应方向 + for (const [dir, { x, y }] of HEX_MOVEMENTS) { if (x === dx && y === dy) return dir; } return FaceDirection.Unknown; } mapDirection(): Iterable { - return MOVEMENTS.keys(); + return HEX_MOVEMENTS.keys(); } mapMovement(): Iterable<[FaceDirection, IFaceDescriptor]> { - return MOVEMENTS.entries(); + return HEX_MOVEMENTS.entries(); } } ``` -注册时选一个未被内置占用的分组键,数字与字符串 id 都可以。数字键没有命名空间保护,将来内置新增分组时可能撞车,后注册的会覆盖先注册的;自定义分组更推荐字符串 id: +注册时选一个未被内置占用的分组键。数字键没有命名空间保护,将来内置新增分组时可能撞车;自定义分组更推荐字符串 id: ```ts import { FaceDirection } from '@user/data-common'; import { ICoreState } from '@user/data-state'; -import { VerticalDir4FaceHandler } from './vertical-face-handler'; +import { HexFaceHandler } from './hex-face-handler'; -function registerVerticalFace(state: ICoreState): void { - state.faceManager.registerById( - 'vertical-dir4', - new VerticalDir4FaceHandler() - ); +function registerHexFace(state: ICoreState): void { + state.faceManager.registerById('hex', new HexFaceHandler()); - const handler = state.faceManager.getById('vertical-dir4'); + const handler = state.faceManager.getById('hex'); if (!handler) return; - // 斜向现在折到上下:LeftUp 的降级结果是 Up - console.log(handler.movement(FaceDirection.LeftUp)); // { x: 0, y: -1 } + // 六个邻接方向能换算偏移;上下会被降级为 Unknown + console.log(handler.movement(FaceDirection.RightUp)); // { x: 1, y: -1 } + console.log(handler.degrade(FaceDirection.Up)); // Unknown } ``` @@ -303,9 +309,9 @@ function registerVerticalFace(state: ICoreState): void { ## 边界与注意 -- 四方向的斜向折左右:`Dir4FaceHandler.degrade` 把 `LeftUp` 与 `LeftDown` 折到 `Left`,`RightUp` 与 `RightDown` 折到 `Right`,不是折到上下。需要折上下时自建处理器。 +- 四方向的斜向折左右:`Dir4FaceHandler.degrade` 把 `LeftUp` 与 `LeftDown` 折到 `Left`,`RightUp` 与 `RightDown` 折到 `Right`,不是折到上下。需要别的归并策略时自建处理器。 - get 与 getById 可能返回 null:自建分组、或在启动期之外取用处理器时,先判空再调用;直接对 `null` 调方法会在运行期报错。 -- dirOf 可能有多解:偏移量相同、方向不同的情况只返回其中一个,不要用它反推「唯一方向」,也不要用它校验位移是否合法。内置 `Dir4FaceHandler` 还会返回八方向结果(`dirOf(1, 1)` 为 `RightDown`),这是实现特例,不要当作四方向结果使用。 +- dirOf 可能有多解:偏移量相同、方向不同的情况只返回其中一个,不要用它反推「唯一方向」,也不要用它校验位移是否合法。 - Unknown 的偏移是零:`movement` 与 `move` 对 `Unknown` 返回 `{ x: 0, y: 0 }`,`opposite` 与 `next` 返回 `Unknown`;把它当普通方向做位置运算,对象会原地不动。 - bind 前必须 malloc:主图块没有分配朝向时,`bind` 会记错误并放弃;绑定的朝向与主朝向相同也会被拒绝。 - 重复 malloc 会覆盖:同一个图块数字再次 `malloc` 会重建该组的映射,之前 `bind` 进去的其它朝向不会被保留;已经 `bind` 过的子图块仍持有旧映射表,`getFaceOf` 可能查到旧结果。重建整组后要重新 `bind` 所有子图块。 @@ -345,6 +351,6 @@ function verifyFaceHandler(state: ICoreState): void { 再验证朝向图块的登记与读取:先 `malloc` 主图块,再 `bind` 另一朝向,然后核对 `getFaceOf`、`getFaceDirection` 与 `getMainFace` 的返回值;把 `bind` 挪到 `malloc` 之前,应当看到错误日志且映射没有建立。 -最后验证自定义处理器:注册后取回,确认 `degrade(LeftUp)` 变成 `Up`、`movement(LeftUp)` 变成 `{ x: 0, y: -1 }`;再把它交给一个新建的移动器或 `finder.useFaceHandler`,重算一次目标格与一条路径,确认斜向输入按新策略归并。已构造好的移动器不会发生变化,若结果没变,先检查是否仍在用旧的处理器。 +最后验证自定义处理器:注册后取回,确认六个邻接方向能换算偏移、`movement(RightUp)` 为 `{ x: 1, y: -1 }`、`degrade(Up)` 为 `Unknown`;再把它交给一个新建的移动器或 `finder.useFaceHandler`,重算一次目标格与一条路径,确认只在六个方向上扩展。已构造好的移动器不会发生变化,若结果没变,先检查是否仍在用旧的处理器。 返回[高级 I 导览](./index.md)。 diff --git a/docs/advanced/mover.md b/docs/advanced/mover.md index ef4ffb6..b78b65b 100644 --- a/docs/advanced/mover.md +++ b/docs/advanced/mover.md @@ -6,11 +6,19 @@ -## 移动器能做什么 +## 移动器模型 -一次移动分两步:**排队**与**执行**。`step`、`face`、`tp` 这些方法只往队列里放步骤,不会立刻移动对象;`start()` 才真正开始执行,并返回本次执行的句柄。用句柄的 `onEnd` 等这一整次移动结束。 +移动器(mover)把一个**可移动对象**和一份**移动计划**绑在一起,按计划逐步推动对象。 -移动相关的接口都在 `@user/data-common` 的 `mover.ts` 里,三个角色分工明确: +- **可移动对象**实现 `IObjectMovable`,只提供坐标与朝向:`x`、`y`、`setPos`、`getCurrentFaceDirection`。勇士的位置对象与动态图块都是这种对象。 +- **移动计划**是一串步骤。一个步骤只描述一件小事:朝某个方向走、只转向、瞬移、跳跃、改速度或改动画方向。`step`、`face`、`tp` 这些规划方法只往队列里追加步骤,不会立刻移动对象。 +- **执行**由 `start()` 发起:它取出待执行队列并逐步执行,每一步算出落点,再把新坐标写回对象。`start()` 返回本次执行的控制器 `IMoverController`;用 `onEnd` 等待整次结束,用 `push` / `insert` 在执行中追加步骤,用 `stop` 提前收尾。 + +整次执行里有两个方向:**面朝朝向**(`faceDirection`,对象脸朝哪)与**移动朝向**(`moveDirection`,这一步往哪走)。多数步骤让两者一致;侧身走、后退时两者会分开。二者的具体差别见后文「读取与设置状态」。 + +移动器只负责「这一步落到哪」,不判断目标格能不能进入、撞到什么。通行规则、进入 / 离开 / 撞击事件与地图边界由调用方的规则层决定;勇士的规则见后文「勇士的顶层实现」。 + +移动相关的接口由 `@user/data-common` 提供,三个角色分工明确: | 接口 | 角色 | 关键成员 | | ------------------ | -------------- | --------------------------------------------- | @@ -18,7 +26,7 @@ | `IObjectMover` | 移动器 | 链式规划、`start()`、状态、钩子 | | `IMoverController` | 一次执行的句柄 | `done`、`onEnd`、`push`、`insert`、`stop` | -多数场景只需要碰后两者:拿现成的移动器排队、执行、等待。移动器只负责「这一步落到哪」,能不能走、撞到什么由调用方的规则决定;勇士的规则见后文「勇士的顶层实现」。 +多数场景只需要碰后两者:拿现成的移动器排队、执行、等待。 ## 拿到现成的移动器 @@ -249,7 +257,7 @@ const { ignoreTerrain, autoSave } = state.hero.location.mover.getConfig(); 面朝朝向(`faceDirection`)指对象「脸朝哪」,移动朝向(`moveDirection`)指「这一步往哪走」,两者是不同的概念,不要混用。`step` 会让两者一起更新;`stepFace` 与 `backward` 会让两者不同,例如向右走但脸朝上,或者朝下后退时移动朝向是上。`forward()` 沿的是面朝朝向,而不是移动朝向。 -`IObjectMover.forward` 的源码注释写的是「沿当前移动方向」,这里指的是**面朝朝向**:`forward` 读的是 `faceDirection`,不是 `moveDirection`。同理,`onSetMoveDir` 注释里的「设置朝向」指设置**移动朝向**。源码实现与这个理解一致,不要把它当成 bug 去改。 +`forward` 沿的是**面朝朝向**(`faceDirection`),不是移动朝向(`moveDirection`);`onSetMoveDir` 设置的是移动朝向,`onSetFaceDir` 设置的是面朝朝向。 `speed` 与 `animDir` 往队列里放的是步骤,真正写入 `currentSpeed`、`currAnimDir` 是在这一步执行时;执行后状态留在移动器上,`start()` 不会恢复默认值,所以会带到下一次移动。 @@ -259,7 +267,7 @@ const { ignoreTerrain, autoSave } = state.hero.location.mover.getConfig(); ## 实现自己的移动器 -这一节的使用概率很低:内置的 `HeroMover` 与 `DynamicTileMover` 已经覆盖勇士与动态图块。只有当你有一个新的可移动对象、并且它的落点规则与这两者都不同时,才需要自己写一个。 +内置的 `HeroMover` 与 `DynamicTileMover` 已经覆盖勇士与动态图块。只有当你有一个新的可移动对象、并且它的落点规则与这两者都不同时,才需要自己写一个。 `ObjectMover` 是移动器基类,负责队列、控制器、状态更新、钩子分发与坐标回写。子类只需要实现五个 `protected` 方法: @@ -375,7 +383,7 @@ export class SimpleMover extends ObjectMover { `handler` 里带着 `currLoc`、`nextLoc`、`direction`、`floorId`、`face` 与 `state`,够你在判定里读位置、朝向与状态。`predicate()` 返回的谓词有两个方法:`canPass(handler)` 为假时本步不移动,走「无法进入」分支;`canPass` 为真且 `shouldHit(handler)` 为真时本步不移动,走「撞击」分支;两者都为真才正常移动。 -内置的 `DefaultHeroMoveTopImpl`(`src/data/hero/moverImpl.ts`)是默认实现,它用通行谓词与事件系统完成边界判断、进出与撞击触发器。想保留默认行为、只改其中一环时,包一层并转发给默认实现最省事: +内置的 `DefaultHeroMoveTopImpl` 是默认实现,它用通行谓词与事件系统完成边界判断、进出与撞击触发器。想保留默认行为、只改其中一环时,包一层并转发给默认实现最省事: ```ts // src/data/hero/moverTop.ts @@ -434,7 +442,7 @@ export class NoLeftMoveTopImpl implements IHeroMoveTopImpl { } ``` -装配时把它换进勇士移动器(`src/data/initialize.ts` 的 `awake` 中,默认实现就是这么装的): +装配时把它换进勇士移动器: ```ts // 依赖加载后才知道配置时,把这段放进 afterLoad