一  效果图

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

战争迷雾效果演示图

战争迷雾调试界面演示图

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

二  战争迷雾概述

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

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

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

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

三  实现原理

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

3.1  贴图生成

  贴图的生成有两种方式:

  1)拼接法:

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

战争迷雾拼接贴图

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

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

战争迷雾六边形地图贴图

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

3.2  屏幕渲染

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

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

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

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

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

四  代码实现

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

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

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

4.1  刷新贴图Buffer

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

 void RevealUsingRadius (IFOWRevealer r, float worldToTex)
{
// Position relative to the fog of war
Vector3 pos = (r.GetPosition() - mOrigin) * worldToTex;
float radius = r.GetRadius() * worldToTex - radiusOffset; // Coordinates we'll be dealing with
int xmin = Mathf.RoundToInt(pos.x - radius);
int ymin = Mathf.RoundToInt(pos.z - radius);
int xmax = Mathf.RoundToInt(pos.x + radius);
int ymax = Mathf.RoundToInt(pos.z + radius); int cx = Mathf.RoundToInt(pos.x);
int cy = Mathf.RoundToInt(pos.z); cx = Mathf.Clamp(cx, , textureSize - );
cy = Mathf.Clamp(cy, , textureSize - ); int radiusSqr = Mathf.RoundToInt(radius * radius); for (int y = ymin; y < ymax; ++y)
{
if (y > - && y < textureSize)
{
int yw = y * textureSize; for (int x = xmin; x < xmax; ++x)
{
if (x > - && x < textureSize)
{
int xd = x - cx;
int yd = y - cy;
int dist = xd * xd + yd * yd; // Reveal this pixel
if (dist < radiusSqr) mBuffer1[x + yw].r = ;
}
}
}
}
}

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

4.2  贴图Buffer模糊

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

 void BlurVisibility ()
{
Color32 c; for (int y = ; y < textureSize; ++y)
{
int yw = y * textureSize;
int yw0 = (y - );
if (yw0 < ) yw0 = ;
int yw1 = (y + );
if (yw1 == textureSize) yw1 = y; yw0 *= textureSize;
yw1 *= textureSize; for (int x = ; x < textureSize; ++x)
{
int x0 = (x - );
if (x0 < ) x0 = ;
int x1 = (x + );
if (x1 == textureSize) x1 = x; int index = x + yw;
int val = mBuffer1[index].r; val += mBuffer1[x0 + yw].r;
val += mBuffer1[x1 + yw].r;
val += mBuffer1[x + yw0].r;
val += mBuffer1[x + yw1].r; val += mBuffer1[x0 + yw0].r;
val += mBuffer1[x1 + yw0].r;
val += mBuffer1[x0 + yw1].r;
val += mBuffer1[x1 + yw1].r; c = mBuffer2[index];
c.r = (byte)(val / );
mBuffer2[index] = c;
}
} // Swap the buffer so that the blurred one is used
Color32[] temp = mBuffer1;
mBuffer1 = mBuffer2;
mBuffer2 = temp;
}

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

4.3  Buffer运用到贴图

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

 void UpdateTexture ()
{
if (!enableRender)
{
return;
} if (mTexture == null)
{
// Native ARGB format is the fastest as it involves no data conversion
mTexture = new Texture2D(textureSize, textureSize, TextureFormat.ARGB32, false); mTexture.wrapMode = TextureWrapMode.Clamp; mTexture.SetPixels32(mBuffer0);
mTexture.Apply();
mState = State.Blending;
}
else if (mState == State.UpdateTexture)
{
mTexture.SetPixels32(mBuffer0);
mTexture.Apply();
mBlendFactor = 0f;
mState = State.Blending;
}
}

4.4  屏幕渲染

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

 void OnWillRenderObject()
{
if (mMat != null && FOWSystem.instance.texture != null)
{
mMat.SetTexture("_MainTex", FOWSystem.instance.texture);
mMat.SetFloat("_BlendFactor", FOWSystem.instance.blendFactor);
if (FOWSystem.instance.enableFog)
{
mMat.SetColor("_Unexplored", unexploredColor);
}
else
{
mMat.SetColor("_Unexplored", exploredColor);
}
mMat.SetColor("_Explored", exploredColor);
}
}

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

 fixed4 frag(v2f i) : SV_Target
{
half4 data = tex2D(_MainTex, i.uv);
half2 fog = lerp(data.rg, data.ba, _BlendFactor);
half4 color = lerp(_Unexplored, _Explored, fog.g);
color.a = ( - fog.r) * color.a;
return color;
}
ENDCG

  

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

4.5  多线程

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

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

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

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

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

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

 public enum State
{
Blending,
NeedUpdate,
UpdateTexture,
}

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

 void ThreadUpdate()
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); while (mThreadWork)
{
if (mState == State.NeedUpdate)
{
sw.Reset();
sw.Start();
UpdateBuffer();
sw.Stop();
mElapsed = 0.001f * (float)sw.ElapsedMilliseconds;
mState = State.UpdateTexture;
}
Thread.Sleep();
}
#if UNITY_EDITOR
Debug.Log("FOW thread exit!");
#endif
}

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

 void RevealMap ()
{
for (int index = ; index < mTextureSizeSqr; ++index)
{
if (mBuffer1[index].g < mBuffer1[index].r)
{
mBuffer1[index].g = mBuffer1[index].r;
}
}
} void MergeBuffer()
{
for (int index = ; index < mTextureSizeSqr; ++index)
{
mBuffer0[index].b = mBuffer1[index].r;
mBuffer0[index].a = mBuffer1[index].g;
}
}

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

 void Update ()
{
if (!enableSystem)
{
return;
} if (textureBlendTime > 0f)
{
mBlendFactor = Mathf.Clamp01(mBlendFactor + Time.deltaTime / textureBlendTime);
}
else mBlendFactor = 1f; if (mState == State.Blending)
{
float time = Time.time; if (mNextUpdate < time)
{
mNextUpdate = time + updateFrequency;
mState = State.NeedUpdate;
}
}
else if (mState != State.NeedUpdate)
{
UpdateTexture();
}
}

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

4.6  模块分离

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

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

 // Revealers that the thread is currently working with
static BetterList<IFOWRevealer> mRevealers = new BetterList<IFOWRevealer>(); // Revealers that have been added since last update
static BetterList<IFOWRevealer> mAdded = new BetterList<IFOWRevealer>(); // Revealers that have been removed since last update
static BetterList<IFOWRevealer> mRemoved = new BetterList<IFOWRevealer>(); static public void AddRevealer (IFOWRevealer rev)
{
if (rev != null)
{
lock (mAdded) mAdded.Add(rev);
}
} static public void RemoveRevealer (IFOWRevealer rev)
{
if (rev != null)
{
lock (mRemoved) mRemoved.Add(rev);
}
}

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

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

 public interface IFOWRevealer
{
// 给FOWSystem使用的接口
bool IsValid();
Vector3 GetPosition();
float GetRadius(); // 给FOWLogic使用的接口,维护数据以及其有效性
void Update(int deltaMS);
void Release();
}

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

五  其它说明

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

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

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

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

六  工程下载

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

Unity3D高性能战争迷雾实现的更多相关文章

  1. Unity3D游戏高性能战争迷雾系统实现

    一 效果图 先上效果图吧,这是为了吸引到你们的ヽ(。◕‿◕。)ノ゚ 战争迷雾效果演示图 战争迷雾调试界面演示图 由于是gif录制,为了压缩图片,帧率有点低,实际运行时,参数调整好是不会像这样一卡一顿的 ...

  2. Unity3D战争迷雾效果

    原地址:http://liweizhaolili.blog.163.com/blog/static/16230744201431835652233/ 最近一直都在做Flash相关的东西,很久没有空搞U ...

  3. 平滑过渡的战争迷雾(一) 原理:Warcraft3地形拼接算法

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

  4. 《C++游戏开发》笔记十四 平滑过渡的战争迷雾(二) 实现:真正的迷雾来了

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

  5. 《C++游戏开发》笔记十三 平滑过渡的战争迷雾(一) 原理:Warcraft3地形拼接算法

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

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

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

  7. unity中绘制战争迷雾

    接上一篇中说的游戏,我们已经实现了client.host上的一个物体可见不可见的行为.之后我们可以加入类似检查两个单位之间的距离.或是两个单位之间有无阻挡物来进一步实现游戏机制. 在这篇随笔中我会首先 ...

  8. [转] AS3地图拼接与战争迷雾的实现

    在开发游戏的过程中,特别是地图编辑器中,需要利用最少的资源,实现最丰富的地形地貌.虽然现在众多的RPG开始使用整图,但是我们偶尔还是需要能够让玩家自己编辑地图,或者其他需要自动进行地图构建的功能.另外 ...

  9. 战争迷雾Fog Of War

    参考:https://forums.unrealengine.com/community/community-content-tools-and-tutorials/26436-tutorial-fo ...

随机推荐

  1. Echarts数据可视化polar极坐标系,开发全解+完美注释

    全栈工程师开发手册 (作者:栾鹏) Echarts数据可视化开发代码注释全解 Echarts数据可视化开发参数配置全解 6大公共组件详解(点击进入): title详解. tooltip详解.toolb ...

  2. Mybatis,Spring,SpringMVC框架面试题

    Mybatis测试 1,   Mybatis的核心是(  sqlsessionfactory    ) 2,   使用Mybatis持久化框架进行数据查询需要返回的一个实体类的集合, 在<sel ...

  3. 【特效】单选按钮和复选框的美化(只用css)

    表单的默认样式都是比较朴素的,实际页面中往往需要美化他们.这里先说说单选按钮和复选框,有了css3,这个问题就变的好解决了.利用input与label相关联,对label进行美化并使其覆盖掉原本的in ...

  4. UEFI启动视频详解:启动分析+N项操作实例

    ============================================================= ※※※※最给力的视频解说※※※※ 2011hiboy全部共享资料:立刻去   ...

  5. win10 uwp 读取保存WriteableBitmap 、BitmapImage

    我们在UWP,经常使用的图片,数据结构就是 BitmapImage 和 WriteableBitmap.关于 BitmapImage 和 WriteableBitmap 区别,我就不在这里说.主要说的 ...

  6. 小星星的php

    大家好,我是小星星,最近新学的php,我迫不及待要跟大家分享了!!come on!一起来看小星星的世界 先为大家介绍php基础 首先我们来看看什么叫php: PHP(外文名:PHP: Hypertex ...

  7. 测试中出现ERROR StatusLogger No log4j2 configuration file

    概述 在hibernate框架搭建完成用log4j2进行测试时,总是出现ERROR StatusLogger No log4j2 configuration file found. Using def ...

  8. IO基础内容(File)

    JavaIO基础内容 IO技术概述 Output 把内存中的数据存储到持久化设备上这个动作称为输出(写)Output操作 Input 把持久设备上的数据读取到内存中的这个动作称为输入(读)Input操 ...

  9. Java常用类(一)String类详解

    前言 在我们开发中经常会用到很多的常用的工具类,这里做一个总结.他们有很多的方法都是我们经常要用到的.所以我们一定要把它好好的掌握起来! 一.String简介 1.1.String(字符串常量)概述 ...

  10. 同步IO和异步IO

    链接: 同步IO和异步IO socket阻塞与非阻塞,同步与异步.I/O模型 Linux的IO系统常用系统调用及分析 linux异步IO的两种方式