阅读 120

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

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





文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐