

PROS的不妙冒险(七) 实现《英雄联盟》中的视野迷雾效果
PROS
探险家麦哲伦
阅读 1419
2020年3月21日
思考是人类最大的乐趣。
*注:目前手机Connect App无法正常显示这篇文章,请复制链接并移步手机浏览器或PC端以获得良好的阅读体验。
今天,我们来一起学习下,如何给MOBA或RTS游戏添加视野迷雾效果。
当然,不排除有读者并不了解视野迷雾,在这里先给大家看下《英雄联盟》中的“战争迷雾”效果是怎样的: <br> 不知道大家有没有构思过这种“战争迷雾”是如何实现的,是否可以使用Unity来还原这个效果……笔者尝试了好几天,用尽了浑身解数,也没能做出像LOL那样自然、连贯、柔和的视野迷雾效果,实在没辙,在百度、CSDN、简书上搜索相关内容,看到了很多实现思路,可要么是空壳理论大谈学术,要么是实操效果与LOL大相径庭。抱着最后一丝希望,在Bilibili上搜了一下: <center><img style="border-radius:0.3125em;box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.08);"src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200322/p/images/ed0d4fe7-65c1-47d7-913b-1e967d3b70b3_0.png"><br><div style="color:orange;border-bottom:1px solid #d9d9d9;display:inline-block;color:#999;padding:2px;">来自B站大神“琴卓”的实现</div></center><br> <font color="#4078c0">视频原址:</font>https://www.bilibili.com/video/av60696335<br> 不得不承认,这个方案对LOL“战争迷雾”的还原度是真的高,up主也在视频下方的简介里附上了示例工程的GitHub链接,供大家学习交流。我当然支持优质的原创内容,所以还是推荐大家去看他的视频,下载他的工程进行学习。工程中的核心内容是5个C#脚本与2个Shader文件,笔者学艺不精,一张纸一支笔,梳理了整整5天时间才完全吃透。
那么看完视频没怎么懂的同学也不要急,下面是我经过改进与优化的版本的详细讲解。
<font color="203040">第一步:区分出哪里是空地,哪里是障碍物</font><br> 例如,我这里随便制作了一张20*20的地图,然后把它均分成一张40*40的网格。<font color="ff0000">同时考虑到视觉效果与运算效率,我们需要使每个小格都是方格。</font>然后对每一个小格做物理检测,看这个小格是表示空地还是障碍物。为了不漏过极窄的障碍物,我们需要让一点点障碍物都不沾的小格来表示空地,其余的都表示障碍物。Physics.OverlapBox(Vector3 center, Vector3 halfExtents, Quaternion orientation, int layerMask) <center><img style="border-radius:0.3125em;box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.08);"src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200324/p/images/458b146c-9ab1-4443-a2c8-bb92fd042e7d_1.gif"><br><div style="color:orange;border-bottom:1px solid #d9d9d9;display:inline-block;color:#999;padding:2px;">演示 检测结果</div></center><br> <font color="203040">第二步:区分出哪里是视野范围内,哪里是视野范围外</font><br> 我们需要通过玩家的视野半径,找出所有在视野内的小格。这里使用到了在系列(四)中提及的广度优先搜索算法,这里摘一段相关代码。
private void GetCache() { …… //使用广度优先搜索算法,缓存所有视野范围内的小格坐标相对于该视野单元坐标的偏移 for (int a = 0; a < offsetCacheList.Count; ++a) { int x = offsetCacheList[a].x; int y = offsetCacheList[a].y; distance = (x - 1) * (x - 1) + y * y; offset = _offsets[_halfOffsetLength + x - 1, _halfOffsetLength + y]; if (distance < targetDistance && !offset.isVisited) { offset.distance = distance; offset.isVisited = true; offsetCacheList.Add(offset); } distance = (x + 1) * (x + 1) + y * y; offset = _offsets[_halfOffsetLength + x + 1, _halfOffsetLength + y]; if (distance < targetDistance && !offset.isVisited) { offset.distance = distance; offset.isVisited = true; offsetCacheList.Add(offset); } distance = x * x + (y - 1) * (y - 1); offset = _offsets[_halfOffsetLength + x, _halfOffsetLength + y - 1]; if (distance < targetDistance && !offset.isVisited) { offset.distance = distance; offset.isVisited = true; offsetCacheList.Add(offset); } distance = x * x + (y + 1) * (y + 1); offset = _offsets[_halfOffsetLength + x, _halfOffsetLength + y + 1]; if (distance < targetDistance && !offset.isVisited) { offset.distance = distance; offset.isVisited = true; offsetCacheList.Add(offset); } } offsetCacheList = offsetCacheList.OrderByDescending(item => item.distance).ToList(); …… }
<center><img style="border-radius:0.3125em;box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.08);"src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200324/p/images/9165d843-972b-4eef-bbc9-3fb76f4561b9_2.gif"><br><div style="color:orange;border-bottom:1px solid #d9d9d9;display:inline-block;color:#999;padding:2px;">演示 不同视野半径</div></center><br> <font color="203040">第三步:两组信息交火,计算出哪里有迷雾,哪里没有迷雾</font><br> 原作者所使用的方法是:先找到所有比障碍物远的空地,然后逐个与玩家所在的小格连线,通过斜率来判断中间有没有障碍物阻挡,如果被阻挡了就给这个小格罩上迷雾,反之则没有迷雾。这种方法类似于Shader运算过程中的ZTest深度测试环节,当时令我眼前一亮。可后来我发现,这个方法对时间的消耗是巨大的,在视野单元数量众多,视野半径庞大时,画面渲染变得举步维艰。有没有什么优化的办法呢?正所谓有心人天不负,我突然想到可以这样:<font color="ff0000">把视野范围内每一个小格子单独产生迷雾的情况缓存下来</font>,然后在实际产生迷雾的运算中,将所有障碍物小格子按由远及近的顺序依次把产生迷雾的情况叠加到结果上!不仅如此,我发现像上图那样的“圆形”,一个用若干小格子拼凑出的“圆形”,在其上总能找到四条对称轴,那也就意味着,<font color="ff0000">我仅需考虑两条相邻对称轴所夹1/8个“圆形”的情况</font>,然后将横纵坐标对调、变号即可表示其他七个扇区的情况。 <center><img style="box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.08);"src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200325/p/images/6be7babb-754a-43ff-b778-179c2f6bf190_3.png"><br><div style="color:orange;border-bottom:1px solid #d9d9d9;display:inline-block;color:#999;padding:2px;">演示 缓存迷雾</div></center><br> 只要是斜率在两条绿线之间,且距离大于障碍物的小格统统加入到该障碍物的迷雾缓存中。由于我使用的坐标都是相对于原点(0, 0)的,所以此时斜率即为小格的纵坐标与横坐标的比值。摘一段相关代码:
private void GetCache() { …… //缓存1/8个圆内障碍物产生迷雾的情况 fogCacheList = new List<FogCache>(); for (int a = 0; a < offsetCacheList.Count - 1; ++a) { offset = offsetCacheList[a]; if (offset.x >= offset.y && offset.y >= 0) { FogCache fogCache = new FogCache {obstructionCoordinate = offset}; float slopeLeftBottom = (offset.y - 0.5f) / (offset.x - 0.5f); float slopeLeftUp = (offset.y + 0.5f) / (offset.x - 0.5f); float slopeRightBottom = (offset.y - 0.5f) / (offset.x + 0.5f); float maxSlope = Mathf.Max(slopeLeftBottom, slopeLeftUp, slopeRightBottom); float minSlope = Mathf.Min(slopeLeftBottom, slopeLeftUp, slopeRightBottom); for (int b = 0; b < a; ++b) { Coordinate fog = offsetCacheList[b]; if (fog.x > 0) { float slope = (float) fog.y / fog.x; if (maxSlope > slope && minSlope < slope) { fogCache.fogCoordinateList.Add(fog); } } } fogCacheList.Add(fogCache); } } …… }
<font color="203040">第四步:将迷雾信息转化为贴图</font><br> 《英雄联盟》中,视野范围内无迷雾,视野范围外薄迷雾;《红色警戒》中,探索过的区域无迷雾,未探索过的区域厚迷雾;还有一些游戏区分的更细……为了适应大部分情况,<font color="ff0000">我将迷雾划分为三部分,每部分应用不同的颜色</font>。<br> 在地图中有很多视野单元的时候会产生这种情况:A认为某个小格有迷雾,而B认为那个小格没有迷雾,这时就需要一个“裁判”来仲裁结果。比如我是A,B是我的友方单位,那么小格最终就没有迷雾; B若是我的敌方单位,那么小格最终就有迷雾。所以为了实际应用考虑,<font color="ff0000">视野单元需要按照阵营进行划分</font>。<br> 这样,我们终于得到了一个表示迷雾信息的二维数组。如果不做任何处理,直接映射到贴图上是这样的效果: <center><img style="border-radius:0.3125em;box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.08);"src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200325/p/images/8b9f6865-61e4-4749-983f-7c2dad1a8045_4.gif"><br><div style="color:orange;border-bottom:1px solid #d9d9d9;display:inline-block;color:#999;padding:2px;">演示 英雄联盟风格</div></center><br> <center><img style="border-radius:0.3125em;box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.08);"src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200325/p/images/1ac1b844-fdf7-4e07-b4c8-f1cb2d82595c_5.gif"><br><div style="color:orange;border-bottom:1px solid #d9d9d9;display:inline-block;color:#999;padding:2px;">演示 红色警戒风格</div></center><br> (170+的帧数,这就是优化的必要性!)<br> <font color="203040">第五步:将迷雾贴图进行模糊、插值处理</font><br> 至此,CPU已经完成了它所需要做的几乎所有的事。接下来,我们需要把上面网格状的迷雾贴图交给GPU进行打磨,使其更加的自然、连贯、柔美,能够在真正的游戏中进行使用。<br> 高斯模糊:
fixed4 fragBlur(v2f i) : SV_Target { fixed4 col = fixed4(0, 0, 0, 0); float2 offset = _BlurRadius * _MainTex_TexelSize; half gau[9] = {1, 2, 1, 2, 4, 2, 1, 2, 1}; for (int x = 0; x < 3; ++x) { for (int y = 0; y < 3; ++y) { col += tex2D(_MainTex, fixed2(x - 1, y - 1) * offset + i.uv) * gau[x * 3 + y]; } } return col / 16; }
渐进插值:
fixed4 fragLerp(v2f i) : SV_Target { half4 lastCol = tex2D(_LastTex, i.uv); half4 col = tex2D(_MainTex, i.uv); return lerp(lastCol, col, unity_DeltaTime.x * 5); }
<font color="203040">第六步:参数调整</font><br> <center><img style="border-radius:0.3125em;box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.08);"src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200326/p/images/0b517ee3-12bb-4571-b991-6de2c1189f59_6.png"><br><div style="color:orange;border-bottom:1px solid #d9d9d9;display:inline-block;color:#999;padding:2px;">视野迷雾组件</div></center><br> 如上图,Area表示迷雾覆盖的范围;Fog Colors三个颜色分别代表厚迷雾、薄迷雾与无迷雾;Fog Update Time Interval代表计算迷雾贴图的时间间隔,0.2表示每秒计算5张贴图;Gaussian Blur Count表示每张贴图经过高斯模糊迭代处理的次数;Grid Count表示小格的数目,这里我们将迷雾分成1万个小格;Main View Unit代表自己,Other View Unit List填写其他所有玩家;Prefab就是一个用于显示迷雾贴图的面片Quad;Render Multiplier代表迷雾贴图相对于网格的放大倍数,这里长宽都放大2倍,得到的是4万像素的迷雾贴图;Shader就是第五步中所介绍的。 <center><img style="border-radius:0.3125em;box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.08);"src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200326/p/images/44d81b6f-543c-4051-abeb-a0360141dc03_7.png"><br><div style="color:orange;border-bottom:1px solid #d9d9d9;display:inline-block;color:#999;padding:2px;">视野单元组件</div></center><br> 视野单元的参数较为简单。Group是一个表示阵营的整数,相同的整数归为同一阵营;Mesh Renderer就是该单位的网格模型;View Field Radius表示玩家视野的真实半径,这里的5相当于一个半径20小格的圆形。
完工!让我们来看一看最终的效果如何:
<center><img style="border-radius:0.3125em;box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.08);"src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200326/p/images/4176384b-510d-48ac-a83b-5e582b275e5b_8.gif"></center><br> <center><img style="border-radius:0.3125em;box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.08);"src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200326/p/images/3d320a63-cb21-41d3-95b4-495a746116af_9.gif"></center><br> <center><img style="border-radius:0.3125em;box-shadow:0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.08);"src="https://u3d-connect-cdn-public-prd.cdn.unity.cn/h1/20200326/p/images/06f6f8fc-dbaf-4ad5-b37a-1e4ac81083f0_10.gif"></center><br> (可以和开头的视频对比一下,完全一样对不对!)<br>
最后,感谢认真读完的你。这一期的内容制作耗费了笔者极大的精力与时间,如果您觉得有所收获,或是想要直接获取完整的源码,请一定要<font color="#ff0000">点赞+收藏</font>支持一下我!如果需要的人多的话,我会在下一期附上源码文件及说明。下期再见~
发布于技术交流
20条评论
x
xxy
PROS![]()

SquirrelMan
PROS![]()

DoLike
回复
PROSPROS
回复
DoLike查看所有回复
JW
Jay W
PROS![]()

JW
Jay W

问
AI
全新AI功能上线
1. 基于Unity微调:专为Unity优化,提供精准高效的支持。
2. 深度集成:内置于团结引擎,随时查阅与学习。
3. 多功能支持:全面解决技术问题与学习需求。

问
AI