6.7 KiB
本示例文档以物体移动为例,代码参考
packages-user/data-common/src/common/mover.ts。文档较为简略,实际编写的时候可以更丰富一些。
需求综述
为提供更便捷的物体移动接口,故进行本设计。本设计提供统一的移动接口 IObjectMover,并完成其抽象实现 abstract class ObjectMover,以方便具体拓展。
需求理解
明确需求
- 移动与物体分离。移动与物体是独立的两个系统,二者之间不应存在任何耦合,移动单方面调用物体。
- 移动计划。移动器需要包含移动计划及其执行两个阶段,先形成计划,再进行执行。
- 移动流程控制。移动时需要提供相应的接口干预移动本身。
隐含需求
- 异步执行。移动自然需要动画,因此移动本身必然是异步的。
- 移动物体。不是所有的物体都能够移动,只有某些可以,这些应该被称为可移动物体。
未确定需求
- 一个物体能不能绑定两个移动器?
设计前提
- 网格移动。所有移动均基于离散网格,不支持连续空间移动。
- 允许自定义拓展。允许自定义移动策略,不是所有的移动都由某一个类统一实现。
核心概念定义
这里是一个示例,实际情况中可以更丰富一些。
移动
一个物体的坐标从一点改变至另一点的行为称为移动。
可移动物体
所有可以被移动器移动的物体都可以被称为可移动物体,它们的核心特征是包含坐标信息,并拥有设置其坐标的接口。最常见的可移动物体是地图上的动态图块以及玩家操控的勇士。
移动器
一种对移动的抽象接口,可以通过调用可移动物体的坐标接口来操控可移动物体,但其提供的接口往往更加丰富,且拥有计划机制,允许进行连续移动。
接口设计分析
默认场景下,移动主要发生在两种物体上:地图动态图块及玩家操控的勇士。二者的主要差别在于后者由玩家操控,需要进行移动判定,例如需要进行通行性判定,事件触发判定等,而前者通常由系统代码控制,且常用于演出,并不需要进行判定。
IObjectMover
设计思路
这部分应当分析需求,并写明接口是如何设计出来的,而不是简单地阐述接口内容。
对于玩家操控,判定往往发生在移动前,因此为统一接口,设计出 onStepStart 与 onStepEnd 两个接口,前者进行判定,后者根据判定结果进行相应的操作。为了让后者知道判定结果,将 onStepStart 的返回值设计为 Promise<number>,onStepEnd 接收之并执行操作。
在实际情况中,常见的使用场景是先规划好线路,然后让物体移动,因此接口需要设计成计划式,计划完成后通过 start 接口开始移动,故需要两个接口 onMoveStart 和 onMoveEnd 分别在移动开始前和结束后执行某些操作。
为了能够在开始移动后对移动进行干预及监听,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):预期频率中频。移动中修改移速有一定使用场景,但远不及forward、step等移动接口的频率,通常只在特殊演出或逻辑中出现,故为中频。IObjectMover.stepFace(move: FaceDirection, face: FaceDirection, count?: number):预期频率低频。移动方向与朝向不同的常见场景(后退)已由backward覆盖,只有极特殊情况才需要此接口(如角色朝向固定但沿垂直方向平移),相当罕见,故为低频。
预期体量
预期代码体量为 200-300 行。分析如下:
- 计划功能预期 100 行:
IObjectMover首先需要完成计划存储与计划的定义,这些接口基本大致就是向数组中添加元素,每个方法内容都不多。 - 移动流程预期 200 行:
IObjectMover还需要完成移动流程的编写工作,需要根据每个移动步按照流程执行不同的行为,这一过程较为复杂。
IMoveController
...
实现思路
仅列出实现思路较为复杂的内容,例如像计划存储等属于非常直观简单的内容,不需要列出实现思路。
移动流程控制
核心难点在于如何进行移动控制,以下为核心思路。
- 为实现移动流程,首先需要写一个大循环来处理每一个移动步。
- 对于每一个移动步,首先调用
onStepStart接口,获取移动code,然后触发钩子,然后进行onStepEnd的执行,其返回值作为移动后的目标位置,最后触发钩子。
涉及文件
@user/data-common/src/common/mover.ts
- 编写
IObjectMover接口:按照本文描述完成设计 - 编写
IObjectMovable接口:按照本文描述完成设计 - 编写
IMoveController接口:按照本文描述完成设计 - 完成每个移动步的接口设计:每个移动步都需要一个接口对象,本文未明确提及,需包含每种移动计划
- 完成移动枚举的编写:每种移动计划都需要一个枚举类型来标注
- 编写
ObjectMover抽象类:实现IObjectMover接口,作为移动基类