用Unity复刻Minecraft简单分享 (1)
渲染方块
在MC中,方块并不是一个3D模型,所有方块都是用4个面来构成的,一开始我选择使用Sprite Render来渲染面,但是这需要创建过多的对象,渲染会非常吃力DrawCall调用太多了,为了解决这个问题,我使用MeshRender来渲染,而且是一个MeshRender负责一个区块的渲染(1616256个方块的范围)。
具体操作是遍历需要渲染的方块信息,然后我们将能看到的面的信息添加到MeshRender中,这样可以减少渲染消耗。
代码参考:
void AddFace2Mesh(int id, Vector3 position, List<Vector3> vertices, List<int> triangles, List<Vector2> uvs, Face face, int count, Rect[] rects) {
Vector3[] v = GetFaceVertices(face);
Vector3 v1 = v[0] + position;
Vector3 v2 = v[1] + position;
Vector3 v3 = v[2] + position;
Vector3 v4 = v[3] + position;
vertices.Add(v1);
vertices.Add(v2);
vertices.Add(v3);
vertices.Add(v4);
triangles.Add(0 + count);
triangles.Add(1 + count);
triangles.Add(2 + count);
triangles.Add(0 + count);
triangles.Add(2 + count);
triangles.Add(3 + count);
//取顶面
//var rect = rects[map.position[i, j, k]%3];
//TODO
id--;
id *= 3;
id += face switch {
Face.Top => 0,// 如果 face 为 Top,增量为 0
Face.Bottom => 2,// 如果 face 为 Bottom,增量为 2
_ => 1,// 其余情况下增量为 1
};
var rect = rects[id];
uvs.Add(new Vector2(rect.xMin, rect.yMin));
uvs.Add(new Vector2(rect.xMax, rect.yMin));
uvs.Add(new Vector2(rect.xMax, rect.yMax));
uvs.Add(new Vector2(rect.xMin, rect.yMax));
}
上述代码主要将面的信息添加到MeshRender中,并且指定贴图范围,为什么要指定这个呢,因为一个区块中(1616256个方块范围)有多种方块类型,每种方块的贴图是不同的,为了提高效率,我将所有方块的面加载到贴图上,然后渲染某个面的时候再读取。
代码参考:
Texture2D atlas;
Rect[] rects;
List<Texture2D> textures;
void CreateAtlas() {
//Texture2D[] textures;
atlas = new Texture2D(2048, 2048);
//textures = new Texture2D[2048];
textures = new();
textures.Add(Resources.Load<Texture2D>("Blocks/dirt"));
textures.Add(Resources.Load<Texture2D>("Blocks/dirt"));
textures.Add(Resources.Load<Texture2D>("Blocks/dirt"));
textures.Add(Resources.Load<Texture2D>("Blocks/water_overlay"));
textures.Add(Resources.Load<Texture2D>("Blocks/water_overlay"));
textures.Add(Resources.Load<Texture2D>("Blocks/water_overlay"));
textures.Add(Resources.Load<Texture2D>("Blocks/grass_top"));
textures.Add(Resources.Load<Texture2D>("Blocks/grass_side"));
textures.Add(Resources.Load<Texture2D>("Blocks/grass_side"));
textures.Add(Resources.Load<Texture2D>("Blocks/cobblestone"));
textures.Add(Resources.Load<Texture2D>("Blocks/cobblestone"));
textures.Add(Resources.Load<Texture2D>("Blocks/cobblestone"));
textures.Add(Resources.Load<Texture2D>("Blocks/snow"));
textures.Add(Resources.Load<Texture2D>("Blocks/grass_side_snowed"));
textures.Add(Resources.Load<Texture2D>("Blocks/dirt"));
textures.Add(Resources.Load<Texture2D>("Blocks/ice"));
textures.Add(Resources.Load<Texture2D>("Blocks/ice"));
textures.Add(Resources.Load<Texture2D>("Blocks/ice"));
AddTex("Blocks/log_oak_toplog_oak_top");
AddTex("Blocks/log_oak");
AddTex("Blocks/log_oak_toplog_oak_top");
AddTex("Blocks/leaves_oak");
AddTex("Blocks/leaves_oak");
AddTex("Blocks/leaves_oak");
rects = atlas.PackTextures(textures.ToArray(), 1, 2048);
atlas.filterMode = FilterMode.Point;
}
这部分值得注意的是添加面的顶点信息是要逆时针,同时由于透明方块的存在,透明方块应该是单独的一个MeshRenderer或者单独的材质。
地形的生成
首先我们要先考虑如何存储方块信息,我使用的是多个三维数组来储存,每个三维数组(区块)储存1625616大小的方块信息,根据玩家的位置来创建新的区块。
每个区块的生成主要是通过各种噪声图来实现,我们首先要生成高度,对于每一个x,z坐标,我们应该生成一个对应的最大高度,最大高度之上的空气(没高过海平面的则为海水),最大高度之下的为泥土,石头等,最下五层的基岩,如何生成高度呢?可以直接使用unity的柏林噪音函数来生成。
具体来说就是生成玩家会看到的区块,对于每个要生成的区块,遍历所有xz坐标获取高度,然后根据高度遍历整个高度,根据预设规则生成方块信息。
同时为了生成更加复杂多样的地形,我们可以采用多个噪音的叠加,赋予不同的频率和极值。
对于洞穴地形,我们同样可以使用噪音生成,比如三维的柏林噪音,但这样生成的噪音往往是不规则的,对于某些特殊的规则的峡谷可以通过变换频率来生成。
生物群系
MC中有多种的生物,分布在不同的环境中,如何决定一个环境主要依靠两个参数,一个是温度,另一是湿度。温度和湿度的生成同样使用噪音函数,值得注意的是温度还与高度有关。
生成建筑
小型建筑
这里举例树是如何生成的,我主要是通过噪音,温度,湿度,随机值来决定一个点(最高点)是否生成树,数的生成可以放在地形生成之后再遍历所有的点进行生成,一般来说根据温度,湿度来决定树的大小和颜色,将预设的树的信息加载到对应的地图数据中
大型建筑
这里就是村庄了,村庄的生成是否麻烦,因为不方便直接使用预设的模型数据生成,可能会与地形不适配,这里我们应该要做的是找到一块适合生成村庄的地。
首先我们先遍历所有的点,随机挑选一些作为村庄的源点,有了源点我们需要判断周围的地块是否适合生成村庄,生成多大的村庄。这里我才去了DFS来获取周边的点的信息来判断是否适合生成村庄,我们从源点出发,遍历周围的点,根据周围的点相对于源点的坡度来判断周围的点是否适合生成村庄,迭代若干次,我们就能获取到适合生成村庄的区域(这些点相对平坦)。再次基础上我们可以相当于将这些点夷为平地(这些点本身不一定是完全平坦的即高度会有偏差,我们根据源点的高度将选中相对偏差不大的点形成的区域夷为平地)在这一块地我们将我们设定好的村庄的某些建筑放入。
道路的生成可以通过A*算法来实现。
MeshRender到实体的转换
以上篇章我们主要是解决生成渲染的问题,如何交互呢?
我们给MeshRenderer的物体套上一个不规则碰撞器(直接使用Mesh)通过射线检测玩家所要交互的方块,对于处于方块交互。
对与处于交互的方块,我们根据其位置生成一个方块实体(比如说由6个Sprite Renderer组成的物体,或者单独的MeshRenderer方块),其交互方式由其脚本控制,并且删除对应的Mesh信息。
比如说,沙子会受到重力的影响,要实现这个功能我们要对每个方块的消失做监听,当方块消失后,监测该方块上方是否有沙子,如果有,则删除该沙子在Mesh中的信息(顶点,面,uv信息)然后再对应位置生成对应的单独方块实体,当沙子重新到达地面(由沙子实体挂载的脚本来监听),我们再将沙子的新信息更新到Mesh中,删除对应的实体。