查看原文
其他

用 Cocos Creator 搭建一个《我的世界》!

ccbbs COCOS 2023-03-16

本文为 Cocos 中文社区第5期征稿活动作品。开发者 ccbbs 使用 v3.6.2 实现了《我的世界》的基本功能与玩法,以下为研发历程分享。


近几年,以《我的世界》(Minecraft)为代表的沙盒建造游戏风靡全球。在这个自由度极高的沙盒世界中,玩家可以随心所欲地创造自己的天地,实现天马行空的创意和想法。


本次我在 Cocos Creator 3.6.2 中复刻了一版《我的世界》,目前主要实现的功能包括:生成地形;方块创建与销毁;角色移动、碰撞、重力和简单动画;地形延展;快捷物品栏等。


实现效果


生成地形


基础概念


  • 块:最小的地形单元,尺寸为 1X1X1。

  • 面:构成块的元素,1个块最多有6个面,最少可以一个不拥有。

  • 区块:尺寸为 16X16X100 。由块组成。在各象限依次排开。


地形随机


地形随机生成使用的是2维柏林噪声函数。输入 x 和 z 坐标,获得 y 坐标作为地表高度,然后从设定的最低高度到地表高度创建填充块。目前简单地设定小于0为水,等于0为沙,其余为泥土草。最底层铺上石头,中间填充泥土。



随机洞穴


洞穴随机使用的是3维柏林噪声。输入 x、y、z 坐标,如果超过设定阈值,并且在地表之下、底层之上,则判断为空气块。如果是空气块就不需要再填充泥土块了。这样一来我们就有了基础的洞穴(没有矿的洞穴…)。



随机杂草和树


随机杂草和树直接用的纯随机。当2维柏林噪音对每个坐标处理后,则进行杂草和树的随机判断。还是设定一个阈值,超过则判断为随机杂草或者树(树是由许多块组成的固定形状)。



地形网格生成


上面的内容都是生成的虚拟的对象。在场景中渲染的块,我前后尝试了两种方法去实现。


1、系统默认块+instancing。


使用 setInstancedAttribute 函数来区分不同的块。这个方法的实现效果其实还挺不错的,但缺点是实例和面很多。


2、动态网格,也是现在的做法。


这个应该是 v3.6 的新增功能,正好用上试试。


对每个区块创建一个动态网格。由于目前还没处理好更新,每次增删块都是整个区块重新构建网格数据,所以每次刷新附近的区块的时候,会有一点卡顿。


使用动态网格可以进行面优化,只创建没有相邻的面。但是进行面优化也要遍历所有块,目前还未达到性能最优,之后也将持续优化。


此外,这里还配置了每种块的每个面的 UV 值,用于显示不同的纹理。


*一个性能优化:方块很多,每个方块是一个类,在类里没有用 vec3 类、而是用了数字来表示坐标,这样占用的内存会少很多。


方块创建与销毁


如果是用的默认块,创建和销毁就很简单粗暴了。但是转到动态网格后,每次增删都需要进行数据重构,对整个动态网格的数据基于虚拟数据重新构造一遍。


方块的选中


这里简单使用了八叉树来管理附近的块,使用射线 AABB 相交检测判断鼠标聚焦的是哪个块。


接着还需要知道聚焦的是哪个面。一开始是用的6个 AABB 来判断,后来发现内置的检测函数里其实已经计算了到备选面的距离,复制过来,找到最小距离的就是当前面了。



选中框


选中框前后尝试了4种方法。


1、使用摄像机的几何渲染。



这个方法的缺点是无法完全遮挡,被遮挡的边会半透显示。


2、使用 shader。



判断 UV 是否在边缘范围,如果是则显示边框颜色。这个方法的缺点是边框近看比较粗、远看比较细,效果不是很美丽。


3、使用 cube 的 Primitive 线条描边。



因为默认的 cube 的顶点顺序是左下、右下、左上、右上,所以如果用 line_list 或者 loop 无法形成一个完整的框。


4、静态网格,也是目前的做法。


这个方案就可以按照自己的想法设置顶点顺序了,改成左下、右下、右上、左上。因为我觉得《我的世界》里每次都是框住一个块,没有只框住聚焦的面来得好,所以最后我选择只创建4个顶点的边框。使用 line 渲染模式,也能满足对边框粗细的显示需求。


方块的纹理



本来是想直接找《我的世界》的纹理贴图的,但始终没看到非常合适的。所以一狠心,直接 PS 开干。使用 PS 的像素化,可以很方便地生成类似的纹理。将所有纹理放在一张512的图里,可以一起用。


破坏粒子


当方块被破坏,在其位置播放一个粒子动效。


这里使用的是 mesh 粒子,引擎自带的 cube。设置为重力模式,使用曲线来控制重力先小后大,让生成的粒子多待一会儿再下落。


然后自定义粒子材质,基于当前销毁的方块属性来设置材质的 offset。贴图使用的是和其它方块同一个贴图。



角色移动、碰撞、

重力和简单动画


这部分花的时间最长,效果依然还有很多优化空间。


角色移动


方向键按下则记录移动方向,松开则重置方向。在 update 里基于移动方向进行移动。


node 的 translate 方法,第二个参数传0是本地坐标系,传1是世界坐标系。我用的是世界坐标系,需要对位移矢量按 y 轴进行旋转。


角色碰撞


利用八叉树检测附近块,然后用圆和多边形相交检测是否碰撞。如果碰撞则沿着移动矢量反方向移动,营造挡住的效果。


角色重力


利用物理公式 vt+0.5vtt 来进行角色垂直方向的位移。然后同样利用碰撞原理,检测是否有碰撞。如果碰撞则置重力加速度为0,速度为0,直到没有碰撞,恢复加速度。


因为是先水平碰撞矫正,所以执行重力位移后,如果有碰撞,肯定是嵌到土里了,可以进行矫正。


跳的时候,设置速度为一个正值,则可以实现跳跃。


现在的效果只是满足了基本的位移、碰撞和重力,但在连跳的感觉、碰撞的强度等方面,都还需要后续优化。


简单动画


角色在不做动作的时候,会有呼吸,手部会上下移动;在跑的时候,会有摄像机的晃动;在删除方块和添加方块的时候,会有手的动作。这些动画的实现都比较简单。


地形延展


因为使用的是柏林噪音,所以只要有了区块的坐标,就可以随机出自然的、连续的地形。


这部分是目前性能最吃紧的地方。只要判断到了别的区块,就会查找附近的25个区块,然后创建新的区块并替换旧区块到新区块的网格。


并且到了新区块,还要创建新的八叉树,非常卡顿。一开始的方案,是对基于中心区块的附近25个区块整体进行八叉树构建,所以每次到新的区块,都会有非常多的重复构建浪费。现在将八叉树构建限制到每个区块,在查询之前,先对区块进行一遍筛选,再对区块的八叉树进行查询,性能提升非常多。


快捷物品栏


这是属于 UI 的部分,难度不大。


监听键盘事件,然后切换单元格的状态。在需要的时候,调用推入物品和使用物品的接口来更新状态。此外还有和手的状态联动,在切换单元格的时候,如果单元格里有物品,则需要将手替换成物品模型。


难度在于绘制物品 ICON,目前暂时只有3个方块有 ICON。


结语


这个项目所用到的核心方案包括:八叉树查询用于场景管理、射线检测用于碰撞和交互、柏林函数用于地形随机、动态网格用于地形显示等。后续我将进行场景性能优化,增加资源、NPC、存档、红石等一系列内容。对于长期学习来说,还是非常不错的项目。


后续的项目进展我将更新在论坛,欢迎大家一起交流探讨:

https://forum.cocos.org/t/topic/143799


往期精彩

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存