游戏状态同步

1.前言

目前市场上单机游戏占比高,因为相对联机游戏开发周期短、成本低,但联机游戏的社交属性强,玩家粘性高。总体来说,开发联机游戏有一定的技术门槛。

2.帧同步和状态同步

•     帧同步过程为各客户端实时上传操作指令集;服务端保存这些操作指令集,并在下一帧将其广播给所有客户端;客户端收到指令集后分别按帧序执行指令集中的操作。同步的是玩家的操作指令,该方式多用于对实时性要求很高的网络游戏。

•     状态同步过程为客户端上传操作到服务端,服务端收到后计算游戏行为的结果,即技能逻辑。战斗计算都由服务端运算,然后以广播的方式下发游戏中各种状态,客户端收到状态后,更新自己本地的动作状态、Buff 状态、位置等。同步的是游戏中的各种状态,该方式多用于回合制游戏。

本文将以状态同步技术为主,使用游戏联机对战引擎,帮助开发者短期低成本实现一款联机游戏。

游戏联机对战引擎

1.简介

游戏联机对战引擎(Mobile Game Online Battle Engine,MGOBE)为游戏提供房间管理、队组管理、在线匹配、帧同步、状态同步等对战服务,帮助开发者快速搭建多人交互游戏。

•     直接通过 SDK 调用后端服务,无需后台代码。

•     无需关心后台网络架构、网络通信技术、帧同步、服务器扩缩容、运维等复杂技术。

•     获得就近接入、低延迟、实时扩容的高性能对战服务,让玩家在网络上互通、对战、自由畅玩。

•     适用于回合制、策略类、实时会话(休闲对战、MOBA、FPS)等游戏。

2.接口概览

MGOBE 客户端 SDK 的接口可以分为五类,包括房间管理、匹配、消息发送、帧同步、广播接口。

•     房间管理类的接口主要是用于将不同玩家组成一个对局,这个过程中可以通过创建房间、邀请他人加入房间等方式将玩家聚合在一起。此外,还提供了如踢人、修改房间属性、查询房间信息等基本的房间管理方法。

•     匹配类的接口主要是用于将不同玩家通过匹配的方式组成对局,开发者可以根据需要定制匹配规则,实现根据玩家等级、积分等属性进行匹配。

•     帧同步和消息发送接口可以用于玩家消息的交互,通过帧同步、状态同步方式实现玩家游戏逻辑的同步。

•     广播类接口主要是用于处理上述接口调用产生的广播事件,比如玩家加房、退房广播、帧消息广播等等。

本文游戏案例讲解将会使用玩家匹配 matchPlayers,与实时服务器的交互 sendToGameSvr 以及相应的广播处理。

3.状态同步-实时服务器

实时服务器实现了对客户端游戏逻辑的扩展,当玩家进入房间以后,对于房间进行的任何操作,都会通过 MGOBE 的房间服务器同步给实时服务器。这样,实时服务器可以拿到最新的房间信息、房间状态,比如玩家进房、玩家退房、掉线,开始帧同步等,这些广播都会同步给实时服务器。
客户端通过 sendToGameSvr 请求接口和相应的广播接口 onRecvFromGameSvr 来实现和实时服务器的交互,通过这种方式实现游戏的状态同步。在实际应用中有多种用法:把实时服务器当成仲裁,计算最终的游戏结果;当玩家掉线时可用实时服务器来执行一些特殊逻辑,如使用机器人托管掉线玩家的操作;实时服务器还可以实现将玩家的数据保存在开发者腾讯云/云开发的数据库上。

游戏案例背景

1.状态同步 - 答题游戏《题题对战》

•     《题题对战》是一款使用游戏联机对战引擎 MGOBE 实时服务器实现状态同步的组队答题类游戏。

•     玩家通过随机匹配组成对局,然后与实时服务器进行交互,获取游戏状态(题目信息、玩家信息)。

•     demo包含六个页面:授权页、首页、匹配页、房间页、答题页、结算页。玩家在首页通过三种匹配方式(1V1、2V2、3V3)进入房间,玩家向实时服务器发送准备指令后会进入答题页,选择答案后提交到实时服务器,由实时服务器的逻辑判断答案的正误,并且下发新的游戏状态给每个玩家客户端。

•     涉及到的 MGOBE 接口有玩家匹配(matchPlayers)、查询指定房间信息(getRoomByRoomId)、退出房间(leaveRoom)、发送实时服务器消息(sendToGameSvr)、实时服务器广播(onRecvFromGameSvr)。

2.《题题对战》体验二维码
感兴趣的开发者可以扫码体验

游戏案例开发实践

一、实时服务器基础知识

1.开通实时服务器
开发者在使用实时服务器之前,需要上 MGOBE 控制台 新建一个游戏,然后创建实时服务器。


开发者需上传代码 zip 包;可选择是否打通腾讯云的 VPC 网络,打通后可以访问 VPC 下的数据库和存储;底层实例自动调节、弹性伸缩。

2.MGOBE 提供 NodeJS 框架可供下载,该框架文件夹里最核心的文件为 index.js。
3.index.js 文件主要作用为导出 mgobexsCode 对象,该对象有四个属性:logLevel、logLevelSDK、gameInfo、gameServer。

  • logLevel 表示开发者使用 MGOBE 提供的接口打印日志、logLevelSDK 表示 SDK 内部的日志打印、gameInfo 填写该游戏的ID和后端密钥信息、gameServer 对象最重要,开发者大部分逻辑都在这个对象里。
exports.mgobexsCode = {logLevel: 'error+',logLevelSDK: 'error+',gameInfo: {gameId: "请填写游戏ID",serverKey: "请填写后端密钥",},onInitGameServer,gameServer};
  • gameServer 对象类型为 GameServer.IGameServer ,主要包括 mode 属性、onInitGameData 接口和一些广播。
export interface IGameServer {mode?: 'async' | 'sync';onInitGameData: (args: { room: IRoomInfo; }) => GameData;onRecvFromClient: onRecvFromClient;onCreateRoom?: onCreateRoom;onJoinRoom?: onJoinRoom;onLeaveRoom?: onLeaveRoom;onRemovePlayer?: onRemovePlayer;onChangeRoom?: onChangeRoom;onChangeCustomPlayerStatus?: onChangeCustomPlayerStatus;onChangePlayerNetworkState?: onChangePlayerNetworkState;onStartFrameSync?: onStartFrameSync;onStopFrameSync?: onStopFrameSync;onDestroyRoom?: onDestroyRoom;}

(1)mode 表示实时服务器处理玩家消息的方式:

•     'async'代表异步,依赖于 gameServer 的事件循环去处理消息。

•     'sync'代表串行,采用串行方式去处理玩家发来的消息,处理完一条再处理下一条。

(2)onInitGameData 初始化游戏数据:游戏数据与房间生命周期一致,随房间销毁而销毁,可认为是对战式的游戏数据。 触发时机为在第一次收到玩家消息之前会调用一次。使用时 return 一个对象即可,比如返回游戏状态 players 的数组。

const gameServer: mgobexsInterface.GameServer.IGameServer = {mode: 'sync',onInitGameData: function (): mgobexsInterface.GameData {return {players:[],};},

(3)onRecvFromClient 收到玩家消息和房间操作的广播
(4)onCreateRoom 创建房间广播
(5)onJoinRoom 加入房间广播
(6)onLeaveRoom 退出房间广播
(7)onRemovePlayer 踢人广播
(8)onChangeRoom 修改房间广播
(9)onChangeCustomPlayerStatus 修改房间玩家状态广播
(10)onChangePlayerNetworkState 玩家网络状态变化广播,用来监听是否有玩家掉线
(11)onStartFrameSync 开始帧同步广播
(12)onStopFrameSync 停止帧同步广播
(13)onDestroyRoom 房间销毁广播

onJoinRoom: function ({ actionData, gameData, SDK, room, exports }) {SDK.logger.debug('onJoinRoom','actionData:', actionData,'gameData:', gameData,'room:', room);},

以上广播的内设参数:

•     actionData 参数表示实际广播数据,对于不同广播来说数据类型不同。

•     gameData 参数表示这个房间的游戏数据,是 onInitGameData 返回的对象。

•     SDK 参数是实时服务器提供的一些方法:
(1)sendData 给房间内玩家发送数据,发送配置包括超时时间和最大重试次数。
(2)dispatchActio 模拟玩家发消息给实时服务器。
(3)clearAction 在串行模式下,有些消息会被放置在队列里,调用该方法可以清空这个队列,所有没有被处理的消息都会被清空。
(4)exitAction 表示当前消息已被处理完毕,可以处理下一条房间消息。
(5)logger 日志打印方法。

SDK: {sendData: (data: { playerIdList: string[]; data: UserDefinedData; }, resendConf?: { timeout: number; maxTry: number; }) => void;dispatchAction: (actionData: UserDefinedData) => void;clearAction: () => void;exitAction: () => void;getRoomByRoomId: (getRoomByRoomIdPara: IGetRoomByRoomIdPara, callback?: ReqCallback<IGetRoomByRoomIdRsp>) => void;changeRoom: (changeRoomPara: IChangeRoomPara, callback?: ReqCallback<IChangeRoomRsp>) => void;changeCustomPlayerStatus: (changeCustomPlayerStatusPara: IChangeCustomPlayerStatusPara, callback?: ReqCallback<IChangeCustomPlayerStatusRsp>) => void;removePlayer: (removePlayerPara: IRemovePlayerPara, callback?: ReqCallback<IRemovePlayerRsp>) => void;logger: {debug: (...args: any[]) => void;info: (...args: any[]) => void;error: (...args: any[]) => void;};};

•     room 参数表示当前房间内信息,每个广播接口里都有这个参数。

•     exports 参数用来修改 gameData。

4.以上是实时服务器的接口简介,开发者将示例代码发布至实时服务器上,可选择“停服发布”或“不停服发布”。发布完成可以点击“查看日志”去日志页面,在调试过程中通过查看日志来调试。

二、《题题对战》开发实践
《题题对战》该游戏使用 LayaAir 引擎开发,本文将跳过UI构建过程和具体游戏逻辑,侧重于介绍 SDK 的关键调用步骤。
1.代码里客户端/src/script/scene 文件里,每一个界面对应一个脚本:Answer.ts答题页、Auth.ts授权页、Finish.ts结算页、Main.ts主页、MatchRoom.ts匹配页、VSRoom.ts房间页,这五个页面都继承于 Base.ts 里的类。

2.Base.ts 里有一些公共方法:初始化场景设置、加载进度条、打开场景、登陆等,还有 onAwake 方法。其他页面在触发 onAwake 时都会调用 Base.ts 里的 onAwake 方法。

  • 在 onAwake 方法里做了一些资源初始化:初始化场景、设置广播、关闭对话框、加载进度条、判断玩家有无授权、判断玩家有无登陆。
async onAwake() {this.initScene();this.setBroadcast();Base.dialog && Base.dialog.close();await this.loadProgressBar();if (!Global.userInfo) {this.openScene("AuthScene");return;}if (!Global.openId) {await this.login();}}

•     判断玩家有无授权,决定能否拿到玩家信息,如无授权则跳转到授权页,授权页会调用微信的 getUserInfo 方法。失败则会创建“授权按钮”,玩家点击后拿到用户信息,就会调转至主页。

wx.getUserInfo({fail: () => {const button = wx.createUserInfoButton({ type: "image", style, image: "image/auth_button.png" });button.onTap(data => {handleUserInfo(data.userInfo);button.destroy();});},success: data => {handleUserInfo(data.userInfo);}});

•     判断玩家有授权后,紧接着判断玩家有无登陆,本文将登陆和初始化 SDK 放在一起实现。先去缓存里拿到游戏基本信息:gameId、玩家openId、密钥secretKey、服务地址server、匹配matchCodes。

// 微信登录态async login(): Promise<boolean> {// 已登录if (Global.openId && MGOBE.Player.id) return Promise.resolve(true);this.showProgressBar(true);let gameInfo = getGameInfoFromStorage();if (!gameInfo) {gameInfo = {gameId: "",openId: Date.now() + "_" + Math.random(),secretKey: "",server: "",matchCodes: {1: "", // 1V1 匹配Code2: "", // 2V2 匹配Code3: "", // 3V3 匹配Code}};setGameInfoToStorage(gameInfo);}const res = await initSDK(gameInfo);// 初始化SDKthis.showProgressBar(false);if (res) {this.setBroadcast();//登陆成功设置广播} else {Base.dialog.showDialog("提示", "初始化失败");}return Promise.resolve(res);//返回登陆是否成功信息}

•     matchCode 是在 MGOBE 控制台创建的在线匹配,本文以1v1匹配为例,其中规则集在控制台上已经有示例,如1v1、2v2、3v3、5v5随机匹配,添加分段、添加误差,会根据玩家属性去匹配。

•     如上图,1v1只有 version、teams 两个属性,teams 里有四个属性:队伍名称name、队伍最大玩家数量maxPlayers、队伍最小玩家数量minPlayers、队伍个数number。此示例表示有2个队,每个队伍的玩家数量都为1。

•     创建好规则集后,可以选择开启机器人,当超过配置的超时时间还未匹配到真人时,可以匹配机器人形成对局房间。全部填好创建匹配后可以获得 matchCode 。

•     获取到游戏基本信息后,初始化 SDK ,返回一个 Promise ,初始化成功返回 true 。

function initSDK(initGameInfo: GameInfo): Promise<boolean> {const { Room, Listener, ErrCode, ENUM, DebuggerLog } = MGOBE;Global.gameId = initGameInfo.gameId;Global.openId = initGameInfo.openId;Global.secretKey = initGameInfo.secretKey;Global.server = initGameInfo.server;Global.matchCodes = initGameInfo.matchCodes;const gameInfo: MGOBE.types.GameInfoPara = {gameId: Global.gameId,openId: Global.openId,secretKey: Global.secretKey,};const config: MGOBE.types.ConfigPara = {url: Global.server,reconnectMaxTimes: 5,reconnectInterval: 4000,resendInterval: 2000,resendTimeout: 20000,isAutoRequestFrame: true,};return new Promise(resolve => {MGOBE.Listener.init(gameInfo, config, event => {if (event.code === MGOBE.ErrCode.EC_OK) {Global.room = new Room();MGOBE.Listener.add(Global.room);return resolve(true);}return resolve(false);});});}

•     本示例封装了 SDK 操作,把这些接口都封装为 Promise,通过这种方法可以很好的结合 async 和 await 语法,这里封装了查询房间信息、退房、发起匹配、取消匹配、发送消息给实时服务器、获取自己的队伍、获得敌人的队伍,设置广播处理。

3.玩家授完权后会进入主页,Main.ts 里有三个按钮,在 initListener 里为它们绑定了点击事件。

•     这三个按钮对应的点击事件都需要实现,比如第一个按钮点击时,将匹配模式设为1,代表1v1,同理设置第二三个按钮为2v2,3v3。

initListener() {this.btn1.on(Laya.Event.CLICK, this, () => {Global.matchMode = 1;!this.isInProgress() && this.openMatch();});this.btn2.on(Laya.Event.CLICK, this, () => {Global.matchMode = 2;// todo});this.btn3.on(Laya.Event.CLICK, this, () => {Global.matchMode = 3;// todo});}

•     首页点击按钮进入匹配页,发起匹配会调用 matchPlayers。

async openMatch() {this.showProgressBar(true);//打开进度条let res;if (!MGOBE.Player.id) {//判断SDK有无初始化res = await this.login();//如未初始化调用login方法if (!res) {return;//如登陆失败,直接返回}}res = await this.getUserRoom();//查询玩家是否在房间里this.showProgressBar(false);//隐藏进度条if (res) {// 玩家已经在房间内Global.room.initRoom(res);this.handleInRoom();//弹窗提示return;}// 开始匹配this.openScene("MatchRoom");//玩家不在房间里}

4.玩家发起匹配后进入匹配页面,匹配页面逻辑在 MatchRoom.ts 里实现。

•     触发 onAwake 之后调用 Base.ts 里的 onAwake 方法。

async onAwake() {await super.onAwake();this.isDisable = false;Global.room.onUpdate = () => {if (this.isInRoom()) {// 已经在房间内this.openVSScene();//打开房间页,跳过匹配环节}};}

•     触发 onEnable 时,会初始化页面、初始化点击事件、调用 callMatch 方法。

onEnable(): void {this.initView();this.initListener();this.showProgressBar(false);this.callMatch();}

•     玩家点击“取消”退出匹配,调用 Base.ts 里的 callCancelMatch 方法。

async callCancelMatch() {let res = await this.cancelMatch();if (this.isDisable) { return; }//判断页面是否被激活// 判断是否取消成功if (res === MGOBE.ErrCode.EC_OK) {this.openScene("Main");//返回主页return;}// 判断玩家是否已经在房间内if (res === MGOBE.ErrCode.EC_ROOM_PLAYER_ALREADY_IN_ROOM) {//已在房间返回错误码this.openVSScene();//不在房间,打开房间页return;}}

•     进入场景后会进行匹配,调用 callMatch 方法实现匹配。

async callMatch() {this.setTimer();//打开计时器let res;res = await this.match();//等待匹配if (this.isDisable) { return; }// 已经在匹配中if (res === MGOBE.ErrCode.EC_MATCH_PLAYER_IS_IN_MATCH) {//判断玩家是否多次发起匹配,如多次则返回错误码Base.dialog.showDialog("提示", "已经在匹配中,请等待");//弹窗提示return;}this.clearTimer();//清除定时器// 判断玩家是否已经在房间内if (res === MGOBE.ErrCode.EC_ROOM_PLAYER_ALREADY_IN_ROOM) {this.openVSScene();//打开房间页return;}// 判断是否匹配成功if (res === MGOBE.ErrCode.EC_OK) {this.openVSScene();//打开房间页return;}Base.dialog.showDialog("提示", "超时未匹配到对手,请您重新匹配",//匹配失败原因{ confirmCallback: () => this.callMatch() },//点击确定,重新发起匹配{ cancelCallback: () => this.openScene("Main") }//点击取消,回到主页);}

•     匹配超时后,调用 handleMatchTimeou 方法取消匹配。

async handleMatchTimeout() {if (this.isInRoom()) {return;}let res = await this.cancelMatch();if (this.isDisable) { return; }// 取消成功if (res === MGOBE.ErrCode.EC_OK) {Base.dialog.showDialog("提示", "超时未匹配到对手,请您重新匹配",{ confirmCallback: () => this.callMatch() },{ cancelCallback: () => this.openScene("Main") });return;}// 已经在房间内if (res === MGOBE.ErrCode.EC_ROOM_PLAYER_ALREADY_IN_ROOM) {this.openVSScene();return;}Base.dialog.showDialog("提示", "超时未匹配到对手,请您重新匹配",{ confirmCallback: () => this.callMatch() },{ cancelCallback: () => this.openScene("Main") });}

5.匹配成功进入VS房间页,VSRoom.ts 里实现方法:发起准备指令给实时服务器,实时服务器初始化游戏信息,并将游戏信息发给客户端。

•     触发 onEnable 事件时调用 this.ready 方法,用来发消息给实时服务器。

async ready() {this.showProgressBar(true);//打开进度条// 获取房间let roomRes = await this.getUserRoom();//判断房间是否还存在if (!roomRes) {return this.readyFail();//如果不存在,调用readyFail,弹窗提示准备失败}Global.room.initRoom(roomRes);//初始化房间信息// 发送准备消息let res = await this.sendToGameSvr({ cmd: CMD.READY });//发消息给实时服务器if (res !== MGOBE.ErrCode.EC_OK) {return this.readyFail();//发送失败,调用readyFail}// 超时弹框重试this.timer = setTimeout(() => this.readyFail(), 15000);}

•     收到实时服务器消息时,触发 onRecvFromGameSvr,做一些相应的逻辑处理。

async onRecvFromGameSvr(event: MGOBE.types.BroadcastEvent<MGOBE.types.RecvFromGameSvrBst>) {const err = await super.onRecvFromGameSvr(event);//调用Base.ts里的onRecvFromGameSvr方法,统一处理实时服务器消息if (err) { return; }clearTimeout(this.timer);//清除定时器,定时器用来记录玩家等待时间this.showProgressBar(false);//关闭进度条// 跳转if (Global.gameState.finish) { return this.openScene("Finish") };//游戏已结束,跳转至结算页if (Global.gameState.curQueId >= 0) { return this.openScene("Answer") };//游戏进行中,跳转至答题页}

6.匹配成功后进入答题页面,具体逻辑在 Answer.ts 脚本里实现。

•     Answer.ts 脚本里不断显示游戏状态,需要绑定4个选择按钮的点击事件,点击后提交答案。

initListener() {this.ans.onSelect = (index) => this.submit(index);}async submit(ans: number) {if (this.isSubmiting) { return; }//判断玩家当前是否正在提交答案this.isSubmiting = true;await this.sendToGameSvr({ cmd: CMD.SUBMIT, ans });//发送给实时服务器命令字this.isSubmiting = false;}

•     onRecvFromGameSvr 方法表示每次收到实时服务器消息广播,就设置 this.setQue 游戏答题信息(题目和选项)。

async onRecvFromGameSvr(event: MGOBE.types.BroadcastEvent<MGOBE.types.RecvFromGameSvrBst>) {if (await super.onRecvFromGameSvr(event)) { return; }this.setQue();}

•     玩家点击按钮,将结果成功提交给实时服务器,实时服务器根据该结果计算一个分数,计算完分数后将其写入游戏状态,整个游戏状态都下发至客户端,客户端拿到游戏状态直接更新画面即可。本游戏案例结果计算逻辑与时间相关,点击越早分数越高。

7.玩家答题结束后进入结算页,除展示游戏结果外,还有两个按钮“再来一局”和“回到首页”,选择“再来一局”则重新发起匹配,选择“回到首页”直接返回。

•     Finish.ts 脚本触发 onEnable 事件时调用一次退房操作,initListener 里有两个点击事件:重试和返回。

onEnable() {this.initView();this.initListener();this.leaveRoom();}initListener() {this.againBth.offAll();this.backBth.offAll();this.againBth.on(Laya.Event.CLICK, this, () => !this.isInProgress() && this.OnAgain());//点击重试this.backBth.on(Laya.Event.CLICK, this, () => !this.isInProgress() && this.OnBack());//点击返回}

•     点击重试,会调用 leaveRoom 退房,直接打开匹配页。

async OnAgain() {this.showProgressBar(true);let res = await this.leaveRoom();this.showProgressBar(false);if (!res) {Base.dialog.showDialog("提示", "操作失败");return;}return this.openScene("MatchRoom");//打开匹配页}

•     点击返回,会调用 leaveRoom 退房,直接打开主页。

async OnBack() {this.showProgressBar(true);let res = await this.leaveRoom();this.showProgressBar(false);if (!res) {Base.dialog.showDialog("提示", "操作失败");return;}return this.openScene("Main");}

8.到目前为止,基本上已经实现了整个游戏1v1的逻辑,客户端未涉及到实时服务器上具体的逻辑。在 MGOBE 官方提供的框架 index.ts 里,实时服务器的逻辑很简单,只需处理玩家准备和提交答案两个指令,下发游戏状态。

•     所有逻辑都在 onRecvFromClient 接收客户端消息广播里实现。

onRecvFromClient: function onClientData({ actionData, gameData, SDK, sender, room, exports }: mgobexsInterface.ActionArgs<mgobexsInterface.UserDefinedData>) {let cmd = actionData.cmd;//取出cmd命令字if (!room) {//判断当前房间是否存在SDK.sendData({ playerIdList: [], data: { err: " ERROR NO_ROOM ", cmd: SER_PUSH_CMD.ERR, gameState: null } });//房间不存在返回错误码return SDK.exitAction();}if (!cmd || !msgHandler[cmd]) {SDK.sendData({ playerIdList: [], data: { err: " ERROR NO_CMD ", cmd: SER_PUSH_CMD.ERR, gameState: null } });//cmd不存在或无对应处理函数,返回错误码return SDK.exitAction();}try {msgHandler[cmd](arguments[0]);//调用相应的处理函数} catch (e) {SDK.sendData({ playerIdList: [], data: { err: " ERROR " + e + sender, cmd: SER_PUSH_CMD.ERR, gameState: null } });SDK.exitAction();}return;},

•     处理函数逻辑在 msgHandler.ts 脚本里实现,主要方法为 readyHandler 和submitHandler。

function readyHandler({ actionData, gameData, SDK, room, sender }: mgobexsInterface.ActionArgs<AnsActionData>) {//玩家发送准备指令let gData = gameData as AnsGameData;if (!gData.gameState) {//判断当前游戏有无状态initGameData(gData, room);//如无则初始化return setTimeout(() => pushHandler.newGame(arguments[0]), 1000);//在1s后将新游戏信息下发出去}// 发送最新游戏信息pushHandler.curGame(arguments[0], pushHandler.SER_PUSH_CMD.CURRENT, [sender]);//如已初始化则将当前游戏信息下发给客户端}function submitHandler({ actionData, gameData, SDK, room, sender }: mgobexsInterface.ActionArgs<AnsActionData>) {pushHandler.checkSubmit(arguments[0], sender, actionData.ans);}

•     玩家发送 Submit 指令,submitHandler 检查玩家提交的答案。

// 检查提交的答案function checkSubmit({ gameData, SDK, room }: mgobexsInterface.ActionArgs<null>, playerId: string, ans: number) {let gData = gameData as AnsGameData;// 超过时间if (gData.gameState.time <= 0) {return curGame(arguments[0], SER_PUSH_CMD.GAME_STEP);;}// 超过题目数量if (gData.gameState.curQueId >= ANS_COUNT) {return curGame(arguments[0], SER_PUSH_CMD.GAME_STEP);}let player: Player = null;let que: Que = gData.gameState.que;gData.gameState.playerGroup.forEach(group => group.forEach(p => p.playerId === playerId && (player = p)));// 异常if (!player || player.score > 0 || player.ans >= 0 || !que) {return curGame(arguments[0], SER_PUSH_CMD.GAME_STEP);}player.ans = ans;if (que.ans !== ans) {// 答错player.score = 0;//玩家分数置为0} else {// 答对let scale = 1;if (gData.gameState.curQueId === ANS_COUNT - 1) { scale = 2; }let score = calcScore(ANS_FULL * scale, Date.now() - gData.startRoundTime);//根据该题目开始时间到当前时间,计算分数player.score = score;//写入player,即为游戏状态player.sumScore += score;}// 所以玩家全部提交就结束一局if (isAllSubmit(arguments[0])) {clearTimeout(gData.roundTimer);return endRound(arguments[0]);//结束一局}return curGame(arguments[0], SER_PUSH_CMD.GAME_STEP);}

•     结束这道题的一局时会去判断整个游戏是否结束,如果已经结束则直接调用 endGame。

// 结束一局function endRound({ gameData, SDK, room }: mgobexsInterface.ActionArgs<null>) {let gData = gameData as AnsGameData;gData.gameState.time = -100;curGame(arguments[0], SER_PUSH_CMD.GAME_STEP);// 2秒后结束游戏if (!gData.gameState.finish && gData.gameState.curQueId >= ANS_COUNT - 1) {return setTimeout(() => endGame(arguments[0]), 2000);}// 2秒后新一局return setTimeout(() => newRound(arguments[0]), 2000);}// 结束游戏async function endGame({ gameData, SDK, room }: mgobexsInterface.ActionArgs<null>) {let gData = gameData as AnsGameData;gData.gameState.curQueId = 100000;gData.gameState.finish = true;//重新修改游戏的结束状态curGame(arguments[0], SER_PUSH_CMD.GAME_STEP);//将游戏最终状态下发出去}

9.到此,该案例游戏的客户端和实时服务器上的代码都已介绍完毕。

三、总结
1.首先介绍了游戏联机对战引擎 MGOBE 的基本功能。
2.结合游戏案例介绍了客户端和实时服务器 API。
3.通过《题题对战》演示状态同步游戏接入游戏联机对战引擎 MGOBE 的方法。

参考文章

游戏联机对战引擎产品介绍:https://cloud.tencent.com/product/mgobe?from=13891

游戏联机对战引擎官网文档:https://cloud.tencent.com/document/product/1038/50849
游戏联机对战引擎控制台:https://console.cloud.tencent.com/minigamecloud
《题题对战》源码下载和LayaAir 引擎开发实践:https://cloud.tencent.com/document/product/1038/38785
状态游戏案例教程--《题题对战》:https://cloud.tencent.com/edu/learning/live-1523?ADTAG=yjsq&from=10680

如何利用状态同步开发一款联机游戏相关推荐

  1. html5游戏联机教程,纯前端如何利用帧同步做一款联机游戏?

    一.游戏帧同步 1.简介 ·现代多人游戏中,多个客户端之间的通讯大多以同步多方状态为主要目标,为了实现这一目标,主要有两个技术方向:状态同步.帧同步. ·状态同步的思想中不同玩家屏幕上的一致性的表现并 ...

  2. 如何利用HTML5快速开发一款小游戏

    如何利用HTML5开发一款小游戏?Cocos2d-js是一款流行的H5游戏开发框架,介绍Cocos2d-js的核心技术和使用方法,学完以后可以独立开发一款休闲游戏,主要介绍cocos2d-js中的图层 ...

  3. 如何快速构建一款联机游戏?

    导语 |近日,云+社区开发者大会(苏州站)圆满落幕.本次开发者邀请了腾讯内部及业内行业大咖就物联网.小程序.微服务等当前互联网领域的热点技术的落地实践问题进行了深度探讨.本文是林洁文老师的分享,关于如 ...

  4. 邹伟:如何开发一款小游戏

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 邹伟,后端高级工程师,对前端也有一定开发经验.2010年于华南理工大学毕业后加入腾讯,参与CDB.TGW等云服务研发,现主要负责微信游戏业务 ...

  5. 邹伟:如何开发一款小游戏 1

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 邹伟,后端高级工程师,对前端也有一定开发经验.2010年于华南理工大学毕业后加入腾讯,参与CDB.TGW等云服务研发,现主要负责微信游戏业务 ...

  6. 如何实战开发一款小游戏

    如何快速开发一款火爆的小游戏?"火爆"是一个偏运营的词,今天介绍的内容可能更倾向于技术方面,即如何利用微信的开放能力开发一款小游戏.小游戏上线120天时发布了几个重要的消息,其中有 ...

  7. 如何从零开始开发一个实时联机游戏?

    本文作者为明星团队汉家松鼠游戏工作室的CEO成功(CG),他将于11月10日在深圳举办的第四期腾讯游戏学院品鉴会上,分享汉家松鼠旗下<汉家江湖>.<江湖X>等游戏从立项.研发到 ...

  8. 如何从0开始开发一个实时联机游戏

    这是一篇严肃的联机游戏开发入门介绍,本文所述代码开源,文末可获得地址. 关于游戏的实时联机对战,目前是很多游戏开发者研究课题,也延伸出了很多概念,如"状态同步"."帧同步 ...

  9. java开发一款雷电游戏

    导读:电脑游戏,是指在计算机上能够运转的游戏软件.这种软件具有较强的娱乐性.电脑游戏的创新和发展与硬件.软件的发展紧密相关.它能够给玩家提供一个虚拟的环境,使游戏带给了人们很多的享受和欢乐.雷电游戏因 ...

最新文章

  1. 002:用Python设计第一个游戏
  2. 【原创】腾讯微博的XSS攻击漏洞
  3. 定了!2021年数据中心《能源管理师》考试,全国报名入口!
  4. python tkinter grid布局
  5. Jupyter Notebook Config
  6. 综合布线系统计算机网络,综合布线 计算机网络系统
  7. OpenCV反色处理
  8. DRILLNET 2.0------第二十三章 井控压井单模型
  9. MySQL 根据身份证查找年龄段
  10. GIF是什么格式的文件
  11. python打分系统_做一个Python颜值打分系统,比比看杨幂和杨超越到底谁更美?
  12. MVC、POJO、PO、DTO、TO、BO、VO、DAO、domian、delegate、sql
  13. 在.NET5 中读取Excel文件,评估下参加神秘献祭会的几位子民
  14. 学大伟业:2019年数学竞赛学习经验分享
  15. 哈拉德·柯施纳的狡猾
  16. Win11系统打开电脑磁盘显示磁盘错误无法打开怎么办?
  17. 一图全解芯片制造的全过程
  18. 上传绕过php文件改为图片,文件上传漏洞另类绕过技巧及挖掘案例全汇总
  19. php 生成条形码(支持任意php框架)
  20. 有趣大会 · ACL2022 (Findings篇)

热门文章

  1. html提取正文字游戏名,游戏取名频道页.html
  2. 微软反击Linux 瞄准Red Hat、Novell和IBM
  3. 怎么把字母缩小当符号_《DNF手游》名字特殊符号怎么打 地下城与勇士M特殊符号取名教学...
  4. VC10 vcpkgsrv.exe 占用CPU高的问题
  5. 『HarmonyOS』探索HarmonyOS应用
  6. SAP PA CO后台配置
  7. Google将推出音乐下载服务?
  8. 2021综述:计算机视觉中的注意力机制(续三):时间注意力
  9. C++小游戏—猜数字
  10. 判断java中两个对象是否相等