SLG游戏中,往往有大量队伍行军线路表现的需求,从数百到数千上万条不等,数量越多,表示显示范围内队伍越多,此时越要注意性能要求,不能因线条表现占用过多计算资源.
- 理想情况下应该: 占用1个DC,能通过指定纹理表现路线(圆点,矩形等),能有动画效果,数据变更时快速更新等.
- 如果将所有顶点的绘制都放到一个mesh中可以做到1个DC,但遇到路径更新,边界计算等时,会维护一个非常庞大的mesh数据,如果使用的List来维护这些定点,还牵涉到拷贝,扩容等操作.
假设:
- 开辟一个mesh,挂载到一个gameobject中,如果该mesh的顶点数量上限是固定的,那就相当于是一个顶点池,可以使用一个固定长度的数组来存储这些顶点,新添加的路径(多个顶点)放入该mesh中,相当与消耗池中剩余顶点.
- 由于使用了固定长度的数组,mesh中的路径删除,就只做归零或者设置一个很远的坐标,避免对数组长度的修改.
- 当一个go中的mesh消耗完毕,就开辟一个新的mesh(挂载到一个新的go中).
- 当一个mesh中所有顶点都标记为删除,则可以移除该mesh(和对应的go).
- 路径中各个定点的计算,一些循环遍历等,如果放入多线程中执行,那就进一步减少对主线程的卡顿.
代码如下:
using UnityEngine;
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
///<summary>
/// 大地图中,大量路径的管理类. 使用分层(多gameobject)形式和异步操作,避免单个gameobject中mesh顶点过多,在更新时导致的卡顿.
///
/// 异步模式流程:
/// 1. 主线程:根据传入的Points,预判断要往哪个layer(的同步队列)添加,并将layerID写入line中,如果layer数量不足时,产生新layer;
/// 2. 主线程:add和update操作会立即在对应layer上分配空间;
/// 3. 主线程:根据分配结果,通过line生成segment信息作为topvalue传入cmdFunc,将cmdFunc添加进对应layer的同步队列;
/// 4. 子线程:从队列中取出cmdFunc并执行,调用segment.calculate()进行具体顶点值计算,并将计算结果设置到layer的info cache中;
/// 5. 主线程:layer的Update()将layer的计算结果info cache同步到mesh中;
/// 6. 主线程:manager的Update()进行material动画处理(tilling,offset);
///</summary>
public class PathNavLineManager : AsyncExecCMDFunc
{
private Dictionary<int, PathNavLine> lines = new Dictionary<int, PathNavLine>();
[Header("layer可容纳线段的数量")] public int layerLineCapacity = 2000;
[Header("最低缓存的layer数量,-1表示全部缓存")] public int cacheLayerCount = 3;
[SerializeField] private Material m_material;
[SerializeField, Header("线条宽度")] private float m_tickness = 1;
[SerializeField, Header("tickness的值是否用于缩放,否则用于平铺")]
private bool m_ticknessInfluence = true;
[SerializeField, Header("重置_MainTex.offset的阈值")]
private float m_resetOffTr = 60;
[SerializeField, Header("offset.x的方向")]
private bool m_xForward = true;
[SerializeField, Header("是否显示动画")] private bool m_showTween = true;
[SerializeField, Header("动画速度倍数")] private float m_speed = 1f;
[SerializeField, Header("mesh边界size")] private Vector3 m_meshBoundSize = new Vector3(2048, 1, 2048);
[SerializeField, Header("是否异步执行")] private bool m_isAsync = false;
///<summary>
/// tickness不能动态设置,因为line顶点在生成时使用了旧值进行计算,
/// 如果重新设置tickness(旧顶点没有做重新计算变化),在设置tiling时会导致不匹配,产生旧的line有缩放效果
///</summary>
public float tickness
{
get { return m_tickness; }
set => this.m_tickness = value;
}
public Material material
{
get => m_material;
set
{
m_material = value;
foreach (var layer in layers)
{
layer.material = value;
}
}
}
public bool ticknessInfluence
{
get => m_ticknessInfluence;
set
{
m_ticknessInfluence = value;
foreach (var layer in layers)
{
layer.ticknessInfluence = value;
}
}
}
public float resetOffTr
{
get => m_resetOffTr;
set
{
m_resetOffTr = value;
foreach (var layer in layers)
{
layer.resetOffTr = value;
}
}
}
public bool xForward
{
get => m_xForward;
set
{
m_xForward = value;
foreach (var layer in layers)
{
layer.xForward = value;
}
}
}
public bool showTween
{
get => m_showTween;
set
{
m_showTween = value;
foreach (var layer in layers)
{
layer.showTween = value;
}
}
}
public float speed
{
get => m_speed;
set
{
m_speed = value;
foreach (var layer in layers)
{
layer.speed = value;
}
}
}
public Vector3 meshBoundSize
{
get => m_meshBoundSize;
set
{
m_meshBoundSize = value;
foreach (var layer in layers)
{
layer.meshBoundSize = value;
}
}
}
public bool IsAsync
{
get => m_isAsync;
set { m_isAsync = value; }
}
private List<PathNavLineLayer> layers = new List<PathNavLineLayer>();
public PathNavLine GetLine(int id)
{
PathNavLine line = null;
this.lines.TryGetValue(id, out line);
return line;
}
///<summary>
/// 新增一条线
///</summary>
public int Draw(Vector3[] points, Color color = default(Color))
{
return this.Draw(-1, points, color);
}
///<summary>
/// 为指定id的线更新顶点信息,如果该线条不存在,则新增
///</summary>
public int Draw(int id, Vector3[] points, Color color = default(Color))
{
if (points == null || points.Length <= 1)
{
Debug.LogWarning("传入的points长度必须大于1");
return -1;
}
if (color == default(Color))
{
color = Color.white;
}
PathNavLine line = id == -1 ? null : this.GetLine(id);
if (line == null)
{
line = new PathNavLine();
line.SetPoints(points);
line.m_color = color;
this.lines.Add(line.GetHashCode(), line);
this.AddPath(line);
}
else
{
if (!CheckLine(line))
{
return Draw(points);
}
line.SetPoints(points);
line.m_color = color;
this.UpdatePath(line);
}
return line.GetHashCode();
}
public int[] Draw(Vector3[][] points)
{
var ids = new int[points.Length];
for (int i = 0; i < points.Length; i++)
{
ids[i] = this.Draw(points[i]);
}
return ids;
}
///<summary>
/// 通过id移除一条线.
///</summary>
public void Remove(int id)
{
var line = this.GetLine(id);
if (!CheckLine(line)) return;
var layerID = line.m_layerID;
var index4InLayer = line.index4InLayer;
var lenInLayer = line.lenInLayer;
line.MarkDisopose();
if (this.IsAsync)
{
this.AddCMDFunc(() =>
{
// Debug.Log(">>>>RemovePath: ", layerID, index4InLayer, lenInLayer);
_RemovePath(layerID, index4InLayer, lenInLayer);
this.lines.Remove(id);
});
}
else
{
_RemovePath(layerID, index4InLayer, lenInLayer);
this.lines.Remove(id);
}
}
public void Remove(int[] ids)
{
for (int i = 0; i < ids.Length; i++)
{
this.Remove(ids[i]);
}
}
///<summary>
/// 设置可见性
///</summary>
public void SetActive(int id, bool b)
{
var line = this.GetLine(id);
if (!CheckLine(line)) return;
var layerID = line.m_layerID;
var index4InLayer = line.index4InLayer;
var lenInLayer = line.lenInLayer;
line.m_active = b;
var c = line.m_color;
if (b)
c.a = line._colorAlphaCache;
else
c.a = 0;
// this._SetColor(line.layerID, line.index4InLayer, line.lenInLayer, c);
if (this.IsAsync)
{
this.AddCMDFunc(() => { this._SetColor(layerID, index4InLayer, lenInLayer, c); });
}
else
{
this._SetColor(layerID, index4InLayer, lenInLayer, c);
}
}
public void SetActives(int[] ids, bool b)
{
for (int i = 0; i < ids.Length; i++)
{
this.SetActive(ids[i], b);
}
}
///<summary>
/// 更新颜色
///</summary>
public void SetColor(int id, Color color)
{
var line = this.GetLine(id);
if (!CheckLine(line)) return;
var layerID = line.m_layerID;
var index4InLayer = line.index4InLayer;
var lenInLayer = line.lenInLayer;
if (this.IsAsync)
{
this.AddCMDFunc(() => { this._SetColor(layerID, index4InLayer, lenInLayer, color); });
}
else
{
this._SetColor(layerID, index4InLayer, lenInLayer, color);
}
}
public void SetColors(int[] ids, Color color)
{
for (int i = 0; i < ids.Length; i++)
{
this.SetColor(ids[i], color);
}
}
///<summary>
/// 添加路径
///</summary>
private void AddPath(PathNavLine line)
{
if (!CheckLine(line)) return;
// Unity.Profiling.ProfilerMarker pm = new Unity.Profiling.ProfilerMarker($"消耗跟踪 [AddPath]");
// pm.Begin();
var vertexCount = line.GetNeedSpace().Item1;
if (vertexCount == 0) return;
var layer = this.GetNextActiveLayer(vertexCount);
// 在task执行前就要分配好空间
layer.Allocate(line);
var seg = line.ToSegment();
if (this.IsAsync)
this.AddCMDFunc(() => { layer.DoAdd(seg); });
else
layer.DoAdd(seg);
// pm.End();
}
///<summary>
/// 移除路径
///</summary>
private void RemovePath(int layerID, int index4InLayer, int lenInLayer)
{
if (this.IsAsync)
{
this.AddCMDFunc(() =>
{
// Debug.Log(">>>>RemovePath: ", line.lenInLayer);
_RemovePath(layerID, index4InLayer, lenInLayer);
});
}
else
{
_RemovePath(layerID, index4InLayer, lenInLayer);
}
}
private void _RemovePath(int layerID, int index4InLayer, int lenInLayer)
{
var layer = this.GetLayerById(layerID);
if (layer == null)
{
Debug.Log($"[_RemovePath], layer is null: {layerID}");
return;
}
// layer.DoRemove(line);
layer.DoRemove(index4InLayer, lenInLayer);
if (!this.IsAsync)
{
this.DoFullDirtyLayer(layer);
}
}
///<summary>
/// 更新路径,路径points有变化时应该调用此方法.
///</summary>
private void UpdatePath(PathNavLine line)
{
var vertexCount = line.GetNeedSpace().Item1;
if (vertexCount == 0)
return;
PathNavLineLayer lineCurrLayer = this.GetLayerById(line.m_layerID);
// 如果line当前要使用的空间和上次记录的长度一致,说明可以直接在原有layer中的原有位置进行修改
if (vertexCount == line.lenInLayer)
{
var seg = line.ToSegment();
if (this.IsAsync)
{
this.AddCMDFunc(() => { lineCurrLayer.DoUpdatePath(seg); });
// Debug.Log("<color=yellow>UpdatePath, 长度相同</color>", line._cmdHash);
}
else
{
lineCurrLayer.DoUpdatePath(seg);
}
}
else
{
var oldIndex4 = line.index4InLayer;
var oldLen = line.lenInLayer;
// Debug.Log("<color=cyan>更新:但长度不同</color>");
// 将原有位置的移除
this.RemovePath(line.m_layerID, oldIndex4, oldLen);
// 作为新的添加
this.AddPath(line);
}
}
private void _SetColor(int layerID, int index4InLayer, int lenInLayer, Color color)
{
var layer = this.GetLayerById(layerID);
layer.SetColor(index4InLayer, lenInLayer, color);
}
private bool CheckLine(PathNavLine line)
{
if (line == null)
{
// throw new System.Exception($"PathNavLine为空.");
// Debug.Log($"<color=yellow>PathNavLine为空.</color>");
return false;
}
if (line.disposed)
{
// throw new System.Exception($"PathNavLine已经销毁. {line}");
// Debug.Log($"<color=yellow>PathNavLine已经销毁. {line}</color>");
return false;
}
return true;
}
private PathNavLineLayer GetNextActiveLayer(int space)
{
PathNavLineLayer layer = null;
for (int i = 0; i < this.layers.Count; i++)
{
var _cache = layers[i];
if (_cache.CanPut(space))
{
layer = _cache;
break;
}
}
if (layer == null)
{
var go = new GameObject();
layer = go.AddComponent<PathNavLineLayer>();
layer.material = this.m_material;
if (layerLineCapacity <= 0)
{
throw new Exception("layerLineCapacity必须大于0");
}
layer.LineCapacity = this.layerLineCapacity;
// 如果新产生的layer的默认空间不足以放下line,则扩容
if (space > layer.VertexMaxLen)
{
// Debug.Log($"触发扩容, {layer.VertexMaxLen}->{space}");
layer.LineCapacity = Mathf.CeilToInt((float)space / (float)PathNavLineLayer.VertexCount);
}
layer.speed = this.m_speed;
layer.tickness = this.tickness;
layer.ticknessInfluence = this.m_ticknessInfluence;
layer.resetOffTr = this.m_resetOffTr;
layer.xForward = this.m_xForward;
layer.showTween = this.m_showTween;
layer.meshBoundSize = this.m_meshBoundSize;
layer.isAsync = this.m_isAsync;
layer.Init(this);
this.layers.Add(layer);
go.transform.parent = this.gameObject.transform;
// Debug.Log($"[GetNextActiveLayer]产生新layer: {layer.ID}");
}
return layer;
}
private PathNavLineLayer GetLayerById(int id)
{
for (int i = 0; i < this.layers.Count; i++)
{
if (this.layers[i].ID == id)
return this.layers[i];
}
throw new System.Exception($"无法找到id为'{id}'的PathNavLineLayer");
}
///<summary>
/// layer的空间使用后,根据情况决定是否移除
///</summary>
private void DoFullDirtyLayer(PathNavLineLayer layer)
{
if (layer.IsFullDirty())
{
// 移除多余的layer
if (cacheLayerCount > -1 && this.layers.Count > cacheLayerCount)
{
this.layers.Remove(layer);
GameObject.DestroyImmediate(layer.gameObject);
}
}
}
private void Update()
{
if (this.IsAsync)
{
if (this.asyncResultDirty)
{
this.asyncResultDirty = false;
for (int i = this.layers.Count - 1; i >= 0; i--)
{
this.DoFullDirtyLayer(this.layers[i]);
}
}
}
TweenLine();
}
private void TweenLine()
{
if (!showTween || this.layers.Count <= 0) return;
if (this.material == null) return;
var offset = material.GetTextureOffset("_MainTex");
offset.x += Time.deltaTime * speed * (xForward ? -1 : 1);
if (offset.x >= resetOffTr || offset.x <= -resetOffTr)
{
offset.x = 0;
}
material.SetTextureOffset("_MainTex", offset);
}
// override protected void OnDestroy()
// {
// base.OnDestroy();
// }
}
/// <summary>
/// 路线layer,存储了具体的线段
/// </summary>
[DisallowMultipleComponent]
internal class PathNavLineLayer : MonoBehaviour
{
internal static readonly int VertexCount = 4;
internal static readonly int TriangleCount = 6;
private int m_lineCapacity = 1000;
internal int VertexMaxLen = 0;
public int LineCapacity
{
get => m_lineCapacity;
set
{
m_lineCapacity = value;
this.VertexMaxLen = value * VertexCount;
}
}
private Vector3[] vertices;
private int[] triangles;
private Vector2[] uvs;
private Color[] colors;
/// <summary>
/// 顶点数量
/// </summary>
private int validIndex4 = 0;
/// <summary>
/// 三角形数量
/// </summary>
private int validIndex6 = 0;
private int validCount = 0;
private Mesh mesh;
private int m_ID;
private bool m_Inited;
private bool m_used;
// private GameObject m_go;
[SerializeField] private Material _material;
internal Material material
{
get { return this._material; }
set
{
if (this.meshRenderer == null) this.meshRenderer = this.GetComponent<MeshRenderer>();
this._material = value;
meshRenderer.sharedMaterial = value;
}
}
private float _tickness = 1;
internal float tickness
{
get { return _tickness; }
set
{
this._tickness = value;
var tiling = Vector2.one; // this.material.GetTextureScale("_MainTex");
// 如果直接设置value给tiling.x,会产生不等比的缩放效果,被1除之后才是等比
tiling.x = 1 / value;
if (ticknessInfluence)
{
tiling.y = 1; // value * tinling.x
}
else
{
tiling.y = tickness;
}
this.material.SetTextureScale("_MainTex", tiling);
}
}
internal bool ticknessInfluence = true;
internal float resetOffTr = 60;
internal bool xForward = true;
internal bool showTween = true;
internal float speed = 1f;
internal Vector3 meshBoundSize = new Vector3(2048, 1, 2048);
internal bool isAsync = false;
[SerializeField] private int __pathCount = 0;
#if UNITY_EDITOR
[SerializeField] private int __vertices = 0;
[SerializeField] private int __validIndex = 0;
[SerializeField] private int __rspace = 0;
#endif
private MeshRenderer meshRenderer;
private MeshFilter meshFilter;
internal static readonly object _lock = new object();
private static readonly Vector3 ZeroVector3 = Vector3.zero;
private PathNavLineManager manager;
private bool resultDirty;
public int ID
{
get => m_ID;
}
internal void Awake()
{
// this.taskExistsTime = 5;
this.m_ID = this.GetHashCode();
this.gameObject.name = this.m_ID.ToString();
mesh = new Mesh();
meshFilter = this.gameObject.AddComponent<MeshFilter>();
meshFilter.sharedMesh = mesh;
meshRenderer = this.gameObject.GetComponent<MeshRenderer>();
if (meshRenderer == null)
{
meshRenderer = this.gameObject.AddComponent<MeshRenderer>();
}
meshRenderer.sharedMaterial = material;
meshRenderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
meshRenderer.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
meshRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
meshRenderer.receiveShadows = false;
}
internal void Init(PathNavLineManager manager)
{
if (this.m_Inited) return;
this.manager = manager;
this.m_Inited = true;
// var bounds = mesh.bounds;
// bounds.size = this.meshBoundSize;
// mesh.bounds = bounds;
this.vertices = new Vector3[VertexMaxLen];
this.triangles = new int[m_lineCapacity * TriangleCount];
this.uvs = new Vector2[VertexMaxLen];
this.colors = new Color[VertexMaxLen];
var _y = this.meshBoundSize.y;
// this.vertices[0] = new Vector3(0, _y, 0);
// this.vertices[1] = new Vector3(0, _y, this.meshBoundSize.z);
// this.vertices[2] = new Vector3(this.meshBoundSize.x, _y, this.meshBoundSize.z);
// this.vertices[3] = new Vector3(this.meshBoundSize.x, _y, 0);
this.vertices[0] = new Vector3(-this.meshBoundSize.x / 2, _y, 0);
this.vertices[1] = new Vector3(-this.meshBoundSize.x / 2, _y, this.meshBoundSize.z / 2);
this.vertices[2] = new Vector3(this.meshBoundSize.x / 2, _y, this.meshBoundSize.z / 2);
this.vertices[3] = new Vector3(this.meshBoundSize.x / 2, _y, -this.meshBoundSize.z / 2);
this.triangles[0] = 0;
this.triangles[1] = 1;
this.triangles[2] = 2;
this.triangles[3] = 0;
this.triangles[4] = 2;
this.triangles[5] = 3;
this.SyncAllToMesh();
mesh.RecalculateBounds();
// meshFilter.sharedMesh = mesh;
this.validCount = VertexMaxLen;
}
internal void Allocate(PathNavLine line)
{
this.m_used = true;
var (vertexCount, triangleCount) = line.GetNeedSpace();
line.index4InLayer = this.validIndex4;
line.index6InLayer = this.validIndex6;
line.lenInLayer = vertexCount;
line.m_layerID = this.m_ID;
// Debug.Log("<color=yellow> add before:: </color>", line.GetHashCode(), line.lenInLayer, line.points.Length, line.index4InLayer, this.vertices.Length);
this.validIndex4 += vertexCount;
this.validIndex6 += triangleCount;
this.validCount -= vertexCount;
}
// internal void DoAdd(int index4InLayer, int index6InLayer, Vector3[] _vertices, int[] _triangles, Vector2[] _uvs, Color[] _colors)
internal void DoAdd(LineSegment segment)
{
this.resultDirty = true;
__pathCount++;
segment.Calculate(this.tickness);
this.SyncFromLine(segment);
}
private Dictionary<int, bool> removedSegIndex = new Dictionary<int, bool>();
internal void DoRemove(int index4InLayer, int lenInLayer)
{
// _SetToZero(index4InLayer, lenInLayer);
// 记录删除段的起始位置,避免重复删除
if (this.removedSegIndex.ContainsKey(index4InLayer))
{
Debug.Log($"[PathNavLineManager.DoRemove] removedSegIndex.ContainsKey:{index4InLayer}");
return;
}
resultDirty = true;
__pathCount--;
this.removedSegIndex.Add(index4InLayer, true);
// 仅做归零处理,不会实际上的删除
for (int i = index4InLayer; i < index4InLayer + lenInLayer; i++)
{
this.vertices[i] = ZeroVector3;
}
this.validCount += lenInLayer;
}
///<summary>
/// 当前layer是否已经被完全使用(即:所有的点都使用过数据,且又清除过)
///</summary>
internal bool IsFullDirty()
{
if (this.m_used && validCount >= this.VertexMaxLen)
{
this.validIndex4 = 0;
this.validIndex6 = 0;
// Debug.Log("<color=cyan>IsFullDirty::: </color>", this.ID, validCount, VertexMaxLen, this.RemainderfSpace());
this.removedSegIndex.Clear();
// 完全移除
return true;
}
return false;
}
///<summary>
/// manager已经保证传入的segment和原始位置长度是完全对应的,所以此处直接进行设置即可
///</summary>
internal void DoUpdatePath(LineSegment segment)
{
this.resultDirty = true;
segment.Calculate(this.tickness);
this.SyncFromLine(segment);
}
internal void SetColor(int index4InLayer, int lenInLayer, Color c)
{
this.resultDirty = true;
for (int i = index4InLayer; i < index4InLayer + lenInLayer; i++)
{
this.colors[i] = c;
}
if (!this.isAsync)
{
this.mesh.SetColors(this.colors);
}
}
private void SyncFromLine(LineSegment segment)
{
Vector3[] _vertices = segment.vertices;
int[] _triangles = segment.triangles;
Vector2[] _uvs = segment.uvs;
Color[] _colors = segment.colors;
int index4InLayer = segment.index4InLayer;
int index6InLayer = segment.index6InLayer;
for (int i = 0; i < _vertices.Length; i++)
{
this.vertices[index4InLayer] = _vertices[i];
this.uvs[index4InLayer] = _uvs[i];
this.colors[index4InLayer] = _colors[i];
index4InLayer++;
}
for (int i = 0; i < _triangles.Length; i++)
{
this.triangles[index6InLayer] = _triangles[i];
index6InLayer++;
}
}
private void SyncAllToMesh()
{
// 不使用 this.mesh.vertices = this.vertices 的形式.避免过多gc
this.mesh.SetVertices(this.vertices); //, 0, _validIndex4);
this.mesh.SetUVs(0, this.uvs); //, 0, _validIndex4);
this.mesh.SetColors(this.colors); //, 0, _validIndex4);
this.mesh.SetTriangles(this.triangles, 0);
}
private void Update()
{
if (this.resultDirty)
{
this.resultDirty = false;
// asyncResultDirty有处理结果,将这些结果同步到mesh
this.SyncAllToMesh();
#if UNITY_EDITOR
this.__vertices = this.VertexMaxLen;
this.__validIndex = this.validIndex4;
this.__rspace = this.RemainderfSpace();
#endif
}
}
protected void OnDestroy()
{
removedSegIndex.Clear();
removedSegIndex = null;
this.mesh = null;
this.vertices = null;
this.triangles = null;
this.uvs = null;
this.colors = null;
}
internal bool CanPut(int space)
{
if (!m_Inited || space > this.RemainderfSpace())
return false;
else
return true;
}
internal int RemainderfSpace()
{
return this.vertices.Length - validIndex4;
}
}
/// <summary>
/// 线段
/// </summary>
internal class LineSegment
{
internal static Vector2[] _UV = new Vector2[2] { new Vector2(0, 0), new Vector2(0, 1) };
internal int index4InLayer = -1;
internal int index6InLayer = -1;
internal int lenInLayer = -1;
internal Vector3[] points;
internal Vector3[] vertices;
internal int[] triangles;
internal Vector2[] uvs;
internal Color[] colors;
internal float _colorAlphaCache = 1;
internal Color m_color = Color.white;
/// <summary>
/// 线段的定点计算
/// </summary>
/// <param name="tickness"></param>
internal void Calculate(float tickness)
{
// 避免重复计算
// if (!calcDirty) return;
// calcDirty = false;
if (points == null) points = new Vector3[0];
var _len = (points.Length - 1) * PathNavLineLayer.VertexCount;
// if (vertices == null || _len != vertices.Length)
vertices = new Vector3[_len];
_len = (points.Length - 1) * PathNavLineLayer.TriangleCount;
// if (triangles == null || _len != triangles.Length)
triangles = new int[_len];
_len = (points.Length - 1) * PathNavLineLayer.VertexCount;
// if (uvs == null || _len != uvs.Length)
uvs = new Vector2[_len];
// if (colors == null || _len != colors.Length)
colors = new Color[_len];
this._colorAlphaCache = this.m_color.a;
if (points != null && points.Length > 0)
{
Vector3 stPos = points[0]; //trans.InverseTransformPoint(points[0]);
var trigIndex = 0;
var halfTickness = tickness * .5f;
for (int i = 1; i < points.Length; i++)
{
var pos = points[i]; //trans.InverseTransformPoint(points[i]);
var rad = Mathf.Atan2(pos.z - stPos.z, pos.x - stPos.x);
float _dist = Vector3.Distance(stPos, pos);
var sinT = Mathf.Sin(rad) * halfTickness;
var cosT = Mathf.Cos(rad) * halfTickness;
// 左下角起,逆时针
var x0 = stPos.x - sinT;
var z0 = stPos.z + cosT;
var x1 = stPos.x + sinT;
var z1 = stPos.z - cosT;
var x2 = pos.x + sinT;
var z2 = pos.z - cosT;
var x3 = pos.x - sinT;
var z3 = pos.z + cosT;
int i4 = (i - 1) * PathNavLineLayer.VertexCount; // * 4
vertices[i4 + 0].x = x0;
vertices[i4 + 0].y = stPos.y;
vertices[i4 + 0].z = z0;
vertices[i4 + 1].x = x1;
vertices[i4 + 1].y = stPos.y;
vertices[i4 + 1].z = z1;
vertices[i4 + 2].x = x2;
vertices[i4 + 2].y = pos.y;
vertices[i4 + 2].z = z2;
vertices[i4 + 3].x = x3;
vertices[i4 + 3].y = pos.y;
vertices[i4 + 3].z = z3;
triangles[trigIndex + 0] = i4 + 0 + this.index4InLayer;
triangles[trigIndex + 1] = i4 + 1 + this.index4InLayer;
triangles[trigIndex + 2] = i4 + 2 + this.index4InLayer;
triangles[trigIndex + 3] = i4 + 0 + this.index4InLayer;
triangles[trigIndex + 4] = i4 + 2 + this.index4InLayer;
triangles[trigIndex + 5] = i4 + 3 + this.index4InLayer;
uvs[i4 + 0] = _UV[0];
uvs[i4 + 1] = _UV[1];
// 使用距离来进行uv.x设置,使其看起来平铺
uvs[i4 + 2].x = _dist;
uvs[i4 + 2].y = 1;
uvs[i4 + 3].x = _dist;
uvs[i4 + 3].y = 0;
colors[i4 + 0] = this.m_color;
colors[i4 + 1] = this.m_color;
colors[i4 + 2] = this.m_color;
colors[i4 + 3] = this.m_color;
stPos = pos;
trigIndex += 6;
}
}
}
}
public class PathNavLine
{
private Vector3[] m_points;
// internal bool calcDirty = true;
// internal readonly object _lock = new object();
public Vector3[] points
{
get => this.m_points;
}
internal bool m_active = true;
public bool Active
{
get => this.m_active;
}
// public bool LenDirty { get => m_lenDirty; }
// internal bool Added { get => m_added; }
internal Color m_color = Color.white;
public Color LineColor
{
get => m_color;
}
internal int m_layerID = -1;
public int LayerID
{
get => m_layerID;
}
private int _index4InLayer = -1;
internal int index4InLayer
{
get => _index4InLayer;
set { _index4InLayer = value; }
}
internal int index6InLayer = -1;
internal int lenInLayer = -1;
internal float _colorAlphaCache = 1;
// private bool m_lenDirty = false;
// internal int[] lenInfo = new int[4];
internal bool disposed = false;
private int m_space = 0;
private int m_space2 = 0;
internal bool pointsDirty = false;
// private bool m_added = false;
internal void SetPoints(Vector3[] points)
{
this.m_points = points;
this.pointsDirty = true;
}
internal void MarkDisopose()
{
this.disposed = true;
}
internal (int, int) GetNeedSpace()
{
if (!this.pointsDirty) return (m_space, m_space2);
this.pointsDirty = false;
if (this.points.Length == 0)
{
m_space = m_space2 = 0;
}
else
{
var len = points.Length - 1;
this.m_space = len * PathNavLineLayer.VertexCount;
this.m_space2 = len * PathNavLineLayer.TriangleCount;
// Debug.Log("GetNeedSpace:", this.GetHashCode(), len, m_space, this.calcDirty);
}
return (m_space, m_space2);
}
internal LineSegment ToSegment()
{
var seg = new LineSegment();
seg.m_color = this.m_color;
seg.index4InLayer = this.index4InLayer;
seg.index6InLayer = this.index6InLayer;
seg.lenInLayer = this.lenInLayer;
seg.points = this.points;
return seg;
}
}
public class AsyncExecCMDFunc : MonoBehaviour
{
private Task asyncTask;
protected System.Collections.Concurrent.ConcurrentQueue<System.Action> asyncLineQueue;
protected volatile bool isAsyncingTask = false;
private volatile bool disposed = false;
protected volatile bool asyncResultDirty = false;
[Header("当后台任务结束后,task保留时间(秒)")] public float taskExistsTime = 0;
private DateTime _taskExistsDateTime = DateTime.Now;
protected void AddCMDFunc(System.Action action)
{
if (this.asyncLineQueue == null)
{
asyncLineQueue = new System.Collections.Concurrent.ConcurrentQueue<System.Action>();
}
asyncLineQueue.Enqueue(action);
this.StartAsyncLoop();
}
private async void StartAsyncLoop()
{
if (this.isAsyncingTask) return;
this.isAsyncingTask = true;
if (this.asyncTask != null && !(asyncTask.IsCompleted || asyncTask.IsCanceled || asyncTask.IsFaulted ||
asyncTask.IsCompletedSuccessfully))
{
try
{
this.asyncTask.Dispose();
}
catch (Exception)
{
}
}
this.asyncTask = Task.Run(() =>
{
while (true)
{
// Manager.Update中会一直检查layer空间是否用完,
// 但layer空间是否用完的标记'validCount'的减少是在add时立即处理(增加是remove时的异步处理),
// 此时会出现在manager中已经将layer移除,但task还在运行的情况,就要通过disposed标记来防止task执行
if (this.disposed)
break;
System.Action action;
// var c = asyncLineQueue.Count;
// Debug.Log(c);
if (asyncLineQueue.TryDequeue(out action))
{
if (this.disposed)
break;
this._taskExistsDateTime = DateTime.Now;
this.asyncResultDirty = true;
try
{
action?.Invoke();
}
catch (System.Exception e)
{
Debug.LogError(e);
}
}
else
{
// var _taskExistsTime = DateTime.Now;
if (taskExistsTime <= 0)
{
break;
}
TimeSpan deltaTime = DateTime.Now.Subtract(this._taskExistsDateTime);
if (deltaTime.TotalSeconds >= this.taskExistsTime)
{
break;
}
}
}
this.isAsyncingTask = false;
});
await asyncTask;
}
virtual protected void OnDestroy()
{
// GameObject.DestroyImmediate(this.mesh);
if (asyncTask != null && !(asyncTask.IsCompleted || asyncTask.IsCanceled || asyncTask.IsFaulted ||
asyncTask.IsCompletedSuccessfully))
{
try
{
this.asyncTask.Dispose();
this.asyncTask = null;
}
catch (Exception)
{
}
}
if (this.asyncLineQueue != null)
{
this.asyncLineQueue.Clear();
this.asyncLineQueue = null;
}
this.disposed = true;
}
}
image.png
测试代码
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(PathNavLineManager))]
public class PathNavLineManagerEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
GUILayout.Space(15);
if (GUILayout.Button("Add", GUILayout.Height(25)))
{
PathNavLineManager manager = (this.target as PathNavLineManager);
for (int j = 0; j < 50; j++)
{
Vector3[] points = new Vector3[30];
for (int i = 0; i < points.Length; i++)
{
points[i] = new Vector3(Random.Range(-512, 512), 0, Random.Range(-512, 512));
}
manager.Draw(points, Color.white);
}
}
}
}
#endif
性能(1660s显卡)
空场景
100条路径(每条30个路径点)
1000条路径
10,350条路径
3个mesh,所以有3个DC
1万条路径下,13.6万顶点,性能消耗在GPU
Gfx.WaitForPresentOnGfxThread: https://docs.unity3d.com/Manual/profiler-markers.html
-
拉近camera后,帧率有所回升. 拉近camera才显示行军路线是SLG游戏中的常规操作,一般不会在最高视角下还显示所有路线. 此时跟空场景的单帧消耗差值在6ms左右. 拉进camera,触发了裁剪,帧率回升
-
少量路线和大量路线情况下,添加路线操作的消耗. image.png
image.png
添加操作的消耗随着定点数的增加,是基本没有变化的,因为同一时刻操作的mesh是一个,且提交的顶点数也是固定的. 当设置的"layer可容纳的线段数量"越少时,线段的添加和更新操作的消耗会更少.
结论
- 每个layer容纳的线段数量越多,mesh开辟越少,但更新频繁时,调用
SyncAllToMesh()
方法就越频繁,对于mesh的Setxxx()就越频繁,对mesh的操作是在主线程,当mesh中数据非常庞大时,这个更新mesh的过程就可能越卡顿. - 但容纳的线段数量越少,mesh空间被消耗的越快,会频繁的新增和删除layer,产生gc.
所以需要根据业务情况来确定一个合适的值. - 上述测试环境中在拉近镜头后与空场景的单帧消耗差值是6ms左右,但实际游戏中往往会通过业务来进一步剔除不必要的显示,比如只显示与自己有关联的队伍,显示相同联盟的队伍等,所以在实际游戏中上述测试环境下的单帧消耗会进一步减少.
- 也可以使用GPUInstance使用类似方式实现.
当前mesh的顶点空间被消耗完毕后,会直接移除(对应layer),相当与是一次性的消耗,增加了gc频率,所以进一步需要的优化方式是: 复用已经标记为移除的顶点. mesh中现在是只使用一个index来标明剩余空间的索引位置,只需要再引入一个index来标明复用位置即可.
可以想象为: 当新增一个路径时,如果剩余空间不够,则从前面已经删除的空间中继续复用, 只有当整个空间(所有layer)都不够存放新路径时,再开辟新的mesh和layer. 此方式只会新增mesh,不会删除,所以没有额外的业务带来的gc消耗.