发表于: 7/18/2022
:::info 本文基于【基于视野的战争迷雾实现】Unity战争迷雾实现 @琴桌 实现。 Github :::
:::default 一个月的磕磕绊绊终于是完成了效果,总感觉既然当初察觉了掌握ECS的契机不如直接上手,现在纠结这些对象、类功能的划分实属难受。 旁人的打断,自己的摆烂,想到这里,不禁潸然。 :::
概述
总体上的思路为将地图按方块划分,将地图上的障碍物标记出来,对每个有视野的单位进行射线检测判断视野是否被遮挡,将方块化的视野转换为RenderTexture作为迷雾对象的材质渲染到地图的上方。
地图块
地图块定义
使用一个枚举代表一个地图块的状态
public enum MapStatus:byte{
Empty,
Obstacle,
AllyUnit,
EnemeyUnit,
SelfUnit
}
// 整个地图的地图块
MapStatus[,] innerMap;
// 地图宽高
public int Width { get => innerMap.GetLength(0); }
public int Height { get => innerMap.GetLength(1); }
// 地图块的边长
public float GridSize { get; }
地图块索引
对地图的索引到真实位置的转换则有
public Vector2 GetRealPosition(int x, int y)
{
return new Vector2(
(x - Width / 2 + 0.5f) * GridSize,
(y - Height / 2 + 0.5f) * GridSize
);
}
public Vector2Int GetGridPosition(Vector2 position)
{
return new Vector2Int(
(int)((position.x / GridSize + Width / 2 - 0.5f)),
(int)((position.y / GridSize + Height / 2 - 0.5f))
);
}
public bool IsInMap(Vector2Int pos)
{
return (pos.x >= 0 && pos.x <= Width) && (pos.y >= 0 && pos.y <= Height);
}
地图块生成
对于地图,可以使用 Physics2D.OverlapBox()
1 来判断地图块状态,使用了Solid Unit
作为障碍物的Layer,排除其他单位碰撞体。
for (int i = 0; i < Width; i++)
for (int j = 0; j < Height; j++)
{
var box = Physics2D.OverlapBox(GetRealPosition(i, j), new Vector2(GridSize, GridSize)/2, 0, LayerMask.GetMask("Solid Unit"));
if (box != null)
{
innerMap[i, j] = MapStatus.Obstacle;
}
else innerMap[i, j] = MapStatus.Empty;
}
单位视野
使用一个Behavior存储单位视野相关的数据
public class UnitVision : MonoBehaviour
{
[SerializeField] // 这个 Attribute 可以让变量显示在 Inspector 窗口中
float sightRange = 5;
public float GetSight()
{
return sightRange;
}
}
迷雾生成
首先归纳下现有代码,将地图块内代码均包装为MapGrid
类
public class MapGrid{
// ...
}
基本设施
public class FogMaker : MonoBehaviour{
// 地图网格
MapGrid mapGrid;
// 观察者
List<UnitVision> viewers = new();
// 迷雾颜色信息
Color32[] colorBuffer;
// 网格索引转换为颜色索引
public int ColorIndex(int x, int y)
{
x = Math.Clamp(x, 0, mapGrid.Width);
y = Math.Clamp(y, 0, mapGrid.Height);
return x + y * mapGrid.Width;
}
public void Init(MapGrid map){
// 初始化迷雾为不透明黑色
colorBuffer = new Color32[mapGrid.Length];
Array.Fill(colorBuffer, new Color32(0,0,0,255));
// ...
}
// ...
}
迷雾的计算
对每个观察者,获取视野范围内的方块
foreach (var v in viewers)
{
var pos = mapGrid.GetGridPosition(v.transform.position);
var sight_radius = v.GetSight() / mapGrid.GridSize;
var sight_square = (int) (sight_radius * sight_radius);
List<Vector2Int> curVisionPos = new();
for(int i = - (int)sight_radius; i <= (int)sight_radius; i++)
for(int j = - (int)sight_radius; j <= (int)sight_radius; j++)
{
if (i * i + j * j <= sight_square)
{
// 该坐标处于可视范围内 且处于地图内
var p = pos + new Vector2Int(i, j);
if(mapGrid.IsInMap(p))
curVisionPos.Add(p);
}
}
// ...
}
对获取到的方块,将其与观察者连线,判断中间是否有障碍物遮挡,若有则从视野中移除。
// 由远到近排序
curVisionPos.Sort((a, b) => DistanceSquared(pos, b) - DistanceSquared(pos, a));
HashSet<Vector2Int> visited = new(), novision = new();
foreach (var p in curVisionPos)
{
if (!visited.Contains(p))
{
var (blocked, unblocked) = GetBlockedAndUnblocked(pos,p);
novision.UnionWith(blocked);
visited.UnionWith(blocked);
visited.UnionWith(unblocked);
}
}
// 移除无视野
curVisionPos.RemoveAll(p => novision.Contains(p));
// 将视野区域设置为透明
foreach (var p in curVisionPos)
{
colorBuffer[ColorIndex(p)].a = 0;
}
这里首先按照由远到近的距离将方块排序,提高后续计算效率,因为在计算遮挡时会将所有被遮挡和不被遮挡的方块返回,可以尽可能多的排除visited方块。
射线检测函数
由起点向终点逐方块检测是否有障碍物,若遇到障碍物则后续方块均为无视野的遮挡区域。
public (List<Vector2Int>, List<Vector2Int>) GetBlockedAndUnblocked(Vector2Int source, Vector2Int target)
{
var blocked = new List<Vector2Int>();
var unblocked = new List<Vector2Int>();
var delta = target - source;
// 起始位置相同
if(delta.sqrMagnitude == 0)
{
if (mapGrid[source[0], source[1]].HasFlag(MapGrid.MapStatus.Obstacle))
blocked.Add(source);
else unblocked.Add(source);
return (blocked, unblocked);
}
unblocked.Add(source);
bool blockMet = false;
// 判断 X 或 Y 作为基础步长方向
if (Math.Abs(delta.x) > Math.Abs(delta.y))
{// X 轴为方向
// 符号
int x_sign = Math.Sign(delta.x);
// 增量
var y_inc = (delta.y / (float) Math.Abs(delta.x));
float step_y = y_inc;
for (int step_x = x_sign;
x_sign * step_x <= x_sign * delta.x;
step_x+=x_sign, step_y+=y_inc)
{
var s = source + new Vector2Int(step_x, Mathf.RoundToInt(step_y));
if (!blockMet)
{
unblocked.Add(s);
blockMet = mapGrid[s].HasFlag(MapGrid.MapStatus.Obstacle);
}
else
blocked.Add(s);
}
}
else
{
// 方向为 Y 轴的情况与 X 轴基本相同,只需调换 x、y 的角色即可
// ...
}
return (blocked, unblocked);
}
迷雾刷新
在区域丢失视野时重新覆盖一层半透明的已探索迷雾
public void RefreshFog()
{
for(int i = 0; i < colorBuffer.Length; i++)
{
if (colorBuffer[i].a == 0)
colorBuffer[i].a = 180;
}
}
配合使用可以实现战争迷雾-(未)探索区域的效果。
迷雾贴图
:::info Shader、材质、纹理,属实头大住了,并没有完全理解此部分的流程,不过姑且实现了效果 :::
初始化资源
Material blurMaterial;
Texture2D textureBuffer;
RenderTexture renderTexture;
RenderTexture renderTexture2;
RenderTexture nextTexture;
RenderTexture currnetTexture;
public void Init(MapGrid map){
// ...
blurMaterial = new Material(Shader.Find("ImageEffect/AverageBlur"));
textureBuffer = new Texture2D(mapGrid.Width, mapGrid.Height, TextureFormat.ARGB32, false);
renderTexture = RenderTexture.GetTemporary(mapGrid.Width, mapGrid.Height, 0);
renderTexture2 = RenderTexture.GetTemporary(mapGrid.Width, mapGrid.Height, 0);
nextTexture = RenderTexture.GetTemporary(mapGrid.Width, mapGrid.Height, 0);
currnetTexture = RenderTexture.GetTemporary(mapGrid.Width, mapGrid.Height, 0);
}
// 在销毁时释放资源
private void OnDestroy()
{
Release();
}
public void Release()
{
RenderTexture.ReleaseTemporary(renderTexture);
RenderTexture.ReleaseTemporary(renderTexture2);
RenderTexture.ReleaseTemporary(currnetTexture);
RenderTexture.ReleaseTemporary(nextTexture);
}
使用颜色信息生成 Texture,并进行几次模糊
public void MakeTextureAndBlur()
{
textureBuffer.SetPixels32(colorBuffer);
textureBuffer.Apply();
Graphics.Blit(textureBuffer, renderTexture, blurMaterial, 0);
Graphics.Blit(renderTexture, renderTexture2, blurMaterial, 0);
Graphics.Blit(renderTexture2, renderTexture, blurMaterial, 0);
Graphics.Blit(renderTexture, nextTexture);
}
缓动,在两帧迷雾间过渡
public void Lerp()
{
Graphics.Blit(currnetTexture, renderTexture);
blurMaterial.SetTexture("_LastTex", renderTexture);
Graphics.Blit(nextTexture, currnetTexture, blurMaterial, 1);
}
在每次更新时更新迷雾
private void Update()
{
RefreshFog();
ComputeFog();
MakeTextureAndBlur();
Lerp();
}
对外提供 Texture 获取
public RenderTexture FogTexture { get => currnetTexture; }
迷雾渲染
获取渲染迷雾的对象的材质,每次更新时将生成的 FogTexture 赋予到材质即可。
public class FogRenderer : MonoBehaviour
{
public GameObject fogRenderer;
public FogMaker fogMaker;
Material fogMaterial;
void Start()
{
fogMaterial = fogRenderer.GetComponent<Renderer>().material;
}
// Update is called once per frame
void Update()
{
if (fogMaker.FogTexture != null)
fogMaterial.SetTexture("_MainTex", fogMaker.FogTexture);
}
}
为 fogRenderer 添加如下组件
:::info 在2D项目中使用Plane需要进行旋转,确保方向符合你的要求。
MeshRenderer的材质是使用Shader创建的,见此 :::
:::default 意外的实现了一个草丛呢…真是世事难料。 在本章节中的MapGrid可以基于此上进一步来实现寻路系统,敬请期待。 :::
Footnotes
-
对于3D项目,使用
Physics.CheckBox()
↩