mirror of
https://github.com/motajs/template.git
synced 2026-11-01 06:04:30 +08:00
feat: 动态图层
This commit is contained in:
1 parent
4c48b63e7e
commit
fbdd609001
19 files changed
+2063
-15
No files matched your search
@@ -0,0 +1,595 @@
|
||||
# 动态图块移动系统
|
||||
|
||||
> 本文档为 [动态图块系统](./dynamic-tile.md) 的移动子系统设计文档,
|
||||
> 重点描述双方向模型、`IDynamicMover` 计划式移动接口及执行流程。
|
||||
> 本文档中新增的类型与接口同样需要添加至 `@user/data-base/src/map/types.ts`。
|
||||
|
||||
---
|
||||
|
||||
# 背景与动机
|
||||
|
||||
`IDynamicLayer.moveDynamicStep` / `moveDynamicWith` 是命令式简单接口,适用于
|
||||
一次性的简单移动,但对以下场景力不从心:
|
||||
|
||||
- **后退移动**:角色面朝上、向下移动时,朝向不变但位移反向,
|
||||
单一 `FaceDirection` 参数无法同时描述朝向和移动方向;
|
||||
- **连续路径中途动态追加步骤**:命令式接口执行后无法继续追加;
|
||||
- **移动速度切换**:路径中途需要变速,命令式接口不支持;
|
||||
- **复杂动画编排**:需要按序执行多段计划时,纯 `Promise` 链表达不够直观。
|
||||
|
||||
因此引入以图块为粒度绑定的 `IDynamicMover`,采用**先建计划、再执行**的设计理念,
|
||||
参考 `@motajs/animate/types.ts` 的链式计划构建模式,
|
||||
以及 `@user/data-state/legacy/move.ts` 中 `ObjectMoverBase` 的双方向与队列设计。
|
||||
|
||||
---
|
||||
|
||||
# 实现思路
|
||||
|
||||
## 1. 双方向模型
|
||||
|
||||
每个动态图块维护两个方向字段:
|
||||
|
||||
| 字段 | 含义 | 影响 |
|
||||
| --------------- | ---------------------------- | --------------------------------------- |
|
||||
| `faceDirection` | **朝向**,视觉上「面朝哪里」 | 通过 `IRoleFaceBinder` 决定渲染图块数字 |
|
||||
| `moveDirection` | **移动方向**,「向哪边走」 | 结合 `getFaceMovement` 计算位移 |
|
||||
|
||||
两者分离后可以表达:
|
||||
|
||||
- **后退**:`moveDirection = Up`,执行 `backward()` 步,
|
||||
`moveDirection = Down`,`faceDirection = Down`,位移向下,朝向同步翻转。
|
||||
- **横向移动不转身**:使用 `stepFace(move, face)` 单独指定朝向,
|
||||
`moveDirection` 与 `faceDirection` 独立设置。
|
||||
|
||||
绝对方向步骤(传入 `FaceDirection`)默认将两者同时更新;
|
||||
`forward()` 沿当前 `moveDirection` 移动并对齐 `faceDirection`;
|
||||
`backward()` 将 `moveDirection` 取反并同步 `faceDirection`。
|
||||
|
||||
## 2. 方向步语义
|
||||
|
||||
绝对方向步(`IDynamicMoveStepDir` / `IDynamicMoveStepDirFace`)直接传入 `FaceDirection`:
|
||||
|
||||
- `step(dir, count?: number)`:追加 `count`(默认 1)个绝对方向步,`moveDirection` 和 `faceDirection` 均更新为 `dir`;
|
||||
- `stepFace(move, face, count?: number)`:追加 `count`(默认 1)个绝对方向步,`moveDirection` 更新为 `move`,
|
||||
`faceDirection` 更新为 `face`——用于移动方向与朝向不一致的场景(如斜切移动保持正向)。
|
||||
|
||||
相对方向步(`IDynamicMoveStepSpecial`)通过独立方法构建:
|
||||
|
||||
- `forward(count?)`:沿当前 `tile.moveDirection` 移动,`moveDirection` 不变,
|
||||
`faceDirection` 对齐为 `moveDirection`;
|
||||
- `backward(count?)`:沿 `opposite(tile.moveDirection)` 移动,`moveDirection` 取反,
|
||||
`faceDirection` 同步更新为新的 `moveDirection`,自动使用逆向动画。
|
||||
|
||||
## 3. DynamicMoveStep 类型
|
||||
|
||||
步骤类型通过 `const enum` 区分,避免字符串比较。
|
||||
前进/后退方向用独立的 `DynamicSpecialStep` 枚举表示;
|
||||
动画播放方向用独立的 `DynamicAnimDirection` 枚举表示,两者互不依赖:
|
||||
|
||||
```ts
|
||||
const enum DynamicMoveStepType {
|
||||
/** 绝对方向步,同步更新 moveDirection 和 faceDirection */
|
||||
Dir,
|
||||
/** 绝对方向步,单独指定 faceDirection */
|
||||
DirFace,
|
||||
/** 速度步 */
|
||||
Speed,
|
||||
/** 纯转向步,不产生位移 */
|
||||
Face,
|
||||
/** 特殊步(前进或后退) */
|
||||
Special,
|
||||
/** 动画方向步,不影响 moveDirection/faceDirection */
|
||||
AnimDir
|
||||
}
|
||||
|
||||
/** 特殊步方向:前进或后退 */
|
||||
const enum DynamicSpecialStep {
|
||||
Forward,
|
||||
Backward
|
||||
}
|
||||
|
||||
/** 动画播放方向:正向或逆向 */
|
||||
const enum DynamicAnimDirection {
|
||||
Forward,
|
||||
Backward
|
||||
}
|
||||
|
||||
/** 绝对方向步:move 方向既是移动方向也是朝向 */
|
||||
interface IDynamicMoveStepDir {
|
||||
type: DynamicMoveStepType.Dir;
|
||||
move: FaceDirection;
|
||||
}
|
||||
|
||||
/** 绝对方向步:move 是移动方向,face 是显式指定的朝向 */
|
||||
interface IDynamicMoveStepDirFace {
|
||||
type: DynamicMoveStepType.DirFace;
|
||||
move: FaceDirection;
|
||||
face: FaceDirection;
|
||||
}
|
||||
|
||||
/** 速度步:修改后续移动的每格耗时(单位 ms,越小越快) */
|
||||
interface IDynamicMoveStepSpeed {
|
||||
type: DynamicMoveStepType.Speed;
|
||||
value: number;
|
||||
}
|
||||
|
||||
/** 转向步:仅更新 faceDirection,不产生位移,等待渲染动画完成 */
|
||||
interface IDynamicMoveStepFace {
|
||||
type: DynamicMoveStepType.Face;
|
||||
value: FaceDirection;
|
||||
}
|
||||
|
||||
/** 特殊步:前进(Forward)或后退(Backward)。`forward(n)`/`backward(n)` 构建时 n>1 会 push n 个相同步骤 */
|
||||
interface IDynamicMoveStepSpecial {
|
||||
type: DynamicMoveStepType.Special;
|
||||
direction: DynamicSpecialStep;
|
||||
}
|
||||
|
||||
/** 动画方向步:指定后续步骤动画正向播放(Forward)还是逆向播放(Backward) */
|
||||
interface IDynamicMoveAnimDir {
|
||||
type: DynamicMoveStepType.AnimDir;
|
||||
dir: DynamicAnimDirection;
|
||||
}
|
||||
|
||||
type DynamicMoveStep =
|
||||
| IDynamicMoveStepDir
|
||||
| IDynamicMoveStepDirFace
|
||||
| IDynamicMoveStepSpeed
|
||||
| IDynamicMoveStepFace
|
||||
| IDynamicMoveStepSpecial
|
||||
| IDynamicMoveAnimDir;
|
||||
```
|
||||
|
||||
## 4. IMoverController 接口
|
||||
|
||||
`IDynamicMover.start()` 返回 `IMoverController`,用于控制进行中的移动。
|
||||
|
||||
```ts
|
||||
interface IMoverController {
|
||||
/** 本次移动是否已全部完成 */
|
||||
readonly done: boolean;
|
||||
/** 当本次移动全部步骤完成时兑现 */
|
||||
readonly onEnd: Promise<void>;
|
||||
/**
|
||||
* 向当前队列末尾追加步骤(仅在移动进行中有效,完成后追加无效)
|
||||
*/
|
||||
push(...steps: DynamicMoveStep[]): void;
|
||||
/**
|
||||
* 停止移动,等待当前步骤完成后停止,返回的 Promise 在停止后兑现
|
||||
*/
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## 5. IDynamicMover 接口
|
||||
|
||||
每个 `IDynamicTile` 持有一个**绑定**的 `IDynamicMover` 实例(`tile.mover`),
|
||||
无需单独创建或管理生命周期。
|
||||
|
||||
### 5.1 状态读取
|
||||
|
||||
```ts
|
||||
// 以下状态属性来自 IObjectMover<IDynamicTile>,IDynamicMover 全部继承
|
||||
interface IObjectMover<T extends IObjectMovable> {
|
||||
/** 是否正在移动 */
|
||||
readonly moving: boolean;
|
||||
/** 当前朝向(与 tile.faceDirection 同步) */
|
||||
readonly faceDirection: FaceDirection;
|
||||
/** 当前移动方向(与 tile.moveDirection 同步) */
|
||||
readonly moveDirection: FaceDirection;
|
||||
/** 所属的可移动对象 */
|
||||
readonly tile: T;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 计划构建(链式)
|
||||
|
||||
所有构建方法均返回 `this`,支持链式调用。
|
||||
计划在调用 `start()` 前不执行,仅追加到内部队列。
|
||||
|
||||
```ts
|
||||
interface IDynamicMover {
|
||||
// ...
|
||||
/** 追加 count(默认 1)个绝对方向步,同步更新 moveDirection 和 faceDirection */
|
||||
step(dir: FaceDirection, count?: number): this;
|
||||
/** 追加 count(默认 1)个绝对方向步,单独指定 faceDirection(用于移动方向与朝向不一致的场景) */
|
||||
stepFace(move: FaceDirection, face: FaceDirection, count?: number): this;
|
||||
/** 追加前进步,沿当前 moveDirection 移动,faceDirection 对齐为 moveDirection */
|
||||
forward(count?: number): this;
|
||||
/** 追加后退步,moveDirection 取反,faceDirection 同步更新,自动使用逆向动画 */
|
||||
backward(count?: number): this;
|
||||
/** 追加一个速度步,改变后续移动的每格耗时(ms) */
|
||||
speed(value: number): this;
|
||||
/** 追加一个纯转向步,仅更新 faceDirection,不产生位移 */
|
||||
face(dir: FaceDirection): this;
|
||||
/** 追加一个动画方向步,控制后续步骤的动画播放方向(Forward 正向 / Backward 逆向) */
|
||||
animDir(dir: DynamicAnimDirection): this;
|
||||
/** 清空尚未执行的计划队列 */
|
||||
clear(): this;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**用法示例**:
|
||||
|
||||
```ts
|
||||
// 先向下走 2 步,切换为半速,再向右走 1 步
|
||||
tile.mover
|
||||
.step(FaceDirection.Down, 2)
|
||||
.speed(200)
|
||||
.step(FaceDirection.Right)
|
||||
.start();
|
||||
|
||||
// 后退 3 步(moveDirection/faceDirection 翻转,逆向动画)
|
||||
tile.mover.backward(3).start();
|
||||
|
||||
// 向右移动但保持朝下(如横向滑步)
|
||||
tile.mover.stepFace(FaceDirection.Right, FaceDirection.Down, 2).start();
|
||||
```
|
||||
|
||||
### 5.3 执行控制
|
||||
|
||||
```ts
|
||||
interface IDynamicMover {
|
||||
// ...
|
||||
/**
|
||||
* 开始执行计划队列,返回控制对象。
|
||||
* 若已在移动则返回 null(不中断当前移动)。
|
||||
*/
|
||||
start(): IMoverController | null;
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 移动执行流程
|
||||
|
||||
每次 `start()` 启动后,执行器按以下总流程处理队列:
|
||||
|
||||
触发 `onMoveStart` → 循环每步:[
|
||||
`abstract onStepStart(step, tile, mover)` 执行并返回 `code: number`(子类计算本步场景)→
|
||||
触发 `IObjectMoverHooks.onStepStart(code, step, tile, mover)` 钩子(渲染预处理,此时 `tile.x/y` 为**移动前坐标**)→
|
||||
等待所有 Promise →
|
||||
`abstract onStepEnd(code, step, tile, mover)` 执行**逻辑效果**(子类根据 code 决定如何执行)→
|
||||
触发 `IObjectMoverHooks.onStepEnd(code, step, tile, mover)` 钩子(此时 `tile.x/y` 为**移动后坐标**)
|
||||
] → 触发 `onMoveEnd`。
|
||||
|
||||
### 6.1 绝对方向步(`DynamicMoveStepType.Dir`)
|
||||
|
||||
**onStepStart 阶段**(`tile.x/y` 为原始坐标):
|
||||
|
||||
1. `abstract onStepStart(step, tile, mover)` 执行,子类计算本步 `code`(正常移动、碰墙等)并返回 `Promise<number>`。
|
||||
2. 父类拿到 `code`,触发 `IObjectMoverHooks.onStepStart(code, step, tile, mover)` 钩子——渲染端在此阶段播放移动动画,
|
||||
可由 `step.move` 与当前 `tile.x/y` 推算目标位置。
|
||||
3. `await Promise.all(promises)`——等待全部动画完成。
|
||||
|
||||
**onStepEnd 阶段**(逻辑效果):
|
||||
|
||||
1. `abstract onStepEnd(code, step, tile, mover)` 执行——子类依据 `code` 决定本步实际效果:
|
||||
更新 `tile.moveDirection = step.move`。
|
||||
2. 计算 `{dx, dy} = getFaceMovement(step.move)`,**立即**更新 `tile.x += dx`、`tile.y += dy`。
|
||||
3. 同步更新 `posMap`。
|
||||
4. 执行转向逻辑(见 [dynamic-tile.md §8](./dynamic-tile.md#8-转向逻辑)),
|
||||
更新 `tile.faceDirection` 与 `tile.num`。
|
||||
5. 子类返回后,父类触发 `IObjectMoverHooks.onStepEnd(code, step, tile, mover)` 钩子,此时 `tile.x/y` 已为新坐标。
|
||||
|
||||
### 6.1.1 含显式朝向的绝对方向步(`DynamicMoveStepType.DirFace`)
|
||||
|
||||
与 `Dir` 步骤相同,但步骤 5 改为直接将 `tile.faceDirection` 设为 `step.face`,
|
||||
并通过 `getFaceOf(tile.num, step.face)` 更新 `tile.num`,不经过 `degradeFace` 降级。
|
||||
|
||||
### 6.2 速度步(`DynamicMoveStepType.Speed`)
|
||||
|
||||
更新内部速度状态,立即生效,无位移,无需等待。
|
||||
|
||||
### 6.3 纯转向步(`DynamicMoveStepType.Face`)
|
||||
|
||||
经由 `onStepStart` 触发钩子(`tile.x/y` 不变,`step.type === DynamicMoveStepType.Face` 供渲染端区分);
|
||||
等待渲染端动画完成后,`onStepEnd` 阶段执行转向逻辑,更新 `faceDirection` 与 `tile.num`,
|
||||
触发 `onStepEnd` 钩子。
|
||||
|
||||
### 6.4 特殊步(`DynamicMoveStepType.Special`)
|
||||
|
||||
**语义说明**:`forward`/`backward` 以**上一步的移动方向**(即当前 `tile.moveDirection`)为基准;
|
||||
若无上一步,则回退到当前 `faceDirection`;若图块无朝向绑定,则抛出错误。
|
||||
|
||||
**Forward**(`DynamicSpecialStep.Forward`):
|
||||
|
||||
1. `actualDir = tile.moveDirection`(不改变 moveDirection);
|
||||
2. 执行与 `Dir` 步骤相同的位移流程;
|
||||
3. 步骤 2:跳过(`moveDirection` 保持不变);
|
||||
4. 步骤 5:执行转向逻辑,以 `actualDir` 更新 `tile.faceDirection` 与 `tile.num`。
|
||||
|
||||
**Backward**(`DynamicSpecialStep.Backward`):
|
||||
|
||||
1. `actualDir = opposite(tile.moveDirection)`;
|
||||
2. 在执行前临时将逆向动画标志设为开启(步骤完成后恢复);
|
||||
3. 执行与 `Dir` 步骤相同的位移流程;
|
||||
4. 步骤 2:`tile.moveDirection = actualDir`;
|
||||
5. 步骤 5:执行转向逻辑,以 `actualDir` 更新 `tile.faceDirection` 与 `tile.num`。
|
||||
|
||||
### 6.5 动画方向步(`DynamicMoveStepType.AnimDir`)
|
||||
|
||||
指定后续步骤动画是**正向播放**(`DynamicAnimDirection.Forward`)还是**逆向播放**(`DynamicAnimDirection.Backward`),
|
||||
与图块朝向无关(朝向始终由 `faceDirection` 决定)。
|
||||
修改 mover 内部的 `currentAnimDir` 状态。
|
||||
渲染端在 `onStepStart` 收到 `AnimDir` 步骤时读取并更新本地动画方向状态,后续步骤的 `onStepStart` 触发时依据此状态决定播放方向。
|
||||
`onStepEnd` 阶段无逻辑操作,立即完成,不影响坐标与方向。
|
||||
|
||||
## 7. onStepStart / onStepEnd 钩子返回值
|
||||
|
||||
`IObjectMoverHooks.onStepStart` 与 `onStepEnd` 均为可选钩子(`?`),返回类型为 `Promise<void>`,
|
||||
执行器始终通过 `Promise.all` 等待所有订阅方完成后再继续。
|
||||
|
||||
关键时序:`onStepStart` 所有 Promise 兑现后,才执行逻辑效果(abstract onStepEnd);
|
||||
`onStepEnd` 全部兑现后,才进入下一步。
|
||||
多个订阅方通过 `Promise.all` 并行等待,与 `forEachHook` 返回所有值的设计天然契合——
|
||||
`const promises = forEachHook(hook => hook.onStepStart?.(code, step, tile, mover));`
|
||||
`await Promise.all(promises);`
|
||||
|
||||
## 8. 与 IDynamicLayer / IDynamicTile 接口的关系
|
||||
|
||||
`IDynamicLayer` 和 `IDynamicTile` **仅保留 `step(dir)` 单步便捷接口**
|
||||
(等价于 `tile.mover.step(dir).start()`),不再提供 `moveDynamicWith` / `moveWith` 等批量接口。
|
||||
复杂移动能力(`forward`/`backward`/`animDir` 等)通过 `tile.mover` 访问。
|
||||
|
||||
**渲染端订阅移动事件的流程**:
|
||||
|
||||
1. 订阅 `IDynamicLayer.onCreateDynamicTile`;
|
||||
2. 创建图块时获取 `tile.mover` 引用,直接向其添加钩子;
|
||||
3. 移动事件从 `tile.mover` 触发,无需经过 `IDynamicLayer` 转发。
|
||||
|
||||
为此,`IDynamicMover` 扩展为可订阅对象,
|
||||
实现 `IHookable<IObjectMoverHooks<IDynamicTile>>`(见接口定义汇总)。
|
||||
|
||||
---
|
||||
|
||||
# 涉及文件
|
||||
|
||||
## 需要修改的文件
|
||||
|
||||
### `@user/data-base/src/map/types.ts`
|
||||
|
||||
修改现有接口(移动相关新增类型已移至 `move/types.ts`):
|
||||
|
||||
- [ ] `IDynamicTile` 新增 `readonly mover: IDynamicMover`
|
||||
- [ ] `IDynamicTile` 已有 `readonly faceDirection: FaceDirection` 与
|
||||
`readonly moveDirection: FaceDirection`(已在主文档确认)
|
||||
- [ ] `IObjectMovable` 接口(`x`、`y`、`moveDirection`、`faceDirection` 只读属性 +
|
||||
`setPos`/`setMoveDirection`/`setFaceDirection`,定义在 `dynamic-tile.md`)
|
||||
- [ ] `IDynamicLayer` / `IDynamicTile` 仅保留 `step(dir)` 便捷接口,
|
||||
移除 `moveDynamicWith` / `moveDynamicStep` / `moveWith` / `moveStep`
|
||||
(主文档接口汇总需同步更新)
|
||||
|
||||
#### `@user/data-base/src/map/types.ts`
|
||||
|
||||
- [ ] `DynamicMoveStepType` `const enum`:
|
||||
含 `Dir`/`DirFace`/`Speed`/`Face`/`Special`/`AnimDir` 六个成员
|
||||
- [ ] `DynamicSpecialStep` `const enum`:含 `Forward`/`Backward` 两个成员
|
||||
- [ ] `DynamicAnimDirection` `const enum`:含 `Forward`/`Backward` 两个成员
|
||||
- [ ] `IDynamicMoveStepDir` 接口(`type: DynamicMoveStepType.Dir`,`move: FaceDirection`)
|
||||
- [ ] `IDynamicMoveStepDirFace` 接口(`type: DynamicMoveStepType.DirFace`,`move`/`face: FaceDirection`)
|
||||
- [ ] `IDynamicMoveStepSpeed` 接口(`type: DynamicMoveStepType.Speed`)
|
||||
- [ ] `IDynamicMoveStepFace` 接口(`type: DynamicMoveStepType.Face`)
|
||||
- [ ] `IDynamicMoveStepSpecial` 接口(`type: Special`,`direction: DynamicSpecialStep`;无 `count` 字段,`forward(n)`/`backward(n)` 改为 push n 个步骤对象)
|
||||
- [ ] `IDynamicMoveAnimDir` 接口(`type: DynamicMoveStepType.AnimDir`,`dir: DynamicAnimDirection`)
|
||||
- [ ] `DynamicMoveStep` 联合类型(含以上六种步骤)
|
||||
- [ ] `IMoverController` 接口
|
||||
- [ ] `IObjectMoverHooks<T extends IObjectMovable>` 接口(含 `onMoveStart`/`onMoveEnd`/`onStepStart`/`onStepEnd`;
|
||||
`onStepStart`/`onStepEnd` 参数顺序为 `(code, step, tile, mover)`,按使用频率排列)
|
||||
- [ ] `IObjectMover<T extends IObjectMovable>` 接口(含 `faceDirection`/`moveDirection` 只读属性及全部计划构建方法;`step`/`stepFace` 含 `count?` 参数)
|
||||
- [ ] `IDynamicMover extends IObjectMover<IDynamicTile>`(无额外成员,继承基类全部能力;`IDynamicMoverHooks` 已移除,直接使用 `IObjectMoverHooks<IDynamicTile>`)
|
||||
|
||||
#### `@user/data-base/src/map/mover.ts`
|
||||
|
||||
- [ ] `abstract class ObjectMover<T extends IObjectMovable>` 抽象基类:
|
||||
- 实现 `IObjectMover<T>`,在此提供全部**计划构建方法**(`step`/`stepFace`/`forward`/
|
||||
`backward`/`speed`/`face`/`animDir`/`clear`/`start`)及队列执行调度逻辑;
|
||||
- 含四个**抽象方法**,子类必须实现,组成移动核心控制流:
|
||||
- `abstract onMoveStart(tile, mover): Promise<void>`: 整次移动开始时调用
|
||||
- `abstract onMoveEnd(tile, mover): Promise<void>`: 整次移动结束时调用
|
||||
- `abstract onStepStart(step, tile, mover): Promise<number>`: 单步逻辑执行前调用,
|
||||
子类计算并**返回** `code`;父类将 `code` 传给 `IObjectMoverHooks.onStepStart` 钩子后等待
|
||||
- `abstract onStepEnd(code, step, tile, mover): Promise<void>`: 单步钩子等待完成后调用,子类依据 `code` 执行本步实际逻辑效果
|
||||
- [ ] `class DynamicMover extends ObjectMover<IDynamicTile> implements IDynamicMover`:
|
||||
持有 `tile: IDynamicTile` 引用,维护 `moveQueue`、`moving`、
|
||||
`faceDirection`、`moveDirection` 状态;实现四个抽象方法(含 posMap 更新、转向逻辑等)
|
||||
- [ ] `DynamicTile` 类新增 `readonly mover: DynamicMover`,
|
||||
在构造时以 `this` 为参数创建
|
||||
|
||||
---
|
||||
|
||||
# 接口定义汇总
|
||||
|
||||
```ts
|
||||
const enum DynamicMoveStepType {
|
||||
Dir,
|
||||
DirFace,
|
||||
Speed,
|
||||
Face,
|
||||
Special,
|
||||
AnimDir
|
||||
}
|
||||
|
||||
const enum DynamicSpecialStep {
|
||||
Forward,
|
||||
Backward
|
||||
}
|
||||
|
||||
const enum DynamicAnimDirection {
|
||||
Forward,
|
||||
Backward
|
||||
}
|
||||
|
||||
interface IDynamicMoveStepDir {
|
||||
type: DynamicMoveStepType.Dir;
|
||||
move: FaceDirection;
|
||||
}
|
||||
|
||||
interface IDynamicMoveStepDirFace {
|
||||
type: DynamicMoveStepType.DirFace;
|
||||
move: FaceDirection;
|
||||
face: FaceDirection;
|
||||
}
|
||||
|
||||
interface IDynamicMoveStepSpeed {
|
||||
type: DynamicMoveStepType.Speed;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface IDynamicMoveStepFace {
|
||||
type: DynamicMoveStepType.Face;
|
||||
value: FaceDirection;
|
||||
}
|
||||
|
||||
/** forward(n)/backward(n) 构建时 n>1 会 push n 个相同步骤,步骤对象本身不携带 count */
|
||||
interface IDynamicMoveStepSpecial {
|
||||
type: DynamicMoveStepType.Special;
|
||||
direction: DynamicSpecialStep;
|
||||
}
|
||||
|
||||
interface IDynamicMoveAnimDir {
|
||||
type: DynamicMoveStepType.AnimDir;
|
||||
dir: DynamicAnimDirection;
|
||||
}
|
||||
|
||||
type DynamicMoveStep =
|
||||
| IDynamicMoveStepDir
|
||||
| IDynamicMoveStepDirFace
|
||||
| IDynamicMoveStepSpeed
|
||||
| IDynamicMoveStepFace
|
||||
| IDynamicMoveStepSpecial
|
||||
| IDynamicMoveAnimDir;
|
||||
|
||||
interface IMoverController {
|
||||
readonly done: boolean;
|
||||
readonly onEnd: Promise<void>;
|
||||
push(...steps: DynamicMoveStep[]): void;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
interface IObjectMoverHooks<T extends IObjectMovable> extends IHookBase {
|
||||
onMoveStart(mover: IObjectMover<T>, tile: T): Promise<void>;
|
||||
onMoveEnd(mover: IObjectMover<T>, tile: T): Promise<void>;
|
||||
/** 触发时 tile.x/y 为移动前坐标,适合渲染端在此播放动画 */
|
||||
onStepStart?(
|
||||
code: number,
|
||||
step: DynamicMoveStep,
|
||||
tile: T,
|
||||
mover: IObjectMover<T>
|
||||
): Promise<void>;
|
||||
/** 触发时 tile.x/y 已更新为移动后坐标 */
|
||||
onStepEnd?(
|
||||
code: number,
|
||||
step: DynamicMoveStep,
|
||||
tile: T,
|
||||
mover: IObjectMover<T>
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
interface IObjectMover<T extends IObjectMovable> extends IHookable<
|
||||
IObjectMoverHooks<T>
|
||||
> {
|
||||
readonly moving: boolean;
|
||||
readonly tile: T;
|
||||
readonly faceDirection: FaceDirection;
|
||||
readonly moveDirection: FaceDirection;
|
||||
|
||||
step(dir: FaceDirection, count?: number): this;
|
||||
stepFace(move: FaceDirection, face: FaceDirection, count?: number): this;
|
||||
forward(count?: number): this;
|
||||
backward(count?: number): this;
|
||||
speed(value: number): this;
|
||||
face(dir: FaceDirection): this;
|
||||
animDir(dir: DynamicAnimDirection): this;
|
||||
clear(): this;
|
||||
start(): IMoverController | null;
|
||||
}
|
||||
|
||||
// 这里的 onStepStart 不再接受 code 作为参数,因为这应当是其返回值 `Promise<number>`,
|
||||
// 具体来说,这四个方法是整个移动的核心方法而非钩子,它应当提供接口让子类实现,
|
||||
// 子类应当真正执行移动效果,并进行一定的控制。显然子类是没办法知道 onStepStart 中的信息的,
|
||||
// 所以才提供了一个 code,用于子类向 ObjectMover 提供信息,然后父类再传递给子类,
|
||||
// 从而子类了解到 onStepStart 中的信息,再进一步决定这一步真正应该如何执行。
|
||||
// 而对于钩子,实际上是在子类的 onStepStart 执行完毕后进行的,所以是知道 code 的,所以才是参数。
|
||||
// 你应该好好想一想这之间的关系。
|
||||
// 以及子类是要求必须实现的,所以不应该会有返回 `void` 的场景,四个方法都应该返回 `Promise<void>`。
|
||||
// 总的来说,父类是一个“系统级”的流程控制器,它通过这四个接口来实现真正的移动控制,
|
||||
// 就像 legacy ObjectMoverBase 中的 startMove 一样。
|
||||
abstract class ObjectMover<
|
||||
T extends IObjectMovable
|
||||
> implements IObjectMover<T> {
|
||||
abstract onMoveStart(tile: T, mover: IObjectMover<T>): Promise<void>;
|
||||
abstract onMoveEnd(tile: T, mover: IObjectMover<T>): Promise<void>;
|
||||
/** 子类计算并返回本步 code;父类取得 code 后再触发 IObjectMoverHooks.onStepStart 钩子 */
|
||||
abstract onStepStart(
|
||||
step: DynamicMoveStep,
|
||||
tile: T,
|
||||
mover: IObjectMover<T>
|
||||
): Promise<number>;
|
||||
/** 子类依据 code 执行本步实际逻辑效果;父类在此之后触发 IObjectMoverHooks.onStepEnd 钩子 */
|
||||
abstract onStepEnd(
|
||||
code: number,
|
||||
step: DynamicMoveStep,
|
||||
tile: T,
|
||||
mover: IObjectMover<T>
|
||||
): Promise<void>;
|
||||
// 计划构建方法(step/stepFace/forward/backward/speed/face/animDir/clear/start)在此实现
|
||||
}
|
||||
|
||||
// faceDirection/moveDirection 已提升至 IObjectMover,IDynamicMover 无需额外声明
|
||||
// IDynamicMoverHooks 已移除,直接使用 IObjectMoverHooks<IDynamicTile>
|
||||
interface IDynamicMover extends IObjectMover<IDynamicTile> {
|
||||
// 继承自 IObjectMover:moving, faceDirection, moveDirection, tile, 全部计划构建方法及钩子
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 问题
|
||||
|
||||
1. **`backward()` 的移动方向基准** ✅ 已确定
|
||||
|
||||
`backward()` 以 `opposite(tile.moveDirection)` 作为移动方向(基于 `moveDirection` 取反,非 `faceDirection`)。
|
||||
`moveDirection` 更新为反向,`faceDirection` 通过转向逻辑跟随同步更新。
|
||||
|
||||
2. **`forward()` 是否更新 faceDirection** ✅ 已确定
|
||||
|
||||
`forward()` 沿当前 `moveDirection` 移动,`moveDirection` 不变,
|
||||
`faceDirection` 通过转向逻辑对齐为 `moveDirection`。
|
||||
|
||||
3. **转向步触发钩子方式** ✅ 已确定
|
||||
|
||||
统一经由 `onStepStart(code, step, tile, mover)` 钩子处理,在步骤逻辑执行前触发。
|
||||
渲染端通过 `step.type === DynamicMoveStepType.Face` 区分纯转向步与位移步;
|
||||
`onStepStart` 触发时 `tile.x/y` 为原始坐标,渲染端可直接读取,无需反推。
|
||||
转向逻辑(`faceDirection`/`num` 更新)在 `onStepEnd` 阶段执行。
|
||||
|
||||
4. **`animDir` 与渲染钩子集成** ✅ 已确定
|
||||
|
||||
`onStepStart` 统一接收所有步骤,`IDynamicMoveAnimDir` 步骤本身即为信息载体。
|
||||
渲染端在 `onStepStart` 收到 `AnimDir` 步骤时更新本地动画方向状态;
|
||||
后续方向步的 `onStepStart` 触发时,渲染端依据本地状态决定动画播放方向。
|
||||
|
||||
5. **玩家移动与图块移动的复用** ✅ 已确定
|
||||
|
||||
玩家(勇士)移动逻辑与 `IDynamicMover` 高度一致,共同抽象为
|
||||
`IObjectMover<T extends IObjectMovable>`(参考 legacy `ObjectMoverBase`,适配新 API);
|
||||
核心抽象类为 `abstract class ObjectMover<T>`,含四个抽象方法组成控制流:
|
||||
- **计划构建方法**(`step`/`stepFace`/`forward`/`backward`/
|
||||
`speed`/`face`/`animDir`/`clear`/`start`)全部在 `ObjectMover` 基类实现,子类无需重复声明。
|
||||
- **移动流程钩子**(`IObjectMoverHooks<T>`,实现 `IHookable<IObjectMoverHooks<T>>`):
|
||||
- `onMoveStart(mover, tile)`:整次移动队列开始时触发;
|
||||
- `onMoveEnd(mover, tile)`:整次移动队列结束时触发;
|
||||
- `onStepStart(code, step, tile, mover)`:单步开始前触发(`tile.x/y` 为移动前坐标);
|
||||
- `onStepEnd(code, step, tile, mover)`:单步完成后触发(`tile.x/y` 已更新);
|
||||
其中 `code: number` 标识移动场景(如碰墙、遇敌、循环地图等),由具体实现层设置。
|
||||
- **四个抽象方法**(`onMoveStart`/`onMoveEnd`/`onStepStart`/`onStepEnd`):子类(`DynamicMover`/`HeroMover`)
|
||||
通过实现这四个方法完全控制移动行为,执行流程由 `ObjectMover` 基类统一调度。
|
||||
关键:`onStepStart` 返回 `Promise<number>`(code),父类将此 code 传给钩子后再调用 `onStepEnd(code, ...)`,
|
||||
子类在 `onStepEnd` 中依据 code 执行实际状态变更。四个方法均返回 `Promise`,无 `void` 选项。
|
||||
- **泛型参数** `T extends IObjectMovable`:`IObjectMovable` 是可移动对象的最小接口
|
||||
(`x`、`y`、`moveDirection`、`faceDirection` 只读属性 +
|
||||
`setPos`/`setMoveDirection`/`setFaceDirection` 方法),定义在 `dynamic-tile.md`;
|
||||
`IDynamicTile extends IObjectMovable`,玩家侧使用 `IHeroMovable extends IObjectMovable`。
|
||||
- **`IDynamicMover extends IObjectMover<IDynamicTile>`**:
|
||||
无额外成员,直接继承基类全部能力(含 `faceDirection`/`moveDirection` 及 `IObjectMoverHooks`)。
|
||||
@@ -0,0 +1,407 @@
|
||||
# 需求综述
|
||||
|
||||
当前地图系统仅支持静态图块(通过 `IMapLayer` / `Uint32Array` 存储),
|
||||
无法支持可移动的动态图块(例如 NPC、推箱子、可交互物件等)。
|
||||
动态图块的核心特点:
|
||||
|
||||
1. **允许重叠**——同一格点可同时存在多个动态图块;
|
||||
2. **整数坐标**——始终处于格点整数坐标,不出现小数,便于渲染优化与交互;
|
||||
3. **异步移动**——移动接口返回 `Promise`,配合渲染动画;
|
||||
4. **朝向转向**——部分图块移动时需同步更新朝向(如四方向 NPC 行走图)。
|
||||
|
||||
---
|
||||
|
||||
# 实现思路
|
||||
|
||||
## 1. 动态图块标识方案
|
||||
|
||||
**采用引用标识**:`createDynamicTile` 与 `transferToDynamic` 均返回
|
||||
`IDynamicTile` 对象引用,调用方持有该引用作为后续操作(移动、删除、
|
||||
设置朝向等)的唯一凭证。无需维护任何 ID,无唯一性管理负担。
|
||||
|
||||
调用方可将返回的 `IDynamicTile` 引用存储在自己的数据结构中,
|
||||
`tile.x`、`tile.y`、`tile.num`、`tile.direction` 等字段始终反映图块的
|
||||
最新状态——内部实现类持有可变字段,接口以 `readonly` 暴露给外部,
|
||||
保证外部不能直接修改,内部可通过类实例更新。
|
||||
|
||||
## 2. 坐标表示
|
||||
|
||||
使用独立的 `x: number, y: number` 参数,与现有 `IMapLayer` 接口风格
|
||||
(如 `setBlock(block, x, y)`、`getBlock(x, y)`)保持一致,不引入 `ITileLocator`。
|
||||
|
||||
## 3. 方向表示
|
||||
|
||||
移动方向与转向方向统一使用 `FaceDirection` 枚举:
|
||||
|
||||
- 语义明确,与已有 `IRoleFaceBinder`(朝向绑定)直接对接;
|
||||
- 已有工具函数 `getFaceMovement(dir)` 可将其转换为坐标偏移,无需重复实现;
|
||||
- 多步路径(`moveDynamicWith` 的 `steps` 参数)使用 `FaceDirection[]`。
|
||||
|
||||
`IDirectionDescriptor` 更偏向纯数学计算(用于范围迭代等),
|
||||
语义上不适合表达「图块朝向某方向移动」的含义。
|
||||
|
||||
**已确定**:移动方向(`moveDirection`)与朝向(`faceDirection`)分离为两个独立字段。
|
||||
后退等相对移动必须依赖两者分离才能正确表达(例如面朝上、向下后退时,`faceDirection`
|
||||
保持 `Up`,`moveDirection` 更新为 `Down`)。
|
||||
|
||||
移动系统另立文档独立设计,包含双方向模型、`IDynamicMover` 抽象、
|
||||
计划式移动接口及执行流程等完整方案,见
|
||||
[动态图块移动系统](./dynamic-tile-move.md)。
|
||||
|
||||
`moveDynamicStep` / `moveDynamicWith` 保留为命令式便捷接口,适用于单次简单移动;
|
||||
复杂路径和计划式移动统一使用 `IDynamicMover`。
|
||||
|
||||
## 4. 动态图块数据模型与图层归属
|
||||
|
||||
动态图块按**所属静态图层**组织层级关系:每个 `IMapLayer` 持有一个
|
||||
`IDynamicLayer`,动态图块的渲染深度即为其所属 `IMapLayer` 的 `zIndex`,
|
||||
与静态图块一致。因此 `IDynamicLayer` 接口挂载在 `IMapLayer` 上,
|
||||
而不是 `ILayerState` 上(见第 5 节)。
|
||||
|
||||
每个动态图块存储以下信息:
|
||||
|
||||
```ts
|
||||
/** 可移动对象的最小公共接口,供 `IObjectMover` 泛型约束使用 */
|
||||
interface IObjectMovable {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly moveDirection: FaceDirection;
|
||||
readonly faceDirection: FaceDirection;
|
||||
setPos(x: number, y: number): void;
|
||||
setMoveDirection(dir: FaceDirection): void;
|
||||
setFaceDirection(dir: FaceDirection): void;
|
||||
}
|
||||
|
||||
interface IDynamicTile extends IObjectMovable {
|
||||
readonly num: number; // 图块数字(决定渲染图像)
|
||||
readonly layer: IDynamicLayer; // 所属动态图层
|
||||
readonly mover: IDynamicMover; // 绑定的移动器(计划式移动)
|
||||
/** 删除此图块,转发至 layer.deleteDynamic */
|
||||
delete(): void;
|
||||
/** 还原为静态图块,转发至 layer.transferToStatic */
|
||||
toStatic(): void;
|
||||
/** 还原为静态图块,如果当前位置有东西则不转换,转发只 layer.transferToStaticIfSafe */
|
||||
toStaticIfSafe(): boolean;
|
||||
/**
|
||||
* 单步便捷移动接口,等价于 `mover.step(dir, count); return mover.start();`。
|
||||
* 适用于简单移动场景,复杂路径通过 `tile.mover` 访问。
|
||||
*/
|
||||
step(dir: FaceDirection, count?: number): IMoverController | null;
|
||||
}
|
||||
```
|
||||
|
||||
`x`、`y`、`moveDirection`、`faceDirection` 及 `setPos`/`setMoveDirection`/`setFaceDirection`
|
||||
均由 `IObjectMovable` 提供,`IDynamicTile` 不再重复声明。
|
||||
`IObjectMover<T extends IObjectMovable>` 以此接口为泛型约束,
|
||||
使图块和玩家 mover 共享核心执行逻辑;具体渲染效果通过 `IObjectMoverHooks` 的 `onStepStart`/`onStepEnd` 钩子实现。
|
||||
|
||||
`DynamicLayer` 内部维护两个结构:
|
||||
|
||||
- `tileSet: Set<IDynamicTile>` — 所有图块的集合,用于迭代与归属判断;
|
||||
- `posMap: Map<number, Map<number, Set<IDynamicTile>>>` — 按坐标索引
|
||||
(外层 key = y,内层 key = x,value = 该格点所有图块对象集合),
|
||||
图块移动时同步更新,支持越界坐标(见第 4.1 节)。
|
||||
|
||||
调用方持有 `IDynamicTile` 对象引用即可完成所有操作,
|
||||
`posMap` 仅供按坐标查询(`getDynamicTilesAt`)使用,
|
||||
因使用频率低,嵌套 `Map` 的开销完全可以接受。
|
||||
|
||||
`IDynamicTile` 上的 `moveStep`/`moveWith`/`delete`/`setDirection`/`setPos`/`toStatic`
|
||||
均为便捷转发方法,内部直接调用 `tile.layer` 对应接口,不修改任何内部状态。
|
||||
调用方可直接通过 `tile.xxx()` 操作,无需额外持有 `IDynamicLayer` 引用。
|
||||
|
||||
### 4.1 越界移动
|
||||
|
||||
动态图块的坐标不受楼层 `width`/`height` 约束,允许出现负值或超出地图范围的坐标。
|
||||
这满足了如「图块飞出屏幕」「过场动画」等临时越界需求。
|
||||
`DynamicLayer` 无需感知楼层尺寸,`resizeLayer` 也无需通知 `DynamicLayer`。
|
||||
|
||||
## 5. IDynamicLayer 挂载在 IMapLayer 上
|
||||
|
||||
动态图块的存储结构与 `IMapLayer`(Uint32Array)本质不同,独立封装为 `IDynamicLayer`。
|
||||
`IMapLayer` 新增 `readonly dynamicLayer: IDynamicLayer` 属性,
|
||||
调用方通过具体的图层对象操作该层的动态图块,z 层级天然与静态图块一致。
|
||||
|
||||
`IDynamicLayer` 实现 `IHookable<IDynamicLayerHooks>` 以支持渲染端订阅变更事件。
|
||||
`IRoleFaceBinder` 通过 `setFaceBinder(binder)` 方法注入(见第 8 节),
|
||||
不在构造时传入,使多楼层共用同一个 `binder` 实例更灵活。
|
||||
|
||||
`transferToDynamic(x, y)` 不需要 `layer` 参数,
|
||||
因为它隶属于某个具体的 `IDynamicLayer`,直接从同级 `IMapLayer` 读写图块即可,
|
||||
并返回创建的 `IDynamicTile` 引用。
|
||||
|
||||
## 6. 移动函数的坐标来源
|
||||
|
||||
`moveDynamicStep(direction, tile)` 与 `moveDynamicWith(steps, tile)` 均不接受
|
||||
`ox, oy` 参数,改为直接接受 `IDynamicTile` 引用。当前坐标直接读取 `tile.x`、
|
||||
`tile.y`,无需调用方额外传入。
|
||||
|
||||
内部行为:
|
||||
|
||||
- 直接读取 `tile.x`、`tile.y` 作为出发点;
|
||||
- **立即**将图块逻辑坐标更新为目标位置(单步 `(tile.x+dx, tile.y+dy)`);
|
||||
- 同步更新 `posMap`;
|
||||
- 触发 `onMoveTile` 钩子,收集所有钩子返回的 `Promise<void> | void`;
|
||||
- `await Promise.all(promises)`——
|
||||
等待所有渲染动画完成后,本步才算兑现,再进入下一步。
|
||||
|
||||
详细多步路径与计划式移动的执行流程见
|
||||
[动态图块移动系统](./dynamic-tile-move.md)。
|
||||
|
||||
## 7. transferToDynamic 与 transferToStatic
|
||||
|
||||
`transferToDynamic(x, y)` 不需要额外的 `layer` 参数。
|
||||
`DynamicLayer` 在构造时持有对所属 `MapLayer` 的引用,
|
||||
直接调用 `mapLayer.getBlock(x, y)` 读取图块数字,再调用 `mapLayer.setBlock(0, x, y)` 清除,
|
||||
最后创建对应的动态图块并返回其引用。
|
||||
若该位置图块为 0(空白),则发出 logger 警告并仍然创建 `num = 0` 的动态图块。
|
||||
|
||||
新增 `transferToStaticIfSafe(tile)` 作为安全版本:仅当目标位置静态图块为 0(空白)时才执行还原,
|
||||
否则不转换并返回 `false`;转换成功返回 `true`。适用于不确定目标格是否已有图块的场景。
|
||||
|
||||
新增 `transferToStatic(tile)` 作为逆操作:将动态图块还原为静态图块。
|
||||
对于「只移动一次就固定」的图块(如推箱子放到指定位置后不再移动),
|
||||
及时转回静态存储可以降低存档体积、简化渲染订阅。执行流程:
|
||||
|
||||
1. 读取 `tile.x`、`tile.y`、`tile.num`;
|
||||
2. 若坐标不在所属 `MapLayer` 的合法范围内(即 `x < 0`、`y < 0`、
|
||||
`x >= width` 或 `y >= height`),发出 logger 警告并放弃操作;
|
||||
3. 若 `mapLayer.getBlock(tile.x, tile.y) !== 0`,在开发环境下发出 logger 警告
|
||||
(目标格点已有静态图块内容,将被覆盖);
|
||||
调用 `mapLayer.setBlock(tile.num, tile.x, tile.y)` 写回静态图块;
|
||||
4. 从 `tileSet` 和 `posMap` 中移除该图块,触发 `onDeleteTile` 钩子。
|
||||
|
||||
## 8. 转向逻辑
|
||||
|
||||
`moveDynamicStep` / `moveDynamicWith` 在每一步移动时自动更新朝向(使用双方向模型,
|
||||
详见 [动态图块移动系统](./dynamic-tile-move.md)):
|
||||
|
||||
1. 更新 `tile.moveDirection` 为本步实际移动方向;
|
||||
2. 通过外部注入的 `IRoleFaceBinder` 调用 `getFaceOf(tile.num, direction)` 查询
|
||||
该方向对应的图块数字;
|
||||
3. 若图块无朝向绑定(返回 `null`),则不修改 `num`,仅更新 `faceDirection`;
|
||||
4. 若有绑定,将 `tile.num` 与 `tile.faceDirection` 更新为查询结果。
|
||||
|
||||
`IRoleFaceBinder` 通过 `setFaceBinder(binder: IRoleFaceBinder): void` 方法注入,
|
||||
不在构造时传入;初始状态视为无朝向绑定(`getFaceOf` 始终返回 `null`)。
|
||||
若多个楼层共用同一个 `RoleFaceBinder` 实例,直接对各楼层各图层分别调用 `setFaceBinder` 即可。
|
||||
|
||||
对于八方向移动,转向查询逻辑扩展如下:
|
||||
|
||||
1. 更新 `tile.moveDirection` 为本步实际移动方向;
|
||||
2. 调用 `getFaceOf(tile.num, direction)` 查询;
|
||||
3. 若返回 `null` 且当前方向为斜向(八方向之一),
|
||||
调用 `degradeFace(direction)` 降级为四方向后再查询一次;
|
||||
4. 若仍返回 `null`,不修改 `num`,仅更新 `faceDirection`;
|
||||
5. 若查询到结果,将 `tile.num` 更新为结果图块数字,并更新 `faceDirection`。
|
||||
|
||||
手动设置朝向通过独立接口 `setDynamicDirection(tile, direction)` 完成,
|
||||
逻辑与上述步骤 1–5 相同,但不触发移动。
|
||||
|
||||
## 9. IDynamicLayerHooks 与 ILayerStateHooks
|
||||
|
||||
`IDynamicLayerHooks` 定义三个钩子:
|
||||
|
||||
- `onCreateTile(tile: IDynamicTile)`:图块被创建(含转换)时触发;
|
||||
- `onDeleteTile(tile: IDynamicTile)`:图块被删除时触发(传入删除前的快照);
|
||||
- `onMoveTile(tile: IDynamicTile, fromX: number, fromY: number): Promise<void> | void`:
|
||||
图块移动一步时触发,`tile` 为更新后的状态,`fromX`/`fromY` 为移动前坐标。
|
||||
返回 `Promise<void>` 时,移动器将等待其兑现后再进行下一步(配合 `Promise.all` 并行等待所有订阅方)。
|
||||
|
||||
`ILayerStateHooks` 新增对应的三个转发钩子,额外携带 `layer: IMapLayer` 参数,
|
||||
与现有的 `onUpdateLayerArea`、`onResizeLayer` 等钩子风格一致:
|
||||
|
||||
- `onCreateDynamicTile(layer: IMapLayer, dynamicLayer: IDynamicLayer, tile: IDynamicTile)`
|
||||
- `onDeleteDynamicTile(layer: IMapLayer, dynamicLayer: IDynamicLayer, tile: IDynamicTile)`
|
||||
- `onMoveDynamicTile(layer: IMapLayer, dynamicLayer: IDynamicLayer, tile: IDynamicTile, fromX: number, fromY: number)`
|
||||
|
||||
`LayerState` 在为每个 `MapLayer` 注册 `StateMapLayerHook` 时,
|
||||
同步订阅该层 `dynamicLayer` 的三个钩子,将事件加上 `layer`、`dynamicLayer` 参数后转发至楼层钩子。
|
||||
|
||||
---
|
||||
|
||||
# 涉及文件
|
||||
|
||||
> 移动相关的接口(`DynamicMoveDir`、`IDynamicMoveStep`、`IMoverController`、`IDynamicMover`)
|
||||
> 设计详情见 [动态图块移动系统](./dynamic-tile-move.md),
|
||||
> 同样需要添加至 `types.ts`。
|
||||
|
||||
## 需要引用的文件
|
||||
|
||||
- `@motajs/common`:`IHookable`, `IHookBase`, `Hookable`, `logger`
|
||||
- `@user/data-base/src/common`:`FaceDirection`, `IRoleFaceBinder`,
|
||||
`getFaceMovement`, `degradeFace`
|
||||
- `@user/data-base/src/map/types.ts`:`IMapLayer`, `IMapLayerHooks`,
|
||||
`ILayerState`, `ILayerStateHooks`
|
||||
|
||||
## 需要修改的文件
|
||||
|
||||
### `@user/data-base/src/map/types.ts`
|
||||
|
||||
- [ ] 新增 `IObjectMovable` 接口:
|
||||
含 `x`、`y`、`moveDirection`、`faceDirection` 四个只读属性及
|
||||
`setPos`/`setMoveDirection`/`setFaceDirection` 三个方法;
|
||||
供 `IObjectMover<T extends IObjectMovable>` 泛型约束使用
|
||||
- [ ] 新增 `IDynamicTile extends IObjectMovable` 接口:
|
||||
含 `num`、`layer`、`mover` 三个只读属性及 `delete`/`toStatic` 两个便捷方法;
|
||||
无 `id` 字段,坐标/方向成员由 `IObjectMovable` 提供
|
||||
- [ ] 新增移动相关类型(详见移动文档接口定义汇总,已移至 `move/types.ts`):
|
||||
`DynamicMoveStepType`、`DynamicMoveStep`、`IMoverController`、
|
||||
`IObjectMoverHooks`、`IObjectMover`、`IDynamicMover`
|
||||
- [ ] 新增 `IDynamicLayerHooks extends IHookBase` 接口:
|
||||
含 `onCreateTile`、`onDeleteTile`、`onMoveTile` 三个钩子方法
|
||||
- [ ] 新增 `IDynamicLayer extends IHookable<IDynamicLayerHooks>` 接口:
|
||||
- [ ] `createDynamicTile(num, x, y): IDynamicTile`: 在指定位置创建动态图块,
|
||||
返回图块引用
|
||||
- [ ] `transferToDynamic(x, y): IDynamicTile`: 从所属静态图层读取并清除
|
||||
指定位置图块,创建对应动态图块并返回引用
|
||||
- [ ] `transferToStatic(tile: IDynamicTile)`: 将动态图块还原为静态图块;
|
||||
坐标越界则警告并放弃,否则写回静态图层并触发 `onDeleteTile`
|
||||
- [ ] `transferToStaticIfSafe(tile: IDynamicTile): boolean`: 仅当目标位置静态图块为 0
|
||||
时才还原,否则不转换;返回是否转换成功
|
||||
- [ ] `deleteDynamicTile(tile: IDynamicTile)`: 删除指定图块,不在此层则警告
|
||||
- [ ] `getDynamicTilesAt(x, y)`: 获取指定格点所有动态图块的可迭代对象
|
||||
- [ ] `moveDynamicWith` / `moveDynamicStep` 已由 `IDynamicMover` 权其责。
|
||||
`IDynamicLayer` 不再提供此两个方法,删除对应条目。
|
||||
- [ ] `setDynamicDirection(tile: IDynamicTile, direction)`: 手动设置朝向,
|
||||
同步更新 `direction` 与 `num`(若有朝向绑定)
|
||||
- [ ] `setDynamicPos(tile: IDynamicTile, x: number, y: number)`: 直接设置图块位置,
|
||||
同步更新 `posMap`,触发 `onMoveTile` 钩子,不更新朝向
|
||||
- [ ] `setFaceBinder(binder: IRoleFaceBinder)`: 注入朝向绑定器
|
||||
- [ ] 修改 `IMapLayer`:新增 `readonly dynamicLayer: IDynamicLayer`
|
||||
- [ ] 修改 `ILayerStateHooks`:新增 `onCreateDynamicTile`、`onDeleteDynamicTile`、
|
||||
`onMoveDynamicTile` 三个转发钩子,额外携带 `layer: IMapLayer` 与
|
||||
`dynamicLayer: IDynamicLayer` 两个参数
|
||||
|
||||
### `@user/data-base/src/map/dynamicLayer.ts`(新文件)
|
||||
|
||||
- [ ] 实现 `DynamicLayer extends Hookable<IDynamicLayerHooks>` 类,
|
||||
实现 `IDynamicLayer`
|
||||
- [ ] 实现内部类 `DynamicTile implements IDynamicTile`:
|
||||
持有 `layer: IDynamicLayer` 引用,`delete`/`toStatic` 转发至 `layer`;
|
||||
`step(dir, count?)` 封装为便捷方法
|
||||
- [ ] `DynamicTile` 构造时同步创建 `readonly mover: DynamicMover`,
|
||||
移动调度由 `move/dynamicMover.ts` 中的 `DynamicMover` 类实现
|
||||
- [ ] `private mapLayer: IMapLayer`:构造参数,所属静态图层引用,
|
||||
供 `transferToDynamic` / `transferToStatic` 读写使用
|
||||
- [ ] `private faceBinder: IRoleFaceBinder | null = null`:朝向绑定器,
|
||||
通过 `setFaceBinder` 注入
|
||||
- [ ] `private tileSet: Set<IDynamicTile>`:所有图块的集合,用于迭代与归属判断
|
||||
- [ ] `private posMap: Map<number, Map<number, Set<IDynamicTile>>>`:
|
||||
按坐标索引(外层 key = y,内层 key = x),支持越界坐标
|
||||
- [ ] 实现全部接口方法,移动时同步更新 `tileSet` 与 `posMap`
|
||||
|
||||
### `@user/data-base/src/map/mapLayer.ts`
|
||||
|
||||
- [ ] 新增 `readonly dynamicLayer: DynamicLayer`:构造时以 `this` 为参数创建
|
||||
|
||||
### `@user/data-base/src/map/layerState.ts`
|
||||
|
||||
- [ ] 修改 `StateMapLayerHook`(或 `addLayer` 中的订阅逻辑):
|
||||
在为每个 `MapLayer` 注册钩子时,同时订阅其 `dynamicLayer` 的三个钩子,
|
||||
将事件加上 `layer`、`dynamicLayer` 参数后转发至楼层的 `ILayerStateHooks`
|
||||
|
||||
---
|
||||
|
||||
# 接口定义汇总
|
||||
|
||||
以下为本次新增与修改的完整接口签名,供实现时参考。
|
||||
|
||||
移动相关接口(`IObjectMoverHooks`、`IObjectMover`、`IDynamicMover`、`IMoverController`、`DynamicMoveStep` 等)
|
||||
详见 [dynamic-tile-move.md](./dynamic-tile-move.md) 接口定义汇总,此处不再重复。
|
||||
|
||||
## 新增接口
|
||||
|
||||
### IObjectMovable
|
||||
|
||||
```ts
|
||||
interface IObjectMovable {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly moveDirection: FaceDirection;
|
||||
readonly faceDirection: FaceDirection;
|
||||
setPos(x: number, y: number): void;
|
||||
setMoveDirection(dir: FaceDirection): void;
|
||||
setFaceDirection(dir: FaceDirection): void;
|
||||
}
|
||||
```
|
||||
|
||||
### IDynamicTile
|
||||
|
||||
```ts
|
||||
interface IDynamicTile extends IObjectMovable {
|
||||
readonly num: number;
|
||||
readonly layer: IDynamicLayer;
|
||||
readonly mover: IDynamicMover;
|
||||
delete(): void;
|
||||
toStatic(): void;
|
||||
toStaticIfSafe(): boolean;
|
||||
/** 等价于 mover.step(dir, count); return mover.start(); */
|
||||
step(dir: FaceDirection, count?: number): IMoverController | null;
|
||||
}
|
||||
```
|
||||
|
||||
### IDynamicLayerHooks
|
||||
|
||||
```ts
|
||||
interface IDynamicLayerHooks extends IHookBase {
|
||||
onCreateTile(tile: IDynamicTile, layer: IDynamicLayer): void;
|
||||
onDeleteTile(tile: IDynamicTile, layer: IDynamicLayer): void;
|
||||
}
|
||||
```
|
||||
|
||||
### IDynamicLayer
|
||||
|
||||
```ts
|
||||
interface IDynamicLayer extends IHookable<IDynamicLayerHooks> {
|
||||
createDynamic(num: number, x: number, y: number): IDynamicTile;
|
||||
transferToDynamic(x: number, y: number): IDynamicTile;
|
||||
transferToStatic(tile: IDynamicTile): void;
|
||||
transferToStaticIfSafe(tile: IDynamicTile): boolean;
|
||||
deleteDynamic(tile: IDynamicTile): void;
|
||||
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile>;
|
||||
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): void;
|
||||
setDynamicPos(tile: IDynamicTile, x: number, y: number): void;
|
||||
setFaceBinder(binder: IRoleFaceBinder): void;
|
||||
}
|
||||
```
|
||||
|
||||
## 修改接口
|
||||
|
||||
### IMapLayer(新增属性)
|
||||
|
||||
```ts
|
||||
interface IMapLayer {
|
||||
// ...现有成员...
|
||||
readonly dynamicLayer: IDynamicLayer;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 问题
|
||||
|
||||
1. **`ox, oy` 是否为必要参数?** ✅ 已确定
|
||||
|
||||
已确定移除。调用方持有 `IDynamicTile` 引用,坐标直接从 `tile.x`、`tile.y` 读取,
|
||||
`ox, oy` 为冗余参数,接口简化为 `moveDynamicStep(direction, tile)` 和
|
||||
`moveDynamicWith(steps, tile)`。
|
||||
|
||||
2. **`IRoleFaceBinder` 的注入方式** ✅ 已确定
|
||||
|
||||
已确定使用 `setFaceBinder(binder)` 方法注入,不在构造时传入。
|
||||
实际修改 `faceBinder` 的场景极少,接口保持简单。
|
||||
|
||||
3. **动态图块的存档支持** ⏸ 暂缓
|
||||
|
||||
动态图块状态(NPC 当前位置、推箱子位置等)属于游戏核心存档内容,
|
||||
长期来看必须纳入存档体系。但当前设计与旧引擎完全不同,
|
||||
存档格式需从头设计,建议在动态图块功能稳定后单独立项设计存档方案。
|
||||
**本次实现不涉及存档,`IDynamicLayer` 暂不实现 `ISaveableContent`。**
|
||||
|
||||
4. **`IDynamicLayer` 是否需要感知楼层尺寸** ✅ 已确定
|
||||
|
||||
已确定不感知。标识方案改为引用,`posMap` 同步改为
|
||||
`Map<number, Map<number, Set<IDynamicTile>>>` 嵌套结构(外层 key = y,内层 key = x),
|
||||
彻底去掉字符串键,天然支持越界坐标,`resizeLayer` 无需通知 `DynamicLayer`。
|
||||
`getDynamicTilesAt` 接口保留,使用频率低,嵌套 Map 开销完全可接受。
|
||||
@@ -0,0 +1,212 @@
|
||||
# 需求综述
|
||||
|
||||
本次改动目标:
|
||||
|
||||
1. **自动化分区激活器**:将楼层按游戏进程划分为若干"区域",
|
||||
到达新区域时自动激活对应楼层、失活旧区域楼层,
|
||||
从而替代目前繁琐的手动 `setActiveStatus` 调用。
|
||||
2. **楼层尺寸上移至 `LayerState`**:同一楼层的所有图层尺寸应当
|
||||
保持一致,因此将尺寸的权威来源从 `MapLayer` 移至 `LayerState`。
|
||||
|
||||
---
|
||||
|
||||
# 实现思路
|
||||
|
||||
## 1. 有序地图 id 列表
|
||||
|
||||
当前 `IMapStore.maps` 是 `ReadonlySet<string>`,无序且随楼层创建
|
||||
自动填充。区域功能需要以**下标**标识范围,因此需改为有序数组。
|
||||
|
||||
修改方案:
|
||||
|
||||
- `IMapStore.maps` 类型改为 `ReadonlyArray<string>`;
|
||||
- 新增 `setMapList(maps: string[]): void`,由外部显式指定有序列表
|
||||
(一般在游戏初始化时调用一次);
|
||||
- 新增 `useManualOrder(sort: (arr: string[]) => string[]): void`,
|
||||
允许自定义地图列表排序函数。调用时将当前 `maps` 的 `slice` 拷贝
|
||||
传入 `sort`,再对输出做合法性校验:将新旧数组各转为 `Set`,
|
||||
校验 `size` 相等且新集合是旧集合的子集(利用 `Set.prototype.isSubsetOf`);
|
||||
校验通过后用返回值替换内部的 `maps`。这样当地图是动态生成时,
|
||||
作者依然可以自定义顺序,而不必手动维护全量列表;
|
||||
- `createLayerState` 不再维护 `maps`,`maps` 完全由 `setMapList` 管理;
|
||||
- 若 `createLayerState` 传入的 id 不在 `maps` 中,仍可正常创建,
|
||||
不影响存档逻辑,但该楼层不参与任何区域判断。
|
||||
|
||||
## 2. 区域定义与管理
|
||||
|
||||
### 类型定义
|
||||
|
||||
```ts
|
||||
/** 单段闭区间 [start, end],start 和 end 均为 maps 下标 */
|
||||
export interface IMapAreaInterval {
|
||||
readonly start: number;
|
||||
readonly end: number;
|
||||
}
|
||||
|
||||
/** 一个区域由一个或多个独立区间组成 */
|
||||
export type MapArea = IMapAreaInterval[];
|
||||
```
|
||||
|
||||
### 接口
|
||||
|
||||
- `setArea(areas: Set<MapArea>): void`:一次性设置所有区域信息,
|
||||
覆盖原有区域定义;每个元素代表一个区域,区域可包含多个区间,
|
||||
使用 `Set<MapArea>` 表示无序区域集合;
|
||||
- `activeArea(id: string): void`:手动激活指定楼层所在区域的所有楼层。
|
||||
系统遍历 `areaList`,找到包含该楼层 id 的区域后,对该区域内的所有
|
||||
楼层调用 `setMapActiveStatus(floor, true)`;
|
||||
- `deactiveArea(id: string): void`:手动取消激活指定楼层所在区域的
|
||||
所有楼层,逻辑与 `activeArea` 对称;判断时遍历 `areaList`,
|
||||
此操作为低频调用,无需缓存;
|
||||
|
||||
## 3. 自动分区激活器
|
||||
|
||||
### 接口
|
||||
|
||||
- `useAutoActivitor(enable: boolean): void`:是否启用自动激活器。
|
||||
|
||||
### 触发接口
|
||||
|
||||
需要一个通知接口供玩家相关模块调用:
|
||||
|
||||
- `notifyEnterFloor(id: string): void`:玩家进入指定楼层时调用此接口,
|
||||
通知地图管理器进行自动激活判断。
|
||||
|
||||
### 逻辑
|
||||
|
||||
`notifyEnterFloor(id)` 的执行流程(每次进入楼层均调用,内部短路):
|
||||
|
||||
1. 若自动激活器未启用,直接返回;
|
||||
2. 若 `isMapActive(id)` 为 `true`,直接返回(楼层已激活,无需操作);
|
||||
3. 遍历 `areaList`,找出包含 `id` 的区域;
|
||||
4. 若未找到,直接返回(该楼层不在任何区域内);
|
||||
5. 若 `lastFloorId !== null`,调用 `deactiveArea(lastFloorId)` 失活上一个区域;
|
||||
6. 调用 `activeArea(id)` 激活新区域,更新 `lastFloorId = id`。
|
||||
|
||||
### 内部状态
|
||||
|
||||
`MapStore` 新增:
|
||||
|
||||
- `private areaList: Set<MapArea>`:所有区域定义;
|
||||
- `private lastFloorId: string | null = null`:上一次触发 `notifyEnterFloor`
|
||||
的楼层 id,用于定位并失活上一个激活区域;
|
||||
- `private autoActivitorEnabled: boolean = false`:自动激活器开关。
|
||||
|
||||
## 4. 楼层尺寸上移至 LayerState
|
||||
|
||||
### 动机
|
||||
|
||||
当前 `MapLayer.width` / `MapLayer.height` 存储在图层中,
|
||||
但同一楼层的所有图层尺寸必须一致,权威来源应当是 `LayerState`。
|
||||
|
||||
### 接口变动
|
||||
|
||||
**`ILayerState` 新增**:
|
||||
|
||||
```ts
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
```
|
||||
|
||||
**`addLayer` 签名调整**:
|
||||
|
||||
目前 `addLayer(width: number, height: number): IMapLayer`,
|
||||
移除 width/height 参数,改为 `addLayer(): IMapLayer`,
|
||||
使用 `LayerState` 内部存储的尺寸创建图层。
|
||||
|
||||
楼层尺寸在 `createLayerState` 创建时指定,`createLayerState` 签名改为:
|
||||
|
||||
```ts
|
||||
createLayerState(id: string, width: number, height: number): ILayerState;
|
||||
```
|
||||
|
||||
运行时仍可通过 `resizeLayer` 修改楼层尺寸,该方法会同步对楼层内所有
|
||||
图层执行 resize,保持尺寸一致。
|
||||
|
||||
**`resizeLayer` 签名调整**:
|
||||
|
||||
当前 `resizeLayer(layer, width, height, keepBlock?)` 只 resize 单个图层,
|
||||
但既然尺寸是楼层级的,建议改为对该楼层的所有图层同步 resize:
|
||||
|
||||
```ts
|
||||
resizeLayer(width: number, height: number, keepBlock?: boolean): void;
|
||||
```
|
||||
|
||||
**`IMapLayer.resize` / `IMapLayer.resize2`**:
|
||||
|
||||
从 `IMapLayer` 接口中移除,保留为 `MapLayer` 的内部实现,
|
||||
仅由 `LayerState.resizeLayer` 调用。
|
||||
|
||||
**`IMapLayer.width` / `IMapLayer.height`**:
|
||||
|
||||
保留在 `IMapLayer` 接口中,供外部通过图层对象直接获取尺寸。
|
||||
其值始终与所属 `LayerState` 的 `width`/`height` 保持一致。
|
||||
|
||||
---
|
||||
|
||||
# 附加建议结论
|
||||
|
||||
1. **`IMapLayer.setMapRef` 可见性**:保留现有设计,偶尔有外部需求。
|
||||
2. **`active` 状态管理**:不需要单独维护区域激活状态;
|
||||
`activeArea(id)` / `deactiveArea(id)` 是 `setMapActiveStatus` 的
|
||||
快捷方式,遍历区域楼层批量调用即可,无需额外的区域状态字段。
|
||||
3. **`notifyEnterFloor` 返回值**:暂不添加,后续有需求再改进。
|
||||
|
||||
---
|
||||
|
||||
# 涉及文件
|
||||
|
||||
## 需要引用的文件
|
||||
|
||||
- `@user/data-base/src/map/types.ts`: 全部现有地图接口
|
||||
- `@user/data-base/src/map/mapStore.ts`: `MapStore` 实现类
|
||||
- `@user/data-base/src/map/layerState.ts`: `LayerState` 实现类
|
||||
- `@user/data-base/src/map/mapLayer.ts`: `MapLayer` 实现类
|
||||
|
||||
## 需要修改的文件
|
||||
|
||||
### `@user/data-base/src/map/types.ts`
|
||||
|
||||
- [x] 新增 `IMapAreaInterval` 接口:区间定义,含 `start`、`end`
|
||||
- [x] 新增 `MapArea` 类型别名:`IMapAreaInterval[]`,表示一个区域
|
||||
- [x] 修改 `ILayerState`:
|
||||
- [x] 新增 `readonly width: number` 和 `readonly height: number`
|
||||
- [x] 修改 `addLayer` 签名,移除 `width`/`height` 参数(使用 `LayerState` 自身尺寸)
|
||||
- [x] 修改 `resizeLayer` 签名:移除 `layer` 参数,改为对整个楼层所有图层同步 resize
|
||||
- [x] 修改 `IMapLayer`:
|
||||
- [x] 移除 `resize` / `resize2`(改为 `MapLayer` 内部方法)
|
||||
- [x] 修改 `IMapStore`:
|
||||
- [x] 将 `readonly maps` 类型改为 `ReadonlyArray<string>`
|
||||
- [x] 修改 `createLayerState` 签名:新增 `width: number`、`height: number` 参数
|
||||
- [x] 新增 `setMapList(maps: string[]): void`
|
||||
- [x] 新增 `useManualOrder(sort: (arr: string[]) => string[]): void`
|
||||
- [x] 新增 `setArea(areas: Set<MapArea>): void`
|
||||
- [x] 新增 `activeArea(id: string): void`
|
||||
- [x] 新增 `deactiveArea(id: string): void`
|
||||
- [x] 新增 `useAutoActivitor(enable: boolean): void`
|
||||
- [x] 新增 `notifyEnterFloor(id: string): void`
|
||||
|
||||
### `@user/data-base/src/map/mapStore.ts`
|
||||
|
||||
- [x] 将 `maps: Set<string>` 改为 `maps: string[]`
|
||||
- [x] 修改 `createLayerState`:添加 `width`/`height` 参数,不再维护 `maps`
|
||||
- [x] 实现 `setMapList`
|
||||
- [x] 实现 `useManualOrder`
|
||||
- [x] 新增 `private areaList: Set<MapArea>`
|
||||
- [x] 新增 `private lastFloorId: string | null`
|
||||
- [x] 新增 `private autoActivitorEnabled: boolean`
|
||||
- [x] 实现 `setArea`、`activeArea`、`deactiveArea`
|
||||
- [x] 实现 `useAutoActivitor`
|
||||
- [x] 实现 `notifyEnterFloor`
|
||||
|
||||
### `@user/data-base/src/map/layerState.ts`
|
||||
|
||||
- [x] 新增 `width: number` 和 `height: number` 成员(由构造参数初始化)
|
||||
- [x] 修改 `addLayer`,移除 `width`/`height` 参数,使用 `this.width`/`this.height`
|
||||
- [x] 修改 `resizeLayer`,移除 `layer` 参数,改为对所有图层同步 resize
|
||||
|
||||
### `@user/data-base/src/map/mapLayer.ts`
|
||||
|
||||
- [x] 将 `resize`/`resize2` 改为内部方法(从公共接口移除)
|
||||
|
||||
---
|
||||
@@ -0,0 +1,294 @@
|
||||
# 需求综述
|
||||
|
||||
当前 `LayerState` 只存储当前激活地图的数据,切换地图时原地图内容彻底丢失,
|
||||
无法参与存档系统。为此引入 `IMapStore`,集中管理所有楼层的 `LayerState`,
|
||||
并实现 `ISaveableContent` 接口以支持存档读档。
|
||||
|
||||
核心目标:
|
||||
|
||||
- 多楼层数据同时存在于内存中,通过 id 访问;
|
||||
- 通过 `active` 标记区分"玩家可能到达"与"无需关注"的楼层,节省存档开销;
|
||||
- 通过 `compareWith` 提供参考基准,配合分级压缩大幅减少存档体积;
|
||||
- `IStateBase.layer` 类型由 `ILayerState` 改为 `IMapStore`,
|
||||
操作楼层必须先通过 `getLayerState(id)` 取得具体楼层。
|
||||
|
||||
---
|
||||
|
||||
# 实现思路
|
||||
|
||||
## 1. 给 LayerState 添加 active 成员
|
||||
|
||||
`ILayerState` 新增 `readonly active: boolean` 和 `setActiveStatus(active: boolean): void`
|
||||
两个接口,`LayerState` 实现类中 `active` 默认为 `false`。
|
||||
|
||||
两者的关系:
|
||||
|
||||
- `ILayerState.setActiveStatus`:直接操作楼层对象;
|
||||
- `IMapStore.setMapActiveStatus(id, active)`:通过 id 操作,
|
||||
内部查找对应楼层后调用其 `setActiveStatus`。
|
||||
|
||||
## 2. 脏数据追踪(dirty tracking)
|
||||
|
||||
为支持 `LowCompression` 和 `HighCompression` 的差分存档,
|
||||
需要知道哪些楼层相对于参考基准是否发生了修改。
|
||||
|
||||
**推荐方案:楼层级简单脏标记 + 存档时实际比较**
|
||||
|
||||
在 `LayerState` 内部维护 `private dirty: boolean = false`:
|
||||
|
||||
- 当楼层内任意 `MapLayer` 触发 `onUpdateBlock`、`onUpdateArea`、`onResize`
|
||||
钩子时,将 `dirty` 置为 `true`;
|
||||
- `dirty` 只在 `compareWith` 首次调用时根据实际数据对比结果初始化,
|
||||
初始化后的 gameplay 过程中不再重置(仅置 true)。
|
||||
|
||||
在 `saveState` 时:
|
||||
|
||||
- 若 `dirty = false`,跳过该楼层(初始化后从未被触碰过);
|
||||
- 若 `dirty = true`:
|
||||
- **LowCompression**:与参考基准进行全量比较,若完全一致则跳过
|
||||
(消除"改了又改回"场景下的误判),否则存储所有行;
|
||||
- **HighCompression**:逐行与参考基准比较,只存储不一致的行。
|
||||
|
||||
在 `loadState` 时:若存档中此楼层没有任何数据(即未出现在 `floors` 中),
|
||||
读档后将 `dirty` 置为 `false`(视为与参考基准一致)。
|
||||
|
||||
**不在 `MapLayer` 内维护 `dirtyRows`**,行级比较在 `saveState` 时直接对照参考基准进行。
|
||||
这避免了每次 `setBlock`/`putMapData` 都更新行级标记的热路径开销,
|
||||
且存档时实际比较已能消除误判,无需 `probablyDirty` + `setInterval` 或哈希方案。
|
||||
|
||||
存档时比较的开销:Uint32Array 内存连续,实际耗时极低,且保存操作本身是低频的,
|
||||
若将来发现存档耗时问题,可考虑将比较逻辑移至 Web Worker。
|
||||
|
||||
## 3. compareWith 接口与参数类型
|
||||
|
||||
```ts
|
||||
compareWith(ref: Map<string, Map<number, Uint32Array>>): void;
|
||||
```
|
||||
|
||||
外层 `Map` 以楼层 id 为键,内层 `Map` 以图层 `zIndex` 为键,
|
||||
值为对应图层的完整图块数组(Uint32Array,含所有行的扁平数据)。
|
||||
|
||||
使用此类型而非 `IMapStore` 的理由:接口更轻量,调用方可直接从游戏原始数据构建,
|
||||
无需额外持有一个完整的 `IMapStore` 实例。
|
||||
|
||||
关于图层标识符:继续使用 `zIndex`,在单个楼层内 `zIndex` 是语义唯一的,
|
||||
与已有 `MapLayer.zIndex` 接口保持一致。
|
||||
|
||||
**`compareWith` 以首次调用为唯一基准**,再次调用不更新参考(以游戏原始数据为基准,
|
||||
避免存档之间产生依赖关系)。
|
||||
|
||||
实现步骤:
|
||||
|
||||
1. 若 `refData` 已存在,直接返回;
|
||||
2. 保存 `ref` 引用到 `private refData`;
|
||||
3. 遍历当前所有楼层,对每个楼层在 `ref` 中查找对应 id:
|
||||
- 不存在:`dirty = true`(新楼层,视为全脏);
|
||||
- 存在:对每个 `MapLayer`(按 `zIndex` 匹配)做全量比较,
|
||||
若所有行与参考数据完全一致则 `dirty = false`,否则 `dirty = true`。
|
||||
|
||||
## 4. 楼层的创建与管理
|
||||
|
||||
`MapStore` 内部以 `Map<string, LayerState>` 存储所有楼层。
|
||||
`getLayerState(id)` 对不存在的 id 直接返回 `null`,不自动创建。
|
||||
|
||||
只提供一个创建接口:
|
||||
|
||||
- `createLayerState(id: string): ILayerState`:创建并注册一个空白楼层
|
||||
(无任何 `MapLayer`,用户拿到后再调用 `addLayer` 配置图层结构),返回楼层对象。
|
||||
|
||||
注册时若 id 已存在,发出 logger 警告并覆盖。
|
||||
|
||||
若 `compareWith` 已调用后再通过上述接口新增楼层,新楼层直接视为全脏(`dirty = true`),
|
||||
因为 `refData` 中不存在对应数据。
|
||||
|
||||
## 5. 存档数据格式
|
||||
|
||||
### 类型定义
|
||||
|
||||
```ts
|
||||
/** 单个 MapLayer 的存档数据 */
|
||||
interface IMapLayerSave {
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
/**
|
||||
* key = 行索引,value = 该行完整的 Uint32Array 数据;
|
||||
* NoCompression/LowCompression 时包含所有行(0 到 height - 1);
|
||||
* HighCompression 时只包含与参考基准不同的行;
|
||||
* 读档时,不在此 Map 中的行从参考基准还原。
|
||||
*/
|
||||
readonly rows: ReadonlyMap<number, Uint32Array>;
|
||||
}
|
||||
|
||||
/** 单个楼层的存档数据 */
|
||||
interface ILayerStateSave {
|
||||
readonly background: number;
|
||||
/**
|
||||
* key = zIndex,value = 对应图层存档数据;
|
||||
* 使用 Map 格式以支持图层的动态增删。
|
||||
*/
|
||||
readonly layers: ReadonlyMap<number, IMapLayerSave>;
|
||||
}
|
||||
|
||||
/** 整个 MapStore 的存档数据 */
|
||||
interface IMapStoreSave {
|
||||
/**
|
||||
* key = 楼层 id,只包含 active 的楼层;
|
||||
* inactive 的楼层不写入,读档时无需处理。
|
||||
*/
|
||||
readonly floors: ReadonlyMap<string, ILayerStateSave>;
|
||||
}
|
||||
```
|
||||
|
||||
### 各压缩等级存储策略
|
||||
|
||||
| 压缩级别 | 楼层粒度 | 行粒度 |
|
||||
| ----------------- | --------------------------------------------------------------- | ------------------------ |
|
||||
| `NoCompression` | 存储所有 active 楼层 | 存储该楼层所有行 |
|
||||
| `LowCompression` | 跳过 `dirty = false` 的楼层;dirty 楼层全量比较后仍一致的也跳过 | 存储该楼层所有行 |
|
||||
| `HighCompression` | 同 LowCompression | 只存储与参考基准不同的行 |
|
||||
|
||||
### 读档策略
|
||||
|
||||
读档时直接操作数组引用(通过 `setMapRef`),避免逐行拷贝的额外开销:
|
||||
|
||||
1. 若参考基准(`refData`)未设置,抛出 logger 错误,**不进行任何读档操作**;
|
||||
2. 遍历 `state.floors`,对每个楼层 id:
|
||||
- 若当前 `MapStore` 中不存在该 id,发出 logger 警告并跳过;
|
||||
- 对该楼层每个图层,先从参考基准取出对应 `zIndex` 的数组,
|
||||
将其深拷贝为新数组作为底层(确保未存档行使用参考基准值);
|
||||
- 再将 `ILayerStateSave.layers` 中对应图层的 `rows` 数据写入该数组的对应行;
|
||||
- 调用 `MapLayer.setMapRef(array)` 直接替换内部引用,无需额外拷贝;
|
||||
3. 对未出现在 `state.floors` 中的 active 楼层,
|
||||
从参考基准深拷贝完整数组后调用 `setMapRef` 还原,并将 `dirty` 置为 `false`。
|
||||
|
||||
## 6. saveState / loadState 实现
|
||||
|
||||
根据压缩等级分别编写三个存档函数和三个读档函数,
|
||||
`saveState(compression)` 和 `loadState(state, compression)` 根据 `compression` 分发,
|
||||
无需在每个楼层的遍历循环内部判断等级:
|
||||
|
||||
- `private saveNoCompression(): IMapStoreSave`
|
||||
- `private saveLowCompression(): IMapStoreSave`
|
||||
- `private saveHighCompression(): IMapStoreSave`
|
||||
- `private loadNoCompression(state: IMapStoreSave): void`
|
||||
- `private loadLowCompression(state: IMapStoreSave): void`
|
||||
- `private loadHighCompression(state: IMapStoreSave): void`
|
||||
|
||||
`saveState` 结果需通过 `structuredClone` 深拷贝后返回。
|
||||
|
||||
## 7. IMapStore 接口设计(新增到 `map/types.ts`)
|
||||
|
||||
```ts
|
||||
interface IMapStore extends ISaveableContent<IMapStoreSave> {
|
||||
/** 所有楼层的 id 集合 */
|
||||
readonly maps: ReadonlySet<string>;
|
||||
|
||||
// --- 楼层访问 ---
|
||||
/** 获取指定 id 的楼层状态,不存在则返回 null */
|
||||
getLayerState(id: string): ILayerState | null;
|
||||
/** 获取指定 id 的楼层状态,要求楼层必须是 active 的,否则返回 null */
|
||||
getActiveMap(id: string): ILayerState | null;
|
||||
|
||||
// --- 楼层管理 ---
|
||||
/** 创建并注册一个空白楼层,返回楼层状态对象 */
|
||||
createLayerState(id: string): ILayerState;
|
||||
|
||||
// --- active 管理 ---
|
||||
/** 获取指定 id 的楼层是否激活,不存在的 id 返回 false */
|
||||
isMapActive(id: string): boolean;
|
||||
/** 设置指定 id 楼层的激活状态 */
|
||||
setMapActiveStatus(id: string, active: boolean): void;
|
||||
/** 迭代所有 active 的楼层,yield [id, ILayerState] */
|
||||
iterateActiveMaps(): Iterable<[string, ILayerState]>;
|
||||
/** 迭代所有 inactive 的楼层,yield [id, ILayerState] */
|
||||
iterateInactiveMaps(): Iterable<[string, ILayerState]>;
|
||||
/** 迭代所有楼层,yield [id, ILayerState] */
|
||||
iterateAllMaps(): Iterable<[string, ILayerState]>;
|
||||
|
||||
// --- 差分压缩基准 ---
|
||||
/**
|
||||
* 设置压缩参考基准,以首次调用为唯一基准,再次调用不更新。
|
||||
* @param ref 外层 key = 楼层 id,内层 key = zIndex,value = 图层完整图块数据
|
||||
*/
|
||||
compareWith(ref: Map<string, Map<number, Uint32Array>>): void;
|
||||
}
|
||||
```
|
||||
|
||||
## 8. ILayerState 接口修改
|
||||
|
||||
在现有 `ILayerState` 上新增:
|
||||
|
||||
```ts
|
||||
/** 此楼层是否处于激活状态 */
|
||||
readonly active: boolean;
|
||||
/** 设置楼层激活状态 */
|
||||
setActiveStatus(active: boolean): void;
|
||||
```
|
||||
|
||||
## 9. IStateBase 修改
|
||||
|
||||
将 `IStateBase.layer: ILayerState` 改为 `IStateBase.layer: IMapStore`。
|
||||
|
||||
---
|
||||
|
||||
# 涉及文件
|
||||
|
||||
## 需要引用的文件
|
||||
|
||||
- `@user/common/types.ts`: `ISaveableContent`, `SaveCompression`
|
||||
- `@user/data-base/map/types.ts`: 全部现有地图接口(`IMapLayer`, `ILayerState`, 等)
|
||||
|
||||
## 需要修改的文件
|
||||
|
||||
### `@user/data-base/src/map/types.ts`
|
||||
|
||||
- [x] 新增 `IMapLayerSave` 接口:单个 MapLayer 存档数据格式
|
||||
- [x] 新增 `ILayerStateSave` 接口:单个楼层存档数据格式
|
||||
- [x] 新增 `IMapStoreSave` 接口:MapStore 整体存档数据格式
|
||||
- [x] 修改 `ILayerState`:新增 `readonly active: boolean` 和
|
||||
`setActiveStatus(active: boolean): void`
|
||||
- [x] 修改 `IMapLayer`:新增 `setMapRef(array: Uint32Array): void`
|
||||
- [x] 新增 `IMapStore` 接口:继承 `ISaveableContent<IMapStoreSave>`,
|
||||
含全部接口(见第 7 节)
|
||||
|
||||
### `@user/data-base/src/map/mapLayer.ts`
|
||||
|
||||
### `@user/data-base/src/map/layerState.ts`
|
||||
|
||||
- [x] 新增 `active: boolean = false` 成员:楼层激活状态
|
||||
- [x] 实现 `setActiveStatus(active: boolean): void`
|
||||
- [x] 新增 `private dirty: boolean = false` 成员:楼层级脏标记
|
||||
- [x] 修改 `StateMapLayerHook.onUpdateArea`、`onUpdateBlock`、`onResize`:
|
||||
在转发钩子的同时,将 `state.dirty` 置 `true`
|
||||
- [x] 新增 `isDirty(): boolean` 方法:返回 `this.dirty`,供 `MapStore` 读取
|
||||
- [x] 新增 `setDirty(dirty: boolean): void` 方法:
|
||||
供 `MapStore.compareWith` 时根据实际比较结果设置
|
||||
|
||||
### `@user/data-base/src/map/mapLayer.ts`
|
||||
|
||||
- [x] 新增 `setMapRef(array: Uint32Array): void` 方法:
|
||||
直接替换内部图块数组引用,跳过拷贝,供 `MapStore` 读档时使用。
|
||||
需确保传入数组长度与 `width × height` 匹配,
|
||||
并触发必要的钩子通知(不触发 `onResize`,应触发 `onUpdateArea` 通知全区域更新)。
|
||||
在方法注释中明确标注:调用后不得再持有或修改传入的数组。
|
||||
|
||||
### `@user/data-base/src/map/mapStore.ts`(新文件)
|
||||
|
||||
- [x] 实现 `MapStore` 类,实现 `IMapStore`
|
||||
- [x] `private mapData: Map<string, LayerState>`:楼层 id 到状态对象的映射
|
||||
- [x] `readonly maps: ReadonlySet<string>`:所有楼层 id 的只读集合视图
|
||||
- [x] `private refData: Map<string, Map<number, Uint32Array>> | null`:参考基准
|
||||
- [x] 实现 `getLayerState`、`getActiveMap`、`createLayerState`
|
||||
- [x] 实现 `isMapActive`、`setMapActiveStatus`、`iterateActiveMaps`、`iterateInactiveMaps`、`iterateAllMaps`
|
||||
- [x] 实现 `compareWith`
|
||||
- [x] 实现 `saveNoCompression`、`saveLowCompression`、`saveHighCompression`
|
||||
- [x] 实现 `loadNoCompression`、`loadLowCompression`、`loadHighCompression`
|
||||
- [x] 实现 `saveState(compression)` 和 `loadState(state, compression)` 分发
|
||||
|
||||
### `@user/data-base/src/map/index.ts`
|
||||
|
||||
- [x] 补充导出 `mapStore.ts`
|
||||
|
||||
### `@user/data-base/src/types.ts`
|
||||
|
||||
- [x] 将 `IStateBase.layer` 类型由 `ILayerState` 改为 `IMapStore`
|
||||
Reference in new issue
Block a user