您的位置:首页 >聚焦 >

服务器+客户端+帧同步!Cocos Creator 最新全栈式联机游戏框架,免费开源!

2022-04-29 13:05:14    来源:程序员客栈

引言:这几年帧同步联机游戏越来越多,关于此类游戏实现方案的讨论争论也有不少。开发者「一块砖两百块」用 TypeScript 写了一个帧同步联机游戏框架,并使用该框架制作了一个联机游戏 Demo,免费分享给大家。

根据我多年社区潜水的经验来看,想做帧同步联机游戏的小伙伴不在少数。这不,我也做了一款,直接看看游戏对战录屏吧:

扫描上方二维码即可在线体验游戏(当前是同服全局游戏模式,所有人都退出后,才会重置游戏)。

玩一局后会发现,这不仅仅是个拼手速的游戏。游戏的基础玩法是:

每个玩家进入游戏会替代其中的电脑国家,每个国家初始占用两个地块,拥有地块为0时失败,占领全图地块则胜利;

每个地块会在少于50兵力的时候自动增长;

用手指拖拽自己的地块到敌方地块来出兵,士兵遇到任何敌方单位的碰撞都算作攻击,根据剩余兵力决定死亡或占领,往自己地块上出兵为增兵。

其实一开始只是想做个对战小游戏,可又是服务端又是客户端的,开发过程中发现里面门道还挺多,并且很多理念都是通用的,于是想到:为何不封装一套联机游戏的全栈框架呢?这样就有更多的人能快速实现自己的联机游戏了。

于是,全栈式联机游戏开发框架 ts-gameframework 诞生了(PS.上面演示的游戏已包含在该框架的示例游戏中)。

TSGF 定位

对标 MGOBE。

开源的联机游戏全栈式解决方案:服务端+客户端,皆由 TypeScript 语言编写。

「黑盒式」实现联机游戏:客户端对接 SDK,不用关心通信和同步逻辑。

自定义服务端逻辑:拓展自己的同步逻辑和各种交互逻辑。

TSGF 详解

服务器结构图

登录流程

gate是入口服务器(或者叫调度服务器)+登录服务器+集群管理服务器(简单三合一),有需要可以分离出来。

backend游戏逻辑服务器,可以设计成本游戏一样,一个服务器对应一个全局游戏,所有连接玩家都视作加入这个游戏。也可以加入房间的概念。

frontend游戏客户端(包含2个 demo)。

需要部署 redis服务器,在 gate和 backend的配置文件中配置连接信息。

使用 TSRPC(https://tsrpc.cn/)作为通讯框架,所以用了这个框架自带的代码生成/同步模块,导致各项目的目录名以及相对路径的固定(不熟悉乱改会导致出错)。

注:各项目根目录需要各自执行 npm i,frontend 需要用 Cocos Creator 打开一次即可消除报错。

1、gate

开发时,执行 dev的脚本,会启动服务并监视 ts 代码,并生成和同步相关 API。

如果仅修改了 TSRPC 的通讯协议,并且没启动 dev,那么可以单独执行 proto and sync。

所有接口都写了 jest的单元测试,执行所有单元测试:test。

如果需要部署,则执行 buildTS,然后打包下列文件:

./deploy./dist./node_modules./gf.*.config.json(配置根据实际情况自行修改)

windows 部署,提供了快捷部署服务方式,右键管理员运行 deploy/install_runasAdmin.cmd即可。

linux 部署,可以使用 pm2启动:pm2 start dist/index.js。

2、backend

开发时,执行 dev的脚本,会启动服务并监视 ts 代码,并生成和同步相关 API。

包含两个示例,都是单服务器游戏实例(即一个游戏服务器一个游戏实例)。

可以拓展成一个房间一个游戏,或者一个房间多个游戏等等。

部署参考 gate。

3、frontend

导入为 Cocos Creator 3.4.2 项目。

场景为 assets/occupationTheWar/occupationScene.scene。

assets/scripts/occupationTheWar/为本游戏的客户端代码目录。

assets/occupationTheWar/为本游戏的客户端资源文件目录。

注:预览模式下,因为将断线重连的信息写到了 web 存储里,所以要开第二个客户端的话需要用 Private 窗口来打开预览地址,或者改用两种浏览器。

使用技巧

包含状态同步的帧同步实现

这个功能是本框架的核心封装,思路来自:《腾讯高级工程师宝爷:帧同步游戏在技术层面的实现细节》。

1、帧同步示意图

输入操作的定义:所有会影响游戏逻辑的输入,如玩家加入离开操作,拖拽产生的攻击操作,而不是移动等因操作而产生的数据变化。(这点很重要!)

客户端的输入操作发给服务端,插入到当前帧的下一帧。

服务端按照帧率广播每一帧数据,每一帧中包含所有客户端的输入操作。

客户端收到帧后,加入帧队列,按给定频率执行。

执行每一帧里的多个输入操作,就需要客户端实现各种输入操作的逻辑,执行完输入操作后,客户端触发逻辑帧更新(用来运算坐标、物理等)。(注意,这里要和渲染帧进行区分!)

2、状态同步信息的加入

由客户端定时将当前的状态数据同步一份给服务端保存。

服务端保存最后的状态数据以及对应的帧索引。

有玩家掉线或新玩家加入时,就下发最后的状态数据+后续的追帧列表,这样可以大大减少追帧时间。

该功能对游戏设计有更高的要求:数据分离,但可选择关闭本功能。

3、核心代码

服务端:src\FrameSyncExecutor.ts

exportclassFrameSyncExecutor{/***一个逻辑帧的处理,正常由内部定时器调用,只有在单元测试时可以让外部调用进行测试*/publiconSyncOneFrameHandler():void{}/***同步游戏状态数据*@paramstateData*@paramstateFrameIndex*/publicsyncStateData(stateData:any,stateFrameIndex:number):void{}/***添加连接的输入操作到下一帧*@paramconnectionId*@paraminpFrame*/publicaddConnectionInpFrame(connectionId:string,inpFrame:MsgInpFrame):void{}/**获取给连接发追帧数据(最后状态数据+追帧包)*/publicbuildAfterFramesMsg():MsgAfterFrames{}}

客户端:assets\scripts\common\FrameSyncExecutor.ts

exportclassFrameSyncExecutor{/**当服务端要求追帧时触发,更新相关数据*/onMsgAfterFrames(msg:MsgAfterFrames){}/**收到服务端的一帧,会推在帧队列里,并做一些数据校验*/onSyncFrame(frame:MsgSyncFrame){}/**真正执行下一帧(按顺序触发多个输入操作的实现),最后触发逻辑帧事件,返回是否执行完所有帧了(可能是执行前就完了,也可能是执行后完了)*/executeOneFrame(dt:number):boolean{}/**开始执行下一帧(或者追上落后帧),并且自动根据情况执行之后的帧,执行完则自动停下来,设置executeFrameStop=true*/executeNextFrame(){}}

逻辑帧和渲染帧分离

逻辑帧和渲染帧是什么?

逻辑帧:服务端广播的帧,这个帧频率可能没渲染帧那么高。

渲染帧:即 FPS 里的帧。

一般单机游戏,都是在 update里去计算移动坐标等。但因为帧同步的频率和渲染的频率大概率是不同步的,如移动功能,输入操作一般是0帧开始向上移动,3帧停止移动,如果放在渲染帧里,那么就不能保证所有客户端的移动距离一致。因此必须区分逻辑帧和渲染帧!

演示游戏的设计原则

我认为,不管什么框架或实现方案,都应该规范设计原则,在团队协作中是很重要的,不然各自有各自的习惯,当发生需要交互时,问题就大条了。

所有设计都是随着使用场景一步步迭代来的,没有万能的框架,只有最适合的设计(权衡时间成本+协作成本),本游戏的设计原则如下,提供参考:

场景/资源引用,单独的类存放,如:OTWGameResource放游戏预制体的引用,OTWSceneManager放场景节点的引用(现在里面有做一些简单的逻辑,如果复杂了,就需要按设计原则进行分离),然后给其他业务逻辑实现组件引用,避免循环依赖。

游戏业务逻辑按模块封装,避免循环依赖,如:OTWGameManager为总的游戏逻辑(进入/退出游戏,逻辑帧执行,输入操作实现等,如果输入操作类型多了,就需要进一步分离一个“Mgr”),OTWGameTouchController为用户操作场景节点的交互,OTWTroopManager为士兵管理和交互逻辑封装。

所有游戏对象的根节点都要有一个基础组件 OTWObjectComponent(方便统一获取信息),各类型的游戏对象都使用了子类,实现自己的信息存储和基本逻辑(如:对象类型属性 objType,逻辑帧渲染方法 updateData)。

因物理用了 Cocos Creator 内置的,碰撞检测是用 Collider 相关组件实现,所以有空间变换的游戏对象,需要分物理节点和渲染节点,并在物理节点加上对应的物理组件,如士兵有分,地块就不需要分(没有移动)。

Data定义为包裹数据或节点引用的 model ,给所有,统一由对应的 Mgr 负责维护(创建和销毁),给各业务逻辑引用。

所有的输入都不能直接自己操作数据,必须在输入操作实现里进行操作( OTWGameManager-> 定义 "execInput_< InputType>"方法),保证所有客户端最终计算数据能一致。

单元测试

我用 jest+ chai作为单元测试模块,推荐 vscode 的插件:Jest Test Explorer(单独执行单元测试用起来比较方便):

但凡一个逻辑或模块复杂起来了,势必需要封装起来,减少各种依赖,尽可能“独立”。否则越复杂的逻辑,依赖性关联性越强的功能,出错后排查所花费的头皮会等比上升,而我认为封装+单元测试是任何一个复杂逻辑的解药。

在自己的项目中使用,执行:

npmi-Djestts-jestchai@types/jest@types/chai

配置和代码可参考源码。

资源下载

请前往下方gitee 地址免费下载ts-gameframework,具体使用方法和使用要点一并附在其中:

https://gitee.com/fengssy/ts-gameframework

目前 TSGF 框架只适用于帧同步小游戏,以及用来快速验证联机游戏的核心玩法。后续版本计划将陆续实现更多功能:

V1.1.0 完善房间和匹配支持

V1.2.0 设计成模块引用的方式进行使用

V1.3.0 支持状态同步

接下来我还计划拿 ts-gameframework 做一款自己的联机游戏,毕竟东西行不行,只有真正用的人知道!再之后可能会尝试运营这个业务,让服务器都不想部署的开发者能直接用!也算是完成一个闭环了。

点击文末【阅读原文】跳转至论坛专贴一起交流讨论,请大家多多关注!共勉!

往期精彩

关键词: 输入操作 单元测试 状态数据

相关阅读