Unity3D简单的帧同步方案

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

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

由于每台设备的环境都不一样,为了保证我们的操作和设备环境无关,我们为每个逻辑帧设置为固定时长,那么我们在游戏中的移动,碰撞等算出来的结果,算出来的就都是相同的结果(不要使用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
{

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

    bool isRunning = false;

    /// 
    ///  消息数据
    /// 
    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;
            }
        }
    }

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

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

    /// 
    /// 玩家输入
    /// 
    /// 
    public void UserInput(Record record)
    {
        updateMsg.records.Add(record);
    }
}

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

public class UpdateMsg
{
    public int frameCount = 0;
    public List 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
{
    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 {

    /// 
    /// 摇杆
    /// 
    private ETCJoystick _joystick;

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

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

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

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


    /// 
    /// 战斗更新
    /// 
    /// 
    private void BattleUpdate(Message evt)
    {
        //TODO
    }


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


    /// 
    /// 正在移动
    /// 
    /// 
    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("下");
        //}
    }

    /// 
    /// 移动结束
    /// 
    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);
    }

}

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

你可能感兴趣的:(Unity3d游戏开发)