【unity】NetCode

前言

之前接触过某些教程中的联机方案和直接用socket来做聊天室。

这俩方案各有利弊。前者提供了现成的网络同步框架,用起来方便,从数据库读写、自定义通信协议到分层处理,很经典,不过实际做起来要考虑各个方面,得面面俱到;而后者提供了相对简单的通信链接,但要想在个人项目中方便使用的话,还需自己封装功能。

在网上搜索时,发现Unity之前提供了一些联机解决方案,如MLAPIUNet,不过它们都被弃用了,现在最新的是Unity.Netcode,所以来学这个了。

Unity提供了对应的入门教程->Get started with NGO

这个没啥好说的。你可以尝试理解并改造教程代码来熟悉其运作方式。

NetworkBehaviour

官方文档指路->NetworkBehaviour

NetworkBehaviour是一个抽象类,继承自MonoBehavior

OnNetworkSpawn

NetworkBehaviour中提供了OnNetworkSpawn,给网络代码做初始化时就应该发生在该方法中。调用顺序如下。

动态生成时,Awake->OnNetworkSpawn->Start
静态放置时,Awake->Start->OnNetworkSpawn

官方文档中提到:

不要期望在这两种方法(Awake和Start)中区分属性(如IsClient,IsServer,IsHost等)的网络代码是准确的。
即使该对象尚未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更适用于分布式应用程序之间的远程过程调用,相当于调用了远程应用程序中的对应方法。

如何使用

image

如下是我对官方教程中的代码进行的改造,[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。

要注意:当主机调用ServerRPC时,它会立刻执行。我阅读文档不够全面,开发时踩到了这个坑。

NetworkVariable

官方文档指路->NetworkVariable

做过自定义消息的应该知道:在服务器和客户端之间通信要约定好消息的格式等。而NetworkVariable<T>帮我们避免了这个问题,以下是官方文档中的描述。

NetworkVariable<T>是一种在服务器和客户端之间同步属性(“变量”)的方法,而无需使用自定义消息或RPC。由于是类型存储值的包装器(“容器”),因此必须使用该属性来访问正在同步的实际值。

当服务器中的NetworkVariable<T>的值发生更改时,任何已连接的客户端会自动同步;在游戏中途加入的客户端会自动同步服务器的当前状态。官方入门教程也展示了这一点。

需要注意官方文档中已写明:NetworkVariable<T>支持大部分非托管类型;如果想要同步托管类型,则需要实现INetworkSerializable,可参考自定义序列化

同时NetworkVariable<T>也对外提供值被修改时触发的回调OnValueChanged,也支持设置服务段和客户端的读写权限。

RPC vs NetworkVariable

官方文档指路->RPC vs NetworkVariable

问题来了:RPCNetworkVariable这两种同步方式有何区别?

RPC用于一瞬间发生的某些事情;而NetworkVariable适用于持久发挥作用的某些状态或变量。

比方说:如果使用RPC来直接控制某扇门的打开,而使用NetworkVariable来记录它的打开状态。那么在主机打开门后,某些客户端才加入游戏的话,这些客户端中对应的门将是关闭状态。这是不合理的,所以在设计阶段要做好分析。

NetworkTime & Ticks

NetworkTime & Ticks

NetworkTime

消息会在服务器和客户端之间传输,传输需要时间,这会造成两个时间:本地时间和服务器时间。

客户端上的LocalTime比服务器上的LocalTime要更早,即更加往后;而客户端上的ServerTime比服务器上的ServerTime更晚,即更加往前,如下。

image

很多游戏中都有按照固定模式移动的环境对象,这个可以不用同步位置来做,用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同步的。不过这个例子仍有实际意义,因为它将输入和逻辑处理相分离,在多人游戏开发中也尽量遵守这一原则,从而提高可维护性。

注意,组件中的Interpolate会以轻微的延迟缓冲传入数据,并对值应用额外的平滑。所有这些因素结合在一起,使转换同步更加顺畅。而插值不会应用到Server上,这可能会导致Host和其他Client所呈现的表现不一致。

你可以继承并重写来把原方法替换成自定义方法。

NetworkAniamtion

这个组件就是给你自动同步Animator的,不用开发者再去造轮子。不过这只适用于最常见的动画系统方案,就是蜘蛛网那一套。如果遇到Playable这一套方案,就得自定义动画同步系统,需要同步动画的ID,以及它们的权重和Transition的持续时间。

你看它的实现就会发现,它里面不包含NetworkVariable,它只使用RPC来进行同步。

NetworkRigidbody

官方文档中这样说:

NetworkRigidbody依赖于NetworkTransformRigidbody。它的主要功能是将Rigidbody组件添加到网络对象上,并确保只有服务器和拥有授权的客户端能修改它。这是通过将Rigidbody设置为运动学模式来实现的,这意味着物理引擎将不再对它进行模拟,而是由网络系统处理其移动和旋转。

你看它的实现就会发现,它什么同步也没做,它里面一个RPCNetworkVariable都没有。
核心代码就下面这一段,大意是:如果当前为服务器或是有权限客户端,就使其刚体运动学开启;否则服从运动学,让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

posted @ 2023-02-23 16:50  AshScops  阅读(2777)  评论(0编辑  收藏  举报