前言:
在软件开发过程中,我们都希望能设计出一个稳健的,可维护的系统,为了实现这个目的,人们总结出了很多相关的设计原则,比如SOLID原则, KISS原则等等。SOLID每个字母代表了一种设计原则,具体大家可以去看《架构整洁之道》这本书或者在网上找相关的博客学习。在本系统中将应用这些原则,力求得到一个可用好用可理解可改动的通用地图模块。SOLID原则中的D,指的是Dependence Inversion Principle
,中文可以叫依赖倒置原则或者依赖反转原则。其强调的是在处理依赖关系时,应当尽可能依赖抽象类型,而不是具体实现。 在绝大多数情况下,抽象的东西是稳定的,在设计这个抽象类或者接口时,我们往往会带着前瞻性的思维去考虑,这个接口设计得是否合理?是否方便使用者去实现去拓展?如果一个抽象的对象发生变化了,那对应的所有实现一定会发生改变,而如果具体实现发生变更,不会影响到抽象接口。因此我们有理由判定,接口是不太容易变更的,如果一个接口被反复修改,那一定是设计这个接口的时候有地方不合理:接口承担的内容过多;或者是接口方法不够通用,带进来了一些业务相关的东西。依赖反转原则也是本文主要讨论的原则。
正文
结构组成
本系统主要由三部分内容组成,一个是基础的数据结构模块,一个是寻路模块,一个是地图模块。三个模块功能内聚,但也存在耦合,为了防止一个模块修改导致其他模块发生变更,所以在耦合时,尽量使其仅依赖其他模块中的接口,因此模块具体实现修改并不会导致其他模块受到影响。下面将简单介绍这三个模块的内容与设计思路。
数据结构模块
基础的数据结构模块提供了一个最小堆和最大堆,这是具体的实现,其目的是为寻路模块提供服务的,寻路模块直接依赖了数据结构模块,如果从设计的角度出发,这其实是不应该的,因为前面说了我们要依赖抽象类或接口,而不是具体实现,不过我们也可以发现,最小堆和最大堆相对来说是一个较稳定的东西,其设计完成一旦通过验证,就不太会有理由再去修改它们,因此可以认为这个虽然是具体实现,但是是一个不容易发生变更的具体实现,所以直接依赖这个具体实现也是没问题的。如果非常希望解除耦合将这个直接依赖给取消掉,我们也可以考虑引入一个IHeap
的接口,让寻路模块依赖这个接口,同时原来的最小堆最大堆实现该接口,因此寻路依赖堆实现类,变成了寻路和堆实现类均依赖IHeap
接口。
原依赖关系
新依赖关系
寻路模块
寻路模块中定义了一个寻路的接口IPathSearch
,并实现了三种A星算法,分别是原始的A星算法和两种经过一定优化后的A星算法,实现这个接口的好处在于,如果后期我们需要针对特定的场景比如横冲技能,使用额外的寻路规则,仅需额外再实现一种新的寻路算法,并在原本实例化寻路算法的地方改成实例化新的寻路算法即可,而使用的地方由于依赖的是IPathSearch
接口,因此完全不需要做任何修改。
地图模块
地图模块主要分成地图抽象层、地图具体实现和地图生成三个部分,具体可查看文章后面的类图或项目代码。
地图抽象层定义了整个地图模块的基本要素和必要方法,比如IMapGrid
接口定义了地图的基本数据,对外的方法等等。该接口依赖INode
接口,INode
接口定义了一个最基础的地图节点是啥样的,而IPathNode
则在INode
的基础上,增加了寻路相关的属性。另外定义了一个IMapShow
的接口,负责处理地图节点的实际显示,比如绑定地图数据层和实际节点对象等功能。INodeEntity
定义了节点的基本操作,比如设置高亮等等。在这个层中定义的接口,均不依赖某个具体实现,但定义了地图的几乎所有行为,在实现功能的前提下保证了该层的稳定性。
地图具体实现则实现了两种常见的游戏地图:网格地图和六边形地图。这两种地图实现了IMapGrid
的方法,并在实例化的地方注入了创建节点的方法,使得地图类型和节点可以摆脱依赖,便于后面实现同种类的地图不同的节点。
地图生成部分定义了一个生成配置类和生成类,配置类负责配置要生成的地图种类,地图所处的平面,以及各种类地图的一些独有参数。生成类依赖配置来创建具体的地图,并为其绑定对应的显示。
系统完整类图
看不清可以去这里查看。
额外的实现细节
系统的整体结构与设计如上文所示,为了实现地图的通用和寻路通用,还有一些比较巧妙的设计,这里进行补充阐述。
- 在寻路的时候,经常会需要从一个节点访问到该节点的一圈邻居,但是不同的地图,获取某个节点邻居的方式不同,为了通用我们需要定义一个方法,能直接获取到某节点的所有邻居。本系统定义了一个
IDir
接口,用来获取一个节点四周邻居相对于该节点的索引偏移,这样在寻路时可以通过该节点和索引偏移计算出周围所有邻居的节点。在网格地图中,分为了两种,一种是四方向的,一种是八方向的,对应的索引偏移列表不同。而对于六边形地图,则是另一种索引偏移,所以在本系统中,分别实现了三个具体实现,并通过配置来决定使用哪组偏移列表。 - 为了高效在六边形地图中实现范围高亮,六边形使用了Cube coordinates,这个名词来自于Amit Patel的《Hexagonal Grids 》中,其使用了三个轴来表示每个节点,使得在访问邻居时,可以在三个轴上做索引偏移,大大减少了查找邻居的消耗。这里我最开始是使用了偏移坐标,但是在实际去高亮大范围节点时发现并没有那么轻松,尤其是高亮可能发生在地图边缘,后面忍痛对此做了重构改用立方体坐标了。
- 在处理扇形高亮时,实际上就是从圆形高亮中抠出一部分,因此其内部是判断一个朝向,并根据扇形宽度算出扇形左右最大偏移角,最后调用圆形高亮的方法并添加一个角度的Filter对节点做筛选。
- 地图配置类中,增加了编辑器类,来对配置类Inspector界面做个性化显示,根据选取的地图类型,来展示该类型所需要的独有参数,并隐藏无关的参数。
总结
以上便是整个地图系统的全部内容和设计思路了,在类图中,箭头代表了依赖关系走向,A指向B表示A依赖B。从图中不难看出,箭头指向的对象基本都是接口,抽象类,或者是一些不太会发生变化的东西。如数据结构小节中的图示,即便依赖的对象是具体实现,我们也可以引入更抽象的接口对象,实现依赖反转,使得整个依赖关系走向更合理,系统更稳定可靠。当然这个系统并不完善,也可能不如料想的那般好用,但我希望通过开源出来给更多的人看见,给更多的人使用,并提供反馈来完善这个东西,使得其可以代代相传(doge。
项目地址:
https://github.com/tang-xiaolong/MapGridInUnity
引用:
- Amit Patel.《Hexagonal Grids 》https://www.redblobgames.com/grids/hexagons/
- Catlike Coding.《Hex Map》https://catlikecoding.com/unity/tutorials/hex-map/
- Robert C.Martin. 《架构整洁之道》