第八节 多区块地形生成与隐藏


yande.re 378993 sample lolita_fashion pantsu thighhighs tinkle.jpg
上一篇:


新建EndlessTerrain脚本

public const float maxViewDistance=450;
public Transform viewer;

public static Vector2 viewerPosition;
int blockSize;
int blocksVisibleInViewDistance;

上一节我们说到实际渲染的mesh网格是地图尺寸-1,我们为多区块地形的每个地形单元新定义变量名blockSize,下面在开始方法中进行赋值:

void Start(){
    blockSize=MapGenerator.mapBlockSize-1;
    blocksVisibleInViewDistance=Mathf.RoundToInt(maxViewDistance/blockSize);
}

接下来我们需要更新可见区块,那么继续构建相应的方法:
多区块地形坐标分布图.png
每个mesh区块240尺寸,我们viewer位置设为(0,0)mesh原点的话,左侧block就是(-240,0),右下角的block就是(240,-240),那么最大视距除以每个block尺寸取整后,在区块坐标来说,左侧block就可以设为(-1,0),右侧就是(1,0),右下角的block所属区块坐标就是(1,-1)
这个规则弄明白后,我们继续构建更新可视区块方法:

    void UpdateVisibleBlocks()
    {

        int currentBlockCoordinateX = Mathf.RoundToInt(viewerPosition.x / blockSize);
        int currentBlockCoordinateY = Mathf.RoundToInt(viewerPosition.y / blockSize);
//求出当前区块x,y坐标值,使用取整方法
//通过遍历正负两个方向最大可视区块坐标范围,给所有视野内区块赋值对应坐标位置。
        for (int yOffset = -blocksVisibleInViewDistance; yOffset <= blocksVisibleInViewDistance; yOffset++)
        {
            for (int xOffset = -blocksVisibleInViewDistance; xOffset <= blocksVisibleInViewDistance; xOffset++)
            {
                Vector2 viewedBlockCoordinate = new Vector2(currentBlockCoordinateX + xOffset, currentBlockCoordinateY + yOffset);
            }
        }
    }

接下来,我们就可以实例化这些block区块了,但是实例化之前,我们需要一个数据集合来存储这些坐标值和对应 地形区块信息,因为需要一个值存储坐标一个存储地形区块信息,那么 Dictionary字典是比较合适的,key键存储坐标信息,对应value值指向地形区块信息,这样到时候就可以记录所有地形,以免重复生成或者漏掉。
创建字典之前,我们对所有地形区块创建一个类来表示:在我们的EndlessTerrain类里面,后面新建一个

public class TerrainBlock{
}

然后调用Dictionary数据集合类型需要用到

using System.Collections.Generic;//默认已有则不用添加

EndlessTerrian方法中新增字典:

Dictionary<Vector2, TerrainBlock> terrainBlockDictionary = new Dictionary<Vector2, TerrainBlock>();

再来到下方UpdateVisibleBlocks方法

if (terrainBlockDictionary.ContainsKey(viewedBlockCoordinate))
{
     //稍后进行存储调取操作
}
else
{
    terrainBlockDictionary.Add(viewedBlockCoordinate, new TerrainBlock());
}//如果不存在,则将符合要求的block和对应坐标存储进字典保存

我们再思考下public class TerrainBlock{这个类,当前需要通过一个空物体或者说mesh管理器之类的东西,生成对应block的mesh载体plane,最后再和我们的MapGenerator脚本交互,进行block生成功能实现。
于是我们开始构建TerrianBlock类的构造函数:

public class TerrainBlock{
GameObject meshObject;
Vector2 position;

public TerrainBlock(Vector2 coordinate, int size)
{
    position = coordinate * size;
    Vector3 positionV3 = new Vector3(position.x, 0, position.y);//坐标为负值的话,代表位置在viewer的左侧

    meshObject = GameObject.CreatePrimitive(PrimitiveType.Plane);//创建基础模型之平面
    meshObject.transform.position = positionV3;
    meshObject.transform.localScale = Vector3.one * size / 10f;//之前[city builder][3]提到过,平面是10x10units的,所以scale再除以10f
}
}

那么,我们就定义好了每个block的三维plane平面载体位置和尺寸,为mesh网格载入做好准备
接下来我们新建一个更新地形区块的方法,好让Terrainblock状态随时更新,更新什么呢?一个距离判断,检查这个block周长上所有的点,与viewer最近的距离,实时监测对比我们maxViewerDistance距离,如果大于最大可视距离,就不生成咱们的meshObject,如果小于最大可视距离,就要纳入可见block,激活对应的meshObject,进行实例化生成并存储到对应terrainBlockDictionary字典里
检测周长,我们需要用到Bounds结构,
public class TerrainBlock{ 类里我们新增申明一个新的Bounds结构:

Bounds bounds;

然后来到public TerrainBlock构造函数里,对应的,我们生成新的bounds,传入对应block的位置和size尺寸:

bounds = new Bounds(position,Vector2.one* size);

然后我们就可以通过更新方法开始检测block边框也就是周长上的点和viewer之间最短距离了,用到的是Bounds结构下的SqrDistance方法,获得viewer这个point,到我们的bound结构最短距离的平方值
public class TerrainBlock类里面新增方法:

public void UpdateTerrainBlock()
{
     float minDistanceToViewer = Mathf.Sqrt(bounds.SqrDistance(viewerPosition));
    //我们需要计算的是最小距离,所以再用数学方法类里面的求平方根函数(square root)还原
     bool visible = minDistanceToViewer <= maxViewDistance;// 做边界值true/false判断开关
 }

依据此bool 判断,我们就可以新建开启显示方法,对符合距离的block激活显示:

public void SetVisible(bool visible)
{
 meshObject.SetActive(visible);
}

然后将此方法在UpdateTerrainBlock()中调用,加入:

SetVisible(visible);

同时在TerrainBlock构造函数里最后增添block初始状态为false,由每帧都执行的Update方法来判断是否激活显示:

SetVisible(false);

TerrainBlock初始构建基本完善,UpdateTerrainBlock检测判断和执行SetVisible方法都构建完毕,我们终于可以回头来继续填写UpdateVisibleBlocks方法了

//新增地形区块,我们传入区块坐标和对应尺寸,然后对已存在于字典里的区块,进行Update实时更新检测,用来时刻更新最小距离判断,决定是否显示该区块
    if (terrainBlockDictionary.ContainsKey(viewedBlockCoordinate)){
        terrainBlockDictionary[viewedBlockCoordinate].UpdateTerrainBlock();//监测最小距离
        }else {
            terrainBlockDictionary.Add(viewedBlockCoordinate,new TerrainBlock(viewedBlockCoordinate,blockSize));//完善传入参数

为了让每帧都实时更新,我们新增unity系统默认的Update方法里让每帧都能调用执行:

void Update(){
    viewerPosition=new Vector2(viewer.position.x,viewer.position.z);//每帧都要监测的信息有两个:我们viewer的位置以及执行刚才的更新可见block方法
    UpdateVisibleBlocks();
    }

有人会说,既然我们每帧都要执行 UpdateVisibleBlocks方法,那么刚才的if判断体里面需要多次判断执行 UpdateTerrainBlock方法,而这个方法里面每次要进行

float minDistanceToViewer=Mathf.Sqrt(bounds.SqrDistance (viewerPosition));
bool visible=minDistanceToViewer<=maxViewDistance;

平方根计算,效率会比较低,我们可以改成

float minDistanceToViewer=bounds.SqrDistance (viewerPosition));
bool visible=minDistanceToViewer<=maxViewDistance*maxViewDistance;

这样,直接比较最小距离平方和最大视距平方值,每次计算一次乘法,从运算量来说,会远小于计算 Mathf.Sqrt计算平方根,
保存,回到unity,将我们的这节的成果:EndlessTerrain脚本挂载到MapGenerator物体上
Hierarchy面板再新建一个方块cube,改名为viewer作为我们的观察点,reset坐标,把这个cube挂载到我们EndlessTerrain脚本上作为对应的viewer transform物体组件
点击hierarchy面板Mesh物体,如果mesh显示勾选了,则取消显示,以免遮挡我们生成的planes。
再运行,点击Viewer物体,选中它,开始在Scene界面移动它看看效果
发现跟随Viewer四周已经可以成功生成一大堆plane了,说明距离判断和实时生成TerrainBlock基本功能已经实现,但是有的plane超过viewer的最大距离后不会自动消失,我们需要继续完善
回到我们的EndlessTerrain脚本,我们可以发现 UpdateVisibleBlocks方法里,仅仅循环判断当前帧小于等于 BlocksVisibleInViewDistance范围内的区块,在新的一帧里,上一帧生成的一些区块,有的超出x,yOffset范围的blocks并没有纳入整个for循环判断,因此无法对这些blocks执行 SetVisible(false)操作,viewer移动的越快,上一帧生成的区块,超出当前帧视野范围的就越多,就会导致大量的blocks不会消失,
这就需要我们新建一个List清单,记录上一帧生成的所有blocks,在执行当前帧循环判断前,将上一帧所有的blocks 进行 setVisible(false)操作,然后清空List里面所有blocks。
当然,你也可以将当前帧生成的视距内的blocks和上一帧生成的blocks全统计起来,最后一并执行 UpdateTerrainBlock方法,统一筛选,也是可以的,只是后者会增大Dictionary空间,增加判断次数,这里不太推荐使用。
我们在EndlessTerrain脚本开头申明一个新的List数据集合,因为只需要存储blocks信息,List足够胜任

List<TerrainBlock>olderTerrainBlocks=new List<TerrainBlock>();

然后在 UpdateVisibleBlocks方法里,循环判断前,将所有上一帧生成的blocks进行隐藏操作:

for (int i=0;i<olderTerrainBlocks.Count;i++){
    olderTerrainBlocks[i].SetVisible(false);
}
olderTerrainBlocks.Clear();//,操作完毕,清空List;

然后,对当前帧所有 SetVisible(true)的blocks添加进入 olderTerrainBlocks List里面:那么我们在TerrainBlock类中新增一个判断方法,检测所有可视状态blocks

public bool IsVisible()
        {
            return meshObject.activeSelf;
        }

回到UpdateVisibleBlocks方法里的循环体,对所有ContainsKey符合要求的字典内Blocks,进行判断,将所有已存在的acitve的blocks加入olderTerrainBlocks列表,方便下一次执行的时候,再次对这些显示的blocks进行 SetVisible(false)操作

if (terrainBlockDictionary.ContainsKey(viewedBlockCoordinate)){
    terrainBlockDictionary[viewedBlockCoordinate].UpdateTerrainBlock();

里面新增判断:

if(terrainBlockDictionary[viewedBlockCoordinate].IsVisible()){
    olderTerrainBlocks.Add(terrainBlocksDictionary[viewedBlockCoordinate])
}

这样,每次执行前,上一帧的所有blocks就可以设置隐藏了,保存后测试,功能正常,
最后,我们注意到左侧Hirarchy面板生成太多plane到根目录,我们将他们生成位置移到我们的MapGenerator物体内,作为子物体,保持美观:
回到EndlessTerrain脚本
我们对TerrainBlock结构进行优化,传入一个新参数:

Transform parent;

然后内部构造新增:

meshObject.transform.parent=parent;//对所有新增meshObject plane物体,设置父物体为当前脚本挂载的MapGenerator物体

然后找到调用TerrainBlock的语句,在UpdateVisibleBlocks循环体里面,对于没有在字典里存在的符合显示的blocks,我们进行实例化一个新的TerrainBlock,这里也传入Transform parent即可:

    else{
        terrainBlockDictionary.Add (viewedBlockCoordinate,new TerrainBlock(viewedBlockCoordinate,blockSize,transform));
//在这里传入新参transform即可实现所有新实例化出来的plane都生成于当前物体下

再次保存,测试,发现所有plane都位于MapGenerator物体下了,我们选择Viewer物体,拖动测试,最后的结果比较ok了
虽然这节我们的收获成果只有这么个破方块,但是对于后面的无限地形系统来说,这节是非常重要的基础框架,无限地形动态视野控制区块功能,算是核心模块了。编程的核心内容经常是枯燥的,这也是成为高级程序员必经之路,那些堆砌美工和素材弄出来很精致的画面的demo,很多新手上手几个月堆砌各种素材就能做出来,然而对于核心系统的构建,功能基底的实现,程序员的思维,才是你与众不同的地方,也决定着你今后能做多深入的项目。
好了,区块视野控制完成,下一节我们可以开始多区块绘制渲染了,这会牵扯到多线程思想,如果是和我一样的新人,建议自己多找下相关资料学习下,这个非常重要的,有时间的话多了解一些没坏处。
下一篇:

Last modification:August 12, 2020
If you think my article is useful to you, please feel free to appreciate