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