精华内容
下载资源
问答
  • 摘要:本文针对该问题给出了逐次比较、基于FIFO队列和基于状态机的3种帧同步方法。通过测试、分析和比较得出,基于有限状态机的方法是嵌入式系统串口通信中很有效的帧同步方法,同时也是一种很不错的串口通信程序...
  • Unity3D用状态机制作角色控制系统

    万次阅读 多人点赞 2018-05-24 11:42:41
    为了让读者对本文知识点有一个比较清晰的了解,我制作了一张图结构图,如下图,图中以移动为例子简单的描述了状态机的基本结构,本文不对角色控制系统做全面的讲解,只对状态机的在角色控制系统中是如何运用的。...

    为了让读者对本文知识点有一个比较清晰的了解,我制作了一张结构图,如下图,图中以移动为例子简单的描述了状态机的基本结构,本文不对角色控制系统做全面的讲解,只对状态机的在角色控制系统中是如何运用做出讲解。



    1.我们先从Actor讲起。Actor作为角色脚本的基类,承载着角色的基本属性,包括角色id,移动速度,坐标等等,因为我们这里讲的是用状态机来控制角色,所以角色的属性还包括角色的当前状态,所有状态,状态类型等等,还有一些对状态操作的方法,包括初始化状态机,初始化当前状态,改变状态机,更新状态机等等,当然还有一些角色表现的方法,比如移动,改变方向,播放动画等等,这些表现方法是通过状态机来实现,从而实现改变状态来驱动表现,这就是状态机的用法。

    // **********************************************************************
    // Copyright (C) XM
    // Author: 吴肖牧
    // Date: 2018-04-13
    // Desc: 
    // **********************************************************************
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public enum Direction
    {
        Front = 0,
        Back,
        Left,
        Right
    }
    
    public abstract class Actor : Base
    {
        /// <summary>
        /// debug模式,程序测试
        /// </summary>
        public bool _debug;
    
        /// <summary>
        /// 玩家id
        /// </summary>
        public int _uid;
    
        /// <summary>
        /// 玩家名字
        /// </summary>
        public string _name;
    
        /// <summary>
        /// 移动速度
        /// </summary>
        public float _moveSpeed;
    
        /// <summary>
        /// 是否正在移动
        /// </summary>
        public bool _isMoving;
    
        /// <summary>
        /// 坐标
        /// </summary>
        public Vector3 _pos;
    
        /// <summary>
        /// 当前状态
        /// </summary>
        public ActorState _curState { set; get; }
    
        public ActorStateType _stateType;
    
        public Direction _direction = Direction.Front;
    
        /// <summary>
        /// 状态机集合
        /// </summary>
        public Dictionary<ActorStateType, ActorState> _actorStateDic = new Dictionary<ActorStateType, ActorState>();
    
        /// <summary>
        /// 动画控制器
        /// </summary>
        [HideInInspector]
        public Animator _animator;
    
        private Transform _transform;
    
        void Awake()
        {
            _transform = this.transform;
            _animator = GetComponent<Animator>();
            InitState();
            InitCurState();
        }
    
        /// <summary>
        /// 初始化状态机
        /// </summary>
        protected abstract void InitState();
    
        /// <summary>
        ///  初始化当前状态
        /// </summary>
        protected abstract void InitCurState();
       
    
        /// <summary>
        /// 改变状态机
        /// </summary>
        /// <param name="stateType"></param>
        /// <param name="param"></param>
        public void TransState(ActorStateType stateType)
        {
            if (_curState == null)
            {
                return;
            }
            if (_curState.StateType == stateType)
            {
                return;
            }
            else
            {
                ActorState _state;
                if (_actorStateDic.TryGetValue(stateType, out _state))
                {
                    _curState.Exit();
                    _curState = _state;
                    _curState.Enter(this);
                    _stateType = _curState.StateType;
                }
            }
        }
    
        /// <summary>
        /// 更新状态机
        /// </summary>
        public void UpdateState()
        {
            if (_curState != null)
            {
                _curState.Update();
            }
        }
    
        /// <summary>
        /// 移动 数据(状态)驱动表现
        /// </summary>
        public virtual void Move()
        {
            //TODO 移动相关状态
            _animator.SetInteger("Dir", (int)_direction);
            if (_debug)
            {
                //数据层位置
                _transform.position = _pos;
            }
            else
            {
                //表现层位置
                _transform.position = Vector3.Lerp(_transform.position, _pos, 100 * Time.deltaTime);
            }
        }
    
        /// <summary>
        /// 改变方向
        /// </summary>
        /// <param name="dir"></param>
        public void ChangeDir(Direction dir)
        {
            _direction = dir;
            if (_direction == Direction.Left)
            {
                _transform.localScale = new Vector3(-1, 1, 1);
            }
            else
            {
                _transform.localScale = new Vector3(1, 1, 1);
            }
        }
    
        /// <summary>
        /// 播放动画
        /// </summary>
        /// <param name="name"></param>
        /// <param name="dir"></param>
        public void PlayAnim(string name)
        {
            _animator.SetBool("Idle", false);
            _animator.SetBool("Run", false);
            _animator.SetBool(name, true);
            _animator.SetInteger("Dir", (int)_direction);
        }
    }

    2.我们现在有了Actor这个角色基类,那么我们现在就可以用它来创造很多的不同角色了。我们先来创造一个自己的角色PayerActor,然后继承Actor,因为Actor的状态机初始化是用虚方法的,所以我们必须在子类中去实现它,来达到不同的角色有不同的状态。

    // **********************************************************************
    // 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);
            }
        }
    
        /// <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);
    
            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);
            }
        }
        
    }
    
    这里有个可以优化的地方就是动画控制器,由于我这里做的是一个2D的角色,并且他有4个朝向,所以我改成了用混合树来做,通过MoveCallBack方法传进来的二维坐标直接控制混合树的X和Y的参数,进而改变角色的朝向,所以MoveCallBack方法里面的实现,如果你们需要可以进行优化,就是不需要通过角度去算方向了,我就不去修改了。

    3.接下来我们来讲讲状态机的基类ActorState,基类包括状态机类型,进入状态,更新状态,退出状态等等。

    // **********************************************************************
    // Copyright (C) XM
    // Author: 吴肖牧
    // Date: 2018-04-13
    // Desc: 
    // **********************************************************************
    
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    /// <summary>
    /// 角色状态
    /// </summary>
    public abstract class ActorState
    {
    
        /// <summary>
        /// 状态机类型
        /// </summary>
        public abstract ActorStateType StateType { get; }
    
        /// <summary>
        /// 进入状态
        /// </summary>
        /// <param name="param"></param>
        public abstract void Enter(params object[] param);
    
        /// <summary>
        /// 更新状态
        /// </summary>
        public abstract void Update();
    
        /// <summary>
        /// 退出状态
        /// </summary>
        public abstract void Exit();
    
    }
    
    
    /// <summary>
    /// 角色状态类型
    /// </summary>
    public enum ActorStateType
    {
        Idle,
        Move,
        //...
    }

    4.既然我们有了状态机的基类,那我们就可以创造出很多的状态了,比如待机,移动,攻击,释放技能等等。同样的,基类ActorState的方法也是虚方法,必须通过子类来实现,所以我们每个不同的状态就可以各自实现自己的操作了。

    // **********************************************************************
    // Copyright (C) XM
    // Author: 吴肖牧
    // Date: 2018-04-13
    // Desc: 
    // **********************************************************************
    
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    /// <summary>
    /// 待机状态
    /// </summary>
    public class IdleState : ActorState
    {
        private Actor _actor;
    
        public override ActorStateType StateType
        { 
            get
            {
                return ActorStateType.Idle;
            }
        }
    
        public override void Enter(params object[] param)
        {
            //Debug.Log("IdleState Enter");
            _actor = param[0] as Actor;
            if (_actor != null)
            {
                _actor.PlayAnim("Idle");
    
                _actor._isMoving = false;
                //TODO 播放动画相关
            }
        }
    
        public override void Update()
        {
    
        }
    
        public override void Exit()
        {
            _actor = null;
            //Debug.Log("IdleState Exit");
    
        }
    }
    
    /// <summary>
    /// 移动状态
    /// </summary>
    public class MoveState : ActorState
    {
        private Actor _actor;
    
        public override ActorStateType StateType
        {
            get
            {
                return ActorStateType.Move;
            }
        }
    
        public override void Enter(params object[] param)
        {
            //Debug.Log("MoveState Enter");
            _actor = param[0] as Actor;
            if (_actor != null)
            {
                _actor.PlayAnim("Run");
    
                _actor._isMoving = true;
                //TODO 播放动画相关
            }
        }
    
        public override void Update()
        {
            if (_actor != null)
            {
                _actor.Move();
            }
        }
    
        public override void Exit()
        {
            //Debug.Log("MoveState Exit");
            _actor._isMoving = false;
            _actor = null;
        }
    }
    

    那我们是如何实现切换状态的呢?我们回到Actor,看看改变状态机的方法,每次切换的时候都会先把当前状态停掉,然后进入新的状态,再把自己的Actor传进状态机,然后根据状态的需要,实现Actor里的方法。

        /// <summary>
        /// 改变状态机
        /// </summary>
        /// <param name="stateType"></param>
        /// <param name="param"></param>
        public void TransState(ActorStateType stateType)
        {
            if (_curState == null)
            {
                return;
            }
            if (_curState.StateType == stateType)
            {
                return;
            }
            else
            {
                ActorState _state;
                if (_actorStateDic.TryGetValue(stateType, out _state))
                {
                    _curState.Exit();
                    _curState = _state;
                    _curState.Enter(this);
                    _stateType = _curState.StateType;
                }
            }
        }

    5.最后我们来讲讲简单的角色管理系统ActorManager,包括角色的创建,删除,获取等等。其中最重要的功能就是更新所有角色状态机UpdateActor(),所有角色的持续状态都是通过这个方法实现的。

    // **********************************************************************
    // Copyright (C) XM
    // Author: 吴肖牧
    // Date: 2018-04-14
    // Desc: 角色管理器
    // **********************************************************************
    
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class ActorManager : Singleton<ActorManager>
    {
    
        /// <summary>
        /// 所有玩家的角色列表
        /// </summary>
        public Dictionary<int, Actor> _actorDic = new Dictionary<int, Actor>();
    
        // Update is called once per frame
        void Update()
        {
            UpdateActor();
        }
    
        /// <summary>
        /// 更新角色状态
        /// </summary>
        private void UpdateActor()
        {
            var enumerator = _actorDic.GetEnumerator();
            while (enumerator.MoveNext())
            {
                enumerator.Current.Value.UpdateState();
            }
            enumerator.Dispose();
        }
    
        /// <summary>
        /// 创建角色
        /// </summary>
        /// <param name="uid">角色id</param>
        public void CreateActor(int uid)
        {
            Actor actor = null;
            if (!_actorDic.TryGetValue(uid, out actor))
            {
                GameObject go = AppFacade.Instance.GetManager<ResourceManager>(ManagerName.Resource).CreateAsset("Prefabs/Actor/Wizard");
                Camera.main.GetComponentInChildren<Cinemachine.CinemachineVirtualCamera>().Follow = go.transform;
                actor = go.GetComponent<WizardActor>();
                actor._uid = uid;
                actor._name = uid.ToString();
                actor._moveSpeed = 5;
                _actorDic[uid] = actor;
            }
            else
            {
                Debug.Log("玩家" + uid + "已经存在");
            }
        }
    
        /// <summary>
        /// 删除角色
        /// </summary>
        /// <param name="uid">角色id</param>
        public void RemoveActor(int uid)
        {
            Actor actor = null;
            if (_actorDic.TryGetValue(uid, out actor))
            {
                Destroy(actor.gameObject);
                _actorDic.Remove(uid);
            }
            else
            {
                Debug.Log("玩家" + uid + "不存在");
            }
        }
    
        /// <summary>
        /// 获取角色
        /// </summary>
        /// <param name="uid">角色id</param>
        /// <returns></returns>
        public Actor GetActor(int uid)
        {
            Actor actor = null;
            _actorDic.TryGetValue(uid, out actor);
            return actor;
        }
    }

    后面有时间的话,我会基于这篇文章再写一篇简单帧同步的文章。

    展开全文
  • Lua有限状态机

    2017-08-08 22:13:54
    目前主要面向Unity3d的lua
  • 但是我在做动画同步的时候,出现了问题,两个脚本同时都有Reload的Trigger,我在脚本中写的是animtor.SetTrigger("Reload"),但是远程端却是无效的,只有本地端的有效 (就是你可以看到自己换弹了,但是其他玩家的...

    开发环境:
    我在做的是一个联机FPS的demo,出于性能优化考虑,本地端只显示手臂+枪械模型,远程端显示全身,所以用的是两个脚本和两个Animator用于控制本地端和远程端

    但是我在做动画同步的时候,出现了问题,两个脚本同时都有Reload的Trigger,我在脚本中写的是animtor.SetTrigger("Reload"),但是远程端却是无效的,只有本地端的有效 (就是你可以看到自己换弹了,但是其他玩家的游戏中看不到)

    解决:
    为此,排查了好久的问题,最终的结果很让我意外

    只需要将animator.SetTrigger改为简单粗暴的 animator.Play即可

    很离谱,不知道原因。

    展开全文
  • 状态同步 状态同步是从服务器向客户端方向上的。本地客户端没有序列化的数据,因为它和服务器共享同一个场景。任何为本地客户端序列化的数据都是多余的。然而,SyncVar钩子函数会被本地客户端调用。 数据不会从...

    状态同步

    状态同步是从服务器向客户端方向上的。本地客户端没有序列化的数据,因为它和服务器共享同一个场景。任何为本地客户端序列化的数据都是多余的。然而,SyncVar钩子函数会被本地客户端调用。

    数据不会从客户端向服务器同步,这个方向上的操作叫做命令(Commands)。

    同步变量

    同步变量是NetworkBehaviour脚本中的成员变量,他们会从 服务器 同步到客户端上。当一个物体被派生出来之后,或者一个新的 玩家 中途加入 游戏 后,他会接收到他的视野内所有物体的同步变量。成员变量通过[SyncVar]标签被配置成同步变量:

    class Player :NetworkBehaviour
    {
    [SyncVar]
    int health;

    public void TakeDamage(int amount)
    {
    if (!isServer)
    return;

    health  -= amount;
    }
    }
    同步变量的状态在OnStartClient()之前就被应用到物体上了,所以在OnStartClient函数中,物体的状态已经是最新的数据。

    同步变量可以是基础类型,如整数,字符串和浮点数。也可以是Unity内置数据类型,如Vector3和用户自定义的结构体,但是对结构体类型的同步变量,如果只有几个字段的数值有变化,整个结构体都会被发送。每个NetworkBehaviour脚本可以有最多32个同步变量,包括同步列表(见下面的解释)。

    当同步变量有变化时, 服务器 会自动发送他们的最新数据。不需要手工为同步变量设置任何的脏数据标志位。

    注意在属性设置函数中设置一个同步变量的值不会使他的脏数据 标志 被设置。如果这样做的话,会得到一个编译期的警告。因为同步变量使用他们自己内部的标识记录脏数据状态,在属性设置函数中设置脏位会引起递归调用问题。

    同步列表(SyncLists)

    同步列表类似于同步变量,但是他们是一些值的列表而不是单个值。同步列表和同步变量都包含在初始的状态更新里。同步列表不需要[SyncVar]属性标识,他们是特殊的类。内建的基础类型属性列表有:

    SyncListString
    SyncListFloat
    SyncListInt
    SyncListUInt
    SyncListBool
    还有个SyncListStruct可以给用户自定义的结构体用。从SyncListStruct派生出的结构体类可以包含基础类型,数组和通用Unity类型的成员变量,但是不能包含复杂的类和通用容器。

    同步列表有一个叫做SyncListChanged的回调函数,可以使客户端能接收到列表中的数据改动的通知。这个回调函数被调用时,会被通知到操作类型,和修改的变量索引。

    public class MyScript :NetworkBehaviour
    {
    public struct Buf
    {
    public int id;
    public string name;
    public float timer;
    };

    public class TestBufs : SyncListStruct<Buf> {}
    TestBufs m_bufs =  new  TestBufs();

    void BufChanged(Operation op, int itemIndex)
    {
    Debug.Log("buf changed:" + op);
    }

    void Start()
    {
    m_bufs.Callback = BufChanged;
    }
    }


    定制序列化函数

    通常在脚本中使用同步变量就够了,但是有时候也需要更复杂的序列化代码。NetworkBehaviour中的虚函数允许开发者定制自己的序列化函数,这些函数有:

    public virtual boolOnSerialize(NetworkWriter writer, bool initialState);
    public virtual voidOnDeSerialize(NetworkReader reader, bool initialState);
    initalState可以用来标识是第一次序列化数据还是只发送增量的数据。如果是第一次发送给客户端,必须要包含所有状态的数据,后续的更新只需要包含增量的修改,以节省带宽。同步变量的钩子函数在initialState为True的时候不会被调用,而只会在增量更新函数中被调用。

    如果一个类里面声明了同步变量,这些函数的实现会自动被加到类里面,因此一个有同步变量的类不能拥有自己的序列化函数。

    OnSerialize函数应该返回True来指示有更新需要发送,如果它返回了true,这个类的所有脏标志位都会被清除,如果它返回False,则脏标志位不会被修改。这可以允许将多次改动合并在一起发送,而不需要每一帧都发送。 

    序列化流程

    具有NetworkIdentity组件的 游戏 物体可以带有多个从NetworkBehaviour派生出来的脚本,这些物体的序列化流程为:

    在服务器上:

    - 每个NetworkBehaviour上都有一个脏数据掩码,这个掩码可以在OnSerialize函数中通过syncVarDirtyBits访问到

    - NetworkBehavious中的每个同步变量被指定了脏数据掩码中的一位

    - 对同步变量的修改会使对应的脏数据位被设置

    - 或者可以通过调用SetDirtyBit 函数 直接修改脏数据 标志

    服务器 的每个Update调用都会检查他的NetworkIdentity组件

    - 如果有标记为脏的NetworkBehaviour,就会为那个物体创建一个更新数据包

    - 每个NetworkBehaviour组件的OnSerialize函数都被调用,来构建这个更新数据包

    - 没有脏数据位设置的NetworkBehaviour在数据包中添加0标志

    - 有脏数据位设置的NetworkBehavious写入他们的脏数据和有改动的同步变量的值

    - 如果一个NetworkBehavious的OnSerialize函数返回了True,那么他的脏标志位被重置,因此直到下一次数据修改之前不会被再次发送

    - 更新数据包被发送到能看见这个物体的所有客户端

    在客户端:

    - 接收到一个物体的更新数据包

    - 每个NetworkBehavious脚本的OnDeserialize 函数 被调用

    - 这个物体上的每个NetworkBehavious脚本读取脏数据标识

    - 如果关联到这个NetworkBehaviour脚本的脏数据位是0,OnDeserialize函数直接返回;

    - 如果脏数据 标志 不是0,OnDeserialize函数继续读取后续的同步变量

    - 如果有同步变量的钩子函数,调用钩子函数

    对下面的代码:

    public class data :NetworkBehaviour
    {
    [SyncVar]
    public int int1 = 66;

    [SyncVar]
    public int int2 = 23487;

    [SyncVar]
    public string MyString = "esfdsagsdfgsdgdsfg";
    }
    产生的序列化 函数 OnSerialize将如下所示:

    public override boolOnSerialize(NetworkWriter writer, bool forceAll)
    {
    if (forceAll)
    {
    // 第一次发送物体信息给客户端,发送全部数据
    writer.WritePackedUInt32((uint)this.int1);
    writer.WritePackedUInt32((uint)this.int2);
    writer.Write(this.MyString);
    return true;
    }
    bool wroteSyncVar = false;
    if ((base.get_syncVarDirtyBits() & 1u) != 0u)
    {
    if (!wroteSyncVar)
    {
    // write dirty bits if this is the first SyncVar written
    writer.WritePackedUInt32(base.get_syncVarDirtyBits());
    wroteSyncVar = true;
    }
    writer.WritePackedUInt32((uint)this.int1);
    }
    if ((base.get_syncVarDirtyBits() & 2u) != 0u)
    {
    if (!wroteSyncVar)
    {
    // write dirty bits if this is the first SyncVar written
    writer.WritePackedUInt32(base.get_syncVarDirtyBits());
    wroteSyncVar = true;
    }
    writer.WritePackedUInt32((uint)this.int2);
    }
    if ((base.get_syncVarDirtyBits() & 4u) != 0u)
    {
    if (!wroteSyncVar)
    {
    // write dirty bits if this is the first SyncVar written
    writer.WritePackedUInt32(base.get_syncVarDirtyBits());
    wroteSyncVar = true;
    }
    writer.Write(this.MyString);
    }

    if (!wroteSyncVar)
    {
    // write zero dirty bits if no SyncVars were written
    writer.WritePackedUInt32(0);
    }
    return wroteSyncVar;
    }
    反序列化 函数 将如下:

    public override voidOnDeserialize(NetworkReader reader, bool initialState)
    {
    if (initialState)
    {
    this.int1 = (int)reader.ReadPackedUInt32();
    this.int2 = (int)reader.ReadPackedUInt32();
    this.MyString = reader.ReadString();
    return;
    }
    int num = (int)reader.ReadPackedUInt32();
    if ((num & 1) != 0)
    {
    this.int1 = (int)reader.ReadPackedUInt32();
    }
    if ((num & 2) != 0)
    {
    this.int2 = (int)reader.ReadPackedUInt32();
    }
    if ((num & 4) != 0)
    {
    this.MyString = reader.ReadString();
    }
    }
    如果这个NetworkBehaviour的基类也有一个序列化 函数 ,基类的序列化函数也将被调用。

    注意更新数据包可能会在缓冲区中合并,所以一个传输层数据包可能包含多个物体的更新数据包。

    远程动作

    网络系统允许在网络上执行远程的动作。这类动作有时也叫做远程过程调用(RPC)。有两种类型的远程过程调用,命令(Commands) – 由客户端发起,运行在服务器上;和客户端远程过程调用(ClientRpc) - 服务器发起,运行在客户端上。

    下图显示了两种远程过程调用的方向

    命令(Commands)

    命令从客户端上的物体发给 服务器 上的物体。出于安全考虑,命令只能从 玩家 控制的物体上发出,因此玩家不能控制其他玩家的物体。要把一个函数变成命令,需要给这个函数添加[Command]属性,并且为函数名添加“Cmd”前缀,这样这个函数会在客户端上被调用时在服务器上运行。所有的参数会自动和命令一起发送给服务器。

    命令 函数 的名字必须要有“Cmd”前缀。在阅读代码的时候,这也是个提示 – 这个函数比较特殊,他不像普通函数一样在本地被执行。

    class Player :NetworkBehaviour
    {
    public GameObject bulletPrefab;

    [Command]
    void CmdDoFire(float lifeTime)
    {
    GameObject bullet =(GameObject)Instantiate(
    bulletPrefab,

    transform.position +transform.right,
    Quaternion.identity);

    var bullet2D =bullet.GetComponent<Rigidbody2D>();
    bullet2D.velocity = transform.right *bulletSpeed;
    Destroy(bullet, lifeTime);

    NetworkServer.Spawn(bullet);
    }

    void Update()
    {
    if (!isLocalPlayer)
    return;

    if (Input.GetKeyDown(KeyCode.Space))
    {
    CmdDoFire(3.0f);
    }
    }
    }
    注意如果每一帧都发送命令消息,会产生很多的网络流量。

    默认情况下,命令是通过0号通道(默认的可靠传输通道)进行传输的。所以默认情况下,所有的命令都会被可靠地发送到 服务器 。可以使用命令的“Channel”参数修改这个配置。参数是一个整数,表示通道号。

    1号通道是默认的不可靠传输通道,如果要用这个通道,把这个参数设置为1,示例如下:

    [Command(channel=1)]

    从Unity5.2开始,可以从拥有客户端授权的非 玩家 物体发出命令。这些物体必须是使用函数NetworkServer.SpawnWithClientAuthority()派生出来的,或者是使用NetworkIdentity.AssignClientAuthority()授权过的。从物体发送出来的命令会在服务器上运行,而不是在相关玩家物体所在的客户端上。

    客户端远程过程调用(ClientRPC Calls)

    客户端远程过程调用从服务器的物体发送到客户端的物体上去。他们可以从任何带有NetworkIdentity并被派生出来的物体上发出。因为 服务器 拥有授权,所以这个过程不存在安全问题。要把一个函数变成客户端远程过程调用,需要给函数添加[ClientRpc]属性,并且为函数名添加“Rpc”前缀。这个函数将在服务端上被调用时,在客户端上执行。所有的参数都将自动传给客户端。

    客户端远程调用必须带有“Rpc”前缀。在阅读代码的时候,这将是个提示 – 这个 函数 比较特殊,不像一般函数那样在本地执行。

    class Player :NetworkBehaviour
    {

    [SyncVar]
    int health;

    [ClientRpc]
    void RpcDamage(int amount)
    {
    Debug.Log("Took damage:" +amount);
    }

    public void TakeDamage(int amount)
    {
    if (!isServer)
    return;

    health  -= amount;
    RpcDamage(amount);
    }
    }

    当使用伺服器模式运行游戏的时候,客户端远程调用将在本地客户端执行 – 即使他其实和 服务器 运行在同一个进程。因此本地客户端和远程客户端对客户端远程过程调用的处理是一样的。

    远程过程的参数

    传递给客户端远程过程调用的参数会被序列化并在网络上传送,这些参数可以是:

    - 基本数据类型(字节,整数,浮点树,字符串,64位无符号整数等)

    - 基本数据类型的数组

    - 包含允许的数据类型的结构体

    - Unity内建的数学类型(Vector3,Quaternion等)

    - NetworkIdentity

    - NetworkInstanceId

    - NetworkHash128

    - 带有NetworkIdentity组件的物体

    远程过程的参数不可以是游戏物体的子组件,像脚本对象或Transform,他们也不能是其他不能在网络上被序列化的数据类型。


    相关资料:

    http://www.itnose.net/st/6318206.html

    展开全文
  • unity 实现帧同步

    万次阅读 多人点赞 2019-02-09 17:14:18
    此框架为有帧同步需求的游戏做一个简单的示例,实现了一个精简的框架,本文着重讲解帧同步游戏开发过程中需要注意的各种要点,伴随框架自带了一个小的塔防sample作为演示. 目录: 哪些游戏需要使用帧同步 如何实现一个...

    阅前提示:

    此框架为有帧同步需求的游戏做一个简单的示例,实现了一个精简的框架,本文着重讲解帧同步游戏开发过程中需要注意的各种要点,伴随框架自带了一个小的塔防sample作为演示.

    哪些游戏需要使用帧同步

    如果游戏中有如下需求,那这个游戏的开发框架应该使用帧同步:
    多人实时对战游戏
    游戏中需要战斗回放功能
    游戏中需要加速功能
    需要服务器同步逻辑校验防止作弊
    LockStep框架就是为了上面几种情况而设计的.

    如何实现一个可行的帧同步框架

    主要确保以下三点来保证帧同步的准确性:

    • 可靠稳定的帧同步基础算法
    • 消除浮点数带来的精度误差
    • 控制好随机数

    帧同步原理

    相同的输入 + 相同的时机 = 相同的显示

    客户端接受的输入是相同的,执行的逻辑帧也是一样的,那么每次得到的结果肯定也是同步一致的。为了让运行结果不与硬件运行速度快慢相关联,则不能用现实历经的时间(Time.deltaTime)作为差值阀值进行计算,而是使用固定的时间片段来作为阀值,这样无论两帧之间的真实时间间隔是多少,游戏逻辑执行的次数是恒定的,举例:
    我们预设每个逻辑帧的时间跨度是1秒钟,那么当物理时间经过10秒后,逻辑便会运行10次,经过100秒便会运行100次,无论在运行速度快的机器上还是慢的机器上均是如此,不会因为两帧之间的跨度间隔而有所改变。
    而渲染帧(一般为30到60帧),则是根据逻辑帧(10到20帧)去插值,从而得到一个“平滑”的展示,渲染帧只是逻辑帧的无限逼近插值,不过人眼一般无法分辨这种滞后性,因此可以把这两者理解为同步的.

    如果硬件的运行速度赶不上逻辑帧的运行速度,则有可能出现逻辑执行多次后,渲染才执行一次的状况,如果遇到这种情况画面就会出现卡顿和丢帧的情况.

    帧同步算法

    基础核心算法

    下面这段代码为帧同步的核心逻辑片段:

    m_fAccumilatedTime = m_fAccumilatedTime + deltaTime;
    
    //如果真实累计的时间超过游戏帧逻辑原本应有的时间,则循环执行逻辑,确保整个逻辑的运算不会因为帧间隔时间的波动而计算出不同的结果
    while (m_fAccumilatedTime > m_fNextGameTime) {
    
        //运行与游戏相关的具体逻辑
        m_callUnit.frameLockLogic();
    
        //计算下一个逻辑帧应有的时间
        m_fNextGameTime += m_fFrameLen;
    
        //游戏逻辑帧自增
        GameData.g_uGameLogicFrame += 1;
    }
    
    //计算两帧的时间差,用于运行补间动画
    m_fInterpolation = (m_fAccumilatedTime + m_fFrameLen - m_fNextGameTime) / m_fFrameLen;
    
    //更新渲染位置
    m_callUnit.updateRenderPosition(m_fInterpolation);
    

    渲染更新机制

    由于帧同步以及逻辑与渲染分离的设置,我们不能再去直接操作transform的localPosition,而设立一个虚拟的逻辑值进行代替,我们在游戏逻辑中,如果需要变更对象的位置,只需要更新这个虚拟的逻辑值,在一轮逻辑计算完毕后会根据这个值统一进行一轮渲染,这里我们引入了逻辑位置m_fixv3LogicPosition这个变量.

    // 设置位置
    // 
    // @param position 要设置到的位置
    // @return none
    public override void setPosition(FixVector3 position)
    {
        m_fixv3LogicPosition = position;
    }
    

    渲染流程如下:

    只有需要移动的物体,我们才进行插值运算,不会移动的静止物体直接设置其坐标就可以了

    //只有会移动的对象才需要采用插值算法补间动画,不会移动的对象直接设置位置即可
    if ((m_scType == "soldier" || m_scType == "bullet") && interpolation != 0)
    {
        m_gameObject.transform.localPosition = Vector3.Lerp(m_fixv3LastPosition.ToVector3(), m_fixv3LogicPosition.ToVector3(), interpolation);
    }
    else
    {
        m_gameObject.transform.localPosition = m_fixv3LogicPosition.ToVector3();
    }
    

    定点数

    定点数和浮点数,是指在计算机中一个数的小数点的位置是固定的还是浮动的,如果一个数中小数点的位置是固定的,则为定点数;如果一个数中小数点的位置是浮动的,则为浮点数。定点数由于小数点的位置固定,因此其精度可控,相反浮点数的精度不可控.

    对于帧同步框架来说,定点数是一个非常重要的特性,我们在在不同平台,甚至不同手机上运行一段完全相同的代码时有可能出现截然不同的结果,那是因为不同平台不同cpu对浮点数的处理结果有可能是不一致的,游戏中仅仅0.000000001的精度差距,都可能在多次计算后带来蝴蝶效应,导致完全不同的结果
    举例:当一个士兵进入塔的攻击范围时,塔会发动攻击,在手机A上的第100帧时,士兵进入了攻击范围,触发了攻击,而在手机B上因为一点点误差,导致101帧时才触发攻击,虽然只差了一帧,但后续会因为这一帧的偏差带来之后更多更大的偏差,从这一帧的不同开始,这已经是两场截然不同的战斗了.
    因此我们必须使用定点数来消除精度误差带来的不可预知的结果,让同样的战斗逻辑在任何硬件,任何操作系统下运行都能得到同样的结果.同时也再次印证文章最开始提到的帧同步核心原理:
    相同的输入 + 相同的时机 = 相同的显示
    框架自带了一套完整的定点数库Fix64.cs,其中对浮点数与定点数相互转换,操作符重载都做好了封装,我们可以像使用普通浮点数那样来使用定点数

    Fix64 a = (Fix64)1;
    Fix64 b = (Fix64)2;
    Fix64 c = a + b;
    

    关于定点数的更多相关细节,请参看文后内容:哪些unity数据类型不能直接使用

    关于dotween的正确使用

    提及定点数,我们不得不关注一下项目中常用的dotween这个插件,这个插件功能强大,使用非常方便,让我们在做动画时游刃有余,但是如果放到帧同步框架中就不能随便使用了.
    上面提到的浮点数精度问题有可能带来巨大的影响,而dotween的整个逻辑都是基于时间帧(Time.deltaTime)插值的,而不是基于帧定长插值,因此不能在涉及到逻辑相关的地方使用,只能用在动画动作渲染相关的地方,比如下面代码就是不能使用的

    DoLocalMove() function()
        //移动到某个位置后触发会影响后续判断的逻辑
        m_fixMoveTime = Fix64.Zero;
    end
    

    如果只是渲染表现,而与逻辑运算无关的地方,则可以继续使用dotween.
    我们整个帧框架的逻辑运算中没有物理时间的概念,一旦逻辑中涉及到真实物理时间,那肯定会对最终计算的结果造成不可预计的影响,因此类似dotween等动画插件在使用时需要我们多加注意,一个疏忽就会带来整个逻辑运算结果的不一致.

    随机数

    游戏中几乎很难避免使用随机数,恰好随机数也是帧同步框架中一个需要高度关注的注意点,如果每次战斗回放产生的随机数是不一致的,那如何能保证战斗结果是一致的呢,因此我们需要对随机数进行控制,由于不同平台,不同操作系统对随机数的处理方式不同,因此我们避免使用平台自带的随机数接口,而是使用自定义的可控随机数算法SRandom.cs来替代,保证随机数的产生在跨平台方面不会出现问题.同时我们需要记录下每场战斗的随机数种子,只要确定了种子,那产生的随机数序列就一定是一致的.
    部分代码片段:

    // range:[min~(max-1)]
    public uint Range(uint min, uint max)
    {
        if (min > max)
            throw new ArgumentOutOfRangeException("minValue", string.Format("'{0}' cannot be greater than {1}.", min, max));
    
        uint num = max - min;
        return Next(num) + min;
    }
    
    public int Next(int max)
    {
        return (int)(Next() % max);
    }
    

    服务器同步校验

    服务器校验和同步运算在现在的游戏中应用的越来越广泛,既然要让服务器运行相关的核心代码,那么这部分客户端与服务器共用的逻辑就有一些需要注意的地方.
    逻辑与渲染进行分离
    逻辑代码版本控制策略
    避免直接使用Unity特定的数据类型
    避免直接调用Unity特定的接口

    逻辑和渲染如何进行分离

    服务器是没有渲染的,它只能执行纯逻辑,因此我们的逻辑代码中如何做到逻辑和渲染完全分离就很重要

    虽然我们在进行模式设计和代码架构的过程中会尽量做到让逻辑和渲染解耦,独立运行(具体实现请参见sample源码),但出于维护同一份逻辑代码的考量,我们并没有办法完全把部分逻辑代码进行隔离,因此怎么识别当前运行环境是客户端还是服务器就很必要了

    unity给我们提供了自定义宏定义开关的方法,我们可以通过这个开关来判断当前运行平台是否为客户端,同时关闭服务器代码中不需要执行的渲染部分

    我们可以在unity中Build Settings–Player Settings–Other Settings中找到Scripting Define Symbols选项,在其中填入

    _CLIENTLOGIC_
    

    宏定义开关,这样在unity中我们便可以此作为是否为客户端逻辑的判断,在客户端中打开与渲染相关的代码,同时也让服务器逻辑不会受到与渲染相关逻辑的干扰,比如:

    #if _CLIENTLOGIC_
            m_gameObject.transform.localPosition = position.ToVector3();
    #endif
    

    逻辑代码版本控制策略

    版本控制:
    同步校验的关键在于客户端服务器执行的是完全同一份逻辑源码,我们应该极力避免源码来回拷贝的情况出现,因此如何进行版本控制也是需要策略的,在我们公司项目中,需要服务器和客户端同时运行的代码是以git子模块的形式进行管理的,双端各自有自己的业务逻辑,但子模块是相同的,这样维护起来就很方便,推荐大家尝试.
    不同服务器架构如何适配:
    客户端是c#语言写的,如果服务器也是采用的c#语言,那正好可以无缝结合,共享逻辑,但目前采用c#作为游戏服务器主要语言的项目其实很少,大多是java,c++,golang等,比如我们公司用的是skynet,如果是这种不同语言架构的环境,那我们就需要单独搭建一个c#服务器了,目前我们的做法是在fedora下结合mono搭建的战斗校验服务器,网关收到战斗校验请求后会转发到校验服务器进行战斗校验,把校验结果返回给客户端,具体的方式请参阅后文:战斗校验服务器简单搭建指引

    哪些unity数据类型不能直接使用

    float
    Vector2
    Vector3
    上面这三种类型由于都涉及到浮点数,会让逻辑运行结果不可控,因此都不能在帧同步相关的逻辑代码中直接使用,用于替代的是在Fix64.cs中定义的定点数类型:
    原始数据类型 替代数据类型
    float Fix64
    Vector2 FixVector2
    Vector3 FixVector3
    同时还有一种例外的情况,某些情况下我们会用Vector2来存放int型对象,在客户端这是没问题的,因为int对象不存在精度误差问题,但是遗憾的是服务器并无法识别Vector2这个unity中的内置数据类型,因此我们不能直接调用,而是需要自己构建一个类似的数据类型,让构建后的数据类型能够跨平台.
    在Fix64.cs中新增了NormalVector2这个数据类型用于替代这些unity原生的数据类型,这样就可以同时在客户端和服务器两端运行同样的逻辑代码了.
    那项目中是不是完全没有float,没有Vector3这些类型了呢,其实也不完全是,比如设置颜色等API调用还是需要使用float的:

    public void setColor(float r, float g, float b)
    {
    #if _CLIENTLOGIC_
        m_gameObject.GetComponent<SpriteRenderer>().color = new Color(r, g, b, 1);
    #endif
    }
    

    鉴于项目中既存在浮点数数据类型也存在定点数数据类型,因此在框架中使用了匈牙利命名法进行区分,让所有参与编码的人员能一眼分辨出当前变量是浮点数还是定点数

    Fix64 m_fixElapseTime = Fix64.Zero;  //前缀fix代表该变量为Fix64类型
    public FixVector3 m_fixv3LogicPosition = new FixVector3(Fix64.Zero, Fix64.Zero, Fix64.Zero); //前缀fixv3代表该变量为FixVector3类型
    float fTime = 0;  //前缀f代表该变量为float类型
    

    哪些unity接口不能直接调用

    unity中某些特有的接口不能直接调用,因为服务器环境下并没有这些接口,最常见接口有以下几种:
    Debug.Log
    PlayerPrefs
    Time
    不能直接调用不代表不能用,框架中对这些常用接口封装到UnityTools.cs,并用上文提到的_CLIENTLOGIC_开关进行控制,

    public static void Log(object message)
    {
    #if _CLIENTLOGIC_
        UnityEngine.Debug.Log(message);
    #else
        System.Console.WriteLine (message);
    #endif
    }
    
    public static void playerPrefsSetString(string key, string value)
    {
    #if _CLIENTLOGIC_
        PlayerPrefs.SetString(key, value);
    #endif
    }
    

    这样在逻辑代码中调用UnityTools中的接口就可以实现跨平台了

    UnityTools.Log("end logic frame: " + GameData.g_uGameLogicFrame);
    

    加速功能

    实现了基础的帧同步核心功能后,加速功能就很容易实现了,我们只需要改变Time.timeScale这个系统阀值就可以实现.

    //调整战斗速度
    btnAdjustSpeed.onClick.AddListener(delegate ()
    {
        if (Time.timeScale == 1)
        {
            Time.timeScale = 2;
            txtAdjustSpeed.text = "2倍速";
        }
        else if (Time.timeScale == 2)
        {
            Time.timeScale = 4;
            txtAdjustSpeed.text = "4倍速";
        }
        else if (Time.timeScale == 4)
        {
            Time.timeScale = 1;
            txtAdjustSpeed.text = "1倍速";
        }
    });
    

    需要注意的是,由于帧同步的核心原理是在单元片段时间内执行完全相同次数的逻辑运算,从而保证相同输入的结果一定一致,因此在加速后,物理时间内的计算量跟加速的倍数成正比,同样的1秒物理时间片段,加速两倍的计算量是不加速的两倍,加速10倍的运算量是不加速的10倍,因此我们会发现一些性能比较差的设备在加速后会出现明显的卡顿和跳帧的状况,这是CPU运算超负荷的表现,因此需要根据游戏实际的运算量和表现来确定最大加速倍数,以免加速功能影响游戏体验

    小谈加速优化

    实际项目中很容易存在加速后卡顿的问题,这是硬件机能决定的,因此如何在加速后进行优化就很重要,最常见的做法是优化美术效果,把一些不太重要的特效,比如打击效果,buff效果等暂时关掉,加速后会导致各种特效的频繁创建和销毁,开销极大,并且加速后很多细节本来就很难看清楚了,因此根据加速的等级选择性的屏蔽掉一些不影响游戏品质的特效是个不错的思路.由此思路可以引申出一些类似的优化策略,比如停止部分音效的播放,屏蔽实时阴影等小技巧.

    战斗回放功能

    通过上面的基础框架的搭建,我们确保了相同的输入一定得到相同的结果,那么战斗回放的问题也就变得相对简单了,我们只需要记录在某个关键游戏帧触发了什么事件就可以了,比如在第100游戏帧,150游戏帧分别触发了出兵事件,那我们在回放的时候进行判断,当游戏逻辑帧运行到这两个关键帧时,即调用出兵的API,还原出兵操作,由于操作一致结果必定一致,因此我们就可以看到与原始战斗过程完全一致的战斗回放了.

    记录战斗关键事件

    1.在战斗过程中实时记录

    GameData.battleInfo info = new GameData.battleInfo();
    info.uGameFrame = GameData.g_uGameLogicFrame;
    info.sckeyEvent = "createSoldier";
    GameData.g_listUserControlEvent.Add(info);
    

    2.战斗结束后根据战斗过程中实时记录的信息进行统一保存

    //- 记录战斗信息(回放时使用)
    // 
    // @return none
    void recordBattleInfo() {
        if (false == GameData.g_bRplayMode) {
            //记录战斗数据
            string content = "";
            for (int i = 0; i < GameData.g_listUserControlEvent.Count; i++)
            {
                GameData.battleInfo v = GameData.g_listUserControlEvent[i];
                //出兵
                if (v.sckeyEvent == "createSoldier") {
                    content += v.uGameFrame + "," + v.sckeyEvent + "$";
                }
            }
    
            UnityTools.playerPrefsSetString("battleRecord", content);
            GameData.g_listUserControlEvent.Clear();
        }
    }
    

    Sample为了精简示例流程,战斗日志采用字符串进行存储,用’$’等作为切割标识符,实际项目中可根据实际的网络协议进行制定,比如protobuff,sproto等

    复原战斗事件

    1.把战斗过程中保存的战斗事件进行解码:

    //- 读取玩家的操作信息
    // 
    // @return none
    void loadUserCtrlInfo()
    {
        GameData.g_listPlaybackEvent.Clear();
    
        string content = battleRecord;
    
        string[] contents = content.Split('$');
    
        for (int i = 0; i < contents.Length - 1; i++)
        {
            string[] battleInfo = contents[i].Split(',');
    
            GameData.battleInfo info = new GameData.battleInfo();
    
            info.uGameFrame = int.Parse(battleInfo[0]);
            info.sckeyEvent = battleInfo[1];
    
            GameData.g_listPlaybackEvent.Add(info);
        }
    }
    

    2.根据解码出来的事件进行逻辑复原:

    //- 检测回放事件
    // 如果有回放事件则进行回放
    // @param gameFrame 当前的游戏帧
    // @return none
    void checkPlayBackEvent(int gameFrame)
    {
        if (GameData.g_listPlaybackEvent.Count > 0) {
            for (int i = 0; i < GameData.g_listPlaybackEvent.Count; i++)
            {
                GameData.battleInfo v = GameData.g_listPlaybackEvent[i];
    
                if (gameFrame == v.uGameFrame) {
                    if (v.sckeyEvent == "createSoldier") {
                        createSoldier();
                    }
                }
            }
        }
    }
    

    框架文件结构

    整个框架中最核心的代码为LockStepLogic.cs(帧同步逻辑),Fix64.cs(定点数)和SRandom.cs(随机数)
    其余代码作为一个示例,如何把核心代码运用于实际项目中,并且展示了一个稍微复杂的逻辑如何在帧同步框架下良好运行.
    battle目录下为帧同步逻辑以及战斗相关的核心代码
    battle/core为战斗核心代码,其中
    -action为自己实现的移动,延迟等基础事件
    -base为基础对象,所有战场可见的物体都继承自基础对象
    -soldier为士兵相关
    -state为状态机相关
    -tower为塔相关
    ui为战斗UI
    view为视图相关

    自带sample流程

    流程:战斗—战斗结束提交操作步骤进行服务器校验—接收服务器校验结果—记录战斗日志—进行战斗回放

    绿色部分为完全相同的战斗逻辑
    蓝色部分为完全相同的用户输入
    示例sample中加入了一个非常简单的socket通信功能,用于将客户端的操作发送给服务器,服务器根据客户端的操作进行瞬时回放运算,然后将运算结果发还给客户端进行比对,这里只做了一个最简单的socket功能,力求让整个sample最精简化,实际项目中可根据原有的服务器架构进行替换.

    战斗校验服务器简单搭建指引

    安装mono环境

    编译可执行文件

    实现简单socket通信回传
    安装mono环境
    进入官网https://www.mono-project.com/download/stable/#download-lin-fedora
    按照指引进行安装即可

    编译可执行文件

    1.打开刚才安装好的monodeveloper
    2.点击file->new->solution
    3.在左侧的选项卡中选择Other->.NET
    4.在右侧General下选择Console Project

    在左侧工程名上右键导入子模块中battle文件夹下的所有源码

    点击build->Rebuild All,如果编译通过这时会在工程目录下的obj->x86->Debug文件夹下生成可执行文件
    如果编译出错请回看上文提到的各种注意点,排查哪里出了问题.

    开发过程中发现工程目录下如果存在git相关的文件会导致monodeveloper报错关闭,如果遇到这种情况需要将工程目录下的.git文件夹和.gitmodules文件进行删除,然后即可正常编译了.

    运行可执行文件

    cmd打开命令行窗口,切换到刚才编译生成的Debug文件目录下,通过mono命令运行编译出来的exe可执行文件

    mono LockStepSimpleFramework.exe
    

    服务器端战斗校验逻辑

    可执行文件生成后并没有什么实际用处,因为还没有跟我们的战斗逻辑发生联系,我们需要进行一些小小的修改让验证逻辑起作用.
    修改新建工程自动生成的Program.cs文件,加入验证代码

    BattleLogic battleLogic = new BattleLogic ();
    battleLogic.init ();
    battleLogic.setBattleRecord (battleRecord);
    battleLogic.replayVideo();
    
    while (true) {
        battleLogic.updateLogic();
        if (battleLogic.m_bIsBattlePause) {
            break;
        }
    }
    Console.WriteLine("m_uGameLogicFrame: " + BattleLogic.s_uGameLogicFrame);
    

    通过上述代码我们可以看到,首先构建了一个BattleLogic对象,然后传入客户端传过来的操作日志(battleRecord),然后用一个while循环在极短的时间内把战斗逻辑运算了一次,当判断到m_bIsBattlePause为true时证明战斗已结束.
    那么我们最后以什么作为战斗校验是否通过的衡量指标呢?很简单,通过游戏逻辑帧s_uGameLogicFrame来进行判断就很准确了,因为只要有一丁点不一致,都不可能跑出完全相同的逻辑帧数,如果想要更保险一点,还可以加入别的与游戏业务逻辑具体相关的参数进行判断,比如杀死的敌人个数,发射了多少颗子弹等等合并作为综合判断依据.

    实现简单socket通信回传

    光有战斗逻辑校验还不够,我们需要加入服务器监听,接收客户端发送过来的战斗日志,计算出结果后再回传给客户端,框架只实现了一段很简单的socket监听和回发消息的功能(尽量将网络通信流程简化,因为大家肯定有自己的一套网络框架和协议),具体请参看Sample源码.

    Socket serverSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
    IPAddress ip = IPAddress.Any;
    IPEndPoint point = new IPEndPoint(ip, 2333);
    //socket绑定监听地址
    serverSocket.Bind(point);
    Console.WriteLine("Listen Success");
    //设置同时连接个数
    serverSocket.Listen(10);
    
    //利用线程后台执行监听,否则程序会假死
    Thread thread = new Thread(Listen);
    thread.IsBackground = true;
    thread.Start(serverSocket);
    
    Console.Read();
    

    框架源码

    客户端

    https://github.com/CraneInForest/LockStepSimpleFramework-Client.git

    服务器

    https://github.com/CraneInForest/LockStepSimpleFramework-Server.git

    客户端服务器共享逻辑

    https://github.com/CraneInForest/LockStepSimpleFramework-Shared.git

    共享逻辑以子模块的形式分别加入到客户端和服务器中,如要运行源码请在clone完毕主仓库后再更新一下子模块,否则没有共享逻辑是无法通过编译的

    子模块更新命令:

    git submodule update --init --recursive
    

    编译环境:

    客户端:win10 + unity5.5.6f1

    服务器:fedora27 64-bit


    作者:CraneInForest
    来源:CSDN
    原文:https://blog.csdn.net/wanzi215/article/details/82053036
    版权声明:本文为博主原创文章,转载请附上博文链接!

    展开全文
  • Unity动画状态机学习笔记一、建平面,拖人物模型、建状态机、动画导入、拖组件——实现Game时人物动画为等待状态。二、拖WAIT01、WAIT02、WAIT03、WAIT04——实现按数字1切换动作1,按数字2切换动作2,按数字3切换...
  • 同步同步状态,只同步玩家的操作指令,操作指令包含当前的帧索引。这里最重要的概念就是 相同的输入 + 相同的时机 = 相同的输出。 实现帧同步的一般流程是: 同步随机数种子。(一般游戏中都设计随机数的使用, ...
  • Photon与Unity核心技术之角色动作同步

    千次阅读 2018-03-14 09:26:24
    上篇介绍了角色更换武器同步处理,本篇介绍关于使用Photon实现角色同步,客户端还是选择Unity开发,在实现动作之前,Unity为我们提供了我们俗称为老动画和新动画状态机,我们先介绍老动画的实现方式,它是通过...
  • 网络游戏同步,从技术方向来说有两个,一个是状态同步,一个是帧同步。 这里有个游戏开发交流小组点击可以加入,一起学习交流 状态同步是如何实现的呢?其实很简单,就是服务器上跑游戏逻辑,服务器通过网络接收...
  • Unity3D简单的帧同步方案

    千次阅读 2018-06-04 11:05:44
    公司的游戏准备上线了,我呢也在准备新项目,这几天看了一下策划文档,写了整体流程和需求接口的代码,终于有一点点时间来写自己的博客了。今天我们就来讲讲帧同步吧。...
  • Unity3d 插值同步

    千次阅读 2017-09-18 15:07:03
    A玩家 移动时,本自行移动,并发送移动指令给服务端,假设移动是成功的,服务端同步其他客户端 B玩家,B玩家 中用一个队列 Queue 来装服务端来的移动指令,然后客户端在updata中做插值 (lerp ) 处理,这样 A...
  • unity 局域网(Network)同步

    千次阅读 2019-02-02 15:43:07
    到这里同步是已经实现了,如果要测试的话需要发布出两个PC端,分别用作服务器和客户端。 创建服务器: 连接服务器: 可以看到在客户端连接服务器的时候已经把服务器的cube给同步了进来。 个人总结...
  • [Unity5.1]同步动画UNET

    千次阅读 2015-09-01 06:34:56
    这一次,我们将同步动画〜 UNET第21部分 - 网络动画 [目标]以同步动画! ①下载 首先,下载资产〜这个时候资产被称为“最大的冒险模式” - 既然出来了,第二,当你搜索“最大...
  • 阅前提示: 此框架专为有帧同步需求的游戏打造,实现了一个最精简的框架,这几篇文章我会详细讲解帧同步...哪些unity接口不能调用(Debug.log) 定点数 如何去掉Vector2和Vector3 不能使用dotween 随机数 如何做加...
  • Unity 3d 帧同步

    千次阅读 2015-10-05 21:44:46
    最近给两个新人解释帧同步,写了一个简单的帧同步模型,但是公司不给上网,代码上传不了,于是回家重写了一个,但是有些工具类不想写了,在网上找了一些可以用的代码凑合着用了。 帧同步的原理就是在一个回合内(一...
  • 获取动画clip var clips = m_Animator.runtimeAnimatorController.animationClips; // 添加到下拉菜单中 for (int i = 0; i ; i++) { m_ListOptions.Add(new TMP_Dropdown.... 播放当前状态动画 m_Animator.Play(0, 0);
  • 同步机制中最常见的应该是CS状态同步,我们的端游也是这样做的,当然,状态同步也有他的优缺点。 先看一下状态同步的优点。 第一,它的安全性非常高,外挂基本上没有什么能力从中收益。 第二,状态...
  • 接触Unity以来就已经有了Animator,Animation用的少,不过也大概理解他俩之间的一个区别于联系。 ...比如在Animator没有出现的时候有些公司写的动画状态机其实就是代码版的Animator。 Anima...
  • 1.垂直同步的解释: 一个图像是由一个二维的矩阵组成的,水平同步是每一行进行读取 垂直同步是竖向的同步 VSync Count中的参数解释 0:Don’t Sync 1:Every V Blank 2:Every Second V Blank 关于帧率: 帧率是一个...
  • 可以重写其他状态机的某一状态; 不能改变原来状态机的切换逻辑;   六、动画的重定向: 什么叫动画的重定向:其他模型的动画用作自身模型,虽然是不同模型,但是播放的是同一个动作; 注意:动作的...
  • unity network 数据同步

    千次阅读 2014-05-08 17:12:03
    //初始化本服务器端口,第一个参数就是本接收多少连接 NetworkConnectionError error = Network.InitializeServer(12,Port,false); //连接状态 switch(error){ case NetworkConnectionError.No...
  • Unity-Animator深入系列---状态机面板深入 https://www.cnblogs.com/hont/p/5099213.html?utm_source=tuicool&utm_medium=referral Unity-Animator深入系列---状态机面板深入 回到Animator深入系列总目录 ...
  • Unity动画系统详解7:Layer是什么?

    千次阅读 2020-07-05 11:01:59
    “大智,我的Animator状态机里面的状态很多,现在太难管理了。我有很多姿势,比如不拿枪,拿手枪,拿冲锋枪,拿步枪,每种姿势都要做一个混合树,现在再去加蹲的状态,简直爆炸,有没有什么好办法?” “我看看你的...
  • Unity网络通信(三)同步物体

    万次阅读 2012-08-28 17:44:27
    在上两篇的基础上,这次我们要做物体同步。使两个物体在两个机器上显示同样的移动效果。这里,使用W、S、A、D四个键位实现前后左右的移动。 注意:如果您复制粘贴代码,千万注意文件编码!最好为ANSI,否则会报出...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,102
精华内容 1,240
关键字:

unity状态机同步