《我的世界》基础篇:添加地形管理

系列概述

      这套教程涵盖了Unity Mesh编程、模拟水算法(water simulations)、方块移动算法(marching-cubes)等等。这是一套比较有深度的教程,可能需要你了解一些Unity和C#相关的知识。

 

预备开始

上一次我们为方块添加了网格碰撞,这一章将会创建一个地形管理相关的类。

首先创建一个名为“WorldPos.cs”的脚本,双击打开,码入如下代码:

using System.Collections;

public struct WorldPos
{
    public int x, y, z;

    public WorldPos(int x, int y, int z)
    {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    // 重写了 Equals 方法,便于比较和方便字典操作(后面有讲)
    public override bool Equals(object obj)
    {
        if (!(obj is WorldPos))
            return false;

        WorldPos pos = (WorldPos)obj;
        if (pos.x != x || pos.y != y || pos.z != z)
        {
            return false;
        }
        else
        {
            return true;
        }
    }
}

 

这个脚本里的代码很简单,也就是创建了一个带有三个 int 类型的结构体,然后重写了 Equals 方法,至于为什么这么做,接着往下看便知道了。

 

接下来创建一个名为“World.cs”的脚本,双击打开脚本,码入如下代码:

 

using UnityEngine;
using System.Collections.Generic;

public class World : MonoBehaviour {

    // 用来管理 chunk
    public Dictionary<WorldPos, Chunk> chunks = new Dictionary<WorldPos, Chunk>();
    // chunk 预设体,用做创建对象的模板
    public GameObject chunkPrefab;
}

 

咳咳咳,到这里就知道为什么了要创建“WorldPos.cs”脚本,和重写 Equals 方法了吧。

 

那就是因为我们使用了字典结构来管理我们的 Chunk,而 key 为 WorldPos,我们都知道字典的 key 是唯一的,如果想要得到某个 key 对应的值,那么我们就需要传入相等的 key 才能够得到对应的值,因此我们就需要重写 WorldPos 的 Equals 方法。如果不重写 Equals 方法的话,默认对比两个 WorldPos 则是通过它们各自的 Hash 值来对比的,而每个 new 出来的对象的 Hash 都不相同,所以这就是为什么要重写 Equals 方法的主要原因。

 

逼逼那么多,也不知道说得对不对,233。 

 

Ok,让我们回到 Unity 编辑器中,然后按步骤执行如下操作:

 

      

 

Ok,接下来让我们运行 Unity,你会发现什么都没有。

 

让我们双击打开“Chunk.cs”脚本,修改如下代码:

 

 

嚯嚯嚯,初始化代码都去掉了,我们要怎么创建 chunk 方块呀?

 

稍等骂爹,我们不是有一个用于管理 chunk 的类吗?对的,接下来让我们在 “World.cs”脚本里对“Chunk”进行初始化。回到“World.cs”脚本,添加如下代码:

 

    void Start()
    {
        // 初始化世界
        for (int x = 0; x < 1; x++)
        {
            for (int y = 0; y < 1; y++)
            {
                for (int z = 0; z < 1; z++)
                {
                    // 之所以乘上 Chunk.chunkSize,是用于确保每一个 Chunk 的范围
                    CreateChunk(x * Chunk.chunkSize, y * Chunk.chunkSize, z * Chunk.chunkSize);
                }
            }
        }
    }

 

上图中的代码较为简单,就不细说了,接下来添加“CreateChunk”函数,如下图所示:

 

    public void CreateChunk(int x, int y, int z)
    {
        // 创建 WorldPos 对象,并初始化
        WorldPos worldPos = new WorldPos(x, y, z);

        // 使用预设体创建游戏对象
        GameObject newChunkObject = Instantiate(
                        chunkPrefab, new Vector3(x, y, z),
                        Quaternion.Euler(Vector3.zero)
                    ) as GameObject;

        // 获取对象上的 Chuck 组件
        Chunk newChunk = newChunkObject.GetComponent<Chunk>();

        // 为组件赋值
        newChunk.pos = worldPos;
        newChunk.world = this;

        // 将该 chuck 添加到字典中管理
        chunks.Add(worldPos, newChunk);

        // 这段代码其实就是原来 Chunk.cs 脚本里初始化的代码
        for (int xi = 0; xi < Chunk.chunkSize; xi++)
        {
            for (int yi = 0; yi < Chunk.chunkSize; yi++)
            {
                for (int zi = 0; zi < Chunk.chunkSize; zi++)
                {
                    SetBlock(x + xi, y + yi, z + zi, new BlockGrass());
                }
            }
        }
    }

 

上图代码基本都上了注释,因此不再细说。接下来添加“SetBlock”函数,代码如下:

 

    public void SetBlock(int x, int y, int z, Block block)
    {
        // 在这里封装了一层,用于做相关检测逻辑
        Chunk chunk = GetChunk(x, y, z);

        if (chunk != null)
        {
            // 调用 chunk 的 SetBlock 函数,其实就是为 chunk 里的 blocks 数组设置对应的值,
            // 只不过也在该函数中做了相关检测处理逻辑
            chunk.SetBlock(x - chunk.pos.x, y - chunk.pos.y, z - chunk.pos.z, block);
            chunk.update = true;
        }
    }

 

接下来先添加“GetChunk”函数,代码如下:

 

    public Chunk GetChunk(int x, int y, int z)
    {
        // 下面五行代码主要用于计算当前 Block 位置对应的 chunk 于字典中的位置,并为 pos 赋值
        // 因为我们创建 chunk 是使用 CreateChunk(x * Chunk.chunkSize, y * Chunk.chunkSize, z * Chunk.chunkSize); 来创建的
        // 下面的操作只是将其操作反向计算了一下,仅此而已。
        WorldPos pos = new WorldPos();
        float multiple = Chunk.chunkSize;
        pos.x = Mathf.FloorToInt(x / multiple) * Chunk.chunkSize;
        pos.y = Mathf.FloorToInt(y / multiple) * Chunk.chunkSize;
        pos.z = Mathf.FloorToInt(z / multiple) * Chunk.chunkSize;

        Chunk chunk = null;

        chunks.TryGetValue(pos, out chunk);

        return chunk;
    }

 

接下来让我们回到“Chunk.cs”脚本中,添加如下关联函数,代码如下所示:

 

    public static bool InRange(int index)
    {
        // 因为我们的 chunk 为一个正方体,
        // 因此这里的逻辑就是判断 x、y、z 的位置时候在该立方体内
        if (index < 0 || index >= chunkSize)
            return false;

        return true;
    }

    public void SetBlock(int x, int y, int z, Block block)
    {
        // InRange 故名思意就是判断传入的位置是否正确
        if (InRange(x) && InRange(y) && InRange(z))
        {
            blocks[x, y, z] = block;
        }
    }

 

Ok,码到这里基本功能就完工了,让我们运行 Unity,查看效果,此时会报如下错误:

 

 

双击点击错误,便来到了报错的位置。报错的位置于“Chunk.cs”脚本中的“SetBlock”函数,这里为什么会报错,怎么看也没毛病啊。经过克森一系列的猜测,果然如此,组件的一些方法调用顺序出了问题。具体解决方案就是把“Chunk.cs”脚本你的“Start”函数该为“Awake”即可,如下所示:

 

    void Awake()
    {
        filter = gameObject.GetComponent<MeshFilter>();
        coll = gameObject.GetComponent<MeshCollider>();
        blocks = new Block[chunkSize, chunkSize, chunkSize];
    }

 

好的,继续运行 Unity,居然还是报错了,错误如下:

 

 

双击点击错误,便来到了报错的位置。报错的位置于“Chunk.cs”脚本中的“GetBlock”函数,报错信息为数组下标越界。

 

嘿,这个错误非常眼熟呀,之前的文章中也报了这个错误。其实就是因为我们在“Block.cs”脚本的“BlockData”函数中进行了如下判断:

 

 

然后我们是在“Chunk.cs”脚本的“UpdateChunk”函数中调用了“BlockData”函数,如下所示:

 

 

在这个函数中,我们会传入的最大值为“chunkSize - 1” ,然而在“BlockData”函数中的判断中会进行“* + 1”(*表示 x、y、z 任意一个),最终会传入“GetBlock”函数中的最大参数值为“chunkSize”,因此便造成了数组下标越界。

 

Ok,逼逼了那么多,我们该怎么处理了,其实很简单,做个简单的判断即可,这个时候我们的“IsRange”函数便派上用场咯,修改的代码如下所示:

 

    public Block GetBlock(int x, int y, int z)
    {
        // 判断传入的位置时候位于该 chunk 中,
        // 如果不存在则默认返回一个“BlockAir”方块(因为 BlockAir 方块是一个空方块,不会影响其它逻辑)
        if (InRange(x) && InRange(y) && InRange(z))
            return blocks[x, y, z];
        return new BlockAir();
    }

 

Ok,这个时候再运行 Unity,便会看到如下图所示:

 

 

嚯嚯嚯,看来是成功了,倍儿棒。

 

Ok,让我们杂耍一下我们的成果,修改如下代码:

 

 

 

运行 Unity,便看到如下图所示:

 

 

嚯嚯嚯,是我们想要的效果,不错,可以的,兄Dei。

 

接下来让我们看一下生成的 Chunk 网格是怎么样的,具体操作步骤如下所示:

 

 

嚯嚯嚯,不错,也是我们想要的效果。chunk 里的多余的面被过滤掉了,这样便节省了贼多性能,哦耶!

 

Ok,文章至此基本结束了,最后让我们为“World.cs”脚本添加两个有用的函数,代码如下所示:

 

    public Block GetBlock(int x, int y, int z)
    {
        Chunk chunk = GetChunk(x, y, z);

        if (chunk != null)
        {
            Block block = chunk.GetBlock(
                x - chunk.pos.x,
                y - chunk.pos.y,
                z - chunk.pos.z);

            return block;
        }
        else
        {
            return new BlockAir();
        }
    }

 

    public void DestroyChunk(int x, int y, int z)
    {
        // 逻辑简单,就是找到指定的 chunk,然后先销毁游戏对象,再移除管理即可(移除对应字典的值)
        Chunk chunk = null;
        if (chunks.TryGetValue(new WorldPos(x, y, z), out chunk))
        {
            Object.Destroy(chunk.gameObject);
            chunks.Remove(new WorldPos(x, y, z));
        }
    }

 

代码逻辑较为简单,因此就不逼逼了,好了,文章至此就结束吧。

 

本章源码:

https://pan.baidu.com/s/19vYQZGMm_nmdI5pBDN1OFQ

 

 

关于Unity墙外的世界

Unity墙外的世界 --- 这里有国外的经典案例、教程、文章,在这里你将会学到更为先进的Unity开发知识。

更新文章请进入《Unity墙外的世界》公众号中查看

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值