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

一.背景

帧同步技术是早期RTS游戏常用的一种同步技术。与状态同步不同的是,帧同步只同步操作,其大部分游戏逻辑都在客户端上实现,服务器主要负责广播和验证操作,有着逻辑直观易实现、数据量少、可重播等优点。

部分PC游戏如帝国时代、魔兽争霸3、星际争霸等,Host(服务器或某客户端)只当接收到所有客户端在某帧输入数据后,才会继续执行,等待直至超时认为该客户端掉线。很明显,当部分客户端因网络或设备问题无法及时上传操作数据,会影响其它客户端的表现,造成不好的游戏体验。考虑到游戏公平竞争性,这种需要等待的机制是必需的,但并不符合手游网络环境的需求。为此,需要使用“乐观”模式,即是Host采集客户端上传操作并按固定频率广播已接收到的操作数据,不在乎部分客户端的操作数据是否上传成功,且不会影响到其它客户端的游戏表现,如图1所示。

(图1)

二.剖析Unity3D

帧同步技术最基础的核心概念就是相同输入,经过相同计算过程,得出相同计算结果。按照该概念,下面将简单描述Unity3D实现帧同步时所需要改造的一些方面,Unity3D中脚本生命周期流程图如图2所示。

(图2)

帧同步需要避免使用本地计时器相关数值。因此,使用Unity3D实现帧同步的过程所需注意的几点:

1. 禁用Time类相关属性及函数,如Time.deltaTime等。而使用帧时间(第N帧 X 固定频率)

2. 禁用Invoke()等函数

3. 避免在Awake()、Start()、Update()、LateUpdate()、OnDestroy()等函数中实现影响游戏逻辑判断的代码

4. 避免使用Unity3D自带物理引擎

5. 避免使用协程Coroutine

三.具体实现

对于本文的实现,有如下定义:

关键帧:服务器按固定频率广播的操作数据帧,使用唯一ID标识,主要包括客户端输入数据或服务器发送的关键信息(例如游戏开始或结束等消息)

填充帧:由于设备性能和网络延迟等原因,服务器广播频率不可能达到客户端的更新频率。若只使用关键帧来驱动游戏运作,就会造成游戏卡顿,影响体验。因此,除关键帧外,客户端需要自行添加若干空数据帧,以使游戏表现更为流畅

逻辑帧更新时间:客户端执行一帧所需时间,可根据设备性能和网络环境等因素动态变化

服务器帧更新时间:服务器广播帧数据的固定频率,一般用于帧间隔时间差的逻辑计算

3.1 主循环

帧同步要求相同的计算过程,这就涉及到两个方面,其一是顺序一致,Unity3D主循环不可控,需自定义游戏循环,统一管理游戏对象以及脚本的执行,确保所有对象更新与逻辑执行顺序完全一致。另一方面是结果一致,凡有浮点数参与的逻辑计算需要特殊处理。

  1. class MainLoopManager : MonoBehaviour
  2. {
  3. bool m_start;
  4. int m_logicFrameDelta;//逻辑帧更新时间
  5. int m_logicFrameAdd;//累积时间
  6.  
  7. void Loop()
  8. {
  9. ......//遍历所有脚本
  10. }
  11.  
  12. void Update()
  13. {
  14. if (!m_start)
  15. return;
  16.  
  17. if (m_logicFrameAdd < m_logicFrameDelta)
  18. {
  19. m_logicFrameAdd += (int)(Time.deltaTime * );
  20. }
  21. else
  22. {
  23. int frameNum = ;
  24. while(CanUpdateNextFrame() || IsFillFrame())
  25. {
  26. Loop();//主循环
  27. frameNum++;
  28. if (frameNum > )
  29. {
  30. //最多连续播放10帧
  31. break;
  32. }
  33. }
  34. m_logicFrameAdd = ;
  35. }
  36. }
  37.  
  38. bool CanUpdateNextFrame();//是否可以更新至下一关键帧
  39. bool IsFillFrame();//当前逻辑帧是否为填充帧
  40. }

3.2 自定义MonoBehaviour

Unity3D脚本生命周期中部分函数、Invoke、Coroutine调用时机与本地更新相关,并不满足帧同步机制的要求。我们通过继承MonoBehaviour类来实现上述函数和功能需求,并使所有涉及逻辑计算的组件都继承该自定义类。

  1. class CustomBehaviour : MonoBehaviour
  2. {
  3. bool m_isDestroy = false;
  4.  
  5. public bool IsDestroy
  6. {
  7. get { returnm_isDestroy; }
  8. }
  9.  
  10. public virtual void OnDestroy() {};
  11.  
  12. public void Destroy(UnityEngine.Objectobj)
  13. {
  14.  
  15. ......//销毁游戏对象
  16.  
  17. }
  18. }

3.2.1 Update()与LateUpdate()

从可控性和高效性两方面来看,不建议采用逐一遍历游戏对象获取CustomBehaviour的方式去调用Update()与LateUpdate(),而是单独使用列表来管理。

  1. delegate void FrameUpdateFunc();
  2. class FrameUpdate
  3. {
  4. public FrameUpdateFunc func;
  5. public GameObject ower;
  6. public CustomBehaviour behaviour;
  7. }
  8.  
  9. class MainLoopManager : MonoBehaviour
  10. {
  11. ......
  12. List m_frameUpdateList;
  13. List m_frameLateUpdateList;nn
  14.  
  15. public RegisterFrameUpdate(FrameUpdateFunc func, GameObject owner)
  16. public UnRegisterFrameUpdate(FrameUpdateFunc func, GameObject owner)
  17. public RegisterFrameLateUpdate(FrameUpdateFunc func, GameObject owner)
  18. public UnRegisterFrameLateUpdate(FrameUpdateFunc func, GameObject owner)
  19. void Loop()
  20. {
  21. //先遍历m_frameUpdateList
  22. //再遍历m_frameLateUpdateList
  23. }
  24. ......
  25. }

采取添加删除的方式,对组件是否需要执行Update()与LateUpdate()进行动态地管理,除了具有相对的灵活性,也保证了执行效率。

3.2.2 Invoke相关函数

Invoke、 InvokeRepeating、 CancelInvoke等函数需要使用C#中的反射机制,根据object对象obj和函数名methodName来获取MethodInfo如:

  1. var type = obj.GetType();
  2. MethodInfo method = type.GetMethod(methodName);

通过接口封装,组成相关数据(InvokeData),放入列表等待执行。

  1. class InvokeData
  2. {
  3. public object obj;
  4. public MethodInfo methodInfo;
  5. public int delayTime;
  6. public int repeatRate;
  7. public int repeatFrameAt;
  8. public bool isCancel = false;
  9. }

如上述结构,delayTime用于记录延迟执行时间,repeatRate代表重复调用的频率,repeatFrameAt则标记上次调用发生的帧序号,而isCancel标记Invoke是否被取消。最后,统一使用MethodBase.Invoke(objectobj, object[] parameters)执行调用。

  1. class MainLoopManager : MonoBehaviour
  2. {
  3. ......
  4. List m_invokeList;
  5.  
  6. void Loop()
  7. {
  8. //先遍历m_frameUpdateList
  9. //再遍历m_frameLateUpdateList
  10. //遍历m_invokeList,并根据相关属性分别进行Invoke、 InvokeRepeating、CancelInvoke
  11. }
  12. ......
  13. }

3.2.3 协程Coroutine

协程Coroutine较复杂,必需采用的情况较少,本文方案未实现协程Coroutine功能,而是避免使用。

3.2.4 Destroy相关

在Destroy游戏对象或组件后,OnDestroy()将在下一帧执行。因此,需要采取可控的方式代替OnDestroy()函数完成资源的释放。

  1. class CustomBehaviour : MonoBehaviour
  2. {
  3. bool m_isDestroy = false;
  4. public bool IsDestroy
  5. {
  6. set { m_isDestroy = value; }
  7. get { return m_isDestroy; }
  8. }
  9. public virtual void DoDestroy() {};
  10. public void Destroy(UnityEngine.Object obj)
  11. {
  12. if (obj.GetType() == typeof(GameObject))
  13. {
  14. GameObject go = (GameObject)obj;
  15. CustomBehaviour behaviours = go.GetComponents();
  16. for (int i = ; i < behaviours.Length; i++)
  17. {
  18. behaviours[i].IsDestroy = true;
  19. behaviours[i].DoDestroy();
  20. }
  21. }
  22. else if (obj.GetType() == typeof(CustomBehaviour))
  23. {
  24. CustomBehaviour behaviour = (CustomBehaviour)obj;
  25. behaviour.IsDestroy = true;
  26. behaviour.DoDestroy();
  27. }
  28. UnityEngine.Object.Destroy(obj);
  29. }
  30. }

3.3 Time类与随机数

帧同步游戏逻辑所有涉及时间的计算都应采用帧时间,即:当前帧序列数 * 服务器帧更新时间 /(填充帧数 + 1),而每帧随机数计算都由服务器下发种子来控制。如下:

  1. class MainLoopManager : MonoBehaviour
  2. {
  3. .......
  4. int m_serverFrameDelta;//毫秒
  5. int m_curFrameIndex;
  6. int m_fillFrameNum;
  7. int m_serverRandomSeed;
  8.  
  9. public int serverRandomSeed
  10. {
  11. get { return m_serverRandomSeed; }
  12. }
  13. public int curFrameIndex
  14. {
  15. get { return m_curFrameIndex; }
  16. }
  17. public static int curFrameTime
  18. {
  19. return m_curFrameIndex * m_serverFrameDelta / ( + m_fillFrameNum);
  20. }
  21. public static int deltaFrameTime
  22. {
  23. return m_serverFrameDelta / ( + m_fillFrameNum);
  24. }
  25. .......
  26. }

可写入CustomBehaviour中,便于自定义Time类的调用,避免误用Unity3D的Time类,Random类同理。

  1. class CustomBehaviour : MonoBehaviour
  2. {
  3. protected class Time
  4. {
  5. public static Fix time
  6. {
  7. get { return (Fix)MainLoopManager.curFrameTime / ; }
  8. }
  9.  
  10. public static Fix deltaTime
  11. {
  12. get { return (Fix)MainLoopManager.deltaFrameTime / ; }
  13. }
  14. }
  15.  
  16. protected class Random
  17. {
  18. public static Fix Range(Fix min, Fix max)
  19. {
  20. Fix diff = max - min;
  21. Fix seed = MainLoopManager.serverRandomSeed;
  22. return min + (int)FixMath.Round(diff * (seed / ));
  23. }
  24. }
  25. }

其中Fix是定点数,3.4小节会简单描述如何将定点数运用在Unity3D中。本文实现中约定随机种子范围在0-100之间,并采用简单的计算方式。如有特殊需求,自行实现。

3.4 定点数

客户端必须保证对网络帧操作的运算过程和结果一致,然而不同系统平台对浮点数的处理有差别,即便差别甚微,也会造成“蝴蝶效应”,导致不同步现象出现。绝大多数情况下,只需要对游戏对象方位进行定点数改造即可。而Unity3D并非开源游戏引擎,无法对底层transform的position和rotation进行修改。因此,逻辑层计算时需要使用到自定义以定点数为基础的position和rotation,并在每次循环结束之前,将自定义的方位逻辑计算之后所得信息转化Unity3D transform,以便Unity3D更新表现层。使用Unity3D的协程功能Coroutine以及WaitForEndOfFrame()可满足上述需求,即在逻辑层计算完成后,在Unity3D渲染之前更新底层transform的position和rotation。

3.5 网络波动

帧同步机制下,玩家输入发送到网络,所有响应都必须要等网络逻辑帧才能进行处理。理想环境下,网络帧操作接收到的频率是固定的,能保证客户端表现正常不卡顿。但事实是,绝大多数情况下网络都是不稳定的,时快时慢难以预测。最简单的方案就是建立一个网络逻辑帧的缓冲区,设置一个缓冲区上限,当存入缓存区的帧数满足上限之后,按照固定频率播放。若缓冲区变空,等待其重新填满。通过累积网络逻辑帧延迟,平均分布到固定频率,平滑处理了网络波动造成的卡顿。

原文地址:http://gad.qq.com/article/detail/7195472

Unity3D RTS游戏中帧同步实现的更多相关文章

  1. Unity3D 2D游戏中寻径算法的一些解决思路

    需求 unity3d的3d开发环境中,原生自带了Navigation的组件,可以很便捷快速的实现寻路功能.但是在原生的2d中并没有相同的功能. 现在国内很多手机游戏都有自动寻路的功能,或者游戏中存在一 ...

  2. MOBA游戏的网络同步技术

    转自:http://www.gameres.com/750888.html 在5月13日Unite 2017 案例分享专场上,蓝港互动<闹闹天宫>项目组的主程序陈实分享了MOBA游戏的网络 ...

  3. 游戏中的网络同步机制——Lockstep(帧同步)

    本文来自: https://bindog.github.io/blog/2015/03/10/synchronization-in-multiplayer-networked-game-lockste ...

  4. 在FPS游戏中,玩家对音画同步感知的量化与评估

    前言 在游戏测试中,音画同步测试是个难点(所谓游戏音画同步:游戏中,音效与画面的同步程度),现在一般采用人工主观判断的方式测试,但这会带来2个问题: 无法准确量化,针对同一场景的多次测试结果可能会相反 ...

  5. Unity3d 游戏中集成Firebase 统计和Admob广告最新中文教程

    之前写过俩相关的教程,最近发现插件官方更新了不少内容,所以也更新一篇Firebase Admob Unity3d插件的教程,希望能帮到大家. Firebase Admob Unity3d插件是一个Un ...

  6. 让 CXK 来教你实现游戏中的帧动画(上)

    一款游戏除了基本功能之外,还需要给玩家更多视觉上的刺激,这个时候就需要用特效来装饰.本文就将介绍 Cocos Creator 的动画系统,除了标准的位移.旋转.缩放动画和序列帧动画以外,这套动画系统还 ...

  7. 【Unity3d游戏开发】游戏中的贝塞尔曲线以及其在Unity中的实现

    RT,马三最近在参与一款足球游戏的开发,其中涉及到足球的各种运动轨迹和路径,比如射门的轨迹,高吊球,香蕉球的轨迹.最早的版本中马三是使用物理引擎加力的方式实现的足球各种运动,后来的版本中使用了根据物理 ...

  8. 在unity3d游戏中添加中文语音控制

    最近打算尝试一下OLAMI在游戏中应用的可能性,这里做一下记录. unity官方教程中的几个项目很精简,但看起来很不错,里面有全套的资源.最后我选择了tanks-tutorial来做这个实验. 下载和 ...

  9. Unity3D游戏开发——收集当前关卡游戏中分散的物件

    运用场景 许多游戏中会有一些供玩家拾起的物件,例如装备.血包.道具等.当玩家与物件进行碰撞后,则会进入仓库. 本篇介绍了简单的碰撞过程. 原理 基本的碰撞机制,用到OnTriggerEnter()碰撞 ...

随机推荐

  1. Web API中的路由(二)——属性路由

    一.属性路由的概念 路由让webapi将一个uri匹配到对应的action,Web API 2支持一种新类型的路由:属性路由.顾名思义,属性路由使用属性来定义路由.通过属性路由,我们可以更好地控制We ...

  2. hibernate状态转换关系图【原】

    hibernate状态转换 其它参考 简单理解Hibernate三种状态的概念及互相转化 简单的Hibernate入门介绍

  3. vue常用的路由对象

    官网上解释:一个路由对象表示当前激活的路由的状态信息 路由对象,在组件内即this.$route,存着一些与路由相关的信息,当路由切换时,路由对象会被更新 //如果要在刷新页面时候通过路由的信息来操作 ...

  4. [Android] [putty连接Android设备] [Android设备网络调试]

    file: system/core/adb/adb.c line: 921 /* for the device, start the usb transport if the ** android u ...

  5. SQL Server进阶(七)集合运算

    概述 为什么使用集合运算: 在集合运算中比联接查询和EXISTS/NOT EXISTS更方便. 并集运算(UNION) 并集:两个集合的并集是一个包含集合A和B中所有元素的集合. 在T-SQL中.UN ...

  6. Android上禁止屏幕旋转

    看网上讲了很多,设置很多属性,设置了很多,其实最关键的一点是这个 @Overrideprotected void onResume() { /** * 设置为横屏 */ if(getRequested ...

  7. 九、文件IO——案例构建标准库

    例子如下: mystdio.h #ifndef __MYSTDIO_H__ #define __MYSTDIO_H__ #include <sys/types.h> #define MYE ...

  8. sqlserver二进制存储

    CREATE TABLE myTable_yq(Document varbinary(max),yq varchar(20)) --SELECT @xmlFileName = 'c:\TestXml. ...

  9. [Python]基于K-Nearest Neighbors[K-NN]算法的鸢尾花分类问题解决方案

    看了原理,总觉得需要用具体问题实现一下机器学习算法的模型,才算学习深刻.而写此博文的目的是,网上关于K-NN解决此问题的博文很多,但大都是调用Python高级库实现,尤其不利于初级学习者本人对模型的理 ...

  10. extern 关键字使用

    extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义.此外extern也可用来进行链接指定. 如在头文件中: extern in ...