上篇链接https://www.jianshu.com/p/92eed746f268
我仔细阅读并研究了文章中的代码,做出了一点改进——在地图上点击会挖掉一个方块。这个新功能本身改动非常小,但是可能恰好是读者们萌妹以求的功能,所以把方法分享给大家,抛砖引玉,自己动手做出Minecraft指日可待 ?
另外,为了符合本专栏诲人不倦的特点,本文分两部分讲,第一部分带着萌新们尽可能看懂Meta42大神用到的代码建模技术,第二部分做代码分析并改动,让地图可以被挖出坑来。高手们可以直接翻看第二部分。
image1、用代码建立3D模型的原理
我们很难通过一篇很短的文章学会程序3D建模的方法。但是我想通过简单的图片和说明,让大家理解一些基本的概念,从而大概看懂Minecraft的大地图生成的大致步骤。
如何用脚本画出一个3D立方体呢?
1、确定某个顶点的位置,由于边长固定,其他顶点位置很容易确定。
2、画六个面,我们先从一个正方形面开始看,其他面是一样的。
3、画三角形,一个正方形面由两个三角形构成,如图:
image对应工程中的代码为:
void BuildFace(BlockType typeid, Vector3 corner, Vector3 up, Vector3 right, bool reversed, List<Vector3> verts, List<Vector2> uvs, List<int> tris)
{
// index是之前画过的所有的顶点的总数,因为还有其它面的存在
int index = verts.Count;
// up和right分别是向上和向右的单位方向向量,正方形很容易确定四个顶点的位置
verts.Add (corner);
verts.Add (corner + up);
verts.Add (corner + up + right);
verts.Add (corner + right);
// UV坐标和贴图有关, 我们先不管它
Vector2 uvWidth = new Vector2(0.25f, 0.25f);
…………省略UV的部分…………
// 顶点顺序不同,就朝向不同的方向(3D的面只能从一个方向看,另一个方向看不见)
// 比如立方体和上面朝上,下面朝下。上面只能从上面看见,下面要从下面才能看见
// 如果一个三角形的顶点0 1 2是顺时针,1 0 2就是逆时针。
if (reversed)
{
tris.Add(index + 0); tris.Add(index + 1); tris.Add(index + 2);
tris.Add(index + 2); tris.Add(index + 3); tris.Add(index + 0);
}
else
{
tris.Add(index + 1); tris.Add(index + 0); tris.Add(index + 2);
tris.Add(index + 3); tris.Add(index + 2); tris.Add(index + 0);
}
// 以上代码把顶点都加入verts列表,把顶点序号都放入了tris列表。之后由其他函数
// 把所有的顶点和序号信息都传给渲染器
}
我给这段代码加上了详细的注释,可以看到,要点就是顶点和顶点顺序,以下说明顶点顺序存在的意义:
image比如这个正方形,有两个三角形,理论上来说一共有6个顶点。但是由于3D模型有大量顶点是公共的,所以渲染的时候都是尽量复用顶点提高效率。上图只需要4个顶点既可,但是要再用一个列表指定顺序,比如0、1、2、2、3、0,就可以画出两个三角面了。那么如果逆时针画1、0、2、3、2、0,也能画出来。但是这个面的方向是反的。
什么是面的正反方向呢?我们只要在Unity里随便拉一个Plane平面,然后从高处往下和低处往上看,自然就明白了。
image<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">从低处往上看,Plane消失了,只能看到选中的轮廓。这说明3D的面是有正反之分的。</figcaption>
明白了BuildFace函数在干嘛,再看BuildBlock函数就简单了,因为BuildBlock函数就是调用了6次BuildFace函数,分别画立方体的上下左右前后六个面而已。但是在画面之前,先判断了该面是不是可以跳过不画:
if (CheckNeedBuildFace(x - 1, y, z))
BuildFace(typeid, new Vector3(x, y, z), Vector3.up, Vector3.forward, false, verts, uvs, tris);
这个CheckNeedBuildFace的原理是:如果这个面朝左,而这个立方体的左边不是空的(有其他方块),那么就不画了,因为玩家看不到。相反如果左边是空的,它就必须要画出来了。
然后呢,BuildChunk来调用BuildBlock生成每一个方块,这个流程大部分是固定的写法,我也写了详细注释。
public void BuildChunk()
{
// 新建一个Mesh,也就是网格
chunkMesh = new Mesh();
// 建立三个List,存放所有的顶点、UV、序号。数量可能非常多
List<Vector3> verts = new List<Vector3>();
List<Vector2> uvs = new List<Vector2>();
List<int> tris = new List<int>();
//遍历chunk, 生成其中的每一个Block
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
for (int z = 0; z < width; z++)
{
BuildBlock(x, y, z, verts, uvs, tris);
}
}
}
// 把List里面的所有信息送到网格里面
chunkMesh.vertices = verts.ToArray();
chunkMesh.uv = uvs.ToArray();
chunkMesh.triangles = tris.ToArray();
//固定写法,网格刷新并赋值给相应的Unity组件meshFilter和meshCollider
chunkMesh.RecalculateBounds();
chunkMesh.RecalculateNormals();
meshFilter.mesh = chunkMesh;
meshCollider.sharedMesh = chunkMesh;
}
到此为止就差不多啦,作为暂时不想深入建模细节的读者,以上解说加上注释,已经足够说明用代码来组装网格的整体思路了。如果你通过阅读以上一段内容对Mesh的构建和使用有了一点感性的认识,我就算没白写这么多?
2、分析代码结构,加入挖坑逻辑
其实Meta42给出的建模方法,是完全支持Minecraft那样的挖坑效果的。实现挖坑不难,但是分析代码的过程必不可少。我花了不少时间搞清楚了原来的代码逻辑,而最终的修改比想象中简单的多。
首先,整个Unity工程的核心脚本文件只有一个,就是Chunk.cs。Chunk类代表了Chunk对象,也就是20 * 20方块组成的一个大格子。我们的代码全都是针对某个Chunk本身进行操作的,但是注意,只有个别static字段和方法,是全局唯一的,它们是:
public static List<Chunk> chunks = new List<Chunk>();
public static Chunk GetChunk(Vector3 wPos)
chunks列表整个世界只有一个,用于存放所有Chunk。而某一个Chunk中的所有方块Block,则是存储在这个字段里:
BlockType[,,] map;
它是一个三维数组,BlockType是一个简单的枚举,可以取值为空格None、泥土格Dirt、草格Grass和岩石格Gravel。
简单地说,整个世界是由一系列Chunk组成,每个Chunk里有个202020的三维数组,存放着每一个格子Block的情况。**我们只要修改BlockType[,,] map这个数组里面某一个格子map[i,j,k] = None,它就变成了空(也就是被挖掉了),同理,只要设置某个坐标为Grass,它就变成了草地,就是这么简单。
注:修改完map的数据后,只要调用BuildChunk() 方法即可生效。所以挖坑方法很简单:
public bool Dig(int x, int y, int z)
{
map[x, y, z] = BlockType.None;
BuildChunk();
return true;
}
有了这个Dig函数,理论上你就可以直接指定x y z挖坑了。现在还缺一步鼠标点击操作。
3、鼠标操作挖坑
我们不在原来的Player.cs脚本里乱改了,新建一个HitBlock.cs来处理鼠标点击。该脚本挂在Player的GameObject上。
public class HitBlock : MonoBehaviour {
void Update () {
if (Input.GetMouseButtonDown(0))
{
// 3D游戏点击屏幕都是发射射线来做,不再赘述
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitt = new RaycastHit();
Physics.Raycast(ray, out hitt, 1000);
if (hitt.transform != null)
{
// 根据3D空间中的点坐标,获得对应的大Chunk对象,Chunk里已经提供了该方法
Debug.Log("hitt.point="+hitt.point);
Chunk chunk = Chunk.GetChunk(hitt.point);
// chunk.transform.position 其实是整块Chunk底面最角上一点的坐标,也就是该Chunk的原点
// hitt.point是世界坐标,减去chunk的起点,就是对该chunk来说的局部坐标
Vector3 v = hitt.point - chunk.transform.position;
// Unity可以直接获取碰撞点的法线
// 根据法线方向就知道了你点击的是侧面还是上面。这个地方坐标换算需要仔细考虑
if(Mathf.Abs(hitt.normal.y)>0.01)
{
Debug.Log("上表面");
chunk.Dig((int)v.x, (int)v.y-1, (int)v.z);
}
else
{
Debug.Log("侧面");
chunk.Dig((int)v.x, (int)v.y, (int)v.z);
}
// 注:我没有实现底面,其实确实存在从下往上挖的情况,再说吧 XD
}
}
}
}
这个脚本的要点:
1、发射射线,获得点击的点的3D坐标。
2、用Chunk.GetChunk()函数可以查找到点击的点是对应哪个Chunk。注:Chunk.GetChunk函数是遍历所有Chunk查找的,显然没有必要、可以优化,不过对本文来说无所谓。
3、Unity可以直接获得碰撞点的法线,这个功能太好用了。对方块来说,如果法线向上,就是上表面,法线向下,就是下表面,否则就是四个侧面。
4、根据是上面还是侧面,可以算出到底玩家想挖哪一个方块。调用之前写的Chunk.Dig方法即可。
完成 ≖‿≖✧~~看看效果:
image
网友评论