比如说你在游戏世界中移动角色。在中心服务器模型中,物理模拟只会在服务器执行。客户端告诉服务器,角色要往哪个方向移动。服务器会执行寻路而且开始移动角色。服务器紧接着就会尽可能频繁地告知每个客户端该角色的位置。对于游戏世界中的每个角色都要运行这样的过程。对于实时策略游戏来说,同步成千上万的单位在中心服务器模型中几乎是不可能的任务。

在帧同步模型中,在用户决定移动角色之后,就会告诉所有客户端。每个客户端都会执行寻路以及更新角色位置。只有用户输入的时候才需要通知每个客户端,然后每个客户端都会自己更新物理以及位置。

这个模型带来了一些问题。每个客户端的模拟都必须执行得一模一样。这意味着,物理模拟必须执行同样的更新次数而且每个动作都需要同样的顺序执行。如果不这么做,其中一个客户端就会跑在其他客户端之前或者之后,然后在新的命令发出之后,跑得太快或者太慢的客户端走出的路径就会不同。这些不同会根据不同的游戏玩法而不同。

另一个问题就是跨不同的机器和平台的确定性问题。计算上很小的不同都会对游戏造成蝴蝶效应。这个问题会在后续的文章中讲到。

这里的实现方案灵感来自于这篇文章:《1500个弓箭手》。每个玩家命令都会在后续的两个回合中执行。在发送动作与处理动作之间存在延迟有助于对抗网络延迟。这个实现还给我们留下了根据延迟以及机器性能动态调整每回合时长的空间。这部分在这里先不讨论,会在后续文章再说。

对于这个实现,我们有如下定义:

帧同步回合

帧同步回合可以由多个游戏回合组成。玩家在一个帧同步回合执行一个动作。帧同步回合长度会根据性能调整。目前硬编码为200ms。

游戏回合

游戏回合就是游戏逻辑和物理模拟的更新。每个帧同步回合拥有的游戏回合次数是由性能控制的。目前硬编码为50ms,也就是每次帧同步回合有4次游戏回合。也就是每秒有20次游戏回合。
动作

一个动作就是玩家发起的一个命令。比如说在某个区域内选中单位,或者移动选中单位到目的地。

注意:我们将不使用unity3d的物理引擎。而是使用一个确定性的自定义引擎。在后续文章中会有实现。

游戏主循环

Unity3d的循环是运行在单线程下的。可以通过在这两个函数插入自定义代码:

  • Update()
  • FixedUpdate()

Unity3d的主循环每次遍历更新都会调用Update()。主循环会以最快速度运行,除非设置了固定的帧率。
FixedUpdate()会根据设置每秒执行固定次数。在主循环遍历中,它会被调用零次或多次,取决于上次遍历所花费的时间。FixedUpdate()有着我们想要的行为,就是每次帧同步回合都执行固定时长。但是,FixedUpdate()的频率只能在运行之前设置好。而我们希望可以根据性能调节我们的游戏帧率。

游戏帧回合

这个实现有着与FixedUpdate()在Update()函数中执行所类似的逻辑。手机号码转让主要不同的地方在于,我们可以调整频率。这是通过增加”累计时间”来完成的。每次调用Update()函数,上次遍历所花费的时间会添加到其中。这就是Time.deltaTime。如果累计时间大于我们的固定游戏回合帧率(50ms),那么我们就会调用gameframe()。我们每次调用gameframe()都会在累计时间上减去50ms,所以我们一直调用,知道累计时间小于50ms。

  1. private float AccumilatedTime = 0f;
  2. private float FrameLength = 0.05f; //50 miliseconds
  3. //called once per unity frame
  4. public void Update() {
  5. //Basically same logic as FixedUpdate, but we can scale it by adjusting FrameLength
  6. AccumilatedTime = AccumilatedTime + Time.deltaTime;
  7. //in case the FPS is too slow, we may need to update the game multiple times a frame
  8. while(AccumilatedTime > FrameLength) {
  9. GameFrameTurn ();
  10. AccumilatedTime = AccumilatedTime - FrameLength;
  11. }
  12. }

复制代码

我们跟踪当前帧同步回合中游戏帧的数量。每当我们在帧同步回合中达到我们想要的游戏回合次数,我们就会更新帧同步回合到下一轮。如果帧同步还不能到下一轮,我们就不能增加游戏帧,而且我们会在下一次同样执行帧同步检查。

  1. private void GameFrameTurn() {
  2. //first frame is used to process actions
  3. if(GameFrame == 0) {
  4. if(LockStepTurn()) {
  5. GameFrame++;
  6. }
  7. } else {
  8. //update game
  9. //...
  10. GameFrame++;
  11. if(GameFrame == GameFramesPerLocksetpTurn) {
  12. GameFrame = 0;
  13. }
  14. }
  15. }

复制代码

在游戏回合中,物理模拟会更新而且我们的游戏逻辑也会更新。游戏逻辑是通过接口(IHasGameFrame)来实现的,而且添加这个对象到集合中,然后我们就可以进行遍历。

  1. private void GameFrameTurn() {
  2. //first frame is used to process actions
  3. if(GameFrame == 0) {
  4. if(LockStepTurn()) {
  5. GameFrame++;
  6. }
  7. } else {
  8. //update game
  9. SceneManager.Manager.TwoDPhysics.Update (GameFramesPerSecond);
  10. List<IHasGameFrame> finished = new List<IHasGameFrame>();
  11. foreach(IHasGameFrame obj in SceneManager.Manager.GameFrameObjects) {
  12. obj.GameFrameTurn(GameFramesPerSecond);
  13. if(obj.Finished) {
  14. finished.Add (obj);
  15. }
  16. }
  17. foreach(IHasGameFrame obj in finished) {
  18. SceneManager.Manager.GameFrameObjects.Remove (obj);
  19. }
  20. GameFrame++;
  21. if(GameFrame == GameFramesPerLocksetpTurn) {
  22. GameFrame = 0;
  23. }
  24. }
  25. }

复制代码

IHasGameFrame接口有一个方法叫做GameFrameTurn,它以当前每秒游戏帧的个数为参数。一个具体的带游戏逻辑的对象应该基于GameFramesPerSecond来计算。比如说,如果一个单位正在攻击另一个单位,而且他攻击频率为每秒钟10点伤害,你可能会通过将它除以GameFramesPerSecond来添加伤害。而GameFramesPerSecond会根据性能进行调整。

IHasGameFrame接口也有属性标记着结束。这使得实现IHasGameFrame的对象可以通知游戏帧循环自己已经结束。一个例子就是一个对象跟着路径行走,而在到达目的地之后,这个对象就不再需要了。

帧同步回合

为了与其他客户端保持同步,每次帧同步回合我们都要问以下问题:

  • 我们已经收到了所有客户端的下一轮动作了吗?
  • 每个客户端都确认得到我们的动作了吗?

我们有两个对象,ConfirmedActions和PendingActions。这两个都有各自可能收到消息的集合。在我们进入下一个回合之前,我们会检查这两个对象。

  1. private bool NextTurn() {
  2. if(confirmedActions.ReadyForNextTurn() && pendingActions.ReadyForNextTurn()) {
  3. //increment the turn ID
  4. LockStepTurnID++;
  5. //move the confirmed actions to next turn
  6. confirmedActions.NextTurn();
  7. //move the pending actions to this turn
  8. pendingActions.NextTurn();
  9. return true;
  10. }
  11. return false;
  12. }

复制代码

动作

动作,也就是命令,都通过实现IAction接口来通信。有着一个无参数函数叫做ProcessAction()。这个类必须为Serializable。这意味着这个对象的所有字段也是Serializable的。当用户与UI交互,动作的实例就会创建,然后发送到我们的帧同步管理器的队列中。队列通常在游戏太慢而用户在一个帧同步回合中发送多于一个命令的时候用到。虽然每次只能发送一个命令,但没有一个会忽略。

当发送动作到其他玩家的时候,动作实例会序列化为字节数组,然后被其他玩家反序列化。一个默认的”非动作”对象会在用户没有执行任何操作的时候发送。而其他则会根据特定游戏逻辑而定。这里是一个创建新单位的动作:

  1. using System;
  2. using UnityEngine;
  3. [Serializable]
  4. public class CreateUnit : IAction
  5. {
  6. int owningPlayer;
  7. int buildingID;
  8. public CreateUnit (int owningPlayer, int buildingID) {
  9. this.owningPlayer = owningPlayer;
  10. this.buildingID = buildingID;
  11. }
  12. public void ProcessAction() {
  13. Building b = SceneManager.Manager.GamePieceManager.GetBuilding(owningPlayer, buildingID);
  14. b.SpawnUnit();
  15. }
  16. }

复制代码

这个动作会依赖于SceneManager的静态引用。如果你不喜欢这个实现,可以修改IAction接口,使得ProcessAction接收一个SceneManager实例。

Unity3D中实现帧同步 (一):对抗延迟相关推荐

  1. 网络游戏中的帧同步与状态同步

    帧同步的基础概念 相同的输入 + 相同的时机 = 相同的输出. 客户端发送操作信息到服务器,服务器收到后转播给所有的客户端,客户端接收服务器的操作信息后计算游戏行为的结果, 然后通过广播下发游戏中各种 ...

  2. 网络游戏之帧同步物理模拟

    笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,已出版书籍:<手把手教你架构3D游戏引擎>电子工业出版社和<Unity3D实战核心技术详解 ...

  3. 【面经】腾讯U3d面试面经 帧同步方向(总)

    近期拿到了一个腾讯的offer,记录一下面试过程. 我找的内推,面试的流程如下: 上传内推简历,接着马上被HR转到项目组里面,一个小时左右面试官电话过来约面,接着电话面,然后去科兴面,最后HR面,OF ...

  4. 网游帧同步的分析与设计

    网游帧同步的分析与设计 https://zhuanlan.zhihu.com/p/105390563?utm_source=wechat_session 今年的春节非同寻常,由于新肺炎疫情的蔓延,假期 ...

  5. 比特同步和帧同步的区别

    在数据通信中最基本的同步方式就是"比特同步"(bit synchronization)或位同步.比特是数据传输的最小单位.比特同步是指接收端时钟已经调整到和发送端时钟完 全一样,因 ...

  6. Unity3D RTS游戏中帧同步实现

    帧同步技术是早期RTS游戏常用的一种同步技术,本篇文章要给大家介绍的是RTX游戏中帧同步实现,帧同步是一种前后端数据同步的方式,一般应用于对实时性要求很高的网络游戏,想要了解更多帧同步的知识,继续往下 ...

  7. 网络同步在游戏历史中的发展变化(二)—— Lockstep与帧同步

    前言: 网络同步属于游戏开发中比较重要且复杂的一部分,但是由于网上的资料内容参差不齐,很多人直接拿别人的结论写文章,导致很多人对这一块的很多概念和理解都是错误的.本文参考了大量的相关论文和资料(三十篇 ...

  8. 游戏中的网络同步机制(二) 王者荣耀对帧同步的应用

    转载自:https://www.jianshu.com/p/81050871cce7 参考 解密:腾讯如何打造一款实时对战手游 从<王者荣耀>来聊聊游戏的帧同步 <王者荣耀>技 ...

  9. 游戏中的网络同步机制(一)帧同步Lockstep

    转载自:https://www.jianshu.com/p/64b3f162dcf4 参考游戏中的网络同步机制--Lockstep 一.前言 每个人或多或少都接触过网游,那个虚拟的世界给予了我们无穷的 ...

最新文章

  1. jenkins 流水线(pipline)
  2. 【转】小白级的CocoaPods安装和使用教程
  3. visual studio 2015安装 无法启动程序,因为计算机丢失D3DCOMPILER_47.dll 的解决方法
  4. 百度知道回答的依赖注入
  5. JNI开发笔记(七)--aar库的生成和调用
  6. 【论文】哈工大SCIR Lab | EMNLP 2019 基于BERT的跨语言上下文相关词向量在零样本依存分析中的应用...
  7. 基于ADS仿真的465khz检波电路
  8. Android域名解析优先ipv6,IPv6 域名解析原理及编程实现
  9. 74HC595引脚图时序图工作原理及pdf中文资料lsh
  10. 探码SaaS帮助企业开展数字化营销之路!
  11. 百词斩不复习_有人用过百词斩和不背单词两款背单词app吗?良心推荐哪一个好一点?...
  12. C 语言中结构体中成员所占内存的大小
  13. 永洪科技贺新颖:业务中台+数据中台,赋能企业核心业务
  14. 在V2EX的开发环境里尝试了一下OneAPM @livid
  15. 你不知道的电脑36个小技巧(纪念2011教师节)
  16. vue自定义弹窗dialog,vue 点击遮罩层功能区以外的地方关闭遮罩层
  17. 欲登千层楼,又何惧寒风
  18. c语言统计英文字母频率,C语言实现英文文本词频统计
  19. Android 分割线
  20. 阿里云【名师课堂】Java零基础入门24 ~ 26:方法的定义与使用

热门文章

  1. sklearn聚类之OPTICS算法
  2. 极速office(PPT)文字如何设置加粗
  3. 蓝桥杯 模板Template Part9:PCF8591 ADC/DAC
  4. 30ea是什么意思_数量单位EA是什么意思?EACH? 单位EA是什么意思
  5. mysql 简述pk uk fk 的区别和对数据库性能的影响_SQL Server 数据库中PK,UK, DF, CK, FK是什么意思?...
  6. 125w短波通信距离_超短波通信距离浅析
  7. 教你怎么用c++基本语法实现一个简单的五子棋小游戏
  8. 【推荐】智慧检察公益诉讼辅助快检AI人工智能大数据平台解决方案合集(共183份,928M)
  9. Linux——shell脚本
  10. 数据结构——树和二叉树