美文网首页网络编程游戏同步unity
[从零开始的Unity网络同步] 8.物理碰撞的网络同步(在客户

[从零开始的Unity网络同步] 8.物理碰撞的网络同步(在客户

作者: 烂笔头_27 | 来源:发表于2018-11-20 15:03 被阅读176次

    前言

    写完上一篇文章([从零开始的Unity网络同步] 7.物理状态的网络同步)之后,在Q群有一位朋友提了一个问题,在这个网络框架下,无法正常处理物体与物体之间的碰撞,经过测试以后,发现确实会出现这样的情况,如图:

    Physics Collision.gif
    可以看到,在客户端物体(蓝色立方体)移动,然后碰撞到服务器物体(红色立方体)时,由于服务器端的物体在客户端是滞后的(在[从零开始的Unity网络同步] 5.服务器将状态同步给客户端(状态缓存,状态插值,估算帧)中有讲到),而客户端物体是本地预测的([从零开始的Unity网络同步] 6.客户端本地预表现),当发生碰撞时,不能及时地产生碰撞反馈,所以导致碰撞的结果两端不一致,然后客户端就预测失败,产生很强烈的抖动和拉扯.这显然不是我们想要的结果.
    那么如何来解决这样的问题呢???

    1.思路

    原因已经找到了,因为在客户端,客户端的物体是本地预测的,而服务器的物体是根据收到的状态包进行插值,两者在当前时刻,物理状态有差异,所以导致的碰撞异常,既然是因为服务端和客户端的物体,模拟的步调不一致导致的,那么可不可以在客户端去预测服务端的物体,使两者能够保持相同的模拟步调呢???
    GDC2018演讲 《火箭联盟》的物理与网络细节(需要科学上网)这个视频中,从37分22秒开始,演讲者演示了在《火箭联盟》中是如何做到在客户端对服务器的球的物理状态进行预测.
    因此,"在巨人的肩膀的肩膀上",在之前的网络同步架构之下,做一点拓展,使在客户端预测服务端物体的物理状态.

    2.模仿《火箭联盟》制作汽车(Car)和球(Ball)

    新建一个预设Car,样子大概这样:


    car.png

    新建一个预设Ball,样子是这样:


    ball.png
    为了让球(Ball)更像真实的球,给它添加带弹性的物理材质:
    PhysicBall.png PhysicBallMat.png

    3.为汽车(Car)和球(Ball)添加控制逻辑,以及需要同步的网络状态

    汽车的控制代码:

    //执行操作输入,根据按键施加不同方向的力
    public override void ExecuteCommand(Command command)
    {
        CommandInput input = command.input;
        if (input.forward)
            rigidbody.AddForce(transform.forward * driveForce * rigidbody.mass, ForceMode.Force);  //按W键,向前加力
        if (input.backward)
             rigidbody.AddForce(-transform.forward * driveForce * rigidbody.mass, ForceMode.Force); //按S键,向后加力
        if (input.left)
             rigidbody.AddTorque(Vector3.down * turnForce * rigidbody.mass, ForceMode.Force);  //按A键,添加扭矩,向左转
        if (input.right)
             rigidbody.AddTorque(Vector3.up * turnForce * rigidbody.mass, ForceMode.Force);  //按D键,添加扭矩,向右转
        if (input.jump)
             rigidbody.AddForce(Vector3.up * jumpForce * rigidbody.mass, ForceMode.Force);  //按Space键,向上加力
    
        Physics.Simulate(Time.fixedDeltaTime);            //物理模拟一次
    
        command.result.velocity = rigidbody.velocity;                //模拟完立刻能取到模拟结果
        command.result.angularVelocity = rigidbody.angularVelocity;  //模拟完立刻能取到模拟结果
    }
    

    球(Ball)不接收按键输入,只有需要同步的物理状态,物理状态跟汽车(Car)是相同的

    // 球的状态
    public class BallState
    {
        public Vector3 position;                   //位置
        public Quaternion rotation;                //旋转
        public Vector3 velocity;                  //刚体速度
        public Vector3 angularVelocity;          //刚体角速度
    }
    // 汽车的状态
    public class CarState
    {
        public Vector3 position;                   //位置
        public Quaternion rotation;                //旋转
        public Vector3 velocity;                  //刚体速度
        public Vector3 angularVelocity;          //刚体角速度
    }
    

    就这样,汽车(Car)和球(Ball)都创建好了,可以进行基本的碰撞同步检测了,效果如图:


    Car Collision Ball.gif

    可以看到,在汽车(Car)冲撞到球(Ball)之后,球发生了剧烈的抖动,接下来,就要解决这个问题了.

    4.在客户端为服务器物体进行物理状态预测

    在目前的同步框架下,服务器的物体在客户端是基于状态进行插值变化的.所以是滞后了,为了能在客户端预测它,我们可以创建一个假的球(DummyBall),然后把真正的球(ServerBall)隐藏(PS:仅仅是隐藏,同步逻辑还是一样的),这样,就可以做到

    汽车(ClientCar)不和ServerBall发生物理碰撞,只和DummyBall发生碰撞
    可以在客户端对DummyBall进行物理预测,而不是影响ServerBall

    这可能有点绕,简而言之,就是为了在客户端预测服务器的物体,客户端创建了一个假的"欺骗"玩家,但不是真的欺骗,DummyBall在预测之前的物理状态必须是服务器下发的最新状态,DummyBall的代码如下:

    // Dummyball,为减少篇幅,使用单例
    public class DummyBall : MonoBehaviour 
    {
        public static DummyBall instance;
        public Entity actualEntity;
        public new Rigidbody rigidbody;
    
        public static void Create(Entity entity)
        {
            //创建一个跟ServerBall一模一样的DummyBall
            GameObject dummy = GameObject.Instantiate(entity.gameObject);
            DontDestroyOnLoad(dummy);
            dummy.name = "Server Dummy";
            dummy.layer = Layer.Dummy;
            instance = dummy.AddComponent<DummyBall>();
            instance.actualEntity = entity;
            //设置成紫红色
            foreach (var mr in dummy.GetComponentsInChildren<MeshRenderer>())
                mr.material.SetColor("_Color", Color.magenta);
    
            //将真正的Ball隐藏起来
            instance.rigidbody = dummy.GetComponent<Rigidbody>();     
            entity.gameObject.SetActive(false);
            Collider[] cols = entity.gameObject.GetComponentsInChildren<Collider>();
            foreach (Collider collider in cols)
                collider.enabled = false;
        }
    
        public void SetDummyBallState()
        {
            rigidbody.position = actualEntity.lastState.position;            //使用最后收到的状态来设置position
            rigidbody.rotation = actualEntity.lastState.rotation;            //使用最后收到的状态来设置rotation
            rigidbody.velocity = actualEntity.lastState.velocity;            //使用最后收到的状态来设置velocity
            rigidbody.angularVelocity = actualEntity.lastState.angularVelocity;  //使用最后收到的状态来设置angularVelocity
        }
    }
    

    然后客户端为自己(ClientCar)做预测的同时,也为DummyBall做预测,代码:

    // 每个模拟帧要执行的方法
    public void Simulate()
    {
        OnSimulateBefore();
        if(isLocalPredicted)            //如果是需要本地预测的单位,获取指令,直接执行指令即可
        { 
            if (DummyBall.instance != null)
                DummyBall.instance.SetDummyState();        //每次客户端预测前,DummyBall都应用最新的State
            foreach (Command cmd in commandQueue)
            {
                if((cmd.flags & CommandFlags.HAS_EXECUTED) && !(cmd.flags & CommandFlags.VERIFIED))          //本地已经执行过 且 没有被服务确认过的指令
                {
                     ExecuteCommand(cmd);            
                }               
            }
            Command cmd = new Command ();        
            cmd.input =  CollectCommandInput();      // 获取指令      
            ExecuteCommand(cmd);                    // 执行指令
            cmd.flags |= CommandFlags.HAS_EXECUTED;        //标记这个命令执行过了
            commandQueue.Enqueue(cmd);              //已经执行过的指令,需要缓存
        }
        OnSimulateAfter();
    }
    

    在汽车(Car)的执行操作指令的逻辑中,因为Physics.Simulate()是全局的,所以客户端预测执行一次,DummyBall也预测模拟了一次.

    Physics.Simulate(Time.fixedDeltaTime);            //物理模拟一次.包括 ClientCar 和 DummyBall.
    

    看看效果吧(蓝色车是客户端控制,紫色球是假球DummyBall,都是客户端做预测的):


    Has Dummy.gif

    可以看到,在客户端的预测下,汽车(Car)碰撞到球(Ball)时,产生了很及时的碰撞反馈.此方案可行
    再把真实的球(ServerBall)给显示出来对比一下(蓝色车是客户端控制,紫色球是假球DummyBall,都是客户端做预测的, 红色球是ServerBall,是由服务器下发的状态包来做插值):


    Has Dummy Server.gif

    5.小结

    通过创建DummyBall在客户端实现对服务器物体的物理预测,虽然感觉像是玩家在踢"假球",但是可以换个说法,玩家是在踢"未来的球",这样听起来就很Amazing了~
    在不确定性的物理模拟和较高的网络波动环境下,这样的做法总会发生误差,为了减少误差带来的游戏体验,在带宽允许的条件下,可以尽可能的增加网络传输的频率,比如:20个包/秒,还有对数据流量进行压缩也很有必要.

    相关文章

      网友评论

      本文标题:[从零开始的Unity网络同步] 8.物理碰撞的网络同步(在客户

      本文链接:https://www.haomeiwen.com/subject/ggwzfqtx.html