在这篇文章里,主要是实现Endless Terrain,以及我对这几个技术的学习、总结以及实践,这些东西的学习真的是花了大量的时间和精力,看了好几个外国大佬的博客,自己写了一个国庆,才终于实现了出来(其实是因为弄unity的多线程花了挺多的时间)。可能有点长,如果你也想做Procedural Terrain,看完这篇你一定能有大收获。
Chunks unity对于一个mesh的绘制是有着顶点数上限限制,所以要实现大地形,需要随机生成很多块地形,每个地形称为一个chunk,生成多少个chunk则取决于玩家能看到的最远处,如果玩家位置发生了改变,那么在玩家目前MaxViewDst外的chunk就设置为不可见,生成的其它chunk就拼接在玩家目前所在chunk的四周,这样就能做出Endless Terrain。
1 2 3 4 5 6 7 public  const  float  maxViewDst = 300 ;public  Transform viewer;public  static  Vector2 viewerPosition;static  NoiseMapGenerator mapGenerator;int  chunkSize;int  chunkVisibleInViewDst;
 
在一条视线方向上可见的chunk个数就为
1 2 chunkSize = NoiseMapGenerator.mapChunkSize - 1 ; chunkVisibleInViewDst = Mathf.RoundToInt(maxViewDst / chunkSize); 
 
我把整个世界划分成网格,每个网格就是一个chunk,玩家所能看到的chunkVisibleInViewDst就决定了这个chunk集的大小,假定能看到附近的一个chunk,那个一共要生成的chunk集就要有九个chunk:
 
我用一个TerrainChunk类去储存要生成的chunk的信息,它还要有一个方法去更新chunk的可见性,判断这个chunk是否在MaxViewDst内,如果在,那么让它可见
1 2 3 4 5 6 public  void  UpdateTerrainChunk ( ) {	float  viewDstFromNearestEdge = Mathf.Sqrt(bounds.SqrDistance(viewerPosition)); 	bool  visible = viewDstFromNearestEdge <= maxViewDst; 	SetVisible(true ); } 
 
对于生成的chunks,我用一个字典类型去存储,每当view的位置变化,都会计算出一个新的中心点,也就要基于这个点去生成新的chunk,如果已经有了这个chunk,那就直接把它setvisible就行了,而是否已经有了这个chunk,直接用key去查就行了,UpdateVisibleChunks函数如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Dictionary<Vector2, TerrainChunk> terrainChunkDictionary = new  Dictionary<Vector2, TerrainChunk>(); List<TerrainChunk> terrainChunksVisibleLastUpdate = new  List<TerrainChunk>(); for  (int  i = 0 ; i < terrainChunksVisibleLastUpdate.Count; i++){ 	terrainChunksVisibleLastUpdate[i].SetVisible(false ); } int  currentChunkCoordX = Mathf.RoundToInt(viewerPosition.x / chunkSize);int  currentChunkCoordY = Mathf.RoundToInt(viewerPosition.y / chunkSize);for  (int  yOffset = -chunkVisibleInViewDst; yOffset <= chunkVisibleInViewDst; yOffset++){ 	for  (int  xOffset = -chunkVisibleInViewDst; xOffset <= chunkVisibleInViewDst; xOffset++) 	{ 		Vector2 viewedChunkCoord = new  Vector2(currentChunkCoordX + xOffset, currentChunkCoordY + 			yOffset); 		if  (terrainChunkDictionary.ContainsKey(viewedChunkCoord)) 		{ 			terrainChunkDictionary[viewedChunkCoord].UpdateTerrainChunk(); 			if  (terrainChunkDictionary[viewedChunkCoord].IsVisible()) 			{                      																 			terrainChunksVisibleLastUpdate.Add(terrainChunkDictionary[viewedChunkCoord]); 			} 		} 		else  		{ 			terrainChunkDictionary.Add(viewedChunkCoord, new  TerrainChunk(viewedChunkCoord, 				chunkSize, transform)); 		} 	} } 
 
然后在update里面一直去更新chunks就可以了
1 2 3 4 5 private  void  Update ( ) {	viewerPosition = new  Vector2(viewer.position.x, viewer.position.z); 	UpdateVisibleChunks(); } 
 
接下来就是用Threading去优化整个过程,避免在生成太多地形的时候导致卡顿。我需要在生成map的脚本里面得到Endless Terrain脚本里生成的所有chunks的数据,它不需要立即得到所有生成的数据。所以在mapGenerator脚本里面我们需要一个RequestMapData函数,它的参数是一个callback function,在Endless Terrain里面当我们计算好了数据,就是用这个对应的callback function。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 Queue<MapThreadInfo<MapData>> mapdataThreadInfoQueue = new  Queue<MapThreadInfo<MapData>>(); Queue<MapThreadInfo<MeshData>> meshDataThreadInfoQueue = new  Queue<MapThreadInfo<MeshData>>(); struct  MapThreadInfo<T>{ 	public  readonly  Action<T> callback; 	public  readonly  T parameter; 	public  MapThreadInfo (Action<T> callback, T parameter )  	{		this .callback = callback; 		this .parameter = parameter; 	} } public  void  RequestMapData (Action<MapData> callback ) {	ThreadStart threadStart = delegate  	{ 		MapDataThread(callback); 	}; 	new  Thread(threadStart).Start(); } void  MapDataThread (Action<MapData> callback ) {	MapData mapData = GenerateMapData(); 	lock  (mapdataThreadInfoQueue) 	{ 		mapdataThreadInfoQueue.Enqueue(new  MapThreadInfo<MapData>(callback, mapData)); 	} } public  void  RequestMeshData (MapData mapData, Action<MeshData> callback ) {	ThreadStart threadStart = delegate  	{ 		MeshDataThread(mapData, callback); 	}; 	new  Thread(threadStart).Start(); } void  MeshDataThread (MapData mapData, Action<MeshData> callback ) {	MeshData meshData = MeshGenerator.GenerateTerrainMesh(mapData.heightMap, meshHeightMultiplier, 		meshHeightCurve, levelOfDetail); 	lock  (meshDataThreadInfoQueue) 	{ 		meshDataThreadInfoQueue.Enqueue(new  MapThreadInfo<MeshData>(callback, meshData)); 	} } 
 
在Update里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if  (mapdataThreadInfoQueue.Count>0 ){ 	for  (int  i = 0 ; i < mapdataThreadInfoQueue.Count; i++) 	{ 		MapThreadInfo<MapData> threadInfo = mapdataThreadInfoQueue.Dequeue(); 		threadInfo.callback(threadInfo.parameter); 	} } if  (meshDataThreadInfoQueue.Count>0 ){ 	for  (int  i = 0 ; i < meshDataThreadInfoQueue.Count; i++) 	{ 		MapThreadInfo<MeshData> threadInfo = meshDataThreadInfoQueue.Dequeue(); 		threadInfo.callback(threadInfo.parameter); 	} } 
 
然后在Endless Terrain里去实做Received Function
1 2 3 4 5 6 7 8 9 void  OnMapDataReceived (MapData mapData ) {	mapGenerator.RequestMeshData(mapData, OnMeshDataRecieved); } void  OnMeshDataRecieved (MeshData meshData ) {	meshFilter.mesh = meshData.CreateMesh(); } 
 
然后我们就可以得到多个chunk拼接起来的地形
 
LOD Switching 在真实的游戏世界中,对于远处的物体,可以不需要太多细节,因此在做地形时,我们也让远处地形的detail少一些,去做一个根据距离的LOD Switching。我使用了一个LODMesh类,它包含了一个回调函数,可以在创建这个地形时告诉目前这个chunk的LOD是多少。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public  class  LODMesh { 	public  Mesh mesh; 	public  bool  hasRequestedMesh; 	public  bool  hasMesh; 	int  lod; 	System.Action updateCallback; 	public  LODMesh (int  lod, System.Action updateCallback )  	{		this .lod = lod; 		this .updateCallback = updateCallback; 	} 	void  OnMeshDataReceived (MeshData meshData )  	{		mesh = meshData.CreateMesh(); 		hasMesh = true ; 		updateCallback(); 	} 	public  void  RequestMesh (MapData mapData )  	{		hasRequestedMesh = true ; 		mapGenerator.RequestMeshData(mapData, lod, OnMeshDataReceived); 	} } 
 
最终的效果是:
 
离玩家的位置越远,地形的细节就越低。