国产精品天干天干,亚洲毛片在线,日韩gay小鲜肉啪啪18禁,女同Gay自慰喷水

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

Minecraft大地圖生成續(xù)集——小鋤頭挖起來

2017-10-27 17:47 作者:皮皮關(guān)做游戲  | 我要投稿



作者:皮皮關(guān)馬遙


上次由Meta42編寫的《300行代碼實(shí)現(xiàn)Minecraft(我的世界)大地圖生成》受到了大量點(diǎn)贊,并獲得了成就

原文鏈接:300行代碼實(shí)現(xiàn)Minecraft(我的世界)大地圖生成

我仔細(xì)閱讀并研究了文章中的代碼,做出了一點(diǎn)改進(jìn)——在地圖上點(diǎn)擊會(huì)挖掉一個(gè)方塊。這個(gè)新功能本身改動(dòng)非常小,但是可能恰好是讀者們萌妹以求的功能,所以把方法分享給大家,拋磚引玉,自己動(dòng)手做出Minecraft指日可待 ????

另外,為了符合本專欄誨人不倦的特點(diǎn),本文分兩部分講,第一部分帶著萌新們盡可能看懂Meta42大神用到的代碼建模技術(shù),第二部分做代碼分析并改動(dòng),讓地圖可以被挖出坑來。高手們可以直接翻看第二部分。

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

我們很難通過一篇很短的文章學(xué)會(huì)程序3D建模的方法。但是我想通過簡(jiǎn)單的圖片和說明,讓大家理解一些基本的概念,從而大概看懂Minecraft的大地圖生成的大致步驟。

如何用腳本畫出一個(gè)3D立方體呢?

1、確定某個(gè)頂點(diǎn)的位置,由于邊長(zhǎng)固定,其他頂點(diǎn)位置很容易確定。

2、畫六個(gè)面,我們先從一個(gè)正方形面開始看,其他面是一樣的。

3、畫三角形,一個(gè)正方形面由兩個(gè)三角形構(gòu)成,如圖:


對(duì)應(yīng)工程中的代碼為:     


void BuildFace(BlockType typeid, Vector3 corner, Vector3 up, Vector3 right, bool reversed, List<Vector3> verts, List<Vector2> uvs, List<int> tris)
    {
        // index是之前畫過的所有的頂點(diǎn)的總數(shù),因?yàn)檫€有其它面的存在
        int index = verts.Count;
       
        // up和right分別是向上和向右的單位方向向量,正方形很容易確定四個(gè)頂點(diǎn)的位置
        verts.Add (corner);
        verts.Add (corner + up);
        verts.Add (corner + up + right);
        verts.Add (corner + right);
       
        // UV坐標(biāo)和貼圖有關(guān), 我們先不管它
        Vector2 uvWidth = new Vector2(0.25f, 0.25f);
        …………省略UV的部分…………
       
        // 頂點(diǎn)順序不同,就朝向不同的方向(3D的面只能從一個(gè)方向看,另一個(gè)方向看不見)
        // 比如立方體和上面朝上,下面朝下。上面只能從上面看見,下面要從下面才能看見
        // 如果一個(gè)三角形的頂點(diǎn)0 1 2是順時(shí)針,1 0 2就是逆時(shí)針。
        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);
        }
        // 以上代碼把頂點(diǎn)都加入verts列表,把頂點(diǎn)序號(hào)都放入了tris列表。之后由其他函數(shù)
        // 把所有的頂點(diǎn)和序號(hào)信息都傳給渲染器
    }


我給這段代碼加上了詳細(xì)的注釋,可以看到,要點(diǎn)就是頂點(diǎn)和頂點(diǎn)順序,以下說明頂點(diǎn)順序存在的意義:

比如這個(gè)正方形,有兩個(gè)三角形,理論上來說一共有6個(gè)頂點(diǎn)。但是由于3D模型有大量頂點(diǎn)是公共的,所以渲染的時(shí)候都是盡量復(fù)用頂點(diǎn)提高效率。上圖只需要4個(gè)頂點(diǎn)既可,但是要再用一個(gè)列表指定順序,比如0、1、2、2、3、0,就可以畫出兩個(gè)三角面了。那么如果逆時(shí)針畫1、0、2、3、2、0,也能畫出來。但是這個(gè)面的方向是反的。

什么是面的正反方向呢?我們只要在Unity里隨便拉一個(gè)Plane平面,然后從高處往下和低處往上看,自然就明白了。

從低處往上看,Plane消失了,只能看到選中的輪廓。這說明3D的面是有正反之分的。



明白了BuildFace函數(shù)在干嘛,再看BuildBlock函數(shù)就簡(jiǎn)單了,因?yàn)锽uildBlock函數(shù)就是調(diào)用了6次BuildFace函數(shù),分別畫立方體的上下左右前后六個(gè)面而已。但是在畫面之前,先判斷了該面是不是可以跳過不畫:



if (CheckNeedBuildFace(x - 1, y, z))
            BuildFace(typeid, new Vector3(x, y, z), Vector3.up, Vector3.forward, false, verts, uvs, tris);


這個(gè)CheckNeedBuildFace的原理是:如果這個(gè)面朝左,而這個(gè)立方體的左邊不是空的(有其他方塊),那么就不畫了,因?yàn)橥婕铱床坏健O喾慈绻筮吺强盏?,它就必須要畫出來了?/p>

然后呢,BuildChunk來調(diào)用BuildBlock生成每一個(gè)方塊,這個(gè)流程大部分是固定的寫法,我也寫了詳細(xì)注釋。



public void BuildChunk()
    {
        // 新建一個(gè)Mesh,也就是網(wǎng)格
        chunkMesh = new Mesh();

        // 建立三個(gè)List,存放所有的頂點(diǎn)、UV、序號(hào)。數(shù)量可能非常多
        List<Vector3> verts = new List<Vector3>();
        List<Vector2> uvs = new List<Vector2>();
        List<int> tris = new List<int>();
       
        //遍歷chunk, 生成其中的每一個(gè)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里面的所有信息送到網(wǎng)格里面
        chunkMesh.vertices = verts.ToArray();
        chunkMesh.uv = uvs.ToArray();
        chunkMesh.triangles = tris.ToArray();

        //固定寫法,網(wǎng)格刷新并賦值給相應(yīng)的Unity組件meshFilter和meshCollider
        chunkMesh.RecalculateBounds();
        chunkMesh.RecalculateNormals();
       
        meshFilter.mesh = chunkMesh;
        meshCollider.sharedMesh = chunkMesh;
    }


到此為止就差不多啦,作為暫時(shí)不想深入建模細(xì)節(jié)的讀者,以上解說加上注釋,已經(jīng)足夠說明用代碼來組裝網(wǎng)格的整體思路了。如果你通過閱讀以上一段內(nèi)容對(duì)Mesh的構(gòu)建和使用有了一點(diǎn)感性的認(rèn)識(shí),我就算沒白寫這么多????


2、分析代碼結(jié)構(gòu),加入挖坑邏輯

其實(shí)Meta42給出的建模方法,是完全支持Minecraft那樣的挖坑效果的。實(shí)現(xiàn)挖坑不難,但是分析代碼的過程必不可少。我花了不少時(shí)間搞清楚了原來的代碼邏輯,而最終的修改比想象中簡(jiǎn)單的多。


首先,整個(gè)Unity工程的核心腳本文件只有一個(gè),就是Chunk.cs。Chunk類代表了Chunk對(duì)象,也就是20 * 20方塊組成的一個(gè)大格子。我們的代碼全都是針對(duì)某個(gè)Chunk本身進(jìn)行操作的,但是注意,只有個(gè)別static字段和方法,是全局唯一的,它們是:


public static List<Chunk> chunks = new List<Chunk>();
public static Chunk GetChunk(Vector3 wPos)


chunks列表整個(gè)世界只有一個(gè),用于存放所有Chunk。而某一個(gè)Chunk中的所有方塊Block,則是存儲(chǔ)在這個(gè)字段里:


BlockType[,,] map;

它是一個(gè)三維數(shù)組,BlockType是一個(gè)簡(jiǎn)單的枚舉,可以取值為空格None、泥土格Dirt、草格Grass和巖石格Gravel。

簡(jiǎn)單地說,整個(gè)世界是由一系列Chunk組成,每個(gè)Chunk里有個(gè)20*20*20的三維數(shù)組,存放著每一個(gè)格子Block的情況。我們只要修改BlockType[,,] map這個(gè)數(shù)組里面某一個(gè)格子map[i,j,k] = None,它就變成了空(也就是被挖掉了),同理,只要設(shè)置某個(gè)坐標(biāo)為Grass,它就變成了草地,就是這么簡(jiǎn)單。

注:修改完map的數(shù)據(jù)后,只要調(diào)用BuildChunk() 方法即可生效。所以挖坑方法很簡(jiǎn)單:


public bool Dig(int x, int y, int z)
    {
        map[x, y, z] = BlockType.None;
        BuildChunk();
        return true;
    }

有了這個(gè)Dig函數(shù),理論上你就可以直接指定x y z挖坑了?,F(xiàn)在還缺一步鼠標(biāo)點(diǎn)擊操作。


3、鼠標(biāo)操作挖坑

我們不在原來的Player.cs腳本里亂改了,新建一個(gè)HitBlock.cs來處理鼠標(biāo)點(diǎn)擊。該腳本掛在Player的GameObject上。


public class HitBlock : MonoBehaviour {

    void Update () {
if (Input.GetMouseButtonDown(0))
        {
            // 3D游戲點(diǎn)擊屏幕都是發(fā)射射線來做,不再贅述
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hitt = new RaycastHit();
            Physics.Raycast(ray, out hitt, 1000);
            if (hitt.transform != null)
            {
                // 根據(jù)3D空間中的點(diǎn)坐標(biāo),獲得對(duì)應(yīng)的大Chunk對(duì)象,Chunk里已經(jīng)提供了該方法
                Debug.Log("hitt.point="+hitt.point);
                Chunk chunk = Chunk.GetChunk(hitt.point);

                // chunk.transform.position 其實(shí)是整塊Chunk底面最角上一點(diǎn)的坐標(biāo),也就是該Chunk的原點(diǎn)
                // hitt.point是世界坐標(biāo),減去chunk的起點(diǎn),就是對(duì)該chunk來說的局部坐標(biāo)
                Vector3 v = hitt.point - chunk.transform.position;
                // Unity可以直接獲取碰撞點(diǎn)的法線
                // 根據(jù)法線方向就知道了你點(diǎn)擊的是側(cè)面還是上面。這個(gè)地方坐標(biāo)換算需要仔細(xì)考慮
                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("側(cè)面");
                    chunk.Dig((int)v.x, (int)v.y, (int)v.z);
                }
                // 注:我沒有實(shí)現(xiàn)底面,其實(shí)確實(shí)存在從下往上挖的情況,再說吧 XD
            }
        }
    }
}


這個(gè)腳本的要點(diǎn):

1、發(fā)射射線,獲得點(diǎn)擊的點(diǎn)的3D坐標(biāo)。

2、用Chunk.GetChunk()函數(shù)可以查找到點(diǎn)擊的點(diǎn)是對(duì)應(yīng)哪個(gè)Chunk。注:Chunk.GetChunk函數(shù)是遍歷所有Chunk查找的,顯然沒有必要、可以優(yōu)化,不過對(duì)本文來說無所謂。

3、Unity可以直接獲得碰撞點(diǎn)的法線,這個(gè)功能太好用了。對(duì)方塊來說,如果法線向上,就是上表面,法線向下,就是下表面,否則就是四個(gè)側(cè)面。

4、根據(jù)是上面還是側(cè)面,可以算出到底玩家想挖哪一個(gè)方塊。調(diào)用之前寫的Chunk.Dig方法即可。


完成 ????~~看看效果:


總結(jié)

本文初衷是修改和擴(kuò)展大家喜愛的一個(gè)游戲原型,但是寫完發(fā)現(xiàn)大部分篇幅都是對(duì)上一篇內(nèi)容的講解和分析,不過這樣也挺好,符合咱們專欄的定位。

其實(shí)修改和維護(hù)別人寫的代碼,也是一項(xiàng)很常見的工作。改進(jìn)別人的代碼其實(shí)不比自己從頭編寫簡(jiǎn)單,所以這項(xiàng)工作也不太受人歡迎,但是相信我,這項(xiàng)工作非常重要????。

本篇中3D模型生成的技術(shù)屬于高級(jí)技術(shù),但是其實(shí)難度并不很大,希望此文能引起游戲開發(fā)愛好者們的興趣。 (?????)  ??



對(duì)游戲開發(fā)感興趣的同學(xué),歡迎圍觀我們:【Levelplay皮皮關(guān)游戲開發(fā)教育】 ,會(huì)定期更新各種教程干貨,更有別具一格的線下小班教育。

我們的官網(wǎng)地址:levelpp.com/

我們的游戲開發(fā)技術(shù)交流群:610475807

我們的微信公眾號(hào):皮皮關(guān)


Minecraft大地圖生成續(xù)集——小鋤頭挖起來的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
荆门市| 涞源县| 绍兴市| 南城县| 南乐县| 江北区| 溆浦县| 伊宁县| 昆明市| 琼海市| 宝丰县| 包头市| 彩票| 大新县| 香河县| 金昌市| 西华县| 武川县| 射阳县| 甘德县| 西平县| 藁城市| 汕尾市| 苏尼特左旗| 阿拉善左旗| 文登市| 二连浩特市| 郎溪县| 镇远县| 灯塔市| 汉寿县| 葵青区| 富蕴县| 永泰县| 万州区| 揭东县| 青铜峡市| 锡林浩特市| 临汾市| 长泰县| 龙山县|