转自:https://cowlevel.net/article/2005281

服务器端

服务器端比较简单,可以根据colyseus(https://github.com/gamestdio/colyseus)官方文档提示,安装。然后新建rooms/IOGRoom.ts用来处理服务器逻辑。

colyseus已经将房间查询,玩家匹配之类的常见功能实现,我们只需要在IOGRoom.ts里实现游戏逻辑代码即可。主要内容就是维护一个frame_list列表,并以一个固定的频率FRAME_RATE增加当前帧frame_index,并将frame_list中当前帧的数据发送给所有玩家。同时将玩家提交的指令push到frame_list里。

//以固定频率发送当前帧setInterval(this.tick.bind(this),1000/this.FRAME_RATE);

tick(){   let frames = [];   frames.push([this.frame_index,this.getFrameByIndex(this.frame_index)]);   this.broadcast(["f",frames]);   this.frame_index += this.frame_acc;}

//接受玩家的输入指令并存入frame_listthis.frame_list[this.frame_index].push(data); 

客户端连接服务器

colyseus提供了js版本的客户端代码,根据官方的API可以十分方便的使用。我写了一个简易的界面用来创建或者加入房间,代码比较简单可以直接看代码注释。主要代码文件如下:

IOG/colyseus/colyseus.js    //colyseus客户端代码IOG/colyseus/colyseus.d.ts    //colyseus TypeScript定义文件IOG/CyEngine.ts    //用来处理服务器链接等IOG/CyPlayer.ts    //用来储存玩家输入等数据 

在CyEngine.ts中调用colyseus方法进行加入,创建,接收发送消息等操作:

this.client = new Colyseus.Client(`ws://${this.ip}:${this.port}`);    //链接服务器this.client.getAvailableRooms(this.roomName, function (rooms, err) {});    //获取可以加入的房间列表this.room = this.client.join(this.roomName);    //加入房间//接受服务器信息onMessage(message){   switch(message[0]){       case "f":        //帧同步信息           this.onReceiveServerFrame(message);           break;       case "fs":           this.onReceiveServerFrame(message);           //把服务器帧同步到本地帧缓存后,读取并执行本地帧缓存           this.nextTick();           break;       default:           console.warn("未处理的消息:");           console.warn(message);           break;   }}

//发送信息到服务器房间sendToRoom(data:any){    this.room.send(data);} 

客户端帧锁定

实现帧同步最重要的是保证所有客户端每一帧计算结果一致,而且当前帧要保证与服务器同步。在服务器端,我们每隔固定时间间隔发送帧信息f,在客户端的onMessage中收到并处理帧信息。在收到帧信息之前需要停止客户端渲染,等待网络接收到新的帧信息之后再进行渲染。

通常的做法是弃用ccc的游戏循环,维护一个新的游戏循环,已达到完全控制游戏循环的目的。这样就可以在等待新的帧信息的时候停止游戏循环中的逻辑处理(物理引擎等)。但是这样做就会完全破坏ccc原有的工作流程,比如cc.Component中的onLoad,start,update,都无法使用,ccc自带的动画,粒子特效等也没有办法继续使用。

所以为了尽量不改变ccc原有的工作流程,我们需要直接控制ccc的游戏循环。好在ccc提供了cc.game.pause()方法来暂停游戏逻辑,然后在接收到服务器的帧信息之后,调用cc.game.step()来运行下一帧,这样我们就可以继续使用Component中的update等回调。

但是这样做有个严重的缺点,就是cc.game.pause()会暂停所有逻辑,包括UI界面等。如果出现网络卡死,连UI界面(比如弹出窗口提示网络断开)都无法显示。所有这里必须得处理好网络多开的情况,在断开网络时及时的恢复游戏循环,cc.game.resume() 

客户端接收并处理帧信息

客户端接收到服务器端的帧信息之后,将其缓存到frames中,并以服务器设置的时间间隔进行读取并处理帧信息中的玩家输入。将当前帧数记录到frame_index中,每次累加,如果frames[frame_index]为undefined,则等待接受服务器发来的新的帧信息。

注意这里按时间间隔执行的延时执行函数就不能再使用ccc自带的schedule,scheduleOnce了,因为这两个函数因为cc.game.pause()被停止了。我们可以使用原始的setTimeout,setInterval

同样因为游戏循环暂停而不能使用的还有update(dt)回调中的dt,因为这个时间间隔在不同的客户端因为网速的原因会有很大差别。我们经常使用dt来计算真实的时间,比如技能CD为10s,在帧同步的情况下就不能以时间为单位了,可以使用帧为单位,技能CD为600帧(每秒60帧)。

正常情况下客户端应该以服务器端规定的间隔处理帧信息。但是如果遇到了网络卡顿,客户端缓存了大量的未处理帧,或者玩家是后加入房间的,需要将之前的历史帧全部执行一边,那么使用相同的帧处理速度将会永远追不上最新的帧进度。这里就需要做追帧处理,即以更快的速度处理帧信息,类似快进。

nextTick() {//处理帧信息    this.runTick();

    if (this.frames.length - this.frame_index > 100) {        //当缓存帧过多时,一次处理多个帧信息        for (let i = 0; i < 50; i++) {            this.runTick();        }        this.frame_inv = 0;    }else if (this.frames.length - this.frame_index > this.serverFrameAcc){        //追帧        this.frame_inv = 0;    } else {        if (this.readyToControl == false) {            this.readyToControl = true;            this.round.onReadyToControl();        }        //正常速度        this.frame_inv = 1000 / (this.serverFrameRate * (this.serverFrameAcc + 1));    }    setTimeout(this.nextTick.bind(this), this.frame_inv)}

当缓存的帧过多的时(>100)我们还可以一次处理多条(50)帧信息,以提高追帧的效率,但是这里处理的过多的话会导致客户端卡死,而且可能导致物理引擎计算结果出现误差,下面会提到。

服务器帧插值

为了减小服务器带宽的压力,服务器发送帧信息的时间间隔不易过短,因为帧信息里只有玩家输入信息,所以50ms(20fps)左右就已经几乎感觉不到输入延时了,但是客户端如果以20fps渲染的话画面还是有明显卡顿的。为了客户端达到60fps,又减小服务器带宽压力(20fps的速度进行同步),我们需要对服务器的帧信息进行插值,即服务器发送的帧号每次增加3(0,3,6帧),客户端接收到后用空数组([])将帧数补充(0,1,2,3,4,5,6帧)。这样就可以达到节省带宽的目的,毕竟输入并不像渲染对延时那么敏感。

发送用户指令到服务器

在客户端中,不能直接在本地修改游戏物体的状态,比如控制人物移动,进行攻击等。因为没有状态同步,所有的本地修改都会导致客户端之间的结果差异。为了保证同步我们需要把用户输入和指令传到服务器,再由服务器以帧信息的信息分发到本地后再进行响应(用户命令->服务器帧->本地客户端响应)。

对于客户端本地的其他玩家是一样逻辑,等待服务器帧信息,然后将帧信息中命令映射到对应的玩家类中CyPlayer。这个过程就相当于在每个玩家的客户端中,维护一个包含所有玩家的输入代理列表,当某个玩家输入命令通过服务器帧信息同步到所有的玩家客户端上时,匹配并映射到本地代理列表中。

runTick() {//处理当前帧信息...    if (frame.length > 0) {        frame.forEach((cmd) => {           //将指令映射到函数中 cmd_input            if (typeof this["cmd_" + cmd[1][0]] == "function") {                this["cmd_" + cmd[1][0]](cmd);            } else {                console.log("服务器处理函数cmd_" + cmd[1][0] + " 不存在");            }        })    }    this.frame_index++;    //下一帧    cc.game.step();    //进行渲染    ...}

cmd_input(cmd) {    //在players中匹配到玩家,并更新输入状态    this.players.forEach((p) => {        if (p.sessionId == cmd[0]) {            p.updateInput(cmd[1][1])        }    })}

在帧信息处理函数里,将玩家输入同步到客户端的CyPlayer里,然后其他组件里(例如CharacterController)中检查并响应CyPlayer的变化。

InputManager和CyPlayer中需要同步的属性可以根据需要随便设置,而且不需要修改服务器代码,十分方便。

客户端随机

为了保证客户端之间的计算结果一致,我们需要使用伪随机函数(线性同余生成器)。

seededRandom(max = 1, min = 0) {    this.seed = (this.seed * 9301 + 49297) % 233280;    let rnd = this.seed / 233280.0;    return min + rnd * (max - min);}

里面的this.seed就是随机种子,在创建房间的时候生成一个种子,分发到玩家手里,就可以保存玩家之间的随机数返回相同的结果。

客户端需要用seededRandom()代替原有的随机函数Math.random()。当然不一定替换所有的,只需替换必要的。比如粒子特效中的随机,没有必要保证所有客户端里的特效都一致,但是玩家出生位置等重要信息就必须使用seededRandom来保证一致性了。

seededRandom返回的结果是有严格的顺序的,在使用此函数获取随机值的时候一定要保证代码执行的顺序,某些函数比如发生碰撞之后的回调,不确定是否是严格按照顺序执行的,目前在测试比较少没有发现不一致的情况。

客户端逻辑

除了上面提到的用户输入,随机函数等,客户端可以使用ccc自带的其他组件。比如动画,Action,粒子特效,物理引擎,碰撞collider等,基本上与单机游戏开发无异。


以下是游戏gif图,

可以看到左侧玩家开始游戏后数秒后,右侧玩家才点击加入进入房间,经过短暂的追帧之后,两边实现了帧同步,之后的拾取,攻击碰撞判定在两个客户端中都没有出现不同步的现象。

物理引擎确定性问题

上面的展示毕竟时间短,情况简单,为了测试复杂情况下的同步问题,我写了简单的AI。

scripts/AIManager.tsscripts/AIController.ts

在执行数秒之后就可以发现肉眼可见的帧不同步的现象

我将追帧的时候一次执行多帧的代码注释之后,情况有了好转

nextTick() {    this.runTick();    if (this.frames.length - this.frame_index > 100) {        //当缓存帧过多时,一次处理多个帧信息        // for (let i = 0; i < 50; i++) {        //     this.runTick();        // }        this.frame_inv = 0;    }else if (this.frames.length - this.frame_index > this.serverFrameAcc){        this.frame_inv = 0;    } else {        if (this.readyToControl == false) {            this.readyToControl = true;            this.round.onReadyToControl();        }        this.frame_inv = 1000 / (this.serverFrameRate * (this.serverFrameAcc + 1));    }    setTimeout(this.nextTick.bind(this), this.frame_inv)}

猜测是追帧的时候执行过快导致box2d执行结果不一致,毕竟box2d并不是确定性的物理引擎,而且不确定性比我现象的要严重的多(也可能是其他原因导致的,毕竟测试的比较少)。

总结

由于ccc自带物理引擎box2d的不确定性,目前还不能完美的帧同步,除非深入调试box2d以确保计算结果的一致。不过如果设计的游戏不需要物理引擎,比如回合制,或者塔防等,目前的代码还是可以胜任。

以下是服务器和客户端的项目github地址,如果有人感兴趣欢迎fork。

客户端项目:https://github.com/cyclegtx/cocos2dx-iog-lockstep-sync

服务器端项目:https://github.com/cyclegtx/colyseus-iog-lockstep-sync

io类游戏快速开发 2相关推荐

  1. io类游戏快速开发 1

    今天起开个新坑,准备维护一个开源项目,用来做为模板快速开发io类游戏. 一直很喜欢io类游戏,经常在国外的聚合网站上尝试种类繁多的io游戏.苦于没有专用的加速器,大部分的游戏是没法顺畅游玩的.而国内的 ...

  2. 利用Cocos+Matchvs开发的IO类游戏源码分享

    游戏指引 <贪吃星球>是一款IO类游戏,只支持多人玩法 随机加入的房间,房间人数为3人时,即可开始游戏. 其他方式加入的房间,房间人数大于等于4人时,房主可点击开始游戏. 注意:随机加入和 ...

  3. 基于cocoCreator版本2.4.5整理一款2D小游戏快速开发的游戏框架

    前言:基于cocoCreator版本2.4.5整理一款2D小游戏快速开发的游戏框架. 一.cocosCreator的UI框架. 中心思想, 将所有的UI窗体分为3类管理(1级窗体, 2级窗体, 3级窗 ...

  4. 多人对战小游戏快速开发实例分享(附源码)

    前言:该游戏项目主要是基于前端引擎Cocos Creator开发,涉及后端联网的部分,则通过接入Matchvs SDK完成快速开发工作. 准备工作:相关引擎工具引擎下载及指南 Matchvs Java ...

  5. 手机横版动作类游戏的开发思路

    转自:当乐网 原文:http://www.d.cn/news/289.html 说起横版动作类游戏,对我们这代人影响最深的作品,应该是日本80.90年代出品的一批街机游戏,像<双截龙>.& ...

  6. 棋牌类游戏的开发心得

    一个多人在线的棋牌类网络游戏的项目临近尾声,我参与了该项目的整个设计流程,并且完成了90%的核心代码.关于这个项目,有很多地方值得聊一聊.本系列不打算把这个项目将得多么详细规范,那是设计文档应该描述的 ...

  7. Cocos Creator多人对战联网游戏快速开发实例(附源码)

    前言:游戏主要是基于前端引擎Cocos Creator开发,涉及后端联网的部分,使用了游戏服务器引擎Matchvs开发完成. 准备工作:相关引擎工具引擎下载及指南 Matchvs JavaScript ...

  8. android 文件上传工具类,Android快速开发架构PlanA(五),文件上传下载了解一下...

    1.PlanA文件上传&下载的使用 PlanA架构集成第五篇,文件上传下载的使用,文件上传&下载在APP里面随处可见,发朋友圈要上传图片或者短视频,换个头像要上传选择的图片,offic ...

  9. 开源集锦(五)开源框架和快速开发工具类

    Volley https://github.com/stormzhang/AndroidVolley http://blog.csdn.net/t12x3456/article/details/922 ...

最新文章

  1. 在Android Studio中有六种依赖
  2. 潘通色卡tcx电子版_【收藏】最全“潘通色卡电子版”,只带手机对色一步到位!...
  3. 深入理解Java虚拟机-Java内存区域透彻分析
  4. Python机器学习:PCA与梯度上升002使用梯度上升法求解PCA问题
  5. 2016云计算大数据安全论坛即将在北京召开
  6. Wpf从资源中重用UI元素
  7. SkinSharp破解版与模版皮肤下载与使用
  8. android 自定义进度条颜色,进度条背景颜色
  9. 图片、图标、代码资源网站
  10. js压缩图片到指定大小
  11. 信息学奥赛一本通:题解目录 (〃‘▽‘〃)点个赞吧
  12. 红队攻击:轻松玩转邮件钓鱼
  13. Python SMTP发送邮件
  14. 教你如何谈朋友噢!!!zz
  15. Microsoft Edge闪退问题解决方案:
  16. 线性表中的头插法双链表的学习
  17. 基础知识补充——白噪声、高斯白噪声
  18. 云服务器一般用什么系统,云服务器一般选什么操作系统
  19. Ubuntu16.04安装谷歌拼音输入法
  20. Win11右键怎么直接显示所有选项?

热门文章

  1. 可以看游资的app_跟随一线游资操作,轻松收获涨停板
  2. 随机森林c语言编程,一种基于随机森林的C语言源代码静态评分方法与流程
  3. echarts 折线图悬停拐点大小不变_echarts-折线图(折线虚实/颜色与拐点样式修改)...
  4. linux中sed的基本用法,linux sed用法
  5. jquery 手指滑动多半屏_JS拖拽专题(五)——「玩出花儿来」移动端滑动事件的封装...
  6. 【AI白身境】搞计算机视觉必备的OpenCV入门基础
  7. 全球及中国新能源汽车电机市场未来发展方向与投资潜力研究报告2022版
  8. RSA加密的填充模式
  9. 纪实:西藏少数民族儿童的“悲苦童年”(组图)
  10. 外国人居留证申请程序