最近在研究多人游戏的同步技巧,想要学习帧同步/状态同步相关概念,于是有了个demo。
原素材用的是unity官方自带的第三人称视角游戏示例demo,这个demo里实现了人物最基本的运动控制器。
网络模块用的是NetCode。
NetCode的官方文档事无巨细地讲了导入和使用,这里就先略过了,只需要了解netcode事先为我们提供了两个属性[ClientRpc]和[ServerRpc]即可。
public override void OnNetworkSpawn()
{
//client和server连接后的调用
}
[ClientRpc]
void ClientRpc(param Param)
{
//client收到消息后的函数调用
}
[ServerRpc]
public void ServerRpc(param Param)
{
//server收到消息后的函数调用
}
netcode主要是维护一系列的NetworkBehaviour,在进行client/server连接的时候,会各自在两端生成一个networkBehaviour gameObject,如果是n个client和1个server连接,那么每个client和server都会有n个network gameObject。有IsClient/IsServer/IsOwner代表是否是客户端/服务器/代表自己的那个network.
客户端和服务器通信只在相同NetworkObjectID的networkBehaviour间传输,通过带ClientRpc和ServerRpc属性的函数进行沟通。
如果只是想简单同步,我们只需要获取玩家输入,发送给服务器端,然后服务器端分发给各个客户端。这个第三人称demo里面有一个StarterAssetsInputs类专门管理玩家输入信息:
很简单,那我们就只需要在每帧把这个类信息传到服务器,然后服务器分发给所有客户端就行,具体操作即是:
ThirdPersonController m_selfPlayer = null;
StarterAssetsInputs m_selfInput = null;
public override void OnNetworkSpawn()
{
if (!IsServer && IsOwner)
{
m_selfPlayer = FindSelfPlayer();
m_selfInput = m_selfPlayer.GetComponent();
ServerRpc(new StarterAssetsInputsInfo(m_selfInput), m_selfPlayer.GetMainCameraEulerAngles());
}
}
public override void OnNetworkDespawn()
{
if (!IsOwner && !IsServer && m_selfPlayer != null)
{
m_selfPlayer.DestroySelf();
m_selfPlayer = null;
}
}
//寻找自身的player组件
ThirdPersonController FindSelfPlayer()
{
return GameObject.Find("PlayerArmature").GetComponent();
}
private void Update()
{
//每一帧同步player的信息
if (!IsServer && IsOwner && m_selfPlayer != null)
{
ServerRpc(new StarterAssetsInputsInfo(m_selfInput), m_selfPlayer.GetMainCameraEulerAngles());
}
}
[ClientRpc]
void ClientRpc(StarterAssetsInputsInfo inputInfo, Vector3 mainCameraEulerAngles)
{
if (m_selfPlayer == null)
{
GameObject goPlayer = Instantiate(AssetDatabase.LoadAssetAtPath("Assets//Prefabs//PlayerArmature.prefab"));
m_selfPlayer = goPlayer.transform.GetComponent();
}
m_selfPlayer.UpdateByInput(inputInfo, mainCameraEulerAngles);
}
[ServerRpc]
public void ServerRpc(StarterAssetsInputsInfo inputInfo, Vector3 mainCameraEulerAngles)
{
//只用来转发,所有客户端都会在服务器收到同步消息后同步
ClientRpc(inputInfo, mainCameraEulerAngles);
}
netcode本身可以序列化一些基础类型的信息,比如Vector3,int这种数据类型,如上代码可以直接传输playerPos这种Vector3信息,上述说的input类本身还是比较复杂的,需要走一次netcode官方提供的接口做一次序列化转换,否则无法实现网络传输。
人物转向相关代码有点问题,于是后期对代码进行了改良,将人物控制器代码原来用到单例相关的(比如mainCamera相关参数,实际上这种只能对自己操控的这个人有影响,不能对其他人物也有参数影响)都改为可变参数传到服务器再由服务器做同步。
因为之前处理回调的时候,操纵方的手感是很差的,动作并不是非常丝滑,有时候我一直按着移动键却迟迟没有看到人物移动,原因是我把input数据传到服务器再回传回来时直接覆盖了此时的input数据,导致目前操作被上一次的操作回调覆盖了。应该是input相关的数据依然让它保持客户端本地更新。
desktop 2023-12-10 16-44-58
优化后network代码(新增Despawn,断联时干掉相关player):
ThirdPersonController m_selfPlayer = null;
StarterAssetsInputs m_selfInput = null;
public override void OnNetworkSpawn()
{
if (!IsServer && IsOwner)
{
m_selfPlayer = FindSelfPlayer();
m_selfInput = m_selfPlayer.GetComponent();
ServerRpc(new StarterAssetsInputsInfo(m_selfInput), m_selfPlayer.GetMainCameraEulerAngles());
}
}
public override void OnNetworkDespawn()
{
if (!IsOwner && !IsServer && m_selfPlayer != null)
{
m_selfPlayer.DestroySelf();
m_selfPlayer = null;
}
}
//寻找自身的player组件
ThirdPersonController FindSelfPlayer()
{
return GameObject.Find("PlayerArmature").GetComponent();
}
private void Update()
{
//每一帧同步player的信息
if (!IsServer && IsOwner && m_selfPlayer != null)
{
ServerRpc(new StarterAssetsInputsInfo(m_selfInput), m_selfPlayer.GetMainCameraEulerAngles());
}
}
[ClientRpc]
void ClientRpc(StarterAssetsInputsInfo inputInfo, Vector3 mainCameraEulerAngles)
{
if (m_selfPlayer == null)
{
GameObject goPlayer = Instantiate(AssetDatabase.LoadAssetAtPath("Assets//Prefabs//PlayerArmature.prefab"));
m_selfPlayer = goPlayer.transform.GetComponent();
}
m_selfPlayer.UpdateByInput(inputInfo, mainCameraEulerAngles);
}
[ServerRpc]
public void ServerRpc(StarterAssetsInputsInfo inputInfo, Vector3 mainCameraEulerAngles)
{
//只用来转发,所有客户端都会在服务器收到同步消息后同步
ClientRpc(inputInfo, mainCameraEulerAngles);
}
多人游戏同步相关知识点还是非常多的,以上还只是在低延迟的情况下,要确保玩家在不同网络条件下均能够正常游戏,还需要考虑到网络延迟和抖动,并使用恰当的插值和平移技术,以使得游戏状态在不同客户端上看起来尽量一致。之后对这个demo的扩充就是进一步帧同步。