在手游市场高度同质化的趋势下,随着各家手机厂商纷纷布局智慧大屏、平板、PC 等不同形态的设备,强调系统与生态侧的场景协同就成为了发展刚需,多终端协同游戏针对游戏体验本身,带来玩法上的更多可能性。
Cocos 官方 Demo Team 推出双人实时联机对战类游戏《别动我的金币》,项目完整开源,含工程源码、美术资源、策划文档(核心逻辑、技能设定、UI 说明),支持 Cocos Creator 3.3.2
在游戏中,玩家通过虚拟摇杆控制角色移动并吃掉金币,所获金币越多,角色体积越大,移动速度也就越缓慢。玩家还可以捡起随机掉落在场上的锤子,攻击对手让其失去一半金币。
官方出品,项目结构与代码质量自然不必多说,手握《幽灵射手》的小伙伴们应该有所了解。而本次《别动我的金币》的工程源码从结构上分为了逻辑层和表现层,相信能为大家在进行游戏项目结构管理时提供一些思路参考。
- 逻辑层:管理房间创建、接受房间信息、模型相交检测、道具生成删除、玩家移动、玩家使用锤子攻击、游戏开始结束等。
- 表现层:管理实际看到的玩家、道具、箱子、UI 界面等, 并根据接收到的最新的逻辑层数据进行刷新状态。
联机游戏一向是游戏市场的热门品类之一,去年老王也写过一个联机对战棋牌游戏《开心鼠吃象》,但由于项目初期没有考虑到玩家掉线以及断线重连、时间同步等需求,走了不少弯路……所以这一次,我们就从联机同步的实现说起,看看《别动我的金币》中都有哪些可以参考的技术方案。
联机同步
游戏的服务端选用了腾讯云游戏联机对战引擎(MGOBE),开发者无需关注底层网络架构、网络通信、服务器扩缩容、运维等,即可获得就近接入、低延迟、实时扩容的高性能联机对战服务。
游戏联机对战引擎-腾讯云:
https://cloud.tencent.com/document/product/1038
在工程中,我们也封装了一个名为 mgobeUtil.ts
的文件。调用相关接口,即可快速实现加入房间、房间管理和帧同步的相关功能。
接着谈谈游戏的同步逻辑。研究源码后我惊讶地发现,这个游戏只通过 MgobeUtil.instance.sendFrame
方法派发了三种事件:移动、停止、攻击,而移动时也只传了一个参数:前进方向。
这样真能保证多方同步么?
Demo Team 的设计思路是:
- 游戏开始时,地图上的障碍、角色和道具数量、所在位置是固定的,而在游戏过程中,道具的刷新时间和位置等计算,统一使用服务端提供的随机数种子,可以保证所有玩家设备上的道具刷新时间和位置一致。
- 游戏中角色的移动速度,通过与角色当前身上的金币数计算而成,所以在移动时仅需要传递角色的方向。而角色是否因碰撞影响移动,将由本地做碰撞检测等计算而成。而网络波动、断线重连等情况可以通过补帧处理。
于是,游戏就在仅派发极少量信息的情况下,做到了多设备的实时同步。
补帧问题
作为帧同步游戏,玩家网络波动和断线重连带来的补帧问题也需要重视。MGOBE 提供了自动补帧方法,但也存在失败的可能,此时 Room 对象在 onAutoRequestFrameError
接口中提供回调,那么我们就需要调用到补帧方法了。
在实际使用中,我们发现还是使用手动补帧更为安全。方法中我们需要传入两个参数:
beginFrameId
:本地已收到的最后一帧的 ID。endFrameId
:此时理论上的当前帧 ID,我们通过当前的设备时间与游戏开始时间之差除以帧率进行推算。
收到服务端提供的这段补帧信息后,游戏就可以继续进行了。
另外,断线重连后的同步也可以通过补帧完成。在调用手动补帧方法时,将 beginFrameId
设置为1,获取从第一帧到最近一帧的帧同步数据,就可以快速恢复游戏最新的状态了。而开发者也可以选取最后的的一些帧数据快进展示,让玩家重连后能快速回到游戏状态中。
生成道具
游戏共有2种道具:金币和锤子。游戏开始时,执行 initProps
方法,在固定位置生成初始道具。
游戏进行中还需要随机生成道具。以锤子为例,我们希望在玩家捡起锤子的3秒后,有一把新的锤子生成在地图上。这要如何实现呢?
首先,我们在 MGOBE 的 RoomInfo
房间属性中,提供了startGameTime
开始帧同步时的时间戳和 frameRate
帧率,我们可以通过它们推算出当前第 n 帧的帧时间 frameTime
,以避免网络延迟造成的时间不准确。
若帧时间 frameTime
大于需要创建锤子的时间 createHammerTime
,则调用 generateProp
函数生成锤子道具数据,保存到 currentGameState.props
道具信息数组中,同时生成用来检测碰撞的对应虚拟锤子节点。
当 currentGameState.props
数据有更新的时候,通过 updateState
更新场景状态方法,在地图上对应位置生成道具。
相交检测
再来看看游戏在不使用物理引擎的情况下,如何判断角色是否与场景中的障碍或道具发生碰撞。
首先是角色与障碍的碰撞判断。地图障碍由16个 Cube 组成,如图,勾选图中的 showMesh,可以看到障碍的 MeshComponent。
角色移动时会调用 _intersectWithObstacle
函数判断是否会碰到地图上的障碍,_intersectWithObstacle
的核心逻辑是检测两个节点的 modelComponent 组件是否相交。通过传入角色节点,获得 modelComponent,然后遍历场景里的障碍获取 modelComponent 并和玩家的 modelComponent 进行相交检测。若不相交,角色才能继续移动。
第二是角色与道具的碰撞判断。角色移动时将调用 handleProp
函数,遍历地图中的道具,检测与角色是否相交。若角色与金币相交则分数加1,若角色与锤子相交则获得锤子,地图上锤子道具消失并刷新锤子的创建时间。
角色拿起锤子后,就可以向其他玩家角色发起攻击了。玩家在设备上点击攻击按键时,会通过帧同步方法,派发角色攻击动作事件。而逻辑层收到攻击动作事件后,将执行 checkAttack
方法。当满足攻击条件,该角色播放攻击动画,角色拥有的锤子数量减1;而被击打的角色将播放受击打动画,扣去分数,且调用 dropCoins
方法在角色附近掉落金币。
除了上述提到的几点,游戏还实现了摇杆控制玩家移动、微信平台玩家通过分享链接接入游戏等等,有需求的小伙伴可以去查阅源码,应该能有不少收获。