用ECS做HexMap:刷子难题

31 篇文章 2 订阅
31 篇文章 3 订阅

基于Unity2019最新ECS架构开发MMO游戏笔记24

本篇效果

准备工作

如果大佬对ECS版的海克斯无限地图感兴趣,不妨参与进来,欢迎Star/Folk源码
0下载Unity编辑器(2019.1.12f1 or 更新的版本),if(已经下载了)continue;
1克隆:git clone https://github.com/cloudhu/HexMapMadeInUnity2019ECS.git --recurse下载Zip压缩包
2如果下载的是压缩包,需要先将压缩包解压。然后将HexMapMadeInUnity2019ECS添加到Unity Hub项目中;
3用Unity Hub打开的开源项目:HexMapMadeInUnity2019ECS,等待Unity进行编译工作;
4打开项目后,启动场景在Scenes目录下,打开AdvancedHexMap场景(优化重构的版本)。

前言

首先对地图做了扩展,由原来的一个Mesh,增加到Mesh矩阵。由于我们需要物理组件MeshCollider,这个之前已经提到过了,ECS还没有物理引擎支持,所以目前被迫选择混合开发,这很尴尬,预计明年会有改善。这段时间用ECS开发,我越加觉得面向对象太好用了,简直就是人性化开发。而ECS更偏机器一些,所以反人类,很多东西需要特别去设计数据,否则操作起来特别麻烦,这是我的亲身感受。本来Hex Map就有OOP的实现,无疑是更容易理解的版本,更好做自定义。
总之,我越用ECS,越想弃坑,这也是这两天拖更的原因,陷入深切的自我怀疑,究竟该不该脱坑?
不管怎样,混合开发并非长久之计,如果真的想要开发自己的游戏,这样做实在太冒险了。为了一点性能优势,难道就牺牲整个项目吗?真是食之无味,弃之可惜,过渡阶段的ECS就是这样的鸡肋。
吐槽就到这里吧,也许我根本不应该选择用ECS来做无限地图的开发。

更大的地图

前言中说了这么多,就是为了下面的OOP部分做铺垫,更大的地图,意味着不止一个Mesh(Unity中Mesh数组最大能存储65000个顶点),于是我开始改造OOP的结构,新建HexGrid脚本,用来管理地图块Chunk:

/// <summary>
/// 地图网格,管理地图块
/// </summary>
public class HexGrid : MonoBehaviour
{

    /// <summary>
    /// 地图块的数量
    /// </summary>
    public int chunkCountX = 4, chunkCountZ = 3;

    /// <summary>
    /// 噪声采样纹理图
    /// </summary>
    public Texture2D noiseSource;

    /// <summary>
    /// 地图块预设
    /// </summary>
    public HexGridChunk chunkPrefab;

    /// <summary>
    /// 地图宽度(以六边形为基本单位)
    /// </summary>
    private int cellCountX;

    /// <summary>
    /// 地图长度(以六边形为基本单位)
    /// </summary>
    private int cellCountZ;
    /// <summary>
    /// 地图块数组
    /// </summary>
    HexGridChunk[] chunks;

    #region Mono

    private void Awake()
    {
        cellCountX = chunkCountX * HexMetrics.chunkSizeX;
        cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ;
        HexMetrics.noiseSource = noiseSource;
        CreateChunks();
    }

    void OnEnable()
    {
        HexMetrics.noiseSource = noiseSource;
    }

    // Start is called before the first frame update
    void Start()
    {
        MainWorld.Instance.SetupMap(cellCountX, cellCountZ,chunkCountX);
    }

    #endregion
    /// <summary>
    /// 添加单元到地图块
    /// </summary>
    /// <param name="chunkId">地图块编号</param>
    /// <param name="chunkIndex">地图块索引</param>
    /// <param name="cellIndex">单元索引</param>
    /// <param name="cell">单元</param>
    public void AddCellToChunk(int chunkId,int chunkIndex,int cellIndex,Entity cell)
    {
        HexGridChunk chunk = chunks[chunkId];
        chunk.AddCell(chunkIndex,cellIndex, cell);
    }

    /// <summary>
    /// 刷新地图块
    /// </summary>
    /// <param name="chunkId">地图块编号</param>
    public void Refresh(int chunkId)
    {
        HexGridChunk chunk = chunks[chunkId];
        chunk.Refresh();
    }

    /// <summary>
    /// 更新地图块
    /// </summary>
    /// <param name="chunkId">地图块编号</param>
    /// <param name="cellIndex">单元索引</param>
    /// <param name="color">颜色</param>
    /// <param name="elevation">海拔</param>
    /// <param name="affected">是否受影响</param>
    /// <param name="brushSize">刷子大小</param>
    public void UpdateChunk( int chunkId, int cellIndex, Color color, int elevation,bool affected=false,int brushSize=0)
    {
        if (chunkId==int.MinValue)
        {
            return;
        }
        HexGridChunk chunk = chunks[chunkId];
        StartCoroutine(chunk.UpdateChunk(cellIndex,color,elevation,affected,brushSize));
    }

    /// <summary>
    /// 创建地图块
    /// </summary>
    void CreateChunks()
    {
        chunks = new HexGridChunk[chunkCountX * chunkCountZ];

        for (int z = 0, i = 0; z < chunkCountZ; z++)
        {
            for (int x = 0; x < chunkCountX; x++)
            {
                HexGridChunk chunk = chunks[i++] = Instantiate(chunkPrefab);
                chunk.transform.SetParent(transform);
            }
        }
    }
}

因为噪声相关的实现需要Texture2D对象支持,于是我把它转移到了OOP中。这里做了地图块的预设,更改了脚本结构:
地图块预设
一个大的地图就是由若干地图块组成,每个地图块都由HexGridChunk脚本管理:

/// <summary>
/// 地图块
/// </summary>
public class HexGridChunk : MonoBehaviour
{
    HexMesh hexMesh;
    private Entity[] cells;
    private int cellCount = 0;
    //地图块和总地图索引配对表
    private int[] chunkMap;
    //实体管理器缓存
    private EntityManager m_EntityManager;

    void Awake()
    {
        //初始化
        hexMesh = GetComponentInChildren<HexMesh>();
        cellCount = HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ;
        cells = new Entity[cellCount];
        chunkMap = new int[cellCount];
    }

    private void Start()
    {
        m_EntityManager = MainWorld.Instance.GetEntityManager();
    }

    /// <summary>
    /// 添加单元
    /// </summary>
    /// <param name="chunkIndex">块内索引</param>
    /// <param name="cellIndex">单元索引</param>
    /// <param name="cell">单元</param>
    public void AddCell(int chunkIndex,int cellIndex,Entity cell)
    {
        cells[chunkIndex] = cell;
        chunkMap[chunkIndex] = cellIndex;
        if (chunkIndex+1==cellCount)
        {
            Refresh();
        }
    }

    /// <summary>
    /// 刷新地图块
    /// </summary>
    public void Refresh()
    {
        StartCoroutine(hexMesh.Triangulate(cells));
    }

    /// <summary>
    /// 更新单元的颜色
    /// </summary>
    /// <param name="cellIndex">单元索引</param>
    /// <param name="color">颜色</param>
    /// <returns></returns>
   public IEnumerator UpdateChunk(int cellIndex, Color color, int elevation,bool affected=false,int brushSize=0)
    {
        yield return null;
        
        if (brushSize > 0)
        {
            int chunkIndex = GetChunkIndex(cellIndex);
            NeighborsIndex neighborsIndexs = m_EntityManager.GetComponentData<NeighborsIndex>(cells[chunkIndex]);
            int NEIndex = neighborsIndexs.NEIndex;
            if (NEIndex > int.MinValue )
            {
                if (GetChunkIndex(NEIndex) == int.MinValue)
                {
                    MainWorld.Instance.AffectedChunk(NEIndex, NEIndex, 0, false);
                }
            }
            if (neighborsIndexs.EIndex > int.MinValue && GetChunkIndex(neighborsIndexs.EIndex) == int.MinValue)
            {
                MainWorld.Instance.AffectedChunk(neighborsIndexs.EIndex, neighborsIndexs.EIndex, 0, false);
            }

            if (neighborsIndexs.SEIndex > int.MinValue && GetChunkIndex(neighborsIndexs.SEIndex) == int.MinValue)
            {
                MainWorld.Instance.AffectedChunk(neighborsIndexs.SEIndex, neighborsIndexs.SEIndex, 0, false);
            }

            if (neighborsIndexs.SWIndex > int.MinValue && GetChunkIndex(neighborsIndexs.SWIndex) == int.MinValue)
            {
                MainWorld.Instance.AffectedChunk(neighborsIndexs.SWIndex, neighborsIndexs.SWIndex, 0, false);
            }
            if (neighborsIndexs.WIndex > int.MinValue && GetChunkIndex(neighborsIndexs.WIndex) == int.MinValue)
            {
                MainWorld.Instance.AffectedChunk(neighborsIndexs.WIndex, neighborsIndexs.WIndex, 0, false);
            }

            if (neighborsIndexs.NWIndex > int.MinValue && GetChunkIndex(neighborsIndexs.NWIndex) == int.MinValue)
            {
                MainWorld.Instance.AffectedChunk(neighborsIndexs.NWIndex, neighborsIndexs.NWIndex, 0, false);
            }

            UpdateData data = new UpdateData
            {
                CellIndex = cellIndex,
                NewColor = color,
                Elevation = elevation,
                NEIndex=NEIndex,
                EIndex= neighborsIndexs.EIndex,
                SEIndex= neighborsIndexs.SEIndex,
                SWIndex= neighborsIndexs.SWIndex,
                WIndex= neighborsIndexs.WIndex,
                NWIndex= neighborsIndexs.NWIndex
            };
            for (int i = 0; i < cellCount; i++)
            {
                Entity entity = cells[i];
                if (!m_EntityManager.HasComponent<UpdateData>(entity)) m_EntityManager.AddComponent<UpdateData>(entity);
                m_EntityManager.SetComponentData(entity, data);
            }
        }
        else
        {
            UpdateData data = new UpdateData
            {
                CellIndex = cellIndex,
                NewColor = color,
                Elevation = elevation,
                NEIndex = int.MinValue,
                EIndex = int.MinValue,
                SEIndex = int.MinValue,
                SWIndex = int.MinValue,
                WIndex = int.MinValue,
                NWIndex = int.MinValue
            };
            for (int i = 0; i < cellCount; i++)
            {
                Entity entity = cells[i];

                if (!m_EntityManager.HasComponent<UpdateData>(entity)) m_EntityManager.AddComponent<UpdateData>(entity);
                m_EntityManager.SetComponentData(entity, data);
                if (affected) continue;//如果当前地图块是受影响的,则跳过
                NeighborsIndex cell = m_EntityManager.GetComponentData<NeighborsIndex>(entity);
                if (chunkMap[i] == cellIndex)
                {
                    //检测六个方向可能受影响的地图块,将变化传递过去
                    if (cell.NEIndex>int.MinValue && GetChunkIndex(cell.NEIndex) == int.MinValue)
                    {
                        MainWorld.Instance.AffectedChunk(cell.NEIndex,cellIndex, 0, true);
                        Debug.Log(cellIndex + "影响NE:" + cell.NEIndex);
                    }

                    if (cell.EIndex > int.MinValue && GetChunkIndex(cell.EIndex) == int.MinValue)
                    {
                        MainWorld.Instance.AffectedChunk(cell.EIndex, cellIndex, 0, true);
                        Debug.Log(cellIndex + "影响E:" + cell.EIndex);
                    }

                    if (cell.SEIndex > int.MinValue && GetChunkIndex(cell.SEIndex) == int.MinValue)
                    {
                        MainWorld.Instance.AffectedChunk(cell.SEIndex, cellIndex, 0, true);
                        Debug.Log(cellIndex + "影响SE:" + cell.SEIndex);
                    }

                    if (cell.SWIndex > int.MinValue && GetChunkIndex(cell.SWIndex) == int.MinValue)
                    {
                        MainWorld.Instance.AffectedChunk(cell.SWIndex, cellIndex, 0, true);
                        Debug.Log(cellIndex + "影响SW:" + cell.SWIndex);
                    }
                    if (cell.WIndex > int.MinValue && GetChunkIndex(cell.WIndex) == int.MinValue)
                    {
                        MainWorld.Instance.AffectedChunk(cell.WIndex, cellIndex,0, true);
                        Debug.Log(cellIndex + "影响W:" + cell.WIndex);
                    }

                    if (cell.NWIndex > int.MinValue && GetChunkIndex(cell.NWIndex) == int.MinValue)
                    {
                        MainWorld.Instance.AffectedChunk(cell.NWIndex, cellIndex, 0, true);
                        Debug.Log(cellIndex + "影响NW:" + cell.NWIndex);
                    }
                }
            }
        }
    }

    /// <summary>
    /// 获取块内索引
    /// </summary>
    /// <param name="cellIndex">单元索引</param>
    /// <returns></returns>
    private int GetChunkIndex(int cellIndex)
    {
        if (cellIndex!=int.MinValue)
        {
            for (int i = 0; i < cellCount; i++)
            {
                if (chunkMap[i] == cellIndex)
                {
                    return i;
                }
            }
        }

        return int.MinValue;
    }

    private void OnDestroy()
    {
        cells = null;
        chunkMap = null;
    }
}

原来在MainWorld处理噪声干扰的部分转移到了HexMesh当中,毕竟是OOP的部分,还是分离出来处理吧。MainWorld更像是ECS在OOP中的代言人,所以想让它更纯粹些,把Mesh相关的逻辑都交给HexMesh脚本处理:

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class HexMesh : MonoBehaviour
{

    Mesh m_Mesh;
    MeshCollider m_MeshCollider;

    void Awake()
    {
        GetComponent<MeshFilter>().mesh = m_Mesh = new Mesh();
        m_MeshCollider = gameObject.AddComponent<MeshCollider>();
        m_Mesh.name = "Hex Mesh";
        m_Mesh.MarkDynamic();
    }

    public IEnumerator Triangulate(Entity[] cells)
    {
        yield return new WaitForSeconds(0.01f);
        int totalCount = cells.Length;
        EntityManager m_EntityManager = MainWorld.Instance.GetEntityManager();
        NativeList<Vector3> Vertices = new NativeList<Vector3>(totalCount, Allocator.Temp);
        NativeList<int> Triangles = new NativeList<int>(totalCount, Allocator.Temp);
        NativeList<Color> Colors = new NativeList<Color>(totalCount, Allocator.Temp);

        for (int i = 0; i < totalCount; i++)
        {
            //0.取出实体,如果实体的索引为m_Builder则跳过
            Entity entity = cells[i];
            //if (!m_EntityManager.HasComponent<Cell>(entity)) continue;
            DynamicBuffer<ColorBuffer> colorBuffer = m_EntityManager.GetBuffer<ColorBuffer>(entity);
            //Debug.Log(colorBuffer.Length);
            //float elevationPerturb = 0f;
            if (colorBuffer.Length > 0)
            {
                DynamicBuffer<VertexBuffer> vertexBuffer = m_EntityManager.GetBuffer<VertexBuffer>(entity);
                for (int j = 0; j < colorBuffer.Length; j++)
                {
                    Triangles.Add(Vertices.Length);
                    Colors.Add(colorBuffer[j]);
                    Vector3 vertex = Perturb(vertexBuffer[j]);
                    //if (j == 0)
                    //{
                    //    elevationPerturb= (HexMetrics.SampleNoise(vertex).y * 2f - 1f) * HexMetrics.elevationPerturbStrength;
                    //}
                    //vertex.y += elevationPerturb;
                    Vertices.Add(vertex);
                }
                vertexBuffer.Clear();
                colorBuffer.Clear();
            }

        }

        Debug.Log("-----------------------------------------------------------------------------------------");
        Debug.Log("Vertices=" + Vertices.Length + "----Triangles=" + Triangles.Length + "----Colors=" + Colors.Length);

        if (Vertices.Length > 1)
        {
            m_Mesh.Clear();
            m_Mesh.vertices = Vertices.ToArray();
            m_Mesh.triangles = Triangles.ToArray();
            m_Mesh.colors = Colors.ToArray();
            m_Mesh.RecalculateNormals();
            m_Mesh.Optimize();
            m_MeshCollider.sharedMesh = m_Mesh;
        }
        Vertices.Dispose();
        Triangles.Dispose();
        Colors.Dispose();
    }

    /// <summary>
    /// 噪声干扰
    /// </summary>
    /// <param name="position">顶点位置</param>
    /// <returns>被干扰的位置</returns>
    Vector3 Perturb(Vector3 position)
    {
        Vector4 sample = HexMetrics.SampleNoise(position);
        position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength;
        position.y += (sample.y * 2f - 1f) * HexMetrics.cellPerturbStrength;
        position.z += (sample.z * 2f - 1f) * HexMetrics.cellPerturbStrength;
        return position;
    }
}

所以OOP相关逻辑都交代清楚了,剩下就是ECS的改进,先从数据Data开始,对原来的数据进行拆分:

/// <summary>
/// 单元数据
/// </summary>
public struct Cell : IComponentData
{
    /// <summary>
    /// 单元索引
    /// </summary>
    public int Index;

    /// <summary>
    /// 位置
    /// </summary>
    public Vector3 Position;
    /// <summary>
    /// 颜色
    /// </summary>
    public Color Color;


    /// <summary>
    /// 当前单元的海拔
    /// </summary>
    public int Elevation;

}

public struct Neighbors : IComponentData
{
    //六个方向相邻单元的颜色
    public Color NE;
    public Color E;
    public Color SE;
    public Color SW;
    public Color W;
    public Color NW;
    //六个方向相邻单元的海拔
    public int NEElevation;
    public int EElevation;
    public int SEElevation;
    public int SWElevation;
    public int WElevation;
    public int NWElevation;
}

/// <summary>
/// 六个方向相邻单元的索引
/// </summary>
public struct NeighborsIndex : IComponentData {

    public int NEIndex;
    public int EIndex;
    public int SEIndex;
    public int SWIndex;
    public int WIndex;
    public int NWIndex;
}

Cell只储存本单元的相关数据,把相邻单元的数据分离出去,使其更纯粹。为了一次可以更新更多单元,对UpdateData进行扩充,使它可以储存更多数据:

/// <summary>
/// 单元的更新数据
/// </summary>
public struct UpdateData : IComponentData
{
    /// <summary>
    /// 单元的索引
    /// </summary>
    public int CellIndex;
    /// <summary>
    /// 新的颜色
    /// </summary>
    public Color NewColor;

    /// <summary>
    /// 海拔
    /// </summary>
    public int Elevation;
    //需要更新的相邻单元索引
    public int NEIndex;
    public int EIndex;
    public int SEIndex;
    public int SWIndex;
    public int WIndex;
    public int NWIndex;
}

其他脚本变化不大,主要是更新系统进行了改进,使其一次可以更新更多单元:

/// <summary>
    /// 循环创建六边形单元,使其生成对应长宽的阵列
    /// </summary>
    struct CalculateJob : IJobForEachWithEntity<Cell, UpdateData,Neighbors,NeighborsIndex> {
        public EntityCommandBuffer.Concurrent CommandBuffer;
        [BurstCompile]
        public void Execute(Entity entity, int index, ref Cell cellData, [ReadOnly]ref UpdateData updata,ref Neighbors neighbors,ref NeighborsIndex neighborsIndex)
        {
            //0.获取更新列表
            NativeList<int> updateList = new NativeList<int>(7, Allocator.Temp);
            updateList.Add(updata.CellIndex);
            if (updata.NEIndex > int.MinValue) updateList.Add(updata.NEIndex);
            if (updata.EIndex > int.MinValue) updateList.Add(updata.EIndex);
            if (updata.SEIndex > int.MinValue) updateList.Add(updata.SEIndex);
            if (updata.SWIndex > int.MinValue) updateList.Add(updata.SWIndex);
            if (updata.WIndex > int.MinValue) updateList.Add(updata.WIndex);
            if (updata.NWIndex > int.MinValue) updateList.Add(updata.NWIndex);
            //1.判断并更新自身单元颜色以及相邻单元颜色
            Color color = updata.NewColor;

            //更新相邻单元的颜色
            if (updateList.Contains(neighborsIndex.NEIndex))
            {
                neighbors.NE = color;
                neighbors.NEElevation = updata.Elevation;
            }

            if (updateList.Contains(neighborsIndex.EIndex))
            {
                neighbors.E = color;
                neighbors.EElevation = updata.Elevation;
            }
            if (updateList.Contains(neighborsIndex.SEIndex)) {
                neighbors.SE = color;
                neighbors.SEElevation = updata.Elevation;
            }

            if (updateList.Contains(neighborsIndex.SWIndex))
            {
                neighbors.SW = color;
                neighbors.SWElevation = updata.Elevation;
            }

            if (updateList.Contains(neighborsIndex.WIndex) )
            {
                neighbors.W = color;
                neighbors.WElevation = updata.Elevation;
            }

            if (updateList.Contains(neighborsIndex.NWIndex) )
            {
                neighbors.NW = color;
                neighbors.NWElevation = updata.Elevation;
            }
            if (updateList.Contains(cellData.Index))//更新自身单元的颜色
            {
                cellData.Color = color;
                cellData.Position.y= updata.Elevation * HexMetrics.elevationStep;
                cellData.Elevation = updata.Elevation;
            }

            updateList.Dispose();
            //2.remove UpdateData after Update,therefor NewDataTag need to be added to active CellSystem
            CommandBuffer.RemoveComponent<UpdateData>(index, entity);
            CommandBuffer.AddComponent<NewDataTag>(index, entity);
        }

    }

但是这些改进都是非常局限的,因为IComponentData无法储存数组,所以这里没有无限扩充更新的数量,仅限本单元和相邻的六个单元,也就是最多7个单元,如下图所示:
一次更新七个相邻单元
这就是刷子难题,无法像原版OOP实现那样可以使用更大的刷子,而且这个刷子还有Bug,例如在地图块边界的地方时,会造成桥的连接突起。总之ECS的数据很不好处理,也许是我没有设计好的缘故!
就是这样吧,bug越来越多,希望有大佬来帮忙解决!

ECS专题目录

ECS更新计划

作者的话

Alt

如果喜欢可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
技术难题?加入开发者联盟:566189328(QQ付费群)提供有限技术探讨,以及,心灵鸡汤Orz!
当然,不需要技术探讨也欢迎加入进来,在这里劈柴、遛狗、聊天、撸猫!( ̄┰ ̄*)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CloudHu1989

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值