2DMapFog
这是一个关于2D游戏地图迷雾的解决方案。
该方案是针对我们项目进行定制的,但是在开发过程中一些思路自认还有点价值,因而提出接受大家的臭鸡蛋(自嘲)!由于是针对我们项目定制,因而一些诸如迷雾打开后再关闭的功能并未实现。
开发经历
项目经过三个开发阶段,功能虽小,却也经历了一次代码开发的完整周期了。
- 原始暴力开发阶段
- 跑错方向的开发阶段
- 找到最适合方向后的开发阶段
每一个阶段都有一些有意思的事情发生,所以会把三个阶段展开,详细聊一聊发生了什么值得开发人员注意的事情。
原始暴力开发阶段
这一阶段很有意思,因为这一阶段所做的事情是我们开发代码常常做的:在我们获取到需求之后,不过多做分析而直接写代码。
解决方案
不得不说这么做在有时候是很快速的,尤其是在做过一些很小的功能之后,经验常常使我们偷懒,这次也一样。
我们为了实现2D迷雾,直接使用一张Texture存储迷雾数据,然后将这张Texture交给材质球使用,做出2D的迷雾效果。一切都很简单,不是吗?
是很简单!可是却是个大麻烦!
麻烦来了
当我们准备将地图尺寸扩大时,麻烦出现了。
- Texture这种东西不可能太大,否则占用手机有限的带宽造成帧率下降之外,还可能超过手机可以支持的最大图片尺寸。
- 在固定Texture尺寸的情况下,如果地图尺寸不断增大,那么分辨率就会越来越小,让玩家看像素或者让玩家看蚂蚁都是我们不能忍受的。
怎么解决麻烦?
好吧,这两个矛盾点够麻烦了。那么第二步就该是我们重构来解决这个问题了。既然当前矛盾点是地图尺寸变大与Texture尺寸不能太大,那么解决矛盾就好了嘛!
好吧,目标是确定了,可是这却造成了第二步从出发点就错误了,这点留到第二步讲。先看看第二步我们是怎么解决的吧,起码**是好**,只是不适合我们项目。
跑错方向的开发阶段
虽说跑错方向,可是**还是有些价值的,起码对我们项目来说,它的价值就是验证了它对我们项目没有价值(绕不绕?作者是说相声的程序员,哈哈)。
解决方案
既然用一张Texture表示整张地图显得捉襟见肘,那么就使用多张Texture来表示吧!
其实就是在确定了上述中心**之后,我们成功将自己引进了一个大坑。虽然坑,可是在不知情的情况下我们依旧挥舞着小锄头。
使用多张Texture来表示整张地图,有一个问题就是当视界(注意,视界就是视野范围)在两份甚至多份Texture之间切换时如何保证平滑?我们的设计可以总结为:
- 相邻的Texture之间存在重合。
- Texture重合部分保证可完整显示一个视界的迷雾信息。
- 相互对角的两份Texture也是相邻的关系,且相互对角的两份Texture的重合区域正好为一个视界的迷雾信息。
按照以上设计思路,我们最终做出来了!
大坑出现了
就是因为我们按照设计思路做出来了,所以发现了他的坑甚至比第一个还大。
- 切换很流畅,分辨率也上去了,一整张地图可以被很完整地展示。
- 内存压力巨大,原本只需要一张1024方图可表示完整的地图,现在4张都不一定表示的清楚。具体关系可以总结为:分辨率越高、视界与地图尺寸的比值越小,需要的Texture越多,Texture需要的尺寸越大。
- 效率没提高多少。原来每一帧只需要更新一张Texture,现在最多时要更新四张Texture。
这个坑可怎么填?
问题来了,也是做总结的时候了。虽然我们解决了前一个问题,可是在地图尺寸又一次变大的过程中,我们总结出了上面三条事实。其实我们犯了一个很严重的问题:解决问题治标不治本。
在第一个解决方案出问题时,我们总结为:地图尺寸变大与Texture尺寸不能太大之间的矛盾。但这只是表象,真正的矛盾应该是愈加完美的迷雾展示与资源有限的手机资源之间的矛盾。所以从根本上,我们需要的是一个完美的解决方案,而不是头疼医疼脚疼医脚。
找到最适合的方向
为了解决上述问题,我们分析了当前代码的核心矛盾,那么剩下就该思考怎么解决了,寻找解决方案。
之前的真的一文不值吗?
上一个方案虽然坑,不过将一整张地图进行分治的**还是应该值得肯定的,只是我们分治的手段太粗糙罢了。
经过重新设计后,我们需要做的就是将迷雾数据进行分治,同时将材质球需要用到的Texture限定为一个,每一帧通过修改Texture来更改视界的迷雾即可。那么接下来我们首先要面对的就是迷雾数据的分治。
四叉树方案
这是我们第一个想到的分治方案,将地图分解为若干块,每有一点被探索,则在该点对应的区块中加入他的信息。若某一块未探索或被完全探索,则使用固定的数记录,减少不必要的内存消耗。
四叉树是一个成熟的方案,尤其在图片压缩等领域有很广泛的应用。但是,四叉树对现在的我们不是最好的选择,因为当我们的地图分布太零碎的时候,地图的迷雾数据会有太多的碎片,在我们没有实现地图数据的压缩之前,这个方案只能算锦上添花而非雪中送炭。
数据压缩方案
其实在分析了之前的实现之后,我们发现我们太奢侈了:我们对于迷雾仅仅需要知道0(透明)和1(不透明)。所以为什么我们不适用bit来表示?
于是乎,我们将分治的**用在了数据压缩上。而且为了追求最大的压缩效率,我们选择ulong类型,因为这样一个64bit的数据类型,正好可以完整表示一个8*8方块的数据。也就是说按照这个方案,我们最起码可以将迷雾数据的内存消耗降低为初始方案的1/512(如果说一个**可以将效率提升500倍,它不是雪中送炭谁是?)。
方案确定
四叉树为我们指引了后续可以优化的方向,但是数据压缩却向我们提供了我们迫切需要的油和盐!!!
由于数据量的压缩,我们现在可以放心大胆使用一个二维数组来表述一整张地图的迷雾数据,就如同下面的代码一样。
public ulong[,] totalFogAlpha;
与此同时,针对某一个点的迷雾数据,使用位移操作就可以取得数据,时间消耗也可以满足我们的需求。这样空间消耗与时间消耗都很满足我们的需求了。
找到最适合方向之后
虽然找到了最适合的方向,不过有一个问题还在面前,那就是每一帧刷新迷雾遮罩图其实也是一个很消耗的过程,尤其是在遮罩图尺寸很大的情况下,性能依旧是瓶颈。
再一次寻找最优解
好吧,我已经不相信我一上来就能找到最优解了。不过在尝试了这么多种解决方案之后,我为什么不每一帧只刷新遮罩图发生变化的像素点呢?
有了思路,有了数据压缩的方案,很快我就设计了一个新的数据:
public ulong[,] lastFrameMaskAlpha;
还是一样的配方,使用一个压缩后的二维数组记录上一帧视界的迷雾数据。当这一帧全地图的迷雾数据更新后,逐个数据块比对迷雾数据是否发生变动,如果变动则提取数据块中发生变动的数据并记录。在数据收集完毕后,只将发生变动的数据更新到迷雾遮罩图那张Texture就好了。
代码实现
好吧,起了个标题,这部分我不打算写了,核心思路已经交代完了。
发现问题
问题是永远都有的,这次也是。
这次的问题出现在每一帧刷新遮罩图,原因是更新的点太多(这也和我们将遮罩图尺寸设置变大有关系)。还好,这个问题不严重,因为这和我们每一个点都单独设置有关系,一旦点的数量多,那么调用API的次数也会增多。在Unity中,只需将对Texture的操作从更改一个点的颜色数据改为更改一堆点的颜色数据,就可以解决了。
说下解决小思路,就是在收集完毕所有变动点的数据后,对数据点数据进行合并,使物理上连续的点的颜色存储在一个数组中,就可以有效节约时间。
告一段落
好吧,这一部分终于结束了,这也是当前代码核心实现的内容。
后续呢?
其实四叉树的方案一直在心里,但是在没有迫切的外力之下,暂时不会想到优化为四叉树的方案,除非某一天内存数据的压力又上来了(但愿不会有这一天,毕竟现在几十K内存就可以表示几千平方的世界范围)。
但愿我们开发中遇到的问题、错误的开发方式、解决问题的思路可以为看到这篇文章的你现在遇到的问题带来一些有用的思路和启示。
后记
18-2-24
今天猛然看到了稀疏数组(好吧,我真的很少用到它),其实稀疏数组某种程度上很符合我们存储的数据的场景,只是需要做一些变动才能达到节省内存空间的目的。
看到新东西,第一时间总是想搬来用,只是后来细细思索发现,由于我们的地图每一帧存在一次比较的过程(为的是使用很少的数据达到修正视界内容),因而对于稀疏数组进行遍历便阻挡我去实现它。