前言
之前接触过某些教程中的联机方案和直接用socket来做聊天室。
这俩方案各有利弊。前者提供了现成的网络同步框架,用起来方便,从数据库读写、自定义通信协议到分层处理,很经典,不过实际做起来要考虑各个方面,得面面俱到;而后者提供了相对简单的通信链接,但要想在个人项目中方便使用的话,还需自己封装功能。
在网上搜索时,发现Unity之前提供了一些联机解决方案,如MLAPI
和UNet
,不过它们都被弃用了,现在最新的是Unity.Netcode
,所以来学这个了。
Unity提供了对应的入门教程->Get started with NGO。
这个没啥好说的。你可以尝试理解并改造教程代码来熟悉其运作方式。
NetworkBehaviour
官方文档指路->NetworkBehaviour
NetworkBehaviour
是一个抽象类,继承自MonoBehavior
。
OnNetworkSpawn
NetworkBehaviour
中提供了OnNetworkSpawn
,给网络代码做初始化时就应该发生在该方法中。调用顺序如下。
动态生成时,Awake->OnNetworkSpawn->Start
静态放置时,Awake->Start->OnNetworkSpawn
官方文档中提到:
即使该对象尚未Spawn,仍会调用FixedUpdate、Update、LateUpdate。所以要加入如下限制:
private void Update()
{
if (!IsSpawned)
{
return;
}
// Netcode specific logic below here
}
OnNetworkDeSpawn
和OnNetworkSpawn
相对,在它被取消生成时。这是所有网络代码被清理时应该发生的地方,但不要与销毁混淆。在任何东西被摧毁之前都会发生DeSpawn。
下面来记录一下几个和同步相关的类。
RPC
官方指南指路->Sending Events with RPCs
熟悉Web的应该知道Http协议,RPC和Http一样,都是应用层协议。但区别在于:HTTP更适用于Web资源的请求和响应;而RPC更适用于分布式应用程序之间的远程过程调用,相当于调用了远程应用程序中的对应方法。
如何使用
如下是我对官方教程中的代码进行的改造,[ClientRPC]类似。
public void Move(int horizontal)
{
//p2p主机既为服务器,又是客户端,则无需分开处理
SubmitPositionRequestServerRpc(horizontal);
}
[ServerRpc]//被该特性标记,则调用方法时不在本地执行,而是存入本地队列中,帧结束时向Server发送
void SubmitPositionRequestServerRpc(int horizontal)
{
Vector3 v = horizontal > 0 ? Vector3.left : Vector3.right;
Position.Value -= v;
}
注意,为了命名规范,最好把RPC方法命名为FunctionName
+ServerRPC/ClientRPC
。
这个特性还支持标注某个RPC使用不可靠的方法来进行调用,如下。
[ServerRpc(Delivery = RpcDelivery.Unreliable)]
官方文档中称:
可靠的 RPC 将按照触发的顺序在远程端接收,但此顺序保证仅适用于同一
NetworkObject
上的 RPC。不同的NetworkObject
可能调用了可靠的 RPC,但执行顺序不同。更简单地说,仅保证单个NetworkObject
按顺序执行可靠的 RPC。
如果您确定某个 RPC 经常更新(即每秒更新几次),则它可能更适合作为不可靠的 RPC。
NetworkVariable
官方文档指路->NetworkVariable
做过自定义消息的应该知道:在服务器和客户端之间通信要约定好消息的格式等。而NetworkVariable<T>
帮我们避免了这个问题,以下是官方文档中的描述。
NetworkVariable<T>
是一种在服务器和客户端之间同步属性(“变量”)的方法,而无需使用自定义消息或RPC。由于是类型存储值的包装器(“容器”),因此必须使用该属性来访问正在同步的实际值。
当服务器中的NetworkVariable<T>
的值发生更改时,任何已连接的客户端会自动同步;在游戏中途加入的客户端会自动同步服务器的当前状态。官方入门教程也展示了这一点。
需要注意官方文档中已写明:NetworkVariable<T>
支持大部分非托管类型;如果想要同步托管类型,则需要实现INetworkSerializable
,可参考自定义序列化。
同时NetworkVariable<T>
也对外提供值被修改时触发的回调OnValueChanged
,也支持设置服务段和客户端的读写权限。
RPC vs NetworkVariable
官方文档指路->RPC vs NetworkVariable
问题来了:RPC
和NetworkVariable
这两种同步方式有何区别?
RPC
用于一瞬间发生的某些事情;而NetworkVariable
适用于持久发挥作用的某些状态或变量。
比方说:如果使用RPC来直接控制某扇门的打开,而使用NetworkVariable
来记录它的打开状态。那么在主机打开门后,某些客户端才加入游戏的话,这些客户端中对应的门将是关闭状态。这是不合理的,所以在设计阶段要做好分析。
NetworkTime & Ticks
NetworkTime
消息会在服务器和客户端之间传输,传输需要时间,这会造成两个时间:本地时间和服务器时间。
客户端上的LocalTime比服务器上的LocalTime要更早,即更加往后;而客户端上的ServerTime比服务器上的ServerTime更晚,即更加往前,如下。
很多游戏中都有按照固定模式移动的环境对象,这个可以不用同步位置来做,用NetworkTime来做。如下。
using Unity.Netcode;
using UnityEngine;
public class MovingPlatform : MonoBehaviour
{
public void Update()
{
// Move up and down by 5 meters and change direction every 3 seconds.
var positionY = Mathf.PingPong(NetworkManager.Singleton.LocalTime.TimeAsFloat / 3f, 1f) * 5f;
transform.position = new Vector3(0, positionY, 0);
}
}
Ticks
和FixedUpdate
类似,Ticks
也按固定速率运行,而且对外提供可注册的回调,如下。
public override void OnNetworkSpawn()
{
NetworkManager.NetworkTickSystem.Tick += Tick;
}
private void Tick()
{
Debug.Log($"Tick: {NetworkManager.LocalTime.Tick}");
}
public override void OnNetworkDespawn() // don't forget to unsubscribe
{
NetworkManager.NetworkTickSystem.Tick -= Tick;
}
注意,如果游戏中要使用FixedUpdate
或物理系统,则要把Ticks
的速率设置为fixed update time一致。
NetworkTransform
这个组件就是给你自动同步Transform的,不用开发者再去造轮子。
这意味这官方入门教程中,那个通过Position变量来做位置同步的写法没必要,你如果去看了它这个类里的实现,你会发现它内部就是使用NetworkVariable
来做Transform同步的。这可能只是想用来解释RPC调用,实际开发中直接用这个组件就可以了。
注意,组件中的Interpolate
会以轻微的延迟缓冲传入数据,并对值应用额外的平滑。所有这些因素结合在一起,使转换同步更加顺畅。而插值不会应用到Server上,这可能会导致Host和其他Client所呈现的表现不一致。
你可以继承并重写来把原方法替换成自定义方法。
NetworkAniamtion
这个组件就是给你自动同步Animator的,不用开发者再去造轮子。不过这只适用于最常见的动画系统方案,就是蜘蛛网那一套。如果遇到Playable
这一套方案,就得自定义动画同步系统。
你看它的实现就会发现,它里面不包含NetworkVariable
,它只使用RPC
来进行同步。
NetworkRigidbody
官方文档中这样说:
NetworkRigidbody
依赖于NetworkTransform
和Rigidbody
。它的主要功能是将Rigidbody组件添加到网络对象上,并确保只有服务器和拥有授权的客户端能修改它。这是通过将Rigidbody设置为运动学模式来实现的,这意味着物理引擎将不再对它进行模拟,而是由网络系统处理其移动和旋转。
你看它的实现就会发现,它什么同步也没做,它里面一个RPC
或NetworkVariable
都没有。
核心代码就下面这一段,大意是:如果当前为服务器或是有权限客户端,就使其刚体运动学开启;否则服从运动学,让NetworkTransform
来做运动,避免了无权限客户端上的碰撞检测。
/// <summary>
/// Sets the authority differently depending upon
/// whether it is server or owner authoritative
/// </summary>
private void UpdateOwnershipAuthority()
{
if (m_IsServerAuthoritative)
{
m_IsAuthority = NetworkManager.IsServer;
}
else
{
m_IsAuthority = IsOwner;
}
// If you have authority then you are not kinematic
m_Rigidbody.isKinematic = !m_IsAuthority;
// Set interpolation of the Rigidbody based on authority
// With authority: let local transform handle interpolation
// Without authority: let the NetworkTransform handle interpolation
m_Rigidbody.interpolation = m_IsAuthority ? m_OriginalInterpolation : RigidbodyInterpolation.None;
}
OnCollisionEnter
等事件仍是该触发就会触发,但它在与其他联网实例发生冲突时,则不会触发碰撞事件。所以尽量在服务器上侦听OnCollisionEnter
函数,并将事件使用ClientRPC
来同步到所有客户端。
参考资料
Get started with NGO
NetworkBehaviour
Sending Events with RPCs
NetworkVariable
自定义序列化
RPC vs NetworkVariable
NetworkTime & Ticks