[Unity]拼接地块的随机地图生成

[Unity]拼接地块的随机地图生成,第1张

一个拼接地块生成随机地图的尝试过程,个人感觉效果不是非常理想,还有许多优化空间,总之先记录一下。

虽说是3D场景,但生成的地图块都在同一个平面上,严格来说依然是2D的思路。对于类似3D地牢的生成,官方已经有一篇文章介绍: 在Unity中程序化生成的地牢环境 。

整体思路:

缺点:

在一个场景中搭建好各类地块:

由于计划要烘焙光照贴图,地块的开口朝向就必须都是固定的,旋转地块、移除墙壁都会导致穿帮。当然也可以设置一些动态的墙壁,需要时通过移除它们来形成开口。

上图搭建好的地块中,右侧两列为主要地块,拥有2~4个方向的开口;中间四个小地块分为横向与竖向,用来连接主要地块;左上角四个为单开口,在地块生成完毕后,用它们来封闭上空余的开口。

地块大致搭完后将它们都做成预制体,进行烘焙。这里使用插件 Unity Lightmap Prefab Baker ,它将整个场景按当前的光照设置进行烘焙,烘焙出的光照贴图移动到指定文件夹,然后通过挂在预制体下的脚本记录关联的光照贴图,当预制体加载到新场景时将它们关联起来。

插件安装好后,每个地块预制体挂上PrefabBaker脚本:

Window -> Prefab Baker 打开面板,调整相应设置后点击烘焙:

默认的光照贴图放在Assets/Resources/Lightmaps目录下,开发阶段可以暂时放在这里。如果2019版本报"Failed to created asset"错误,可尝试修改Plugins/PrefabBaker/Scripts/EditorUtilscs,在200行:

烘焙完成后:

为了让地块之间的开口能顺利对接上,地块的每个开口处需要放置一个空对象作为连接点,令两个地块的连接点重合即完成拼接。地块还需要有类型,这里分为了Room(房间,单开口), Corridor(走廊,东西或南北开口), Corner(拐角), TShaped(丁字), Hall(大厅,四面开口)。

连接点,Z轴朝向开口方向:

地块预制体:

拼接时需要能获取到地块的连接点情况,加入相关方法:

在一个新场景中生成地图,编写 LevelGeneratorcs 脚本:

先对地块预制体进行分类:

分好类之后开始生成,编写生成地图的方法GenerateLevel,由于需要多次将满足条件的对象放入列表来随机抽取,事先准备好一些列表避免反复创建:

先生成一个初始地块,这里选择走廊作为初始的地块,也可用其他的地块类型:

GetRandom方法:

需要注意新场景中的光照(主要是平行光)需要和烘焙场景保持一致。

当前的算法是逐个拼接生成地块,最后用单开口地块对所有空余的开口进行封闭。为了控制地块数量,当前的地块数量为已生成个数加上即将生成个数。

编写循环生成:

GenerateMatchingCell方法用于寻找与当前地块连接口匹配的地块预制体,找到之后将它们拼接:

要寻找匹配的地块,首先要找到跟当前连接点匹配的连接点类型:

然后在地块预制体中寻找包含这种连接点类型的地块:

效果:

最后GenerateLevel方法中将有空余连接点的地块封口:

效果:

生成较多地块时,会发生冲突:

由于是逐个地块生成,冲突检测只能在要生成下一个地块时进行判断,如果当前连接点前方一段距离内已经存在地块,则该连接点不能用于继续生成,需要封闭;另外,如果连接点前方不存在地块,还需要判断前方的前方、左方、右方是否存在地块,若存在则只能生成开口朝向别处的地块。

每个地块添加碰撞体:

LevelGeneratorcs 加入冲突检测相关配置:

GenerateLevel方法中:

CheckConflict方法:

如果没有直接冲突,按照上面的逻辑判断前方的前方、左方、右方是否存在地块,若存在则记录到不期望的连接类型列表,随后由GenerateMatchingCell传入到GetMatchingCell中:

效果:

Demo项目地址:

Procedural-Map-Demo

感觉你的问题应该有更直接的办法,一是不太懂你的意思,比如你具体是要实现怎样的功能?为什么要根据XY计算,难道同样的装备随机次数不同,power计算也不同? 第一、第二个装备又是什么意思?理论上应该限定总的可装备上限,比如一共就可以装备三个东西,那么你自然需要定义一个三个元素的数组了。如果每个装备名称对应的xy是固定的话,计算power似乎没意义了,还不如把power参数和装备写在一起,而你只要随机xy来取不同装备就行了吧。

以下顺带讲一下写入数据的办法。

(不好意思我用的是JS,语法上转换一下就行了。)

不用playerprefs的方法是:

新建一个类,这个类里面定义你要储存在磁盘上的变量,最后把这个类写成dat文件(binary文件),这样这个类里的数据就写在磁盘上了,以后你就可以随时调取、更新所存储的数据了。

(1)编程时你要用到几个基本的包:

import System;

import SystemRuntimeSerializationFormattersBinary; //用来写binary文件

import SystemIO; //基本的输入输出

详细的你还可以去查net 的MSDN 参考。

(2)你要自定义一个类用来规定数据,比如:

Class GameData {

var itemID:int;

var power:float;

}

(3)你还需要一个实例化的脚本(比如命名成,GameDataManager ),把这个脚本放在一个场景中GameObject上就可以了,这个脚本用来实际 *** 作读取和写入。把这个类做成一个Singleton,就是说仅在整个游戏刚启动时初始化一个静态的实例,而且在此后的场景退出时都不要清除,这样可以避免反复覆盖读取和存储数据的风险。比如:

static var instance:GameDataManager;

Awake() {

if(instance == null){ //当前场景中没有其他实例化的脚本,

DontDestroyOnLoad(gameObject); //那么说现在本脚本是唯一的实例,所以不要销毁

instance = this; //把唯一的静态指针指向自己。

}else if(instance != this){

Destroy(gameObject); //当前场景中已经有了其它实例!说本脚本是重复的实例,销毁!

}

}

(4)接下来要判断是否已经存在先前的存档binary文件,如果没有,就需要初始化一个GameData类。

var myGameData:GameData;

function Start () {

myGameData= Load(); //此处Load()是脚本后面定义的一个读取binary文件Dat的方法

if(myGameData== null){ // 如果没有读取到文件,就初始化一个新的数据类

myGameData= new GameData();

myGameDatapower= 999; // 数据初始化,这里你可以自定义更复杂的方法或算法

Save(); //写入数据,此处Save()也是后面定义的一个存储binary文件Dat的方法

}

}

(5)具体完成Load() 和 Save()方法:

function Save (){

var bFile:BinaryFormatter;

var file:FileStream;

bFile = new BinaryFormatter();

file = FileCreate(ApplicationpersistentDataPath + "/GameDatadat"); //在系统默认应用程序路径创建Dat文件

bFileSerialize(file, currentGameData); // 写入数据

fileClose(); //完成文件

}

function Load ():GameData{

var bFile:BinaryFormatter;

var file:FileStream;

var loadData:GameData;

if(FileExists(ApplicationpersistentDataPath + "/GameDatadat")){//判断dat文件是否存在

bFile = new BinaryFormatter();

file = FileOpen(ApplicationpersistentDataPath +"/GameDatadat", FileModeOpen);//打开系统默认路径中的Dat文件

loadData = bFileDeserialize(file) as GameData; //获取读取到的数据

fileClose();//关闭文件

}

return loadData; //返回获取到的数据类

}

最后,如果你英文过的去,unity的官方网站上有全套视频,其中一个章节就是讲解如何存储数据的!不过前提是你得会 夫安 七一昂,否则视频可能看不了。今年封的更严了,国情你懂的,

1、单击hierarchy视图下的create,在子菜单的选项中创建一个地形Terrain

自制游戏 unity 如何创建地图

2、然后单击创建的地形,窗口右侧会出现地形的属性面板。我们要用到的是地形组件。

自制游戏 unity 如何创建地图

3、选中属性面板中的你想要的类型,及笔刷大小。

自制游戏 unity 如何创建地图

4、在视图中按鼠标左键抬高地形。

自制游戏 unity 如何创建地图

5、如果光线太暗。单击hierarchy视图下的create,在子菜单的选项中创建一个灯光。

自制游戏 unity 如何创建地图

6

6、小提示:按左shift和鼠标左键可以降低地形。

自制游戏 unity 如何创建地图

我是看懂了,不过怎么说呢。

mapwidth/mapWidth

x

得到是小地图中x

的位置

,mapwidth/2

为什么要加这个呢!因为在世界地图中原点是在大地图的正中间(估计你就是这没看懂吧),map_cubewidth/2

是主角贴图的一半,

Screenwidth

-

mapwidth

是屏幕左侧到小地图左侧的距离。

至于在y轴上有点意思,GUI

坐标是左上角为起点和屏幕坐标以左下角为起点,所以mapheight

-

((mapheight/mapHeight

z)

就正好是主角在小地图上的位置,mapheight

/

2

和上面一样是将原点放到中间。

实际执行的过程中发现,其实没有现成的方案,参考国内地图的SDK,发现没有专门给Unity做的SDK,大多数SDK的主要效果就是Unity的渲染跟SDK的渲染,它们是相互独立的。你想看地图得切到SDK那个上面去,这样的话Unity的一些UI就看不见了。如果你要做一些UI *** 作的话,就必须在底层,SDK那块再写一套,再写一套是可以,因为市面上有一些游戏是这样做过,但是有一个问题,你这样做出来的UI,只能做的非常简单,很难做一些复杂 *** 作。

大地形数据加载方案

大地形加载考虑到现有的内存机制, 不可能一次性将其加入到内存中,这个问题是显而易见的,其实在游戏开发中经常遇到,比如我们常见的进度条,加载进度条的目的就是等待程序加载场景,进度条只是一个蒙板遮罩而已。大地形的加载,别无他法,只能用分块,这个是大方向,因此作为程序来说,要做的事情是如何分块?块的大小是多少?这些具体的问题我们要根据需求划分,比如飞行模拟器块大小可能就要大一些,因为俯瞰的视角比较大,场景漫游块可以小一些等等,下面我们就以游戏的经典之作——魔兽世界地形加载方案为例给读者先介绍一下它的实现原理,魔兽世界这款游戏实现的就是无缝地图的拼接,所以非常具有参考价值,先看下图所示:

魔兽世界是如何实现无限地图的?其实它也是很多的场景块拼接而成的,我们通过编辑器分析魔兽世界的地形块的大小划分,魔兽世界场景我们称为MapWorld是由一系列MapTile组成,这些MapTile的大小是1600/3 ≈ 53333m,而每个MapTile又是由 16x16 个MapChunk组成,由此可以计算出每个MapChunk≈3333m。再就是每个MapChunk又由9x9+8x8个地形顶点高度,法线,若干贴图层(一般为4层)组成的地表纹理。魔兽世界地形的大小,在这里我们就不讨论了,但它划分块的思想我们是可以借鉴的。

继续分析魔兽世界的分块方法:它们是根据矩阵的方式进行划分的,在XZ平面上进行的,每个块都会包含一定的信息数据的,比如:在XZ(3,3)位置的MapTile,每个MapTile都包含了该tile内使用的贴图、模型实例等等。所谓模型实例也就是我们的道具,可以理解成相同模型在tile内不同摆放位置、大小、角度的信息,它们都是被保存在二进制文件中的,为了节省文件尺寸,模式实例是通过index模型方式保存的,同顶点索引类似,在每个MapTile里面还有贴图信息比如贴图的名字和UV信息等等。本篇课程的分块思维方式跟魔兽世界的类似,会在后面的章节中详细介绍,块分好了以后,下面就是实现原理了。

实现原理:在任何时刻,程序总是保存着玩家所在的及其周围的3x3个MapTile,随着玩家的移动,这些MapTile会被动态更新,新的MapTile被加载以替换被卸载的旧MapTile。为了提高调度效率,魔兽引入了Cache机制,Cache中保存着最多16个MapTile数据。需要加载新的MapTile时,首先会在Cache中查找;卸载的旧MapTile也不会被立刻删除,而是保存在Cache中以备再次调用。由于一段时间内玩家的活动范围通常不会有太大变化,这一Cache策略在应用中表现的非常出色,这是无缝地图的基本原理。地形的动态加载卸载我们会使用多线程去实现,我们会整两个线程:一个线程专门用于加载地形,另一个线程专门用于卸载或隐藏地形MapTile。让我们再来回忆一下游戏的经典之作,游戏场景效果如下所示:

本篇课程实现的方法可以使用两种方式处理块的加载显示问题,一种是利用对象池的方式,预先加载分块地形,根据视距进行检测判断显示那些地块以及隐藏那些地块,在这里并不删掉它们。这样只需要一个线程就可以。另一种方式是利用多线程,起一个线程专门用于移除卸载不在视线范围内的地块,这样可以提升效率,下面介绍使用多线程的加载方案。

多线程实现大地形加载方案

多线程在PC端游戏中使用的比较多,比如可以起一个线程专门进行资源的加载,游戏服务器中同样也会有多线程的使用,下面给读者介绍多线程实现方案,多线程处理问题就是把所有的加载逻辑放到了新的进程中,和主线程做一些进程间的通信,接受主线程的加载建议,做按需加载,也会自主做一些提前预加载,放进分配的内存,就跟魔兽世界的处理方式一样,通过进程间的内存共享机制,把加载的地形数据,共享给主进程使用。主游戏进程,永远只要维护一个很小的内存即可,大量的内存数据,都在另一个进程中处理。这样就可以优化大地形块的加载,实现方式如下所示:

首先主线程会先加载九块地形,主线程只负责维护这九块地形,无论角色怎么移动,角色所在的整个区域永远是九块地形,如上图所示的,这九块可以直接使用主线程加载到内存中,剩下的16块我们通过另一个线程将其放到缓存中,角色的位置是在已经加载好的九块地形中间,也就是在A所在的位置。随着角色的移动,会有新的地形块加入进来,同时现有的地形块会被置换出去,这样一直显示九块地形,被置换的地形并不会马上卸载掉,会根据角色移动情况做预判,它会等主线程通知,按照一定的规则进行卸载地块和加载地块。其实这种实现方式就是我们通常所说的双缓存-多线程技术。实现的效果如下所示:

地形分块加载完事了后,下面就要考虑地形上面的纹理贴图问题了,地形的贴图资源也会占用大的内存,下面介绍如何加载海量贴图数据。

大地形海量的加载方案

大地形中的场景非常多,地形中的贴图至少会有四层,这么多贴图我们在加载时需要考虑的,我们分块时也需要考虑这些因素,另外场景中使用的LightMap烘培也是要考虑的问题,为了缓解内存压力,我们事先会将不同块中的地形材质以及建筑物材质进行打包,先介绍如何分块加载场景贴图?它实现方式如下所示:

该思路就是将场景中的贴图根据我们划分的块打成不同的图集,当然也可以将两个块中的贴图打成一个图集,图集大小对于PC端来说,最大是4096,在移动端最大是2048。这个也是为了避免内存频繁的加载卸载会导致很多内存碎片,不利于后面大内存的分配。在打图集之前我们需要做点事情就是需要将地形块中的纹理贴图与我们的打包图集之间建立一一对应关系,方便对号入座。因为我们打包的图集跟实际地形之间不会有任何关系,要确立二者之间的对应关系我们需要在它们中间再整一张索引文件表格,它是连接图集与实际地形纹理的桥梁,通过我们建的索引文件,我们可以找到实际地形中纹理与图集纹理之间的对应关系,我们建的索引表格是要加载到内存中的,而我们的图集是根据加载任务后期才加到内存中的,这就要求我们的索引文件尽可能的少,因为它们是常驻内存的,除了海量的加载,我们还需要处理密集建筑的加载。

- 密集建筑的加载方案

密集的建筑加载,大家试想一下,如果把场景中所有的建筑一次性加载到内存中,内存瞬间就会占满,帧数瞬间下降,这也是为什么大家在游戏场景中移动时,遇到密集的建筑就会卡顿一下的原因。以前处理方式是使用LOD处理,被遮挡的物体使用简模,这样也会加大内存的负载效果,如果角色一直在建筑物之间来回穿梭,这样不同LOD模型就需要来回切换,对内存也是一个负担,效果不理想。这些问题对于程序员面来说必须解决的问题,如何解决呢?很多人想到了合并大Mesh,这种方法行不通的,大网格并不适合做裁剪 *** 作,试想一下,我们合并的网格,如果摄像机只看其一小部分,因为它们是一个整体这样就需要把他们一起加载到内存中,而实际上我们并不需要这么多模型数据,在合并网格时,在这里也给读者一个建议,尽量把靠的很近的模型进行合并,避免上述问题发生。其实最有效解决方案还是划分块,这个划分块可以利用地形划分的思想进行,它是与地形块紧密相关的,每个地形块中的建筑物跟随地形块一起加载。如果块中的建筑非常密集,这种方法还不能够完全解决,还需要进一步的处理,就是要加入OC遮挡算法结合LOD算法,这样就可以完全解决我们当前的问题了,这也是本篇课程

要讲解的方法,再进一步的优化方法是可以将OC遮挡算法和LOD算法放到GPU中计算,这样效率还会提升,在Siggraph2015发表了一篇文章GPU-Driven Rendering Pipelines,它的思想就是使用GPU进行遮挡裁剪处理,主要分两个阶段,使用的是DX12图形API,如下图所示:

它的思想就是第一步先做一个初略的遮挡裁剪列表,而后在此基础上再根据视线距离或者射线检测做进一步的细化裁剪 *** 作,这个思想跟我们的碰撞检测算法类似,引擎中碰撞检测算法也是基于这个原理实现的,给读者介绍一下:实际可用的碰撞检测算法,一般要分2个阶段:

第一阶段,broad phase 快速找出潜在的碰撞物体对列表,不在这个列表里的是绝对没可能碰撞的。broad phase确定了一批需要进一步检查的物体对。

第二阶段,narrow phase 准确找出发生碰撞的物体对列表。因为上一个阶段的部分物体对实际上是没有碰撞的,需要在这个阶段剔除。

broad phase其中有一个简单算法叫sweep and prune(SAP),本质上是利用了排序算法。第一步是初始化排序列表,列表中的元素是包围盒,可以用任意排序算法完成,例如快排;之后的排序就不是用快排了,而是用冒泡排序,为什么用冒泡排序更好呢?是因为一个默认的前提:物体的运动有时间相关性(temporal coherence),即当前帧和下一帧的位置是相近的,所以在冒泡排序过程中,发生的位置交换预期都很靠近。

其实算法中有很多类似的地方,这里我们也要互相借鉴它们解决问题的思想用于解决我们的问题。笔者以前做的是端游,端游中很多优化思想同样适用于移动端,移动端跟PC端比,就是一台配置比较低的电脑而已。接着我们的遮挡裁剪继续给读者介绍,论文作者也做了一个效率测试,以250’000物体,1G的网格为例,测试效果如下所示:

是不是很酷啊!在项目开发中完全可以用它解决问题,下面我们再谈谈使用GPU去优化我们的大地形场景。

GPU大地形渲染优化解决方案

我们的大地形首先会有自己的地表贴图,常用的地表贴图是四张纹理融合,最多可以有八张贴图融合,地形纹理渲染会涉及到LOD算法,远处的地形网格可以简化一些,对应的贴图也是最低的,这就是MipMap的使用。另外肯定有草有花以及其他大量相同的物件渲染,先说说草和花的绘制,他们在游戏中会非常的多,常用的做法是引擎提供的面片或者是十字交叉,或者三张交叉,然后将带有Alpha通道的贴图映射在上面,如下图所示效果:

CPU绘制这些草或者花在PC端是可以的,因为现在的电脑都是多核的,在手机端就会影响到效率问题了。使用CPU绘制,DrawCall会非常的多,而且草或者花还需要摆动,计算量很大的,这严重影响了运行效率,CPU有难,GPU可以帮忙,我们可以将草或者花的绘制放到GPU中执行,效果如下所示:

以上就是关于[Unity]拼接地块的随机地图生成全部的内容,包括:[Unity]拼接地块的随机地图生成、unity中游戏装备数据怎么 建立,存储和读取、unity怎么在game窗口生成地图等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/web/9815114.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-05-02
下一篇2023-05-02

发表评论

登录后才能评论

评论列表(0条)

    保存