Unity3D简单的帧同步方案

百度了一下帧同步,百度百科上对帧同步的解释是:在数字时分多路通信系统中,为了能正确分离各路时隙信号,在发送端必须提供每帧的起始标记,在接收端检测并获取这一标志的过程称为帧同步。哈哈哈,一脸懵逼吧,简而言之,就是在游戏中同步的是玩家的操作指令,操作指令包含当前的帧索引。一般的流程是客户端上传操作到服务器, 服务器收到后并不计算游戏行为, 而是转发到所有客户端。这里最重要的概念就是,相同的输入 + 相同的时机 = 相同的输出

由于每台设备的环境都不一样,为了保证我们的操作和设备环境无关,我们为每个逻辑帧设置为固定时长,那么我们在游戏中的移动,碰撞等算出来的结果,算出来的就都是相同的结果(不要使用Unity的物理引擎和浮点型)。众所周知,由于每台设备的环境都不一样,其渲染帧一般为30帧到60帧不等,而我们的逻辑帧一般设置为10帧到20帧,在考虑到性能和平滑的问题做一个取舍的展示。逻辑帧实际上是有一个个定时下发的网络帧来驱动,而渲染帧则由设备的CPU的Update来驱动。如果逻辑帧突然中断,游戏就会卡在那一帧的状态。在理想的状态下,每一个网络帧都会被及时的接收,而客户端的渲染帧就跟播放电影一样。我们可以想象一下帧动画的播放,如果我们设置每秒10帧,也就是0.1秒播放一张帧动画,那帧同步差不多就是这种效果。但是在网络游戏中,每个客户端的硬件和网络环境不尽相同,这就可能导致客户端收到过去时间里的一堆网络帧,比如我们在玩某些游戏的时候,突然卡了一下,然后角色又快速的跑到别的地方去了,就跟电影的快进一样,还有一种情况就是,角色直接跳到最新的位置,就跟闪现一下,这种情况是经过处理的,直接抛弃了中间的网络帧。为了能让玩家在游戏里面顺畅的玩游戏而不感觉到很生硬,我还是建议采用快进的方式,其实我们不用做任何处理。

看到这里,可能你们会有一个疑问,为了保证一致性我们用逻辑帧来处理数据,那我们游戏的画面不就一卡一卡的吗?

这里就涉及到另一个重要的问题了——数据层和表现层的分离。

逻辑帧处理数据层,渲染帧处理表现层。当我们用逻辑帧来跑游戏的时候,我们会发现游戏一卡一卡的,是因为我们逻辑帧的帧数少,跟不上渲染帧的速度,那为什么我们不把逻辑帧的帧数提上去呢?前面已经讲了,是考虑到性能和平滑展示。如果我们设置的帧数过高,就会影响到游戏性能,反之就会造成数据处理不及时,影响到数据的有效性。为了解决这个一卡一卡的问题,我们可以在渲染层做平滑处理,其实我们还是跑的数据层,只不过你们看到的都是经过处理的表现而已。

叽叽歪歪了一大堆,那怎么实现呢?

1.模拟服务端的网络帧。

一个好的客户端,不能受制于服务端的蹂躏,我们自己在本地模拟网络帧。

  1. // **********************************************************************
  2. // Copyright (C) XM
  3. // Author: 吴肖牧
  4. // Date: 2018-05-25
  5. // Desc: 战斗模拟
  6. // **********************************************************************
  7. using UnityEngine;
  8. using System.Collections;
  9. public class BattleMock : Singleton< BattleMock>
  10. {
  11.       float FrameRate = 0.1f; //帧速率,0.1秒1帧,即1秒10帧
  12.       int FrameCount = 0; //帧数
  13.       float NextRunTime = 0;
  14.        bool isRunning = false;
  15. /// <summary>
  16. /// 消息数据
  17. /// </summary>
  18.        UpdateMsg updateMsg;
  19. // Use this for initialization
  20.   void Start()
  21. {
  22.        NextRunTime = Time.time + FrameRate;
  23. }
  24. // Update is called once per frame
  25. void Update()
  26. {
  27.         if (isRunning && BattleManager.Instance)
  28. {
  29.          while (Time.time >= NextRunTime)
  30. {
  31.           UpdateFrame();
  32.           NextRunTime += FrameRate;
  33. }
  34. }
  35. }
  36. /// <summary>
  37. /// 更新帧数据
  38. /// </summary>
  39. void UpdateFrame()
  40. {
  41.            updateMsg.frameCount += 1;
  42.            BattleManager.Instance.UpdateBattle(updateMsg);
  43.            int fc = updateMsg.frameCount;
  44.            updateMsg = new UpdateMsg();
  45.            updateMsg.frameCount = fc;
  46. }
  47. /// <summary>
  48. /// 开始战斗
  49. /// </summary>
  50. public void StartBattle()
  51. {
  52.           updateMsg = new UpdateMsg();
  53.           FrameCount = 0;
  54.           NextRunTime = 0;
  55.           isRunning = true;
  56. }
  57. /// <summary>
  58. /// 玩家输入
  59. /// </summary>
  60. /// <param name="uid"></param>
  61.     public void UserInput(Record record)
  62.     {
  63.         updateMsg.records.Add(record);
  64.     }
  65. }

随便写个数据结构,用于模拟协议内容。

  1. public class UpdateMsg
  2. {
  3.       public int frameCount = 0;
  4.       public List<Record> records;
  5. }
  6. public class Record
  7. {
  8.       public int uid;
  9.       public float posX;
  10.       public float posY;
  11.       public RecordType recordsType;
  12. }
  13. public enum RecordType
  14. {
  15.        NONE,
  16.        MOVE,
  17. }

2.创建一个战斗的管理器。

战斗管理器主要的功能是把消息内容分发给游戏所有的角色,物品,触发器等,然后更新数据。

  1. // **********************************************************************
  2. // Copyright (C) XM
  3. // Author: 吴肖牧
  4. // Date: 2018-05-25
  5. // Desc: 战斗管理
  6. // **********************************************************************
  7. using UnityEngine;
  8. using System.Collections;
  9. using System.Collections.Generic;
  10. public class BattleManager : Singleton< BattleManager>
  11. {
  12.        int frameCount = 0;
  13.        bool isStart = false;
  14. // Use this for initialization
  15. void Start()
  16. {
  17. //创建一个uid是10000的角色
  18.          ActorManager.Instance.CreateActor( 10000);
  19. }
  20. public void UpdateBattle(UpdateMsg update_msg)
  21. {
  22.          if (update_msg.frameCount != (frameCount + 1))
  23. {
  24. //丢帧
  25.          Debug.Log( "lost frame :" + "server:" + update_msg.frameCount + " client:" + frameCount);
  26. }
  27. else
  28. {
  29. //把服务器发来的帧数赋值给本地的帧数
  30.          frameCount = update_msg.frameCount;
  31. if (isStart)
  32. {
  33.           foreach ( var record in update_msg.records)
  34. {
  35. if (record.recordsType == RecordType.MOVE)
  36. {
  37.           ActorManager.Instance._actorDic[record.uid].TransState(ActorStateType.Move);
  38. }
  39. else
  40. {
  41. //TODO
  42. }
  43. }
  44. }
  45.             Dispatcher.Instance.SendMessage(BattleMsg.BattleUpdateMsg);
  46. }
  47. }
  48. }

3.角色发送数据。

我们在之前的文章《Unity3D用状态机制作角色控制系统》修改PlayerActor移动方法MoveCallBack,把直接修改角色坐标的方式改为发送消息,然后通过战斗管理器去修改角色的坐标。

  1. // **********************************************************************
  2. // Copyright (C) XM
  3. // Author: 吴肖牧
  4. // Date: 2018-04-13
  5. // Desc:
  6. // **********************************************************************
  7. using System;
  8. using System.Collections;
  9. using System.Collections.Generic;
  10. using UnityEngine;
  11. public class PlayerActor: Actor {
  12. /// <summary>
  13. /// 摇杆
  14. /// </summary>
  15. private ETCJoystick _joystick;
  16. /// <summary>
  17. /// 初始化状态机
  18. /// </summary>
  19. protected override void InitState()
  20. {
  21.           _actorStateDic[ActorStateType.Idle] = new IdleState();
  22.           _actorStateDic[ActorStateType.Move] = new MoveState();
  23. }
  24. /// <summary>
  25. /// 初始化当前状态
  26. /// </summary>
  27. protected override void InitCurState()
  28. {
  29.           _curState = _actorStateDic[ActorStateType.Idle];
  30.          _curState.Enter( this);
  31. }
  32. void Start()
  33. {
  34.             _joystick = GameObject.FindObjectOfType<ETCJoystick>();
  35. if (_joystick != null)
  36. {
  37.               _joystick.onMoveStart.AddListener(StartMoveCallBack);
  38.              _joystick.onMove.AddListener(MoveCallBack);
  39.              _joystick.onMoveEnd.AddListener(EndMoveCallBack);
  40. }
  41.              Dispatcher.Instance.AddListener(BattleMsg.BattleUpdateMsg, BattleUpdate);
  42. }
  43. /// <summary>
  44. /// 战斗更新
  45. /// </summary>
  46. /// <param name="evt"></param>
  47. private void BattleUpdate(Message evt)
  48. {
  49. //TODO
  50. }
  51. /// <summary>
  52. /// 开始移动
  53. /// </summary>
  54. private void StartMoveCallBack()
  55. {
  56.          TransState(ActorStateType.Move);
  57. }
  58. /// <summary>
  59. /// 正在移动
  60. /// </summary>
  61. /// <param name="arg0"></param>
  62. private void MoveCallBack(Vector2 vec2)
  63. {
  64. float value = 0.02f * _moveSpeed / Mathf.Sqrt(vec2.normalized.x * vec2.normalized.x + vec2.normalized.y * vec2.normalized.y); //勾股定理得出比例,第一个值是摇杆的比例
  65. //_pos = new Vector3(_pos.x + vec2.x * value, _pos.y + vec2.y * value, 0);
  66. Vector3 pos = new Vector3(_pos.x + vec2.x * value, _pos.y + vec2.y * value, 0);
  67. Record record = new Record();
  68. record.uid = _uid;
  69. record.recordsType = RecordType.MOVE;
  70. record.posX = pos.x;
  71. record.posY = pos.y;
  72. BattleMock.Instance.UserInput(record);
  73. //int angle = (int)(Mathf.Atan2(vec2.normalized.y, vec2.normalized.x) * 180 / 3.14f);
  74. //Debug.Log(angle);
  75. //if (angle > 45 && angle < 135)
  76. //{
  77. // ChangeDir(Direction.Back);
  78. // //Debug.Log("上");
  79. //}
  80. //else if (angle <= 45 && angle >= -45)
  81. //{
  82. // ChangeDir(Direction.Right);
  83. // //Debug.Log("右");
  84. //}
  85. //else if (Mathf.Abs(angle) >= 135)
  86. //{
  87. // ChangeDir(Direction.Left);
  88. // //Debug.Log("左");
  89. //}
  90. //else
  91. //{
  92. // ChangeDir(Direction.Front);
  93. // //Debug.Log("下");
  94. //}
  95. }
  96. /// <summary>
  97. /// 移动结束
  98. /// </summary>
  99. private void EndMoveCallBack()
  100. {
  101. TransState(ActorStateType.Idle);
  102. }
  103. void OnDestroy()
  104. {
  105. if (_joystick != null)
  106. {
  107. _joystick.onMoveStart.RemoveListener(StartMoveCallBack);
  108. _joystick.onMove.RemoveListener(MoveCallBack);
  109. _joystick.onMoveEnd.RemoveListener(EndMoveCallBack);
  110. }
  111. Dispatcher.Instance.RemoveListener(BattleMsg.BattleUpdateMsg, BattleUpdate);
  112. }
  113. }
转自 https://blog.csdn.net/yye4520/article/details/80524698

猜你喜欢

转载自blog.csdn.net/qq_31967569/article/details/81064327
今日推荐