用 Cocos Creator 做一个物理画线游戏!支持 UGC 关卡创作和微信关卡分享
引言:在 3D 跑酷游戏和 3D 三消游戏之后,本次孙二喵带来了一个 2D 物理画线游戏源码,使用 Cocos Creator 3.6.2 开发。源码见文末。
演示效果
物理画线是一种比较经典的 2D 游戏玩法,这几年也出了一系列的爆款产品。本文将从立项、游戏逻辑和具体功能点的实现,来讲解如何基于 Cocos Creator 3.x 开发一款物理画线游戏,并实现 UGC 关卡创作和微信关卡分享。
立项与准备
游戏立项
游戏的核心玩法是玩家通过画线保护自己的小鸡,让其免受黑洞中掉下的障碍小鸡、或地图上的其他障碍伤害。
游戏关卡
考虑到制作成本,游戏美术使用了简单的手绘风格,用 paletton 选择类似纸张的颜色,拖动色盘,把比较顺眼的方案保存起来,确定游戏整体的风格。
色板中用到的颜色统一可以使用圆角矩形缩到最小(比如 15px,就输出成 15+15+2=32px,预留2个 px 给九宫格拉伸用),游戏所有的 UI 框架加起来就只有几 KB,减少显存的同时,还可以提高加载速度。
考虑到 spine 专业版数千元的价格,游戏内的动画效果统一使用了 Cocos 内置的动画编辑器+序列帧动画。序列帧动画使用 PS 制作,在画好我们的角色后,PS 内复制数个分组,调整角色的五官,输出序列帧,一些相似的做了剔除,减少动画的体积占用。
在 Cocos 内制作序列帧动画比较简单。帧动画使用30帧,新建好动画,并在精灵上面创建好动画组件,在对应的关键帧替换图片即可。
制作好的序列帧动画无需进行合并,拖入到一个文件夹内,使用 Cocos 自带的自动图集打包即可。
自定义网格背景
考虑到游戏的背景是一个纸上世界、同时需要支持 UGC 和简单的 AI,我们需要一个图片背景+网格。然而使用图片背景+网格图片不够灵活,同时做出来的效果会比较死板,缺少随机性,因此我从 shadertoy 上移植了一个纸张网格的效果。
原 shader 的 for 循环比较多,且风格比较写实,这里进行了一定的简化。
这里把 shader 中常用的属性都进行了暴露,整体风格调整到和色卡一致。
游戏内的3个场景(游戏界面、游戏内、游戏编辑)都用到了这个 shader 的图片作为背景,这里使用精灵图自带的颜色 a_color 来控制网格颜色。
并使用了简单的脚本接受全局事件,可以修改网格的颜色(这里只修改了透明度)。
效果如下:
考虑到游戏的 UGC 地图编辑需要使用到格子,Astar 网格导航也需要。以图片的视觉方向为例,下图左是在 shader 里 uv 的 xy 方向,下图右是 astar 算法的格子序号从小到大。
所以我们需要在 shader 里对格子进行转换。
同时考虑到算法简单,这里不做屏幕分辨率的适配了,默认的设计分辨率是 750x1334,支持的最大显示高度是 750x2.2倍=1650(绝大多数全面屏手机的最大高度)。
uv 统一后,我们需要设置一下网格显示,默认黄色是安全区域坐标,表示可以放置物体或者有物体,红色是危险区域,表示不可以放置物体。
定义好颜色和颜色的顺序,在 shader 中使用简单的 aabb 四个方向进行判断,在范围内的就会把颜色改成黄色安全色或者红色警告色。
同时 shader 把 worley 褶皱效果也加进材质的宏里,方便自定义开关。
如果需要使用自己的图片作为背景,可以打开 Use_Texture 的宏,使用 750x1650 的背景拖即可。
有了上述的准备工作,我们就可以开始地图编辑器的制作了。
地图编辑
在地图编辑之前,我们先确定整体的网格大小为75像素,设计的最大分辨率是 750x1650,也就是 10x22 个格子,这里定义一个全局的 class 去记录下这些配置。
每个物品的格子数量,可以用物体的图片长和宽分别除以75,然后四舍五入。
如尖刺,是 60x148 分辨率,正好是 1*2 个格子
这部分数据我们在初始化物体时候会提前处理,避免重复计算。
接下来根据格子的奇数或者偶数决定落点的位置,可以分为 x 和 y 两周轴向分开处理。
如果是偶数就不需要做偏移,在格子中间落点即可,如果是奇数,需要偏移1个格子单位,同时偶数的格子用 Math.floor 求最小的整数格子,奇数用 Math.round 四舍五入求最近的格子。
检测上使用了 map 做查询,当检测到数据时候我们要把有碰撞的格子范围乘0.1,这样格子的 x 范围从 0-10,变成了 0-1,y 范围从 0-22,变成了 0-2.2,正好和我们 shader 的 uv 对应(shader 内部对原始的 uv.y 也乘了2.2,并不是 0-1了)。
当我们检测到碰撞体时候,当前物体的周围就会变红,存在碰撞体的周围位置会表黄。
游戏内定义了几种物体的名字的枚举,当他们的名字一致时候,初始化时候就会标记为是画线工具,删除工具或者旋转工具,当使用这些工具检测到结果时候,就会对选中物体标亮并进行对应操作。
当保存关卡时候,我们只需要记录物件的格子开始和结束范围,也就是之前 map 保存的数据,同时记录物件的 scale.x 和 scale.y 记录左右和上下方向的旋转,保存为 json 数据,再把 json 数据 Stringify 即可。
在地图编辑器和游戏内读取关卡都采用相同的逻辑,如果是 runtime 时候,我们会顺便初始化一下 Astar Graph 里的网格权重。
这样即便是在复杂的关卡中,障碍小鸡也可以精准定位我们的游戏角色。
玩法核心逻辑
画线算法
下面介绍游戏的核心逻辑部分。
游戏的核心逻辑是使用 Graphics 画线,并把画线的路径点记录下来,同时我们的地图编辑器也会使用到这个功能来画辅助线。
这里使用了曼哈顿距离来记录移动的距离。当距离大于设定值,我们就会存储一次路径点,同时使用 graphics 绘制一次路径。
游戏环境和编辑器环境使用了不同的长度设置,编辑器会更短,来保证储存的关卡长度足够小。
画线的第一个点,我们使用了开销最低的 testPoint,记录是否存在碰撞体。第二个点开始,从上个点到当前点发射一条射线进行检测。
需要注意的是,这里的坐标点需要使用物体的世界坐标。
当画线结束,就可以根据路径点去生成碰撞体。这里直接使用了 polygon 多边形生成碰撞体,已经生成的碰撞体在关卡开始的时候,会回收使用到的 vec2 类,减少 GC。
我们首先把每2个点连成一条线,再对比每条线之间方向向量的斜率。考虑到性能,这里没有使用三角函数,斜率在一定范围内,就判断为是平行的,只会去推第二条线的最后一个点。
这样我们参与计算的路径点就可以减少 30%-40%,我们用优化过的路径点再来算需要生成的 polygon 多边形,这里把起始点定为1,结束点定为2,其他中间点是0。
先计算出2个点之前的方向向量。
再计算方向向量上,2个垂直方向的向量,分别乘以我们线段一半的宽度,最后起始点和结束点分辨加上这2个向量,就可以得到3个或者4个路径点,可以构建出一个三角形或者平行四边形(考虑到起始点和结束点可能贴边,这里使用了三角形,避免碰撞冲突的修正)。
生成了多边形的路径点后,我们需要额外 apply() 一下,我们所有的 vec2 都从 VecPool 单例内存储,减少 GC。
这里额外支持了 Graphic 的自定义材质,玩家可以在游戏内更换画笔的颜色。
我们把材质的颜色和材质名在入口脚本里进行配置,商店和 gameplay 根据配置进行加载即可。
感谢社区大佬的 Shader~
状态机
画好线后,就可以通知障碍出发去攻击我们的角色了。
这里使用了 Cocos 原本的 update,当切换状态时候会先执行 onEnter 方法,再执行 onUpdate 方法,这里可以把当前状态和之前状态传入,方便做逻辑切换,这里会记录下 dt 和 duration 时长,方便继承状态机的类使用。
小黑子的 AI 继承了 FSM 状态机,整个状态比较简单,只有寻路和攻击两个状态。
寻路阶段使用了 Astar 算法,每找到一个路径点,就会向下一个路径点前进,寻路时候会使用人物的方向向量乘以一定距离,来检测是否存在物理画线。
当检测到物理画线的时候就会对物理画线的 rigidbody2d 进行攻击,对 rigidbody2d 施加速度向量,就可以出现线条被抬起来的行为了。
我们会在 x 方向上做左右的随机,y 方向以向上围住,也会随机一定的数值。
小鸡的状态机就相对比较简单,只有基本的碰撞检测,当检测到危险的碰撞体就会触发受伤然后游戏失败。
寻路导航
这里使用到了和之前 EasyNavmesh 同款的 A* 导航算法,不同的是我们使用了一个单例进行管理。
这里把 Astar 翻译成了 TS 版本,方法都加了强类型判断,同时把距离算法改成了曼哈顿距离。
游戏内 FSM 同样使用了曼哈顿距离,当我们的路径点走完,且离目标超过2个格子距离时候就会再次寻路一次。
到这里整个游戏大的逻辑就分解完了,下面介绍一下游戏关卡分享的逻辑。
关卡分享逻辑
游戏内拍照
游戏内把物体和 UI 分辨分成了 Default 和 UI_2D 分组,并创建了2个相机,Screencam 平时的可见分组为空,只有当拍照时候才能看到 Default 分组。
截图后一瞬间,我们会读取相机 RenderTexture 的像素点,并把角色的偏移量传进来,保证截图范围不会太大,同时保证相机对着角色不会出框。
关卡分享逻辑
关卡分享并没有使用云服务器,这里使用了微信分享里的 query 参数,query 最大可以存储2048长度的 string(测试中4096也可以跑,担心部分手机可能存在兼容问题,这里最大设置成了2046)。
我们在分享时候可以把当前关卡都转换成 string,放到 query 里。
当我们分享给其他玩家,其他玩家打开后,在游戏初始化和微信 onshow 时候就会检测到是否有对应的 query。
微信的 getLaunchOption 是第一次进入游戏时候会有的,wx.onShow 是后台切换回来触发的时间,当检测到 query 且 query 内的关卡数据有效,就可以初始化这个关卡了。
游戏内显示好友 UGC 关卡效果:
资源链接
源码下载:
https://store.cocos.com/app/detail/4240
在线体验:
http://learncocos.com/chick
论坛专贴:
https://forum.cocos.org/t/topic/142673
demo 源码现已发布到 Cocos Store,希望可以对大家有所帮助!如有疑问或者其他想交流的,欢迎移步论坛专贴。