游戏开发论坛

 找回密码
 立即注册
搜索
查看: 2947|回复: 0

用Unity盖房子(二)——《勇者斗恶龙:建造者2》地形生成和人物控制

[复制链接]

1万

主题

1万

帖子

3万

积分

论坛元老

Rank: 8Rank: 8

积分
36572
发表于 2019-6-28 14:31:57 | 显示全部楼层 |阅读模式
v2-c0677c077368a92768f2e5107f5d0710_1200x500.jpg

前言

上篇用一个相对好理解方式简单的把基本功能实现了,但想要达到最终目的,仅在此基础上扩展还不够,这篇会把地图的逻辑修改部分,用更为高效的方式来实现。那么之前说好的两篇完结这个系列估计篇幅太长,暂定扩展到三篇。

当然,这不是说第一篇的内容就是无用功,大家之前对于实现方式的思考也不是白费的,其实这表现了真实开发过程中的一个常态,完善的代码逻辑都是要通过反复思考和工程迭代得来,极少有人能一步到位。

另外,在上期结尾时我突然想起来咱们专栏之前就过此类功能实现方式的教程,传送门:

300行代码实现Minecraft(我的世界)大地图生成

Minecraft大地图生成续集——小锄头挖起来

为了不让这篇内容相对重复,我会在详细说明思路的基础上,对前两篇文章没有说到的位置做补充说明,大家可以把这篇文章当做此类游戏的思路总结来看。

地图逻辑的重构

不知道大家有没有想过,为什么说在上一篇文章的基础上难以为继?

之前使用的方式是在每一个方块的中心点生成一个空物体,根据它的坐标为基础生成mesh数据。这种方式的好处在于很直观又容易保存每个方块的相邻关系,且在处理不同朝向的方块时可以方便的把世界坐标系下的方块顶点转换为本地坐标系的向量。

虽然说了这么多好处,但还有一个致命问题让我们不得不放弃这种方法,那就是效率问题。

现在地图规模还太小,不太感觉的出来。一旦规模稍微大一点,比如生成一个长宽高皆为100个小方块长度的区域,那么总的小方块个数就是100X100X100=100万个!这么小一点区域在实际游戏中的大小微不足道,但生成这么一小块区域就已经是在场景中实例化100万个物体的巨大效率问题,就别说生成完整的地形了。

不用舍不得老代码,直接新起一个工程重新开始。虽说之前的代码全部废弃了,但仍有一些思路是可以复用的。

其实之前的思路方向大体没问题,现在只是要稍做一些修改。我们不再把单独的一个方块看做一个实例,而是把多个方块的集合体看做一个整体块(chunk),然后用更小的数据体去存每个方块的具体信息。简而言之就是用一个能装很多个小方块的“大方块”来替代原来小方块的位置。

为方便计算,大方块也应该是正方形的.所以只用存每个边长小方块的个数,同时小方块的自己的边长不再限制为1,改设为一个常量。之前的方块坐标计算也做相应的修改。这里设置的每个方块的边长为2,每个chunk的边长为16个方块的长度。

image001.jpg

现在重新来写mesh生成的逻辑。这次不同于之前的工程,我们把mesh生成单独拆开一个脚本.让其只用输入坐标和三角形就能生成任意形状,方便之后的扩展。大部分逻辑与之前相同,为缩减篇幅,这里就只贴出大致的结构,不贴出完整代码了。

image003.jpg

在chunk中也给它一个自定义的坐标结构体用来定位,并修改为属性,同时在传入时同时修改chunk的实际位置和名字。

image004.jpg

image006.jpg
chunk的预制体,用Gizmos显示它的范围


接下来就是根据信息来生成mesh,这里要修改的地方就比较多了。

从上面的代码可以看到,用的是一个byte类型的数组来存储cube的数据,也就是每一个cube的数据为一个字节。可能有同学会好奇,一个字节的容量似乎太少,存不了什么东西。但我们可以用位运算的方式可以把需要的基本信息进行压缩,达到节省存储空间的目的。

简单来说就是,把一个字节内的八位分别用一位存储是否在场景中存在的信息,一位存是否是边缘透明的信息(这个会在之后用到),两位用来存朝向信息,四为用来存种类信息。这样我们就能保存四种朝向和16种方块类型,这对于我们目前的小工程来说,也足够用了。

image008.jpg
其实这里没有方块也可以看做是方块的类型,可以合在一起存储,这样一来方块的种类就达到了31种

然后用一个结构体和几种枚举写出相互转换的方法,方便读取和存储数据。

image010.jpg

当然这里的方法不是唯一的,自己做简单的数据映射也是一种很有效的信息压缩方法,大家可以使用自己习惯的方式。

接着就是根据chunk内存储的数据生成mesh,这里的逻辑与第一篇里的大致相同,只有两个地方需要修改。

首先,我们在CubeMetrics里存储的方块的顶点是相对于方块自身坐标系的,而现在方块的朝向是有四种可能变化的。在上一篇文章中,我们在修改方块朝向的同时也修改了transfrom自身的朝向,然后使用transform.InverseTransformVector()方法把顶点转换为自身坐标系,达到修改生成的mesh朝向问题。

但现在不存在这个实体了,这一步我们没法偷懒使用现成组件的方法了,因此只能手动去写一个针对我们这四个朝向的矩阵变换方法,并在生成mesh时对每一个顶点进行转换。

image012.jpg

image014.jpg

cube数据化带来的另外一个问题是,没法直接保存每个cube的相邻引用了。所幸每个cube的相对位置可以通过其在数据数组中的下标算出来。

image016.png

然后提前把chunk的相邻引用保存下来,在越界的时候去检测相邻的chunk内是否存在方块。

image017.jpg

于是就可以通过方块的面对方向算出这个面是否需要隐藏。

image019.jpg
后面就不截出来了,逻辑是一样的

接着写一点测试代码,根据噪声图去生成地形数据。如果没有现成的噪声纹理,unity里的mathf库里是有封装好的API的,可以直接使用。

image021.jpg

image023.jpg

现在还没设置UV,但不妨碍通过mesh的形状验证逻辑是否正确。

image024.jpg

现在我们把UV贴上。这次我终于认识到了自己不是搞美术材料的事实,老老实实的去挑了一份还看得过去的纹理图。

image025.jpg

这里可以根据距离地形的深度,在每个地质层以随机概率生成一些矿石之类的东西,就好像真的地形一样。

image026.jpg

image028.jpg
看上去还像是那么回事

地形动态生成

现在根据指定的坐标生成地形是没问题了,但在真正的游戏中,人是可以移动的。我们不能在一开始就把地图生成很大的范围,那样的话加载时间会拉到很长,内存占用也是极大。所以只能使用折中的办法,让地形随着人物位置生成,大部分开放世界的逻辑都是如此。

首先思考一下实现方式。让人物每移动一点距离就把周围地图刷新一次显然不太合适,这样相当于只要人物在移动时一直都在不停刷新地图,性能肯定不允许。

理想中的方法是人物在一个区域内移动时地图不会刷新,但一旦超过这个区域的位置时刷新区域的坐标会根据人物当前位置更新,换句话说就是根据移动距离有一个阈值来判断刷新。我们可以直接实时获取人物坐标,以此为基准计算,但吃不准到底刷新区域的大小和刷新频率设置成多少比较好。因此最好能专门写一个方法,能方便的调整测试,我们新建一个类去试试。

用Unity内置的Bounds(包围盒)类来表示刷新的区域,让其中一个包围盒的中心点实时的跟随人物位置的中心点移动。设置一个比例参数去调整跟随人物移动的包围盒与刷新区域包围盒之间的大小比例。

逻辑其实很简单,当较小包围盒(人物身上的)的最大点和最小点不在较大包围盒(刷新区域)里时,调用刷新方法。

image030.jpg

1.gif

写这个刷新方法之前,先处理一下根据移动坐标刷新的大包围盒中心的方法。由于我们的chunk在世界中的坐标是根据边长的固定比例计算的,所以最好把处理后的坐标点与此同步。

image033.jpg

接下来就是根据包围盒的位置和大小去找到需要刷新哪些chunk。

image035.jpg

在chunk的根节点上新建一个Grid脚本,存储和管理所有的chunk和其数据。按我们的逻辑,在处于刷新区域内时,刷新并显示当前所有处于其内的chunk,而在离开时把可能改动的数据存储在以坐标为key的字典中。在刷新时优先读取以存在的数据,没有时才根据自身坐标生成数据。否则我们辛辛苦苦盖的房子出去走了一圈回来就不见了可就不太对劲了。顺便使用对象池来管理chunk,进一步减少GC,这是很常见的优化方法,就不把这部分贴出来了。

image037.jpg

image038.jpg

最后,把需要刷新的chunk放在一个容器中,在协程中以每帧一个的速度刷新到场景中。这里使用的容器是栈(stack),主要是考虑在移动速度可能过快的情况下,优先刷新后改变的chunk,也就是面朝移动方向的先刷新。

image039.jpg

最后验证一下效果:

2 (1).gif

可以看到速度过快时刷新速度会有些跟不上,但是加快刷新速度又会明显降低帧数。这里有很多办法去优化,比如在静止不动时刷新更大的区域给予更多缓冲区域之类的。但对我们的demo来说,只要限制速度,也足够用了,这部分的优化留给大家自己发挥。当务之急是检测一下对地图修改后的数据能不能正确保存下来。

在地上挖一个大坑,看看出去一圈有没有变化。

3.gif

可以看到这部分逻辑也没问题,那么现在对我们来说,只要内存允许,我们的地图就是“无限“的。可以针对这部分做的优化还有很多,比如增加进入游戏的加载时间让开始的地图变得大一些、优化刷新逻辑使其更流畅、地图数据转存到硬盘实现存档功能等等,不过这些都是后话了。

简单的人物控制

经过前面的逻辑实现,这部分已经没什么好写的了。功能与方法的接口在前面都已经搭好了,照着去调用就是了。

关于人物的移动和视角还有个很有意思的地方。大家都知道,大部分的类似方块建造的开放世界游戏大多使用的是第一人称视角,但DQ2却是第三人称的!虽然也能切回第一人称视角就是了。我能大概理解可能是基于RPG要素的考虑设计了这么一个操作模式,但是在游玩的时候觉得有些需要精确建造的位置确实有点不方便,很难把视角对准。但当我尝试去复现的时候,才发现DQ2的第三人称视角已经是做过许多设计和功能上优化了(以玩家角度很难感觉到开发者的苦啊),完全复现实在是工作量太大。所以抱歉的容我在这偷个懒,用第三人称移动和第一人称建造的混合模式去糊弄过去(这其实有点像DQ2后期的建造师手套功能,也算是某种程度上的还原吧)。

首先是人物,二头身的3D模型还加动画,这个要求分明就是在刁难胖虎,不过还好找了很久之后,终于找到一个水手服双马尾萝莉,还不要钱。(我没有在开车,你们也没证据)

image042.jpg

image044.jpg

现在人物有了,下一步就是人物动画,移动控制和相机控制,这部分任何游戏都可能会遇到,与今天的主题没多大直接关系,不在此赘述了,对这部分有兴趣的同学自己下载工程吧。

总之,在挂上控制脚本后,把之前的刷新地图脚本也挂载在人物身上,我们的小萝莉就可以在无限宽广的世界中任意遨游了。

4.gif

这里还需要说一下一个我遇到的小问题,mesh刷新时这里是直接把原来的缓存数据的mesh清空,读值之后再给meshCollider赋值的。这样会带来的问题是当赋了一个没有顶点的空mesh之后,这个chunk的meshCollider就会失效,之前测试时我的小萝莉经常跑着跑着就掉下去了。

后经查阅,推测是在给meshCollider赋值时,如果引用不变,无法触发set属性达到刷新碰撞的目的,因此这个地方稍作修改就行了。

image046.jpg

还没完,现在还需要让小萝莉能盖房子,不然都对不起这个标题。这个时候另外8个刚才没用到的方块纹理就派上用场了,先做个简单的UI,用toggle组件把图标与放置方块时的类型一一对应起来。

image047.jpg

image048.png

image050.jpg

根据鼠标位置发射一条射线,把打倒chunk的坐标点换算到chunk的自身坐标里,再去修改方块的数据。

image051.jpg

image053.png

image055.jpg

最后,在人物的update里实时去刷新这个坐标的值,仅在点击时调用setData方法。为了方便观察逻辑是否正确,为当前射线击中坐标添加一个预览方块,并添加一点粒子效果表示chunk上数据的修改。

image058.jpg

就别吐槽这个鸡啄米的动画了...免费素材能用的动画就那么几个。

另外提一句预览框的效果怎么做。png格式的图片是可以保存alpha通道信息的,但是直接把这种镂空纹理的图拖入材质球是没效果的,还要把标准着色器里的渲染模式改成Cutout。

5.gif

结束

现在基本逻辑都大概还原了,总算是站在了最先想要实现功能的起点上,老实说在一开始的时候真没想到要绕这么大一个圈。这期的内容可能会有些多,但是以文章为载体我没办法说的很详细,所以感兴趣的同学还是下载工程研究吧,有不明白的或者我写的有问题的地方,都可以在评论区留言,感谢观看至此。

本期工程地址:https://github.com/tank1018702/CubeBuilder

有意向参与线下游戏开发学习的童鞋,欢迎访问http://levelpp.com/

前文:用Unity盖房子(一):《勇者斗恶龙:建造者2》游戏功能的猜想用Unity盖房子(二)——《勇者斗恶龙:建造者2》地形生成和人物控制

作者:沈琰
专栏地址:https://zhuanlan.zhihu.com/p/65547739

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-5-17 13:16

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表