一  效果图

  先上效果图吧,这是为了吸引到你们的ヽ(。◕‿◕。)ノ゚

战争迷雾效果演示图

战争迷雾调试界面演示图

  由于是gif录制,为了压缩图片,帧率有点低,实际运行时,参数调整好是不会像这样一卡一顿的。

二  战争迷雾概述

  战争迷雾一般用于Startcraft等RTS类型游戏,还有就是War3等Moba类型游戏,主要包括三个概念:未探索区域、已探索区域、当前视野。

  1)未探索区域:一般展示为黑色区域,像星际争霸这样的游戏,开局时未探索区域一般是暗黑的,只有地图上的原始晶体矿产能够被看到,敌人建筑、角色等都不暴露。

  2)已探索区域:一般显示为灰色区域,已探索表示某块区域曾经被你的视野覆盖过,星际争霸中已探索的区域会保留你当时视野离开时该区域的建筑状态,所以可以看到敌人的建筑。

  3)当前视野:一般全亮,视野范围内除了隐身单位等特殊设定,所有的建筑、角色、特效等都是可见的,视野一般锁定在可移动角色或者特定魔法上面,会随着角色的移动而移动,随着魔法的消失而消失。

三  实现原理

  战争迷雾的实现方式大体上可以分为两个步骤:贴图生成、屏幕渲染。

3.1  贴图生成

  贴图的生成有两种方式:

  1)拼接法:

    使用类似地图拼接的原理去实现,贴图如下:

战争迷雾拼接贴图

    这种方式个人认为很不靠谱,局限性很大,而且迷雾总是会运动的,在平滑处理这点上会比较粗糙,不太自然。这里不再赘述它的实现原理。

  2)绘制法:绘制法和使用的地图模型有很大关系,一般使用的有两种模型:一个是正方形地图,另外一个是六边形地图。六边形地图示例如下:

战争迷雾六边形地图贴图

    原理简单直白,使用正方形/者六边形划分地图空间,以正方形/六边形为单位标记被探索过和当前视野区域。这里探索过的区域是棱角分明的,可以使用高斯模糊进行模糊处理。一般来说,正方形/六边形边长要选择合适,太长会导致模糊处理效果不理想,太短会导致地图单元格太多,全图刷新消耗增大。另外说一句,战争迷雾的地图和战斗系统的逻辑地图其实是可以分离的,所以两者并没有必然联系,你可以单独为你的战争迷雾系统选择地图模型。我也建议你不管是不是同一套地图,实现时都实现解耦。

3.2  屏幕渲染

  得到如上贴图以后,就可以渲染到屏幕了,渲染方式一般来说有3种:

  1)屏幕后处理:在原本屏幕显示图像上叠加混合战争迷雾贴图。

  2)摄像机投影:使用投影仪进行投影,将战争迷雾投影到世界空间。

  3)模型贴图:使用一张覆盖整个世界空间的平面模型来绘制战争迷雾贴图。

  不管你选择使用哪一种方式,在这一步当中都需要在Shader里进行像素级别的平滑过渡。从上一个时刻的贴图状态过渡到当前时刻的贴图状态。

四  代码实现

  原理大致上应该是清楚了,因为这个系统的设计原理实际上也不算是复杂,下面就一些重要步骤给出代码实现。这里实践的时候采用的是正方形地图,模型贴图方式。正方形地图模型不管是模糊处理还是Shader绘制都要比六边形地图简单。正方形贴图Buffer使用Color32的二维数组表示,根据位置信息,每个正方形网格会对应一个Color32数据,包含颜色值和透明度,能够很好的进行边缘平滑效果。

1 // Color buffers -- prepared on the worker thread.
2 protected Color32[] mBuffer0;
3 protected Color32[] mBuffer1;
4 protected Color32[] mBuffer2;

  这里使用了3个Buffer,是因为图像处理是很耗时的,所以为它单独开辟了线程去处理,为了线程同步问题,才增设了Buffer,关于线程这点稍后再说。

4.1  刷新贴图Buffer

  贴图Buffer需要根据游戏逻辑中各个带有视野的单位去实时刷新,在正方形地图模型中,是根据单位当前位置和视野半径做圆,将圆内圈住的小正方形标记为探索。

 1 void RevealUsingRadius (IFOWRevealer r, float worldToTex)2 {3     // Position relative to the fog of war4     Vector3 pos = (r.GetPosition() - mOrigin) * worldToTex;5     float radius = r.GetRadius() * worldToTex - radiusOffset;6 7     // Coordinates we'll be dealing with8     int xmin = Mathf.RoundToInt(pos.x - radius);9     int ymin = Mathf.RoundToInt(pos.z - radius);
10     int xmax = Mathf.RoundToInt(pos.x + radius);
11     int ymax = Mathf.RoundToInt(pos.z + radius);
12
13     int cx = Mathf.RoundToInt(pos.x);
14     int cy = Mathf.RoundToInt(pos.z);
15
16     cx = Mathf.Clamp(cx, 0, textureSize - 1);
17     cy = Mathf.Clamp(cy, 0, textureSize - 1);
18
19     int radiusSqr = Mathf.RoundToInt(radius * radius);
20
21     for (int y = ymin; y < ymax; ++y)
22     {
23         if (y > -1 && y < textureSize)
24         {
25             int yw = y * textureSize;
26
27             for (int x = xmin; x < xmax; ++x)
28             {
29                 if (x > -1 && x < textureSize)
30                 {
31                     int xd = x - cx;
32                     int yd = y - cy;
33                     int dist = xd * xd + yd * yd;
34
35                     // Reveal this pixel
36                     if (dist < radiusSqr) mBuffer1[x + yw].r = 255;
37                 }
38             }
39         }
40     }
41 }

  第一个参数包含了视野单位的信息,包括位置和视野半径;第二个参数为世界坐标到贴图坐标的坐标变换,R通道用于记录视野信息。

4.2  贴图Buffer模糊

  每次贴图刷新以后,进行一次贴图模糊处理。

 1 void BlurVisibility ()2 {3     Color32 c;4 5     for (int y = 0; y < textureSize; ++y)6     {7         int yw = y * textureSize;8         int yw0 = (y - 1);9         if (yw0 < 0) yw0 = 0;
10         int yw1 = (y + 1);
11         if (yw1 == textureSize) yw1 = y;
12
13         yw0 *= textureSize;
14         yw1 *= textureSize;
15
16         for (int x = 0; x < textureSize; ++x)
17         {
18             int x0 = (x - 1);
19             if (x0 < 0) x0 = 0;
20             int x1 = (x + 1);
21             if (x1 == textureSize) x1 = x;
22
23             int index = x + yw;
24             int val = mBuffer1[index].r;
25
26             val += mBuffer1[x0 + yw].r;
27             val += mBuffer1[x1 + yw].r;
28             val += mBuffer1[x + yw0].r;
29             val += mBuffer1[x + yw1].r;
30
31             val += mBuffer1[x0 + yw0].r;
32             val += mBuffer1[x1 + yw0].r;
33             val += mBuffer1[x0 + yw1].r;
34             val += mBuffer1[x1 + yw1].r;
35
36             c = mBuffer2[index];
37             c.r = (byte)(val / 9);
38             mBuffer2[index] = c;
39         }
40     }
41
42     // Swap the buffer so that the blurred one is used
43     Color32[] temp = mBuffer1;
44     mBuffer1 = mBuffer2;
45     mBuffer2 = temp;
46 }

  用周围的8个小正方形进行了加权模糊,这里并没有像高斯模糊那样去分不同的权重。

4.3  Buffer运用到贴图

  Buffer一旦处理完毕,就可以生成/刷新贴图供屏幕显示用,不管你使用上述方式中的哪一种,在Shader执行贴图采样时,这张贴图是必须的。

 1 void UpdateTexture ()2 {3     if (!enableRender)4     {5         return;6     }7 8     if (mTexture == null)9     {
10         // Native ARGB format is the fastest as it involves no data conversion
11         mTexture = new Texture2D(textureSize, textureSize, TextureFormat.ARGB32, false);
12
13         mTexture.wrapMode = TextureWrapMode.Clamp;
14
15         mTexture.SetPixels32(mBuffer0);
16         mTexture.Apply();
17         mState = State.Blending;
18     }
19     else if (mState == State.UpdateTexture)
20     {
21         mTexture.SetPixels32(mBuffer0);
22         mTexture.Apply();
23         mBlendFactor = 0f;
24         mState = State.Blending;
25     }
26 }

4.4  屏幕渲染

  主要是做两件事情:CS测在OnWillRenderObject给Shader传递参数;另外就是Shader中根据最新的战争迷雾贴图和战争迷雾颜色设定执行平滑过渡。

 1 void OnWillRenderObject()2 {3     if (mMat != null && FOWSystem.instance.texture != null)4     {5         mMat.SetTexture("_MainTex", FOWSystem.instance.texture);6         mMat.SetFloat("_BlendFactor", FOWSystem.instance.blendFactor);7         if (FOWSystem.instance.enableFog)8         {9             mMat.SetColor("_Unexplored", unexploredColor);
10         }
11         else
12         {
13             mMat.SetColor("_Unexplored", exploredColor);
14         }
15         mMat.SetColor("_Explored", exploredColor);
16     }
17 }

  其中blendFactor是过渡因子,会在Update中根据时间刷新,用于控制Shader的平滑过渡过程。

1 fixed4 frag(v2f i) : SV_Target
2 {
3     half4 data = tex2D(_MainTex, i.uv);
4     half2 fog = lerp(data.rg, data.ba, _BlendFactor);
5     half4 color = lerp(_Unexplored, _Explored, fog.g);
6     color.a = (1 - fog.r) * color.a;
7     return color;
8 }
9 ENDCG

  

  data是贴图,rg和ba通道是连续的两个战争迷雾状态的数据,其中r通道表示当前是否可见(是否在视野内),g通道表示是否被探索过(大于0则探索过)。

4.5  多线程

  本例当作,贴图Buffer的刷新和模糊处理是在子线程处理的;而Buffer运用到贴图在主线程中;屏幕渲染在GPU当作。所以Unity主线程只是在不停地刷新贴图,而贴图Buffer和模糊处理这两个很耗性能的操作全部由子线程代劳,这就是标题所说的“高性能”原因所在,即使子线程每次的处理周期达到30毫秒,它依旧不会影响到游戏帧率。

  多线程编程必然要考虑的一点是线程同步,此处主要的问题有两个:

    1)工作子线程输入:刷新贴图Buffer需要Unity主线程(或者游戏逻辑主线程)中游戏中的视野体数据(位置、视野半径)

    2)工作子线程输出:由最新的游戏逻辑数据刷新贴图Buffer,以及进行贴图Buffer混合以后,要在Unity主线程将数据运用到贴图

  工作子线程的输入同步问题稍后再说,这里说下第二步是怎样去保证同步的,其大致步骤是:

    1)设置3个状态用于线程同步:

1 public enum State
2 {
3     Blending,
4     NeedUpdate,
5     UpdateTexture,
6 }

    2)NeedUpdate表示子线程需要处理贴图Buffer,这个状态的设置是由设定的刷新频率和实际处理时的刷新速度决定的:

 1 void ThreadUpdate()2 {3     System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();4 5     while (mThreadWork)6     {7         if (mState == State.NeedUpdate)8         {9             sw.Reset();
10             sw.Start();
11             UpdateBuffer();
12             sw.Stop();
13             mElapsed = 0.001f * (float)sw.ElapsedMilliseconds;
14             mState = State.UpdateTexture;
15         }
16         Thread.Sleep(1);
17     }
18 #if UNITY_EDITOR
19     Debug.Log("FOW thread exit!");
20 #endif
21 }

    3)子线程会将Unity主线程(或者游戏逻辑线程)提供的最新视野状态数据刷新到贴图Buffer1的R通道,然后使用Buffer2做临时缓存对Buffer1执行模糊,模糊以后交换双缓存,最后将Buffer1的rg通道拷贝到Buffer0,所以Buffer0的ba和rg通道分别存放了上一次刷新和当前本次刷新的战争迷雾状态数据,Buffer0运用到贴图以后由Shader在这两个状态间进行平滑过渡。

 1 void RevealMap ()2 {3     for (int index = 0; index < mTextureSizeSqr; ++index)4     {5         if (mBuffer1[index].g < mBuffer1[index].r)6         {7             mBuffer1[index].g = mBuffer1[index].r;8         }9     }
10 }
11
12 void MergeBuffer()
13 {
14     for (int index = 0; index < mTextureSizeSqr; ++index)
15     {
16         mBuffer0[index].b = mBuffer1[index].r;
17         mBuffer0[index].a = mBuffer1[index].g;
18     }
19 }

    4)子线程工作处理完以后设置UpdateTexture状态,通知Unity主线程:“嘿,饭已经做好了,你来吃吧!”,Unity主线程随后将Buffer0缓存运用到贴图。

 1 void Update ()2 {3     if (!enableSystem)4     {5         return;6     }7 8     if (textureBlendTime > 0f)9     {
10         mBlendFactor = Mathf.Clamp01(mBlendFactor + Time.deltaTime / textureBlendTime);
11     }
12     else mBlendFactor = 1f;
13
14     if (mState == State.Blending)
15     {
16         float time = Time.time;
17
18         if (mNextUpdate < time)
19         {
20             mNextUpdate = time + updateFrequency;
21             mState = State.NeedUpdate;
22         }
23     }
24     else if (mState != State.NeedUpdate)
25     {
26         UpdateTexture();
27     }
28 }

    5)UpdateTexture执行完毕以后,进入Blending状态,此时Unity主线程要等待下一次更新时间,时间到则设置NeedUpdate状态,通知子线程:“嘿,家伙,你该做饭了!”。

4.6  模块分离

  上面讲到贴图Buffer刷新子线程和Unity渲染主线程的同步与临界资源的互斥,现在来说说Unity主线程(游戏逻辑主线程)与贴图Buffer刷新子线程的同步。

  1)使用互斥锁同步视野体生命周期

 1 // Revealers that the thread is currently working with2 static BetterList<IFOWRevealer> mRevealers = new BetterList<IFOWRevealer>();3 4 // Revealers that have been added since last update5 static BetterList<IFOWRevealer> mAdded = new BetterList<IFOWRevealer>();6 7 // Revealers that have been removed since last update8 static BetterList<IFOWRevealer> mRemoved = new BetterList<IFOWRevealer>();9
10 static public void AddRevealer (IFOWRevealer rev)
11 {
12     if (rev != null)
13     {
14         lock (mAdded) mAdded.Add(rev);
15     }
16 }
17
18 static public void RemoveRevealer (IFOWRevealer rev)
19 {
20     if (rev != null)
21     {
22         lock (mRemoved) mRemoved.Add(rev);
23     }
24 }

    这个应该没啥好说的,子线程在处理这两个列表时同样需要加锁。

  2)视野体使用IFOWRevelrs接口,方便模块隔离和扩展。同步问题这里采用了一种简单粗暴的方式,由于战争迷雾属于表现层面的东西,即使用于帧同步也不会有问题。

 1 public interface IFOWRevealer2 {3     // 给FOWSystem使用的接口4     bool IsValid();5     Vector3 GetPosition();6     float GetRadius();7 8     // 给FOWLogic使用的接口,维护数据以及其有效性9     void Update(int deltaMS);
10     void Release();
11 }

    继承IFOWRevealer接口用来实现各种不同的视野体,本示例中给出了角色视野体与临时视野体的实现,其它视野体自行根据需要扩展。

五  其它说明

  其它还有FOWlogic模块用来隔离FOW系统和游戏逻辑,FOWRender用于fow渲染等,不再一一说明,自行阅读代码。

  有关六边形地图的战争迷雾实现稍作变通应该做起来问题也不是太大,相关信息可以参考:Hex Map 21 Exploration和Hex Map 22 Advanced Vision。

  这一系列文章都有译文,英文不好的同学参考:Unity 六边形地图系列(二十一):探索和Unity 六边形地图系列(二十二) :高级视野效果。

  然后,本演示工程的核心算法是由TasharenFogOfWar移植而来的,该插件由NGUI作者发布,不过已经被我大幅修改。

六  工程下载

  最后附上本演示工程的GitHub地址:https://github.com/smilehao/fog-of-war。

博客:http://www.cnblogs.com/SChivas/

仓库:https://github.com/smilehao/

邮箱:703016035@qq.com

感谢您的阅读,如果您觉得本文对您有所帮助,请点一波推荐。

欢迎各位点评转载,但是转载文章之后务必在文章页面中给出作者和原文链接,谢谢。

Unity3D游戏高性能战争迷雾系统实现相关推荐

  1. 在Unity中为即时战略游戏实现战争迷雾(上)

    本文将由游戏开发工程师Ariel Coppes分享在Unity中为即时战略游戏实现战争迷雾效果. 过去三年中,我一直在Ironhide Game Studio开发移动即时战略游戏<钢铁战队> ...

  2. 在Unity中为即时战略游戏实现战争迷雾(下)

    本文将在Unity中为即时战略游戏实现战争迷雾的一种新方法. 在上一篇文章中,游戏开发工程师Ariel Coppes分享了<钢铁战队>中战争迷雾效果的实现方法,本文他将介绍新的一种实现方法 ...

  3. EasyFogofWar 简单战争迷雾系统 unity3d插件 使用教程

    EasyFogofWar是一款非常简单易用的战争迷雾插件,完全开源,极易扩展,高效低耗,不管win还是手机端,都兼容并流畅运行. 使用教程 首先导入插件. 文件很少,一个demo,一个插件资源文件夹. ...

  4. 2D游戏的战争迷雾实现方式

    转自: http://bbs.gameres.com/forum.php?mod=viewthread&tid=201878&extra=page%3D16%26filter%3Dso ...

  5. Unity MOBA类型游戏的战争迷雾效果

    基于视野(FOV)的战争迷雾,例如LOL的视野:鼠标右键点击地板,目标移动,同时显示角色周围视野,鼠标滚轮可以调节远近. Unity版本:2019.4.1f1 1.新建工程---右键3D Object ...

  6. Unity Shader unity文档学习笔记(十一):战争迷雾核心算法

    核心算法 非常简单 主要就是把一个点的世界坐标转换到贴图的UV坐标 给整个场景一个大的plane 加上写的shader 摄像机位置调成plane的正上方 Shader "Unlit/FogR ...

  7. 《C++游戏开发》笔记十二 战争迷雾:初步实现

    本系列文章由七十一雾央编写,转载请注明出处. http://blog.csdn.net/u011371356/article/details/9475979 作者:七十一雾央 新浪微博:http:// ...

  8. 【笨木头Cocos2dx 041】战争迷雾效果 第4章_真正的迷雾来了!

    错过了前面章节? 没关系,传送门在这: 战争迷雾效果 第1章_要探索,不要地图全开! 战争迷雾效果 第2章_先把地图加进来 战争迷雾效果 第3章_准确地获取屏幕上的瓦片位置 经过这么多铺垫,我们要来正 ...

  9. 战争迷雾效果 第4章_真正的迷雾来了!

    原文地址:http://www.benmutou.com/blog/archives/485 经过这么多铺垫,我们要来正式编写实现迷雾效果的代码了. (小若:快点开始写,别唠叨了!) 笨木头花心贡献, ...

  10. cocos2dx 游戏当中的战争迷雾

    使用cocos2dx的tilemap制作了个斜45度的rts游戏,这东西战争迷雾怎么可能少了,于是乎就琢磨怎么做一个. 最开始的想法很简单,建立一个二维数组,里面清0,在你的视野的地方放1. 继续用t ...

最新文章

  1. 2017海克斯康拉斯维加斯美国大会 精彩即将开始
  2. Vim 快捷键整理【转】
  3. python批量读取csv文件-Python读取/批量读取文件
  4. Import和Assembly
  5. 技术干货 | 视频直播关键技术和趋势
  6. 给爸妈最硬核的春节礼物,走入百度大字版APP研发幕后
  7. 二次规划问题转换为半正定问题(QPtoSDP)
  8. vb6.0连接access数据库
  9. app源码+php+l,android商城APP全套源码(服务端+客户端)
  10. JBoss下载与安装
  11. 把 14 亿中国人都拉到一个微信群在技术上能实现吗?
  12. greenplum segment恢复的过程
  13. 平均电流型LED降压恒流驱动器 常用恒流IC
  14. 网页如何展示PPT文档
  15. 给定0-1矩阵,求连通域
  16. Could not find a getter for name in class org.tarena.entity1.City
  17. 无线电能传输 wpt 磁耦合谐振 过零检测 matlab simulink仿真 pwm MOSFET,过零检测模块 基于二极管整流的无线电能传输设计
  18. python找到一行单词中最长的_python - 查找.txt文件中最长的单词,不带标点符号 - SO中文参考 - www.soinside.com...
  19. 一些名企秋招网申链接合集
  20. mermaid sequenceDiagram使用指南

热门文章

  1. TRS专题制作选件手册
  2. 让 Flutter 在鸿蒙系统上跑起来
  3. 根据手势拿到superview
  4. python四子棋游戏
  5. 自增约束(auto_increment)
  6. java 无法加载dll_java中调用本地动态链接库(*.DLL)的两种方式详解和not found library、打包成jar,war包dll无法加载等等问题解决办法...
  7. 【数字图像处理】Hough变换C语言实现
  8. 解决“UnicodeDecodeError: ‘gbk‘ codec can‘t decode byte 0xd0 in position 493: illegal multibyte sequen“
  9. PyQt(Python+Qt)学习随笔:Model中项的标记flags取值及枚举类型Qt.ItemFlag
  10. Caused by: redis.clients.jedis.exceptions.JedisConnectionException: JedisPubSub was not subscribed t