欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Unity3D简单的帧同步方案

程序员文章站 2024-03-25 20:04:52
...

公司的游戏准备上线了,我呢也在准备新项目,这几天看了一下策划文档,写了整体流程和需求接口的代码,终于有一点点时间来写自己的博客了。今天我们就来讲讲帧同步吧。

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

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

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

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

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

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

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

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

// **********************************************************************
// Copyright (C) XM
// Author: 吴肖牧
// Date: 2018-05-25
// Desc: 战斗模拟
// **********************************************************************

using UnityEngine;
using System.Collections;

public class BattleMock : Singleton<BattleMock>
{

    float FrameRate = 0.1f; //帧速率,0.1秒1帧,即1秒10帧
    int FrameCount = 0;//帧数
    float NextRunTime = 0;

    bool isRunning = false;

    /// <summary>
    ///  消息数据
    /// </summary>
    UpdateMsg updateMsg;

    // Use this for initialization
    void Start()
    {
        NextRunTime = Time.time + FrameRate;
    }

    // Update is called once per frame
    void Update()
    {
        if (isRunning && BattleManager.Instance)
        {
            while (Time.time >= NextRunTime)
            {
                UpdateFrame();
                NextRunTime += FrameRate;
            }
        }
    }

    /// <summary>
    /// 更新帧数据
    /// </summary>
    void UpdateFrame()
    {
        updateMsg.frameCount += 1;
        BattleManager.Instance.UpdateBattle(updateMsg);
        int fc = updateMsg.frameCount;
        updateMsg = new UpdateMsg();
        updateMsg.frameCount = fc;
    }

    /// <summary>
    /// 开始战斗
    /// </summary>
    public void StartBattle()
    {
        updateMsg = new UpdateMsg();
        FrameCount = 0;
        NextRunTime = 0;
        isRunning = true;
    }

    /// <summary>
    /// 玩家输入
    /// </summary>
    /// <param name="uid"></param>
    public void UserInput(Record record)
    {
        updateMsg.records.Add(record);
    }
}

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

public class UpdateMsg
{
    public int frameCount = 0;
    public List<Record> records;

}

public class Record
{
    public int uid;
    public float posX;
    public float posY;
    public RecordType recordsType;
}

public enum RecordType
{
    NONE,
    MOVE,
}

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

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

// **********************************************************************
// Copyright (C) XM
// Author: 吴肖牧
// Date: 2018-05-25
// Desc: 战斗管理
// **********************************************************************

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class BattleManager : Singleton<BattleManager>
{
    int frameCount = 0;

    bool isStart = false;

    // Use this for initialization
    void Start()
    {
        //创建一个uid是10000的角色 
        ActorManager.Instance.CreateActor(10000);
    }

    public void UpdateBattle(UpdateMsg update_msg)
    {
        if (update_msg.frameCount != (frameCount + 1))
        {
            //丢帧
            Debug.Log("lost frame :" + "server:" + update_msg.frameCount + " client:" + frameCount);
        }
        else
        {
            //把服务器发来的帧数赋值给本地的帧数
            frameCount = update_msg.frameCount;

            if (isStart)
            {
                foreach (var record in update_msg.records)
                {
                    if (record.recordsType == RecordType.MOVE)
                    {
                        ActorManager.Instance._actorDic[record.uid].TransState(ActorStateType.Move);
                    }
                    else
                    {
                        //TODO
                    }
                }
            }

            Dispatcher.Instance.SendMessage(BattleMsg.BattleUpdateMsg);
        }
    }

}

3.角色发送数据。

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

// **********************************************************************
// Copyright (C) XM
// Author: 吴肖牧
// Date: 2018-04-13
// Desc: 
// **********************************************************************
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerActor: Actor {

    /// <summary>
    /// 摇杆
    /// </summary>
    private ETCJoystick _joystick;

    /// <summary>
    /// 初始化状态机
    /// </summary>
    protected override void InitState()
    {
        _actorStateDic[ActorStateType.Idle] = new IdleState();
        _actorStateDic[ActorStateType.Move] = new MoveState();
    }

    /// <summary>
    /// 初始化当前状态
    /// </summary>
    protected override void InitCurState()
    {
        _curState = _actorStateDic[ActorStateType.Idle];
        _curState.Enter(this);
    }

    void Start()
    {
        _joystick = GameObject.FindObjectOfType<ETCJoystick>();
        if (_joystick != null)
        {
            _joystick.onMoveStart.AddListener(StartMoveCallBack);
            _joystick.onMove.AddListener(MoveCallBack);
            _joystick.onMoveEnd.AddListener(EndMoveCallBack);
        }

        Dispatcher.Instance.AddListener(BattleMsg.BattleUpdateMsg, BattleUpdate);
    }


    /// <summary>
    /// 战斗更新
    /// </summary>
    /// <param name="evt"></param>
    private void BattleUpdate(Message evt)
    {
        //TODO
    }


    /// <summary>
    /// 开始移动
    /// </summary>
    private void StartMoveCallBack()
    {
        TransState(ActorStateType.Move);
    }


    /// <summary>
    /// 正在移动
    /// </summary>
    /// <param name="arg0"></param>
    private void MoveCallBack(Vector2 vec2)
    {
        float value = 0.02f * _moveSpeed / Mathf.Sqrt(vec2.normalized.x * vec2.normalized.x + vec2.normalized.y * vec2.normalized.y);//勾股定理得出比例,第一个值是摇杆的比例

        //_pos = new Vector3(_pos.x + vec2.x * value, _pos.y + vec2.y * value, 0);
        Vector3 pos = new Vector3(_pos.x + vec2.x * value, _pos.y + vec2.y * value, 0);

        Record record = new Record();
        record.uid = _uid;
        record.recordsType = RecordType.MOVE;
        record.posX = pos.x;
        record.posY = pos.y;

        BattleMock.Instance.UserInput(record);

        //int angle = (int)(Mathf.Atan2(vec2.normalized.y, vec2.normalized.x) * 180 / 3.14f);
        //Debug.Log(angle);
        //if (angle > 45 && angle < 135)
        //{
        //    ChangeDir(Direction.Back);
        //    //Debug.Log("上");
        //}
        //else if (angle <= 45 && angle >= -45)
        //{
        //    ChangeDir(Direction.Right);
        //    //Debug.Log("右");
        //}
        //else if (Mathf.Abs(angle) >= 135)
        //{
        //    ChangeDir(Direction.Left);
        //    //Debug.Log("左");
        //}
        //else
        //{
        //    ChangeDir(Direction.Front);
        //    //Debug.Log("下");
        //}
    }

    /// <summary>
    /// 移动结束
    /// </summary>
    private void EndMoveCallBack()
    {
        TransState(ActorStateType.Idle);
    }

    void OnDestroy()
    {
        if (_joystick != null)
        {
            _joystick.onMoveStart.RemoveListener(StartMoveCallBack);
            _joystick.onMove.RemoveListener(MoveCallBack);
            _joystick.onMoveEnd.RemoveListener(EndMoveCallBack);
        }

        Dispatcher.Instance.RemoveListener(BattleMsg.BattleUpdateMsg, BattleUpdate);
    }

}

这是一个大致的思路,不是一个完整的帧同步框架,请不要直接用于项目。