cocos creator 项目总结二(战斗帧同步解析)

一、帧同步原理:

        帧同步,指的是将游戏过程中关键的操作帧数据同步给各个客户端实现游戏同步的方案。这个原理看上去一句话很简单,但是其实内部涉及的细节却很多,影响到游戏的卡顿,同步是否完全同步等问题,接下来我一一列举我制作过程中遇到的问题和解决方案。

二、客户端同步一致性问题

1、逻辑驱动归一管理,这个主要是要将战斗过程的所有逻辑运算update驱动要统一管理,而不是简单的通过UI 层的update分散驱动,这样做的话你的update是不确定的,这里的确定分两部分1)执行时间上的确定性,2)执行顺序上的确定性。

      这个问题我的实现方案是使用ECS 结构,战斗的数据逻辑完全和界面层解偶,界面显示通过数据层派发自定的message给界面层监听驱动ui节点的添加、删除、更新等操作。然后逻辑层的update统一有 Engine 管理器的update驱动 System Componet 的update。这里还有一个细节就是设计Engine和 System Componet 的时候我们遍历System Componet 一定要是数组方式,而不能是map,因为map的遍历顺序的是不确定的。所以数据结构要设计好。我的设计是System和Componet的存储时同时又map、和array两份数据,map是为了快速查找,array为了顺序遍历。所以我封装了一个公共的数据结构,他内部就是map和array存储,对外能实现map和array的两种特性。这样我们就解决了数据层 遍历顺序确定性,和update统一入口的问题

2、float 精度问题,这个问题主要是我们游戏数据运算过程中往往会存在浮点数,但是浮点数的精度在不同的平台又存在差异导致计算结果不能保证唯一的问题。网上大家对这个问题可能主要是关注在的物理引擎上的float上。但是其实只要是战斗过程中运算存在float数的我们都要处理。不然唯一性就存在问题。

        这个问题我的方案是,战斗过程中所有的数据统一放大1000倍向上取整(具体放大多少倍这个可以自行按自己的数值情况而定),所以我的战斗运算过程中就没float的情况,只是产品配表的时候需要放大1000倍。另外就是物理引擎,由于我的战斗其实主要是 碰撞和反弹这些物理情况,所以这些模块我都是自己写的CollisionSystem和CollisionComponet达到效果,在计算过程中也都自行处理了float的问题。方法也是先 放大1000倍然后向上取整计算完然后再 除1000。这里有一个细节需要注意的就是三角函数,因为碰撞等计算频繁需要sin,cos 等,我直接将 0-360 的数值全部打表输出了,这样能提升运算速度也能防止sine cos 的不确定性(我也不知道会不会有问题,但是打表能提升性能就这点我也要打表)。

3、随机数确定性,这个问题其实不是很复杂的问题,因为只要是战斗游戏都有战斗随机性问题。但是由于要同步我们就又得保证每个客户端得随机是一致得。

        这个问题其实很好解决,战斗开始前由服务器统一派发随机种子,然后自己找了一个随机算法生成随机数就行了。一般得随机数得生成都是按随机种子做剩法和取余之类的操作所以只要种子是一样得随机的结果必然也是一致的。

三、网络同步过程中网络抖动造成的不流畅问题

解决上面的几个问题,基本状态同步的一致性基本就没什么问题了。接下来我们要解决的就是流畅性的问题

4、网络逻辑帧数是否要和实际逻辑帧数是否要一致的问题。一开始的时候我的驱动方式是每收到一次网络帧事件就触发一次逻辑帧,这样的话导致网络波动就会明显的界面不流畅了。因为为了ui的移动显示流程我们的逻辑帧必须间隔事件足够小我的设计是 1/30,但是这样的话就需要 0.033 秒就必须收到一次网络帧数据,否则就会卡顿。因为ui的移动帧率不稳了(因为却动ui移动的逻辑帧间隔事件不一样了)。其实我们仔细分析下,我们人为操作的频率其实是达不到 1/30 秒的频率的(手游谁手速能达到这么快?)。但是如果我把网络帧调成 1/10 秒那ui的移动就会一下移动太远显得不流程了。

        这个问题我尝试了很多的方法,比如网络上说的插值方法,所谓插值法就是移动的时候不是立马赋值新的位置,而是动画的方式慢慢的移动过去,比如提前预演方式(就是提前按照上一帧移动方向在没收到新网络帧的时候提前移动)但是这样会导致如果两者存在改变方向的话就得矫正了。而且这个矫正还不好做(就是比较费劲,要保存之前得信息用于回退)。最后我选择得方法是直接一次网络帧代表客户端得三次(具体倍数其实可以具体情况具体设置,主要看你自己对操作反馈要求多高了,这个值肯定是越大越好,因为每秒同步得数据少了,用来等待网络包的间隔时间也长了网络抖动影响就小了)逻辑帧。比如我 网络帧是 1/10 秒,那我客户端逻辑帧得驱动就是 1/30 秒,我客户端没 1/30 秒就触发走逻辑帧,如果是 1/3 的帧的时候就找网络层要这帧的网络数据,如果没有就等待,有立马执行(其实主要是触发操作数据的派发),2/3 3/3 帧的时候这种时候就是普通帧没有等待网络包的需求按间隔时间update就行了。这样我们就解决了单帧间隔时间太长导致动画像闪现一样的问题,也能保证网络帧间隔太短对网络波动要求高的问题。

5、逻辑帧的驱动是否要严格按时间计算去驱动?这个问题可能不说下具体情况可能大家都不理解是什么意思。主要问题在于我们客户端的时间是按我们设定给引擎的 帧率执行的。比如1/60 那也就是每次update的间隔时间必定 >= 0.01666666,如果秒的,我们的网络帧率的间隔时间和这个update时间很难形成倍数关系,这样就导致了了本来按计划我们应该每2个update要执行一次逻辑帧,但是会因为不成倍数的问题,以一个周期性的出现一次 3个update才能执行一次逻辑帧,这样就严重了,因为间隔时间不一样了,就周期性的ui抖动一次。

        这个问题,我也尝试了各种方案,最后仔细想了下,我发现我陷入了一个误区就是我为什么一定严格去计算时间,动画流程只要保证按固定的update驱动动画就会流畅了,所以我最终选择的就是直接没两次update就走一次逻辑帧,不用管时间如何。这样改后我们的逻辑帧的驱动就是每两次update检测一次逻辑帧,如果当前是 1/3 的等待网络帧的就一定要等到网络帧才执行一次逻辑帧,如果是 2/3 3/3 的普通逻辑帧就直接执行(其实这两针我个人觉得和网络上大家说的提前预演的方法也是同样的效果,用两个空操作时间的逻辑帧去吞噬网络抖动造成的网络帧数据包不平稳的问题,让我们对网络延迟的容忍率更高了,当然这个方法的牺牲是导致操作的灵敏降低了,但是对于手游每秒能保证 5 次以上的操作数据我觉得应该是够了,何况我使用的还是保证了每秒10次的间隔时间)。

四、关于ECS 设计上的一些心得

 1、很多人可能觉得 creator 本来就是组件式的开发,没必要再自己弄ECS结构,直接用系统的就行了,但是我个人还是觉得自己可控性会更强而且整个ECS基础框架写下来代码量真心不大。自己写的心里明明白白有什么问题都方便查找。而且脱离引擎实现也为我以后将战斗逻辑再服务器用来校验防作弊,或者客户端性能瓶颈时开多线程 跑逻辑层会更加的方便。下面自己讲讲我自己实现的ECS的一些细节问题和我个人如何考虑的点一一分析。

 ECS 主要部分如下:

1、Engine 整个引擎的核心,所有的对象的管理者,主要维护 所有的 System、Entity,全局事件派发器,全局定时器,这里需要注意的就是我们维护的对象在各种遍历操作的时候记得保证他的顺序性,也就是每个客户端的遍历时一定顺序一致的

2、Entity 实体对象,他按Entity的类型包含了很多不同的Componet,这里也是一样需要保证componet在遍历时的顺序一致性。

3、System 系统对象,没什么特别的按功能模块划分不同的system

4、Message 这个是 数据层和 界面层通信的数据结构分为 M2V 和 V2M,为什么要加入这个结构,主要是为了将界面层和数据层完全解耦,我不希望数据层持有界面层的任何东西去控制界面的显示。为以后单独拆分逻辑层,到单独线程,或者放服务器运行做准备

5、Scheduler 定期器对象,主要是维护需要定时逻辑的运行,这里也需要注意设计时保证立马item的严格顺序执行

6、关于 Engine system entity 加载的问题,我将每个战斗场景的system,entity的组成全部做成了一个json配置表来配置,这样我能通过这个配置的变化达到我不同战斗场景的需要差异的调整。这个对于战斗业务逻辑的千变万化需求有很重要的意义。

7、关于 typescript 如何动态根据class 名字获得 class 的方法,我的方式时通过一个统一的ts 将需要的 class 全部导出然后,加载的地方import 这个综合文件然后按名字去取class

// system/index.ts
export { SceneLoadSystem } from "./SceneLoadSystem"
export { HeroBornSystem } from "./HeroBornSystem"
export { CollisionSystem } from "./CollisionSystem"
export { MainCameraSystem } from "./MainCameraSystem"
export { MoveSystem } from "./MoveSystem"
export { DropSystem } from "./DropSystem"
export { SkillSystem } from "./SkillSystem"
export { PhysicsSystem } from "./PhysicsSystem"
export { EffectSystem}  from "./EffectSystem"
export { LevelSystem}  from "./LevelSystem"
export { GrassSystem } from "./GrassSystem"
export { CollectSystem } from "./CollectSystem"
export { BattleBaseSystem } from "./base/BattleBaseSystem"
export { BattleTreasureSystem } from "./treasure/BattleTreasureSystem"

import { _decorator, Component, Node } from 'cc';
import { ECSSystem } from '../base/ECSSystem';
const { ccclass, property } = _decorator;
import * as allSystems from "../../model/system"
import { SystemConfig } from '../../config/BattleConfigTypes';


type SYSTEM_CREATOR = new( name : string, priority : number)=>ECSSystem;

@ccclass('ECSSystemLoader')
export class ECSSystemLoader{
    public loadSystems( configs : Array<SystemConfig> ) : Array<ECSSystem>{
        let systems = new Array();
        for(let i = 0; i < configs.length; i++){
            let item = configs[i];
            let classMap : any = allSystems;
            let cls : SYSTEM_CREATOR = classMap[item.class];
            systems.push(new cls(item.name, item.priority));
        }
        return systems;
    }
}

8、合理的辅助数据结构helper帮助性能的提升和代码去重,在System 或者Componet 中我们往往可能会对某些 Entity 或者 某个类别Componet进行遍历或者查找操作,这时候如果我们能够提前将这些内容通过一个单例的Helper 数据容器结构提前规划存储好就能对我们游戏中的遍历更加快速便捷,另外每种类型的Entity 的 添加,移除,统一由一个helper来维护,对以后扩展和维护会更加的便捷也减少了重复代码的出现。

 9、未来规划,前文也提到了其实我在设计之初就由打算将逻辑层未来准备移植到 单独线程,或者服务器上单独运行做战斗校验。这个方案其实我之前在很早之前的cocos+lua的游戏中有尝试过是没有任何问题的。只是目前由于项目进度赶而且目前游戏性能没任何问题所以还没做。如果有需求的同学我个人可以往这方面做。只要设计之初将逻辑层和ui层通message+观察者模式解耦就没有任何问题了。

五、关于AI实现方案

       对于战斗中AI的实现,我个人选择的是 behavior3 这个开源的行为树,为了能用ts编写有提示,我特意翻译了一个 ts版本的有需要的朋友可以看看 https://github.com/xzben/behavior3_ts 这里面有库的ts实现代码和行为树编辑的一个web编辑器,需要自己搭建nignx服务器部署。

         目前在我们这个游戏中使用行为树,基本上实现AI是真的很方便,我只要实现了产品需要AI具备的一些行为Action,和AI需要思考选择用的 Condtion 然后行为的编辑交给产品自己编辑就OK了。爽歪歪有没有,产品再也不会经常来烦我改一些AI的细节流畅问题,有木有!

六、关于游戏配置表工具

        配置表工具我选择的是skynet社区一个网络分享的exec导表工具 https://github.com/yanghuan/proton  ,能方便的导出json,lua,等格式的配置表。但是使用过程中发现这个工具的功能还是不够灵活所以我又补充了一些新的配置方法的。有需要的可以前往下载

https://github.com/xzben/excel_config

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Cocos Creator采用JavaScript/TypeScript语言开发,本质上是单线程的。如果在同一帧加载或销毁过多的资源,可能会导致界面卡顿。为了解决这个问题,可以采用分帧加载的方法。 一个常见的分帧加载的例子是在点击按钮时创建1000个预制体,并获取预制体上的脚本并赋值。可以使用for循环来逐个创建预制体,并在每个预制体创建完毕后进行下一帧的加载。具体代码如下: ```javascript this.mask.active = true; this.animation.node.active = true; this.animation.play(); let count = 0; let totalCount = 1000; let batchSize = 10; // 每帧加载的数量 function loadBatch() { for (let i = 0; i < batchSize; i++) { if (count >= totalCount) { break; } let temp = cc.instantiate(this.hello); temp.parent = this.content; temp.getComponent('test2').label.string = count; count++; } if (count < totalCount) { requestAnimationFrame(loadBatch); } else { this.mask.active = false; this.animation.stop(); this.animation.node.active = false; } } requestAnimationFrame(loadBatch); ``` 在这个例子中,我们使用了`requestAnimationFrame`函数来实现分帧加载。首先,我们将加载动画打开,并播放加载动画。然后,我们使用一个计数器`count`来记录已经加载的预制体数量。在每一帧中,我们使用一个循环来创建一批预制体,并将计数器递增。如果还有剩余的预制体需要加载,我们使用`requestAnimationFrame`函数来请求下一帧的加载。当所有预制体都加载完毕后,我们关闭加载动画。 通过这种分帧加载的方式,可以避免在同一帧中加载过多的资源,从而提高界面的流畅性。 #### 引用[.reference_title] - *1* [Cocos Creator 分帧加载(js - schedule)](https://blog.csdn.net/qq_14965517/article/details/107846189)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [Cocos Creator 2.1.1 性能优化 ()](https://blog.csdn.net/weixin_43995470/article/details/103443928)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值