template/docs/dev/template.md
2026-07-10 15:30:20 +08:00

6.7 KiB
Raw Blame History

本示例文档以物体移动为例,代码参考 packages-user/data-common/src/common/mover.ts。文档较为简略,实际编写的时候可以更丰富一些。

需求综述

为提供更便捷的物体移动接口,故进行本设计。本设计提供统一的移动接口 IObjectMover,并完成其抽象实现 abstract class ObjectMover,以方便具体拓展。

需求理解

明确需求

  1. 移动与物体分离。移动与物体是独立的两个系统,二者之间不应存在任何耦合,移动单方面调用物体。
  2. 移动计划。移动器需要包含移动计划及其执行两个阶段,先形成计划,再进行执行。
  3. 移动流程控制。移动时需要提供相应的接口干预移动本身。

隐含需求

  1. 异步执行。移动自然需要动画,因此移动本身必然是异步的。
  2. 移动物体。不是所有的物体都能够移动,只有某些可以,这些应该被称为可移动物体。

未确定需求

  1. 一个物体能不能绑定两个移动器?

设计前提

  1. 网格移动。所有移动均基于离散网格,不支持连续空间移动。
  2. 允许自定义拓展。允许自定义移动策略,不是所有的移动都由某一个类统一实现。

核心概念定义

这里是一个示例,实际情况中可以更丰富一些。

移动

一个物体的坐标从一点改变至另一点的行为称为移动。

可移动物体

所有可以被移动器移动的物体都可以被称为可移动物体,它们的核心特征是包含坐标信息,并拥有设置其坐标的接口。最常见的可移动物体是地图上的动态图块以及玩家操控的勇士。

移动器

一种对移动的抽象接口,可以通过调用可移动物体的坐标接口来操控可移动物体,但其提供的接口往往更加丰富,且拥有计划机制,允许进行连续移动。

接口设计分析

默认场景下,移动主要发生在两种物体上:地图动态图块及玩家操控的勇士。二者的主要差别在于后者由玩家操控,需要进行移动判定,例如需要进行通行性判定,事件触发判定等,而前者通常由系统代码控制,且常用于演出,并不需要进行判定。

IObjectMover

设计思路

这部分应当分析需求,并写明接口是如何设计出来的,而不是简单地阐述接口内容。

对于玩家操控,判定往往发生在移动前,因此为统一接口,设计出 onStepStartonStepEnd 两个接口,前者进行判定,后者根据判定结果进行相应的操作。为了让后者知道判定结果,将 onStepStart 的返回值设计为 Promise<number>onStepEnd 接收之并执行操作。

在实际情况中,常见的使用场景是先规划好线路,然后让物体移动,因此接口需要设计成计划式,计划完成后通过 start 接口开始移动,故需要两个接口 onMoveStartonMoveEnd 分别在移动开始前和结束后执行某些操作。

为了能够在开始移动后对移动进行干预及监听,start 应返回 IMoveController 作为返回值,其中应包含临时在当前步后插入一步、在当前移动队列后插入一步、下一步结束后立刻停止移动、当移动结束后兑现的 Promise 等内容。

接下来考虑移动计划,最通用的移动方法是指定移动方向与物体的面朝朝向,然后物体面朝指定朝向,向移动方向移动一格,故需要 stepFace 接口。在绝大多数情况下,移动方向与朝向是一致的,故提供 step 接口。除了这种情况,还会包含前进一格的需求,故提供 forward 接口;与之对应,还提供 backward 后退接口,而且在多数情况下,后退往往代表面朝前向后走,这里也使用这一语义。在后退时,移动动画的播放进度应该相反,故提供 animDir 接口。在移动时,移动速度是一个重要参数,故提供 speed 接口。在某些情况下,物体可能会需要瞬移,故提供 tp 接口。除了这些,还需要提供转向接口 face,调整面朝朝向;以及一个相对比较常见的 jump 接口。

对于移动对象,仅需要 x y setPos getCurrentFaceDirection 接口即可完成所有上述行为,故将这几个接口包装为 IObjectMovable,同时 interface IObjectMover<T extends IObjectMovable> 允许自定义拓展。

接口分析

此处仅列出部分。

  • IObjectMover.forward(count?: number):预期频率高频。向前移动一格是地图行走、动画演出等场景的核心需求,在逻辑与演出中都会频繁出现,故为高频。
  • IObjectMover.speed(speed: number):预期频率中频。移动中修改移速有一定使用场景,但远不及 forwardstep 等移动接口的频率,通常只在特殊演出或逻辑中出现,故为中频。
  • IObjectMover.stepFace(move: FaceDirection, face: FaceDirection, count?: number):预期频率低频。移动方向与朝向不同的常见场景(后退)已由 backward 覆盖,只有极特殊情况才需要此接口(如角色朝向固定但沿垂直方向平移),相当罕见,故为低频。

预期体量

预期代码体量为 200-300 行。分析如下:

  • 计划功能预期 100 行:IObjectMover 首先需要完成计划存储与计划的定义,这些接口基本大致就是向数组中添加元素,每个方法内容都不多。
  • 移动流程预期 200 行:IObjectMover 还需要完成移动流程的编写工作,需要根据每个移动步按照流程执行不同的行为,这一过程较为复杂。

IMoveController

...

实现思路

仅列出实现思路较为复杂的内容,例如像计划存储等属于非常直观简单的内容,不需要列出实现思路。

移动流程控制

核心难点在于如何进行移动控制,以下为核心思路。

  1. 为实现移动流程,首先需要写一个大循环来处理每一个移动步。
  2. 对于每一个移动步,首先调用 onStepStart 接口,获取移动 code,然后触发钩子,然后进行 onStepEnd 的执行,其返回值作为移动后的目标位置,最后触发钩子。

涉及文件

@user/data-common/src/common/mover.ts

  • 编写 IObjectMover 接口:按照本文描述完成设计
  • 编写 IObjectMovable 接口:按照本文描述完成设计
  • 编写 IMoveController 接口:按照本文描述完成设计
  • 完成每个移动步的接口设计:每个移动步都需要一个接口对象,本文未明确提及,需包含每种移动计划
  • 完成移动枚举的编写:每种移动计划都需要一个枚举类型来标注
  • 编写 ObjectMover 抽象类:实现 IObjectMover 接口,作为移动基类