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

上篇链接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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355