SLG游戏行军线路

SLG游戏中,往往有大量队伍行军线路表现的需求,从数百到数千上万条不等,数量越多,表示显示范围内队伍越多,此时越要注意性能要求,不能因线条表现占用过多计算资源.

  1. 理想情况下应该: 占用1个DC,能通过指定纹理表现路线(圆点,矩形等),能有动画效果,数据变更时快速更新等.
  2. 如果将所有顶点的绘制都放到一个mesh中可以做到1个DC,但遇到路径更新,边界计算等时,会维护一个非常庞大的mesh数据,如果使用的List来维护这些定点,还牵涉到拷贝,扩容等操作.

假设:

  1. 开辟一个mesh,挂载到一个gameobject中,如果该mesh的顶点数量上限是固定的,那就相当于是一个顶点池,可以使用一个固定长度的数组来存储这些顶点,新添加的路径(多个顶点)放入该mesh中,相当与消耗池中剩余顶点.
  2. 由于使用了固定长度的数组,mesh中的路径删除,就只做归零或者设置一个很远的坐标,避免对数组长度的修改.
  3. 当一个go中的mesh消耗完毕,就开辟一个新的mesh(挂载到一个新的go中).
  4. 当一个mesh中所有顶点都标记为删除,则可以移除该mesh(和对应的go).
  5. 路径中各个定点的计算,一些循环遍历等,如果放入多线程中执行,那就进一步减少对主线程的卡顿.

代码如下:

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可容纳的线段数量"越少时,线段的添加和更新操作的消耗会更少.


结论

  1. 每个layer容纳的线段数量越多,mesh开辟越少,但更新频繁时,调用SyncAllToMesh()方法就越频繁,对于mesh的Setxxx()就越频繁,对mesh的操作是在主线程,当mesh中数据非常庞大时,这个更新mesh的过程就可能越卡顿.
  2. 但容纳的线段数量越少,mesh空间被消耗的越快,会频繁的新增和删除layer,产生gc.
    所以需要根据业务情况来确定一个合适的值.
  3. 上述测试环境中在拉近镜头后与空场景的单帧消耗差值是6ms左右,但实际游戏中往往会通过业务来进一步剔除不必要的显示,比如只显示与自己有关联的队伍,显示相同联盟的队伍等,所以在实际游戏中上述测试环境下的单帧消耗会进一步减少.
  4. 也可以使用GPUInstance使用类似方式实现.

当前mesh的顶点空间被消耗完毕后,会直接移除(对应layer),相当与是一次性的消耗,增加了gc频率,所以进一步需要的优化方式是: 复用已经标记为移除的顶点. mesh中现在是只使用一个index来标明剩余空间的索引位置,只需要再引入一个index来标明复用位置即可.
可以想象为: 当新增一个路径时,如果剩余空间不够,则从前面已经删除的空间中继续复用, 只有当整个空间(所有layer)都不够存放新路径时,再开辟新的mesh和layer. 此方式只会新增mesh,不会删除,所以没有额外的业务带来的gc消耗.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容