系列概述
这套教程涵盖了Unity Mesh编程、模拟水算法(water simulations)、方块移动算法(marching-cubes)等等。这是一套比较有深度的教程,可能需要你了解一些Unity和C#相关的知识。
预备开始
首先,我们先来创建一个空的项目,命名随意即可。
然后创建在Assets下创建一个文件夹,命名为“Scripts”,并创建三个C#脚本,如下图所示:
Chunk用于存储方块数据和创建网格,并且对网格进行渲染和碰撞;Block用于存放方块需要的信息;MeshData用于存储网格数据。
Chunk.cs脚本:
首先,我们要求该脚本必须包含上个组件:MeshFilter、MeshRenderer、MeshCollider。我们的数据块(就是由一堆方块组成的大方块~)需要这三个组件完成网格的创建和碰撞。
然后,我们有三个变量。我们有一个Block类型的三维数组变量 blocks,Block类用于存放方块需要的信息,因此我们这个blocks变量就是用于存放一个数据块的方块的信息。
chunkSize是一个静态变量,它用于表示我们的数据块各个方向的大小(就是长宽高的大小)。
最后我们有一个bool类型的变量,用于标志该数据块是否在每帧结束后更新。
其次有三个函数,分别为GetBlock、UpdateChunk、RenderMesh。GetBlock用于获取对应位置的方块;UpdateChunk用于更新数据块的网格数据,然后将更新的数据提供给RenderMesh去渲染。
MeshData.cs脚本:
由于这个脚本只是为了存储数据,因此不必继承自MonoBehaviour。
前三个变量(vertices、triangles、uv)是用于渲染网格用的,后两个用于网格碰撞(colVetices、colTriangles)。
Block.cs脚本:
同样的,Block脚本也不需要继承自MonoBehaviour,并且它将会是所有方块的基类。主要用于存储方块所需信息。
BlockData函数用于生成该方块的网格信息。
我们来设想一下,假如我们的数据块是有25个方块组成的,那么在相邻的方块之间,有一些面就不必渲染出来,浪费系统资源。因此,接下来让我们进行剔除多余面数的处理。
首先,我们需要一个函数去判断两个方块是否相邻,如果相邻则对各个方向的面进行剔除处理。
在那之前,让我们先定一个表示方向的枚举(Block脚本中):
然后让我们为Block脚本添加判断的函数:
因为Block脚本是所有方块类的基类,所有我们在Block脚本中对IsSolid函数没有进行判断处理,全部返回true。
现在让我们开始写一些我们的BlockData函数,在这个函数中,我们会根据当前方块对应方向上相邻方块的面进行剔除处理。
上图中的注释也说得很清楚了。举个栗子:判断当前方块顶上相邻的方块是否有底面,如果有则当前方块就不制作顶面,如下图分析所示:
PS:这个,,,图画得有点Low,希望大家能看懂它的意思。
接下来就是添加需要绘制对应的面的函数了。
上:
上面的注释也说得很清楚了,但是克森还是给你们秀一秀我的美术功底。
其它的面就不细讲,代码如下:
下:
东:
北:
南:
西:
添加点之后,我们还要把这些点组合成三角形,因此在函数的最后调用了MeshData里的AddQuadTriangles(),由名字可知道,该函数用于添加面片,因此我们要用这四个顶点组合成一个面片,让我们回到MeshData中添加该函数:
再给大家上一次克森的美术作品,相信大家都能理解了吧。
接下来,让我们创建一个新的脚本,命名为“BlockAir”,让它继承值Block类,如下所示:
Okey,现在让我们回到Chunk脚本中,开始添加方块进行测试咯。添加如下代码:
首先声明两个变量,一个为MeshFilter(网格过滤器)类型,另一个为MeshCollider(网格碰撞器)类型。分别用于存储和设置我们对应数据库上的组件属性。
首先通过GetComponent方法获取对应物体上的对应的组件,然后初始化了我们的数据块(blocks),我们的数据块是一个16*16*16大小的正方体,里面由一堆小方块组成。当前数据库的小方块类型为 BlockAir。
然后修改了该数据库blocks[3, 5, 2]的方块数据,修改为Block类型方块。
最后调用UpdateChunk函数进行数据的更新。
好的,接下来让我们完善我们的UpdateChunk函数:
首先声明一个类型为MeshData的变量。然后循环遍历blocks进行数据的更新(就是调用每一个方块的BlockData函数,而BlockData函数则是用来处理方块的网格数据,例如剔除面等等)。
最后就是调用RenderMesh函数将更新好的网格数据传入,然后进行网格的渲染。
那么,接下来让我们完成我们的RenderMesh函数:
这个函数很简单,就是先调用Clear函数清除上一次网格的数据,然后重新设置即可。
这一篇只是简单的介绍怎么生成数据块,还没涉及到贴图和碰撞,所以在RenderMesh函数里只是更新了网格数据。下一篇则教大家如何添加贴图到数据块上。
说那么多,先看看效果。
首先创建一个空物体,然后为该物体添加Chunk脚本。你将会发现如下效果:
该物体便会自动添加脚本中需要的那三个组件。对了,记得将positio属性置为0,不然你很可能看不到我们的方块,之后启动游戏,你将会看到如下效果:
为什么方块会跑那去了,为什么会是紫色的呢?因为该方块没有材质,所以是紫色的。因为我们设置了该方块的位置为(3,5,2),那我们是在哪里设置的呢,其实是在这个地方设置了,如下图所示:
这个时候你可以创建一个Cube物体去比对一下就知道了,下图演示:
看来是这样的没错。好,接下来克森带大家来走一走这个运行时候的步骤:
1.首先我们先进行实例化数据块,也就是Chunk类里面的blocks变量。
2.调用UpdateChunk更新网格数据,在UpdateChunk中又调用了各个方块的BlockData函数生成网格数据。
3.在BlockData中我们对当前方块根据检测相邻方块进行剔除面操作。
(由于函数太大,所以只截一小部分)
4.最后调用RenderMesh对网格数据进行更新。
在这里,为什么我们只生成了一个方块呢。因为要想生成方块,就必须调用BlockData函数,而BlockAir的BlockData函数里我们只做了一个返回,并没有生成网格数据,
因此只生成了一个方块,也就是我们在后面修改的那个位置为(3,5,2)的方块,因为在Block类里BlockData函数已经生成了网格数据。
为什么克森不直接将所有的方块实例化为Block类呢,原因是这样做会造成数组下标越界。大家还记得下面这个函数吗?
假设当前方块的y为16,这y+1便会越界。对于这个处理,后续的文章中会有介绍。敬请关注吧。
忘记说最后一点了,对于为什么会生成一个方块呢。原因就是在下图判断中,如果返回为false则制作该方块对应的面,然后我们的BlockAir的IsSolid函数返回的就是false,因此我们的方块就出来了。
好吧,克森的文章总是又臭又长,其实理解我的人都知道我是为了照顾更多的人。