欢迎光临散文网 会员登陆 & 注册

Minecraft大地图生成续集——小锄头挖起来

2017-10-27 17:47 作者:皮皮关做游戏  | 我要投稿



作者:皮皮关马遥


上次由Meta42编写的《300行代码实现Minecraft(我的世界)大地图生成》受到了大量点赞,并获得了成就

原文链接:300行代码实现Minecraft(我的世界)大地图生成

我仔细阅读并研究了文章中的代码,做出了一点改进——在地图上点击会挖掉一个方块。这个新功能本身改动非常小,但是可能恰好是读者们萌妹以求的功能,所以把方法分享给大家,抛砖引玉,自己动手做出Minecraft指日可待 ????

另外,为了符合本专栏诲人不倦的特点,本文分两部分讲,第一部分带着萌新们尽可能看懂Meta42大神用到的代码建模技术,第二部分做代码分析并改动,让地图可以被挖出坑来。高手们可以直接翻看第二部分。

1、用代码建立3D模型的原理

我们很难通过一篇很短的文章学会程序3D建模的方法。但是我想通过简单的图片和说明,让大家理解一些基本的概念,从而大概看懂Minecraft的大地图生成的大致步骤。

如何用脚本画出一个3D立方体呢?

1、确定某个顶点的位置,由于边长固定,其他顶点位置很容易确定。

2、画六个面,我们先从一个正方形面开始看,其他面是一样的。

3、画三角形,一个正方形面由两个三角形构成,如图:


对应工程中的代码为:     


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列表。之后由其他函数
        // 把所有的顶点和序号信息都传给渲染器
    }


我给这段代码加上了详细的注释,可以看到,要点就是顶点和顶点顺序,以下说明顶点顺序存在的意义:

比如这个正方形,有两个三角形,理论上来说一共有6个顶点。但是由于3D模型有大量顶点是公共的,所以渲染的时候都是尽量复用顶点提高效率。上图只需要4个顶点既可,但是要再用一个列表指定顺序,比如0、1、2、2、3、0,就可以画出两个三角面了。那么如果逆时针画1、0、2、3、2、0,也能画出来。但是这个面的方向是反的。

什么是面的正反方向呢?我们只要在Unity里随便拉一个Plane平面,然后从高处往下和低处往上看,自然就明白了。

从低处往上看,Plane消失了,只能看到选中的轮廓。这说明3D的面是有正反之分的。



明白了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里有个20*20*20的三维数组,存放着每一个格子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方法即可。


完成 ≖‿≖✧~~看看效果:


总结

本文初衷是修改和扩展大家喜爱的一个游戏原型,但是写完发现大部分篇幅都是对上一篇内容的讲解和分析,不过这样也挺好,符合咱们专栏的定位。

其实修改和维护别人写的代码,也是一项很常见的工作。改进别人的代码其实不比自己从头编写简单,所以这项工作也不太受人欢迎,但是相信我,这项工作非常重要????。

本篇中3D模型生成的技术属于高级技术,但是其实难度并不很大,希望此文能引起游戏开发爱好者们的兴趣。 (•̀ᴗ•́)  ̑̑



对游戏开发感兴趣的同学,欢迎围观我们:【Levelplay皮皮关游戏开发教育】 ,会定期更新各种教程干货,更有别具一格的线下小班教育。

我们的官网地址:levelpp.com/

我们的游戏开发技术交流群:610475807

我们的微信公众号:皮皮关


Minecraft大地图生成续集——小锄头挖起来的评论 (共 条)

分享到微博请遵守国家法律