一直以来很多人都比较好奇,《我的世界》里的大地图是如何随机生成且还具有无限大小的,那么这一期教程,我就以最简化的代码(300行左右)在Unity引擎中实现这一机制。 GIF 运行后,随机生成角色周围的地形,且随着角色的位置变化,动态加载。 在实现之前呢,我们可以先来简单分析一下这个需求: 我的世界的地图元素可以分为4个层次 World->Chunk->Block->Face 下面分别来解释一下这4个层次。 1.Face: 正方体的一个面 2.Block: 6个面组成的一个正方体 3.Chunk: N个正方体组成的一个地图块 4.World: 多个地图块组成的世界,就是“我的世界”啦。 我们可以看到这4个层次,其实有点类似俄罗斯套娃对吧,一层包含一层。 我们要生成World,那么就是要在这些层次中,一层一层的去处理生成的逻辑, 在World里动态加载Chunk, 在Chunk里生成Block, 在Block里生成Face。 OK 大概的思路我们已经说完了,接下来我们来拆解一下实现步骤 1.首先我们先实现Chunk的生成,内部会包含 Block的生成,这里会用到simplex noise(一种Perlin噪声的改进) 有关噪声的知识,如果读者没有接触过,可以自行网上找找相关资料看看 这里推荐一篇(小姐姐写的比较细致):http://blog.csdn.net/candycat1992/article/details/50346469 在这个部分我们会写一个类Chunk.cs, (大约200行代码) 2.接下来我们要通过玩家的位置信息来动态加载Chunk 这个部分我们会写一个类Player.cs (大约100行代码) Chunk生成 接下来我们在场景中创建一个Cube
打开刚才新建的Chunk.cs,我们来先声明好Chunk类里需要用到的成员变量 public class Chunk : MonoBehaviour //存储着世界中所有的Chunk //每个Chunk的长宽Size //随机种子 //最小生成高度 //噪音频率(噪音采样时会用到) //存储着此Chunk内的所有Block信息 //Chunk的网格 //噪音采样时会用到的偏移 MeshRenderer meshRenderer; } 如下: void Start () //获取自身相关组件引用 //初始化地图 void InitMap() //根据生成的信息,Build出Chunk的网格 在上面这段代码中,我们需要注意两个点 1.这里的map存的是Chunk内每一个Block的信息 2.GenerateBlockType函数和BuildChunk函数,我们还没有实现 3.我们在Start函数被调用时,便将这个Chunk生成好了 在第二点中说的两个函数,便是我们接下来生成Chunk的两个核心步骤 1.生成map信息(每个Block的类型,以及地形的高度信息) 2.构建Chunk用来显示的网格 那么我们接下来分别看看如何实现这两步 1.GenerateBlockType int GenerateHeight(Vector3 wPos) BlockType GenerateBlockType(Vector3 wPos) //获取当前位置方块随机生成的高度值 //当前方块位置高于随机生成的高度值时,当前方块类型为空 上面这两个函数实现了生成Block信息的过程 在上面这段代码中我们需要注意以下几点 1.GenerateHeight用于通过噪音来随机生成每个方块的高度,这种随机生成的方式相比其他方式更贴近我们想要的结果。普通的随机数得到的值都是离散的,均匀分布的结果,而通过simplex noise得到的结果,会是连续的。这样会获得更加真实,接近自然的效果。 2. GenerateHeight中那些数字字面量,没有特殊意义,就是经验数值,为了生成结果能够产生更多变化而已。可以自己调整试试看。 3.GenerateHeight中对多个噪声的生成结果进行了叠加,这是为了混合出理想的结果,具体可以网上检索查阅噪声相关资料。 4.GenerateBlockType内,会利用在指定位置随机生成的高度,来决定当前Block的类型。最内层是岩石,中间混杂着泥土,地表则是草地。 在我们有了地形元素的类型信息后,我们就可以来构建Chunk的网格,以来显示我们的Chunk了。 接下来我们实现BuildChunk函数 public void BuildChunk() 如上所示,BuildChunk函数内部遍历了Chunk内的每一个Block,为其生成网格数据,并在最后将生成的数据(顶点,UV, 索引)提交给了chunkMesh。 接下来我们实现BuildBlock函数 void BuildBlock(int x, int y, int z, List<Vector3> verts, List<Vector2> uvs, List<int> tris) bool CheckNeedBuildFace(int x, int y, int z) public BlockType GetBlockType(int x, int y, int z) //当前位置是否在Chunk内 BuildBlock内,我们分别去构建了一个Block中的每一个Face, 并通过CheckNeedBuildFace来确定,某一面Face是否需要显示出来,如果不需要,那么就不用去构建这面Face了。也就是说这个检测,会只把我们可以看到的面,显示出来。
我们的角色在地形上时,只能看到最外部的一层面,其实看不到内部的方块,所以这些看不到的方块,就没有必要浪费计算资源了。也正是这个原因,我们不能直接用正方体去随机生成,而是要像现在这样,以Face为基本单位来生成。实现这个功能的函数,便是CheckNeedBuildFace。 接下来让我们完成Chunk部分的最后一步 void BuildFace(BlockType typeid, Vector3 corner, Vector3 up, Vector3 right, bool reversed, List<Vector3> verts, List<Vector2> uvs, List<int> tris) 这一步我们构建了正方体其中一面的网格数据,顶点,UV, 索引。这一步实现完后, 如果我们将这个组件挂在我们最初创建的Cube上,并运行,我们即会得到随机生成的一个Chunk。
public static Chunk GetChunk(Vector3 wPos) 这个函数用于给定一个世界空间的位置,获取这个指定位置所在的Chunk对象。其中遍历了chunks列表,并找出对应的chunk返回。这个函数我们将在后面的代码中用到。 接下来由于动态加载是根据玩家位置的变化来进行的,所以我们首先添加一个Player类 新建一个C#代码文件:Player.cs,并在其中添加如下代码: public class Player : MonoBehaviour private void Start() void Update () void UpdateInput() 这段代码中有几点需要注意 1.UpdateWorld我们还没有实现,这个函数将用来动态生成Chunk。 2.UpdateInput函数中,我们实现了一个最简单的处理玩家输入的小模块(但并不成熟,甚至都没有做视角的限制,感兴趣的可以自己加入更多的处理),其可以根据玩家的鼠标和键盘的输入来控制角色移动和旋转。 3.控制玩家移动的处理,我们使用了Unity内置的CharacterController组件,这个组件自身就又胶囊体碰撞盒。 在这一步中我们从Update函数中已经看出一些端倪了。这里会每一帧先处理玩家的输入,然后根据处理后的结果(更新后的玩家位置)来动态加载Chunk。 接下来我们添加最后一个函数UpdateWorld void UpdateWorld() 这个函数 使用了我们刚才实现过的静态函数Chunk.GetChunk,来获取相应位置的chunk, 如果没有获取到的话,那么就通过chunkPrefab在相应位置生成一个新的chunk。 这个函数会通过这种方式来动态加载自身周围的chunk。 viewRange参数可以控制需要加载的范围。 到这里代码部分我们就全部实现完了。 接下来我们,添加一个角色对象,并在其上挂载一个CharacterController组件,以及我们的Player组件。
然后是Chunk。
本期教程工程源码:https://github.com/meta-42/Minecraft-Unity (责任编辑:蚂蚁团队) |