最近看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提供了一个APIPhysics.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.服务器将状态同步给客户端(状态缓存,状态插值,估算帧)文章中,物体的状态只同步了position
和rotation
,现在把rigidbody的velocity
和angularVelocity
加上
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之外,开放源代码,确定性的物理引擎)等等.