5评论

[从零开始的Unity网络同步] 7.物理状态的网络同步

烂笔头_27 2018-11-15 1.8k浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏开发行业精英群711501594

最近看GDC2018演讲《火箭联盟》的物理与网络细节(需要科学上网),在Rocket League(以下简称RT)中,汽车和足球用的都是真实物理模拟,开发的引擎是Unreal Engine 3,使用的物理引擎是Bullet,在游戏中,客户端操控的东西都是带预测的,包括汽车的运动,球的运动,那么如何在Unity实现物理状态的网络同步呢?接下来就尝试着实现一下.

1.根据操作输入,改变物理状态

在场景中创建一个Cube,然后挂上Rigidbody组件,这样就拥有一个很简单的带物理的物体了.
接收按键输入W,S,A,D,按下一个键就对Cube施加一个力.代码如下:

//模拟一次
public void Simulate()
{
    OnSimulateBefore();
    if(isServer || isOwner)            //如果是服务器的物体,获取指令,直接执行指令即可
    {
        Command cmd = new Command ();
        cmd.input =  CollectCommandInput();      // 获取指令      
        ExecuteCommand(cmd);                    // 执行指令
    }
    OnSimulateAfter();
}
//收集键输入
public CommandInput CollectCommandInput()
{
    Command cmd = Command.Create();
    cmd.input.forward = Input.GetKey(KeyCode.W);
    cmd.input.backward = Input.GetKey(KeyCode.S);
    cmd.input.left = Input.GetKey(KeyCode.A);
    cmd.input.right = Input.GetKey(KeyCode.D);
    return cmd.input;
}
//执行操作输入,根据按键施加不同方向的力
public override void ExecuteCommand(Command command)
{
    CommandInput input = command.input;
    if (input.forward)
        rigidbody.AddForce(Vector3.forward * force * rigidbody.mass, ForceMode.Force);
    if (input.backward)
        rigidbody.AddForce(Vector3.back * force * rigidbody.mass, ForceMode.Force);
    if (input.left)
        rigidbody.AddForce(Vector3.left * force * rigidbody.mass, ForceMode.Force);
    if (input.right)
        rigidbody.AddForce(Vector3.right * force * rigidbody.mass, ForceMode.Force);
}

通过对刚体施加力,基本的物理操作就完成了.

2.Unity下的确定性物理模拟

在Unity中,内置的物理引擎是PhysX,依据确定性原则,首先得了解PhysX是确定性的吗?可以预测吗?,我在网上找了很多文章,也没找到一个确定的结果,有的说法说PhysX是确定性的,测试过很多次的结果都一致(文章Deterministic physics options),又有的说法说PhysX为了实现高效,牺牲了确定性,在不同平台下可能导致物理模拟结果不同等等,反正还没有得到确定的答案(毕竟PhysX的物理组件在Unity中没有开源,只有访问接口).
Any way,先暂且把Unity中的物理模拟是确定性的(如果认为它是不确定的,那这篇就没必要写了:)),要实现物理的预测,需要自己手动调用物理模拟,Unity提供了一个API-Physics.Simulate,通过将来Physics.autoSimulation = false关闭Unity内部的自动物理模拟,然后手动执行Physics.Simulate(fixedDeltaTime)来模拟一次物理.在调用结束后,就可以拿到模拟的结果了.

private void Awake()
{
    Physics.autoSimulation = false;                //关闭物理的自动模拟
}
public override void ExecuteCommand(Command command)
{
    CommandInput input = command.input;
    if (input.forward)
        rigidbody.AddForce(Vector3.forward * force * rigidbody.mass, ForceMode.Force);
    if (input.backward)
        rigidbody.AddForce(Vector3.back * force * rigidbody.mass, ForceMode.Force);
    if (input.left)
        rigidbody.AddForce(Vector3.left * force * rigidbody.mass, ForceMode.Force);
    if (input.right)
        rigidbody.AddForce(Vector3.right * force * rigidbody.mass, ForceMode.Force);

    Physics.Simulate(Time.fixedDeltaTime);              //每执行一次指令,就手动模拟一次    

    command.result.velocity = rigidbody.velocity;                //模拟完立刻能取到模拟结果
    command.result.angularVelocity = rigidbody.angularVelocity;  //模拟完立刻能取到模拟结果
}

3.服务器将物理状态同步给客户端

[从零开始的Unity网络同步] 5.服务器将状态同步给客户端(状态缓存,状态插值,估算帧)文章中,物体的状态只同步了positionrotation,现在把rigidbody的velocityangularVelocity加上

public class State
{
    public Vector3 position;                   //位置
    public Quaternion rotation;                //旋转
    public Vector3 velocity;                  //刚体速度
    public Vector3 angularVelocity;          //刚体角速度
}

按照第五篇文章的流程,服务器将状态同步给客户端的表现如下(6packet/second ):

4.客户端上传操作给服务端,同时预测物理模拟

[从零开始的Unity网络同步] 6.客户端本地预表现的流程一样,因为客户端调用了手动物理模拟Physics.Simulate(Time.fixedDeltaTime),每次收到服务器下发新的状态后,预测的客户端都重置状态,然后把缓存的指令执行一遍,同时调用Physics.Simulate(Time.fixedDeltaTime).客户端预测的表现如下(6packet/second ):

5.小结

从上面的结果来看,Unity中物理模拟在网络同步中都用挺不错的表现,但是,其实还是存在问题的:
>手动物理模拟方法Physics.Simulate()是全局的,这就意味着调用一次,游戏内所有的物理物体都会模拟一次,没办法仅对单个物体进行模拟,想要改进的话,只有自己在代码逻辑中对物体的状态做备份
如果涉及到游戏内很多的物体,频繁的单帧调用多次Physics.Simulate()尚不明确会带来多严重的效率问题.

所以如果真的想在Unity中实现物理模拟的网络同步,建议最好能够使用自己可控的,确定性的物理模拟,比如说自己实现的简单物理模拟逻辑,或者使用插件Bullet Physics For Unity(独立于Unity之外,开放源代码,确定性的物理引擎)等等.