课程里的版本好像是1.9,目前使用版本为3.8.3
开始~
状态同步
一般游戏的同步策略有两种:状态同步和帧同步
如下时序图为状态同步
- c1操作从a走到b再走到c,因为不必关注细节所以c1将a走到c这个信息告诉给服务端
- 服务端验证这个操作是否合规,将验证结果告诉回c1
- 同时服务端还将这个信息转发给了c2和c3
- c2网络比较快,立刻收到了这条数据,根据插值模拟,模拟出c1从a走到c的动作(当然如果请求频率够快将a到b和b到c都告诉了c2,那就模拟的更精细)
- c3因为网络较慢,过了很久才收到服务端的数据,此时c1已经做下一个动作了,那c3就会采取一个瞬移或加速的方式模拟c1的移动
优点:
- 容易断线重连(因为数据都在服务端)
- 容易防外挂(因为逻辑都在服务端)
- 简单粗暴(客户端只需获取服务端数据并展示)
缺点:
- 服务器压力大(需要承担很大的计算量)
- 流量大(数据均由服务端传送过来)
- 不同玩家屏幕表现不一样
帧同步
帧同步的计算全部在客户端,服务端只负责做每帧收集操作,合并操作并且转发
那么问题来了,怎么保证所有客户端结果相同?
相同输入 + 相同逻辑 = 相同结果
逻辑层和显示层的分离
- 同一帧,c1想要移动,c2想要进行攻击,他们将数据都发送给了服务端
- 服务端只进行合并转发,给两个客户端
- c1收到了c1想要移动,c2想要攻击的数据,并进行模拟
- c2同样收到了c1想要移动,c2想要攻击的数据,并进行模拟
- 下一帧中客户端都没有操作,没有数据传给服务端,服务器同样进行发送操作
注意:
- 现代网游用这种模式,当c2掉线了,服务端仍然逐帧执行,c1并没有受到影响
- 而老代网游模式,每一帧客户端都需要给服务端传送数据(没操作也要传),当c2掉线后,服务端发现收不到c2的数据就会锁帧,所有客户端都需要等待,直到再次所有客户端的数据
- 老老代网游,如果有一个客户端网速很慢,客户端就会拉长帧时间去保证每个客户端的帧更新是同步的,会导致网速快的客户端也很慢
优点:
- 流量低(所有逻辑都在客户端)
- 方便做录像功能(直接客户端再次执行运算逻辑即可)
- 服务器逻辑简化
缺点:
- 有可能作弊
- 需要保证各端运算结果一致性(比如随机数,无限小数等)
- 实时性要求比较高(帧同步的特点就是每秒同步很多次)
- 断线重连需要重新跑所有逻辑(可优化)
状态同步和帧同步比较
帧同步客户端
客户端职责:
- 玩家操作不直接处理,而是发送给服务器
- 收到服务器一帧数据,模拟一帧
(例:玩家操作向右移动5步,将移动信息发送给服务器,服务器直接分发,各客户端计算5步后位置,进行渲染)
[代码实现]在下面👇
帧同步服务端
具体步骤可以看之前的文章
还是再总结一下:
快速开始NodeJs项目
npm init -y
安装TSnpm install typescript --save-dev
安装WSnpm install ws --save-dev
安装提示npm install @types/ws --save-dev
生成tsconfig.jsontsc --init
安装即时编译npm install ts-node --save-dev
添加nodemonnpm install nodemon --save-dev
服务端职责:
- 把玩家加入到一局游戏中,下发初始消息
- 每一帧收集操作,合包转发
ECS框架概念
ECS的解释
- E - Entity 实体
- C - Component 组件
- S - System 系统
ECS的特点
- 网络-数据-逻辑-显示层相互独立
- C/S端可以运行同一份逻辑代码,方便验证作弊
服务端没有creator内api,逻辑显示放在一起的话,服务端跑不了 - 可以方便的进行预测和回退
Entity
- 每个Entity对象都有不同的EntityId
- Component挂在的对象,同种类型的Component只能拥有一个
- 提供Component的增删查和备份
Component
- Component只包含数据,不能拥有逻辑函数
- Component挂载在Entity上
- ComponentId使用不同的位来区分(因为Component操作比较频繁,所以需要使用位运算效率优化)
位运算
System
- System只拥有逻辑,不能包含数据
- System拥有不同Type,比如逻辑帧System,渲染帧System
- 依靠SystemType来确定执行顺序和每帧执行次数
- onUpdate函数由系统自动触发,不能手动调用
World
- ECS世界的入口,单例
- 驱动System和Update
- 提供Entity的增删查功能
- 使用forEach根据ComponentId组合来遍历Entity
理解
看了第二遍稍微有些理解了(不一定对),做下笔记
渲染帧执行即为cocos的Component中的update执行
我们的最终需求就是要让逻辑帧在不同终端上同时间内执行的次数相同
如下 为渲染帧驱动逻辑帧调用的情况
-
理想情况下,每一次渲染帧的周期是固定的(33.3ms执行一次),每执行一次渲染帧时只需执行两次逻辑帧,即可满足每秒执行60帧的需求
-
而现实情况是,渲染帧的周期是不固定的,且每台设备的帧率也不一样
所以如果还是按每执行一次逻辑帧就执行两次逻辑帧,那就会导致执行的逻辑帧数不同
20ms时逻辑帧执行次数计算: Math.floor((20 - 0) / 16.6) = 1
68ms时逻辑帧执行次数计算:
首先计算出上次剩余的时长:20 - Math.floor((20 - 0) / 16.6) * 16.6 = 3.4;
再将剩余时长加上周期时长:3.4 + (68 - 20) = 51.4;
计算次数:Math.floor(51.4 / 16.6) = 3
如果我们不把剩余时长算上那结果就是2,133.3ms时结果就是3,1+ 2+ 3 = 6,会导致执行逻辑帧的次数不一样
【当然,100ms内执行的逻辑帧次数可能会不一样,但只要保证大时间段内(比如1s内)执行次数相同即可,肉眼看不出来的】
ECS实现
逻辑帧&渲染帧
如下图,为一帧执行的内容和不同机器的运行速度
- 我们期望1s内能执行60帧,每一帧执行一个逻辑帧和一个渲染帧,那就需要每一帧耗时不超过16.6ms
- 高性能机器每一帧耗时16ms(8+8),可以满足1s执行60帧
- 低性能机器每一帧耗时24ms(12+12),无法满足1s执行60帧,那就会优先执行逻辑帧,不执行渲染帧,去满足1s执行60次逻辑帧,保证高低性能机的逻辑次数一致,以确保执行结果相同
ECS框架使用
实现和使用放在一起了
使用ECS实现一个简单的demo(箭头旋转前进)
实现代码