• 了解多人游戏下的客户端与服务器体系结构


    直连

    直连模式下,选择一个玩家充当服务器(房主)。如果游戏出现不同步,那么均按房主的世界来,玩家1可以作弊修改其游戏来影响其他玩家的世界

    针对两个玩家来说,直连连接质量更好,延迟小

    如果玩家数量很多,不同玩家间的通信则需要靠房主为中介,那通信质量与房主主机配置、网络情况有很大关系


    专用服务器

    所有玩家与专用服务器通信,专用服务器通常运行没有游戏界面的游戏代码的修改版本,因此可以在配置较低的计算机上运行,还可以检测作弊,服务器维护的游戏世界是最权威的


    ① 客户端和服务器

    客户端全听服务器的

    游戏状态由服务器单独管理,客户端只把操作(按键,指令)发送给服务端,服务器定期更新游戏状态,将新的状态发送给客户端,客户端只需要在屏幕上渲染即可

    可以预防大范围作弊

    • 玩家本地修改生命值为99999,服务器那依旧是10,玩家依旧会收到死亡事件消息
    • 玩家本地修改位置,服务器处理玩家向右移动一个单位,玩家位置被同步修复

    应用举例:慢节奏游戏,如策略或卡牌


    延迟问题

    网络数据传输要经过大量路由器,还可能遇到网络拥塞情况,远距离传输延迟可能会很高(100~500ms)

    玩家发送一个向右移动一格指令,花费了100ms传到服务器,服务器处理状态,再花费100ms将状态传给玩家

    在玩家看来,按下了右键后有0.2秒的时间游戏没有任何反应,然后角色才向右移动一格,这是能明显感知到的卡顿,严重时玩家无法进行游戏


    ② 客户端预测和服务器对账

    客户端预测

    大部分玩家的输入都是按预期效果执行的。 输入 = 操作 = 行为 = 动作 = 鼠标点击移动,键盘按下释放等

    如果给定游戏状态和输入操作,客户端能完全预测游戏世界的变化(跟服务器一样的处理),且只有唯一一种结果(绝对性)

    我们可以将输入操作发送给服务端,先不等服务端返回,在客户端立即生效预测的结果,这种方式消除了输入操作和状态变化之间的延迟,而且客户端预测的结果大多是正确的,能跟服务器返回的结果相匹配。

    假设有100ms的网络延迟(往返),移动位置的动画需要100ms,整个操作需要200ms,这是方案①导致的结果

    使用方案②后,客户端检测到输入预测玩家位置并更新,动画播放和网络请求同步进行,原来的200ms现在只需要100ms。服务器返回的结果与客户端预测的结果一致。注:服务器依旧是权威的,维护着所有玩家真实的状态


    同步问题

    假设有250ms的网络延迟,玩家按了两次右键,向右移动两次,客户端立即模拟,间隔地向服务器发送两次"向右移动"操作

    两次移动均完成后过50ms才收到服务器传来的第一次“向右移动”操作的结果,此时出现状态冲突

    由于服务器权威性,客户端根据“真实的状态”重新设置了角色位置(角色居然跳回去了),然后又跳回来


    服务器对账

    对账:会计先做个手工表,每算一会把系统算的表拿出来核对,如果不对就纠正手工表,然后接着算

    要修复同步问题需要知道服务器可能没有处理完客户端的所有输入,客户端的状态是现在时,而服务器传来的状态是过去某一时刻的,两者有一定的时间差

    可以为客户端的每个输入添加一个数字标记,按照输入的顺序递增这个标记。客户端发送的两个输入分别被标记了#1、#2,且保存了各自的副本。下面的例子演示了一个客户端和一个服务端的网络交互

    服务器每发来一个真实状态,客户端就重新模拟一次:当t=250ms,客户端接收到服务器对#1输入的结果时,客户端丢弃#1输入的副本,并在#1的结果上根据#2副本重新计算当前游戏状态。计算后与当前状态对比,如果有差异则重新设置

    当t=350ms,客户端接收到服务器对#2输入的结果时,丢弃#2输入的副本,此时因为输入副本队列为空,不需要重新模拟。只需要将结果与当前状态对比,如果有差异则重新设置

    应用:在回合制战斗中,玩家A攻击另一个角色B时,可以优先显示血液效果和造成伤害的数字,但不应该在服务器返回结果前更新角色健康状态(不是绝对的,可能有多种结果,如这一击是否把角色B打断了腿,或者是角色B使用医疗包的事件比攻击早)

    不容易逆转:如果A可以预测并更新健康状态、角色B打医疗包的事件又在A攻击之前,此时A客户端还没有受到角色B健康状态的更新,A客户端预测B生命值降至0,触发角色死亡事件(可能销毁了这个实体),而实际上角色还在服务器那活得好好的,那还原前一步就十分复杂了

    客户端预测的结果与服务器预测的结果不匹配问题,在多个客户端情况下经常发生


    ANTI WEB SPIDER BOT www.cnblogs.com/linxiaoxu

    ③ 实体插值(平滑插值)

    服务器时间步长

    考虑方案②,当大量客户端接入一个服务器时,服务器将收到大量的操作(按键、鼠标)输入,每接收一个输入就要更新当前世界并广播游戏状态,需要消耗大量的CPU和带宽

    最好的办法是整个游戏世界以低频率周期性地更新,例如每秒10次,每次更新延迟为100ms,称为时间步长

    把接收到的输入全部放入队列中,不做处理(个人认为接收到输入直接处理也行)。每100ms,处理队列,将更新后的状态广播给每个客户端

    在这种情况下,游戏世界以可预测的速率独立于客户端输入的存在、输入数量进行更新


    航位推算

    航位推算是通过使用先前确定的位置或定位,并结合对速度、航向(或方向或航向)和经过时间的估计,来计算移动物体的当前位置的过程。

    多人模式下,在实体方向和速度都会立即改变的情况下,航位推算的结果是不准确的。比如多人赛车,服务器每100ms将其他车辆的位置、速度、方向状态传递给客户端,客户端仅能根据这些信息来模拟赛车运动100ms,最后模拟得到的位置跟服务器下一次发来的位置有较大的差别,位置纠正,汽车瞬移。

    详细的说(省略了对账过程):

    • 有玩家A(0,0)跟玩家B(0,0),服务器C
    • C告诉A:B正在往左移动,速度100m/s,位置是(0,0)
    • C告诉B:A现在没移动,位置是(0,0)
    • 50ms后中B开始(往右移动,速度瞬间变成1000m/s);这两个事件被加入了C的队列
    • 又过了50ms,到现在 t = 100ms
    • B在A上渲染的位置(-10,0)
    • B自己渲染的位置(-5+50,0)
    • C处理队列,将状态发送给A跟B
    • C告诉A:B正在往右移动,速度1000m/s,位置是(45,0)
    • C告诉B:A现在没移动,位置是(0,0)
    • A发现服务器给的B位置与自己预测的位置有出路
    • A修复B的位置,B被瞬移了

    玩家自身跟服务器通信是不会出现这种问题的,因为玩家操作的实体是实时的,没有延迟;其他玩家不是实时的,同步数据都是服务器给的,即其他实体相关信息的一种稀疏性


    实体插值

    由于玩家的方向和速度都会立即改变,航位推算无法应用。如FPS,玩家通常以非常高的速度奔跑、蹲比和转弯,这使得航位推算毫无用处,因为无法再根据之前的状态准确地预测位置和速度。

    为了给玩家带来连续性和流畅移动的错觉,采用一种巧妙的做法

    每个玩家本身是现在时,而其他玩家都是过去式:玩家自己是实时的,而看到的其他玩家都是他们过去某一时刻的状态。

    将服务器最新发来的状态记P1,前一个时间步长的状态记P2(旧状态)客户端在本地,那接下来一个时间步长内整个世界状态将线性从P2变为P1

    也就是说,你比其他所有人都快了一个时间步长,其他人比你都慢一拍

    下图很好解释了线性插值,v=(11.75,10)意味着客户端2在收到P1后已经过了75ms(时间步长100ms,线性插值的步长也是100ms),当时间过了100ms,客户端2的世界状态将完全变为P1,此时可能已经有了新的P1或者还没收到P1,我们可以根据网络延迟动态修改线性插值的步长


    目前的功能

    • 客户端在本地发送输入并模拟效果
    • 服务器从所有客户端获取带有时间戳的输入
    • 服务器处理输入并更新世界状态
    • 服务器向所有客户端发送服务器世界状态的快照
    • 客户端接收服务器发来的世界状态更新
      • 根据这个状态与没被服务器处理的输入 重新模拟
      • 对其他实体状态进行线性插值

    ④ 延迟补偿

    对时间和空间敏感的事件来说,比如射击事件,当玩家向另一名玩家射击时,由于其他玩家都是过去的玩家,所以你的瞄准延迟为100毫秒,你在对100毫秒延迟之前的敌人射击。

    通用解决方法 服务器根据射击事件的时间戳,重建该时间戳时的世界状态,可以准确地知道你开枪的那一刻准星瞄准的实体

    但由于是过去式,在敌人看来,100ms之后可能已经移动到掩体之后,却依旧被爆头了,不过这个解决方案已经很不错了


    ⑤ 帧同步

    帧同步

    该部分还没讲全,未来某天补上代码

    状态同步讲完,接下来讲主流同步方式的另外一种:帧同步。通常用于实时战略和FPS

    帧同步通过同步玩家的动作,确保每个人都能获得相同的输入,并在每一帧上执行相同的逻辑,最终获得一致的性能和结果

    相同的输入 + 相同的时序 = 相同的输出

    如何确保同一时间点

    等待所有玩家加载完成,由于加载完成后还会有一系列初始化操作,可以播个开场动画,做到所有玩家都在同一时间点开始游戏

    同步设备时间

    客户端访问服务器,服务器返回一个ping值,乘以2加上服务器返回的时间就是准确的当前服务器时间。游戏期间后续同步中根据较小的ping值修改时间

    同步种子

    游戏里经常会使用随机数,同步随机数种子可以保证各个客户端模拟的一致性

    命令同步

    服务器每帧收集所有玩家操作,然后将其广播给所有玩家,没有玩家操作就广播一个空指令,向前推动游戏帧

    核心逻辑-命令队列

    命令队列的设计可以轻松实现战斗回放。创建两种侦听器,分别是本地模式和网络模式

    • 本地模式下侦听玩家的操作并将操作填充到队列中
    • 网络模式下侦听玩家的操作并发送给服务器,同时监视服务器发来的数据并将操作填充到队列中

    核心逻辑-游戏主循环识别

    帧同步需要我们严格控制整个游戏的执行顺序,通常情况下,不能直接使用引擎更新,需要把一切掌握在自己手中。首先需要控制的是帧速率

    • 以特定的帧率来运行游戏,如每秒60帧
    • 跟踪帧进度并控制,如果当前设备帧索引落后过多,加快它的帧率

    对象的更新应当是按特定顺序执行的,需要进行排序

    网络延迟

    添加帧缓冲区和前滚动画,用UDP取代底层TCP如KCP

    由于TCP超时重传机制。没有收到一帧的数据包时,游戏的逻辑无法正常执行,直到数据包被重新发送

    或者直接帧锁定,直到有数据来,以超快的帧率同步

    不用帧锁定,客户端请求服务器状态副本,实现回滚跟重试然后恢复

    重新连接

    如果是一个小的重新连接,只丢失了几帧数据,会用这几帧的数据进行补充。如果是一个大型重新连接,服务器序列化的数据此时将缓存5秒。如果在这段时间内重复断开连接并重新连接,服务器将重用这些缓存的数据。

    优点

    使用帧同步可以节省消息量,状态同步需要服务器对每个客户端发送大量状态信息(大量实体,每个实体各自维护大量字段),帧同步只需要发送操作指令和帧索引

    由于消息量得到了节省,在网络情况不佳的情况下,也能实现实时战斗游戏的同步问题

    我们可以轻松实现回放,服务器记录所有操作,客户端请求回放文件执行每一帧


    参考资料

    Client-Server Game Architecture - Gabriel Gambetta

    Networking (part 2) · GitBook (rvagamejams.com)

    Game server synchronization of large amounts of data in a battle (monstar-lab.com)

    Tutorial: Technical Implementation Details of Frame Synchronization in Games


    ENet

    ENet是LOVE使用的一个第三方网络库,采用UDP协议,在运输层帮我们完成了各种事情,包括消息确认

    带心跳检测功能,当有一方不回复超过5~30秒时则认为其disconnect


    __EOF__

  • 本文作者: 小能正在往前冲
  • 本文链接: https://www.cnblogs.com/linxiaoxu/p/17647493.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    【愚公系列】2022年08月 Go教学课程 038-异常处理
    FPGA面试笔试一些基础概念题目2
    FullGC频繁,线程数持续增长排查
    Crack: SpreadJS v16.2.2 本地/在线设计器同步版本
    ElasticSearch 7配置密码认证及创建用户
    Redis可以干什么
    成人高考的入学考试有哪几门-大专升本科的那种
    Java实验六
    Git操作远程仓库及解决合并冲突
    LinkedList与链表
  • 原文地址:https://www.cnblogs.com/linxiaoxu/p/17647493.html