美文网首页前端开发那些事儿
大地图生成续集(我的世界)

大地图生成续集(我的世界)

作者: 忆中异 | 来源:发表于2021-04-15 15:35 被阅读0次

    上篇链接https://www.jianshu.com/p/92eed746f268

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

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

    image

    1、用代码建立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

    转载于皮皮关https://zhuanlan.zhihu.com/p/30275469

    相关文章

      网友评论

        本文标题:大地图生成续集(我的世界)

        本文链接:https://www.haomeiwen.com/subject/dyndlltx.html