全套源码丨超实用的双人联机对战游戏开发分享,拒绝踩坑!

在手游市场高度同质化的趋势下,随着各家手机厂商纷纷布局智慧大屏、平板、PC 等不同形态的设备,强调系统与生态侧的场景协同就成为了发展刚需,多终端协同游戏针对游戏体验本身,带来玩法上的更多可能性。

Cocos 官方 Demo Team 推出双人实时联机对战类游戏《别动我的金币》,项目完整开源,含工程源码、美术资源、策划文档(核心逻辑、技能设定、UI 说明),支持 Cocos Creator 3.3.2点击文末【阅读原文】即可跳转下载地址。

在游戏中,玩家通过虚拟摇杆控制角色移动并吃掉金币,所获金币越多,角色体积越大,移动速度也就越缓慢。玩家还可以捡起随机掉落在场上的锤子,攻击对手让其失去一半金币。

官方出品,项目结构与代码质量自然不必多说,手握《幽灵射手》的小伙伴们应该有所了解。而本次《别动我的金币》的工程源码从结构上分为了逻辑层和表现层,相信能为大家在进行游戏项目结构管理时提供一些思路参考。

  • 逻辑层:管理房间创建、接受房间信息、模型相交检测、道具生成删除、玩家移动、玩家使用锤子攻击、游戏开始结束等。

  • 表现层:管理实际看到的玩家、道具、箱子、UI 界面等, 并根据接收到的最新的逻辑层数据进行刷新状态。

联机游戏一向是游戏市场的热门品类之一,去年老王也写过一个联机对战棋牌游戏《开心鼠吃象》,但由于项目初期没有考虑到玩家掉线以及断线重连、时间同步等需求,走了不少弯路……所以这一次,我们就从联机同步的实现说起,看看《别动我的金币》中都有哪些可以参考的技术方案。

联机同步

游戏的服务端选用了腾讯云游戏联机对战引擎(MGOBE),开发者无需关注底层网络架构、网络通信、服务器扩缩容、运维等,即可获得就近接入、低延迟、实时扩容的高性能联机对战服务。

游戏联机对战引擎-腾讯云:

https://cloud.tencent.com/document/product/1038

在工程中,我们也封装了一个名为 mgobeUtil.ts 的文件。调用相关接口,即可快速实现加入房间、房间管理和帧同步的相关功能。

93cf386cbe8659a12a3b4f95680b37ce.png

接着谈谈游戏的同步逻辑。研究源码后我惊讶地发现,这个游戏只通过 MgobeUtil.instance.sendFrame 方法派发了三种事件:移动、停止、攻击,而移动时也只传了一个参数:前进方向。

ff967bf8bbb3942d98f2f95d689bc89a.png

这样真能保证多方同步么?

Demo Team 的设计思路是:

  1. 游戏开始时,地图上的障碍、角色和道具数量、所在位置是固定的,而在游戏过程中,道具的刷新时间和位置等计算,统一使用服务端提供的随机数种子,可以保证所有玩家设备上的道具刷新时间和位置一致。

  2. 游戏中角色的移动速度,通过与角色当前身上的金币数计算而成,所以在移动时仅需要传递角色的方向。而角色是否因碰撞影响移动,将由本地做碰撞检测等计算而成。而网络波动、断线重连等情况可以通过补帧处理

于是,游戏就在仅派发极少量信息的情况下,做到了多设备的实时同步。

补帧问题

作为帧同步游戏,玩家网络波动和断线重连带来的补帧问题也需要重视。MGOBE 提供了自动补帧方法,但也存在失败的可能,此时 Room 对象在 onAutoRequestFrameError 接口中提供回调,那么我们就需要调用到补帧方法了。

cc91350a162055bd70cf9804a7921429.png

在实际使用中,我们发现还是使用手动补帧更为安全。方法中我们需要传入两个参数:

  • beginFrameId:本地已收到的最后一帧的 ID。

  • endFrameId:此时理论上的当前帧 ID,我们通过当前的设备时间与游戏开始时间之差除以帧率进行推算。

收到服务端提供的这段补帧信息后,游戏就可以继续进行了。

另外,断线重连后的同步也可以通过补帧完成。在调用手动补帧方法时,将 beginFrameId 设置为1,获取从第一帧到最近一帧的帧同步数据,就可以快速恢复游戏最新的状态了。而开发者也可以选取最后的的一些帧数据快进展示,让玩家重连后能快速回到游戏状态中。

生成道具

游戏共有2种道具:金币和锤子。游戏开始时,执行 initProps 方法,在固定位置生成初始道具。

c5cca8af99e6d984b26d97bdd794e8bc.png

游戏进行中还需要随机生成道具。以锤子为例,我们希望在玩家捡起锤子的3秒后,有一把新的锤子生成在地图上。这要如何实现呢?

首先,我们在 MGOBE 的 RoomInfo 房间属性中,提供了startGameTime 开始帧同步时的时间戳和 frameRate 帧率,我们可以通过它们推算出当前第 n 帧的帧时间 frameTime,以避免网络延迟造成的时间不准确。

28cb7cfe3929e4b1397a6284d3b6f41b.png

若帧时间 frameTime 大于需要创建锤子的时间 createHammerTime,则调用 generateProp 函数生成锤子道具数据,保存到 currentGameState.props 道具信息数组中,同时生成用来检测碰撞的对应虚拟锤子节点。

fcf35eadcd115e6a50a11c64cf96c991.png

currentGameState.props 数据有更新的时候,通过 updateState 更新场景状态方法,在地图上对应位置生成道具。

abfbae67464f43454e82be9be9fdb08f.png

相交检测

再来看看游戏在不使用物理引擎的情况下,如何判断角色是否与场景中的障碍或道具发生碰撞。

首先是角色与障碍的碰撞判断。地图障碍由16个 Cube 组成,如图,勾选图中的 showMesh,可以看到障碍的 MeshComponent。

c94d8b48e63a902fbc603442d6100d9d.png

角色移动时会调用 _intersectWithObstacle 函数判断是否会碰到地图上的障碍,_intersectWithObstacle 的核心逻辑是检测两个节点的 modelComponent 组件是否相交。通过传入角色节点,获得 modelComponent,然后遍历场景里的障碍获取 modelComponent 并和玩家的 modelComponent 进行相交检测。若不相交,角色才能继续移动。

95b5dbc31d99693fd54ff39caf164d62.png

第二是角色与道具的碰撞判断。角色移动时将调用 handleProp 函数,遍历地图中的道具,检测与角色是否相交。若角色与金币相交则分数加1,若角色与锤子相交则获得锤子,地图上锤子道具消失并刷新锤子的创建时间。

c18b5e3489f122314905c6e2e6311e55.png

角色拿起锤子后,就可以向其他玩家角色发起攻击了。玩家在设备上点击攻击按键时,会通过帧同步方法,派发角色攻击动作事件。而逻辑层收到攻击动作事件后,将执行 checkAttack 方法。当满足攻击条件,该角色播放攻击动画,角色拥有的锤子数量减1;而被击打的角色将播放受击打动画,扣去分数,且调用 dropCoins 方法在角色附近掉落金币。

6221f0170008f4fcf5ae7a251d179e1d.png


除了上述提到的几点,游戏还实现了摇杆控制玩家移动、微信平台玩家通过分享链接接入游戏等等,有需求的小伙伴可以去查阅源码,应该能有不少收获。

Cocos 官方 Demo Team 现已推出了多款游戏源码,同时更邀请各路大神陆续输出配套文字/视频教程。大家可以在公众号后台回复游戏名(如:幽灵射手),获取相关游戏的全套资源链接。

自研源码 list

3D 射击《幽灵射手》

io 类 3D《奔跑吧小仙女》

3D 冒险《奔跑吧巨人》

3D 推金币《金币推推推》

经典 2D 消除《天天消一消》:

https://store.cocos.com/app/detail/2905


📢📢《别动我的金币》¥9.9 限时一折秒杀中,扫描下方二维码进入 Cocos 官方微店或点击文末【阅读原文】前往 Cocos Store 下载吧!

e33300cb0eca1e6a3ba1b5ec5dde0597.png

前往 Cocos Store 下载源码

https://store.cocos.com/app/detail/3359

双十二年终大促来啦!

cb48a91e65e35eb7122f897c606c475a.png


c9cb0ed450085fb9238f6becb1a80c2f.jpeg

>> 点击查看嘉宾及演讲主题

12月18日下午14:00Cocos 开发者沙龙「厦门站」将在厦门香格里拉酒店举办。Cocos 引擎、亚马逊云科技、网易易盾、青瓷游戏、风领科技围绕引擎技术与生态、游戏开发与发行等内容,为各位开发者准备了一场干货盛宴。

报名来到现场的小伙伴,还将获得「Cocos 最新定制周边大礼包」,人手一份哦!扫描下方二维码免费报名吧↓

>> 开发者报名通道

往期精彩

fa654d6f09d6f1398b9448722a0a2935.png

117377b4ff25d24092e08c814ed7e786.png

1c281cfe059de50980bbf2b6b0bbddf4.png

e8e1712153d9b99ee48fb12ee48411a9.gif

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个基于HTML、CSS、JavaScript实现的双人对战五子棋游戏源码,供参考: HTML代码: ``` <!DOCTYPE html> <html> <head> <title>双人对战五子棋游戏</title> <style> #game { margin: 0 auto; width: 500px; height: 500px; border: 1px solid black; position: relative; } .piece { width: 40px; height: 40px; border-radius: 50%; position: absolute; cursor: pointer; } .black { background-color: black; } .white { background-color: white; border: 1px solid black; } </style> </head> <body> <div id="game"> <script src="game.js"></script> </div> </body> </html> ``` JavaScript代码: ``` var game = document.getElementById("game"); var pieces = []; var currentPlayer = "black"; var winner = null; // 创建棋盘 for (var i = 0; i < 15; i++) { for (var j = 0; j < 15; j++) { var piece = document.createElement("div"); piece.className = "piece"; piece.style.left = j * 40 + "px"; piece.style.top = i * 40 + "px"; piece.addEventListener("click", play); game.appendChild(piece); pieces.push({ element: piece, x: j, y: i, player: null }); } } // 下棋 function play() { if (winner !== null) { return; } if (this.className.indexOf("black") !== -1 || this.className.indexOf("white") !== -1) { return; } this.classList.add(currentPlayer); var piece = getPieceByElement(this); piece.player = currentPlayer; checkWin(piece); switchPlayer(); } // 切换玩家 function switchPlayer() { if (currentPlayer === "black") { currentPlayer = "white"; } else { currentPlayer = "black"; } } // 获取棋子对象 function getPieceByElement(element) { for (var i = 0; i < pieces.length; i++) { if (pieces[i].element === element) { return pieces[i]; } } return null; } // 检查是否有玩家获胜 function checkWin(piece) { var count = 0; // 横向 for (var i = piece.x - 4; i <= piece.x + 4; i++) { var p = getPieceByCoordinate(i, piece.y); if (p !== null && p.player === piece.player) { count++; if (count === 5) { winner = piece.player; alert(winner + " wins!"); return; } } else { count = 0; } } // 纵向 for (var j = piece.y - 4; j <= piece.y + 4; j++) { var p = getPieceByCoordinate(piece.x, j); if (p !== null && p.player === piece.player) { count++; if (count === 5) { winner = piece.player; alert(winner + " wins!"); return; } } else { count = 0; } } // 正斜线 for (var i = piece.x - 4, j = piece.y - 4; i <= piece.x + 4; i++, j++) { var p = getPieceByCoordinate(i, j); if (p !== null && p.player === piece.player) { count++; if (count === 5) { winner = piece.player; alert(winner + " wins!"); return; } } else { count = 0; } } // 反斜线 for (var i = piece.x - 4, j = piece.y + 4; i <= piece.x + 4; i++, j--) { var p = getPieceByCoordinate(i, j); if (p !== null && p.player === piece.player) { count++; if (count === 5) { winner = piece.player; alert(winner + " wins!"); return; } } else { count = 0; } } } // 根据坐标获取棋子对象 function getPieceByCoordinate(x, y) { for (var i = 0; i < pieces.length; i++) { if (pieces[i].x === x && pieces[i].y === y) { return pieces[i]; } } return null; } ``` CSS代码: ``` .piece.black { background-color: black; } .piece.white { background-color: white; border: 1px solid black; } ``` 以上代码实现了一个简单的双人对战五子棋游戏,玩家可以通过点击棋盘上的空位下棋,游戏会自动判断是否有玩家获胜。该代码中使用了HTML、CSS和JavaScript技术,可以通过浏览器打开运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值