Netcode
本文最后更新于9 天前,其中的信息可能已经过时,如有错误请留言

Netcode for GameObject(简称 NGO)里提到的三种角色
Server(服务器):只负责游戏逻辑,不参与游戏。自己不会去控制角色,也不会显示画面
Client(客户端):只用来玩,不负责管理。玩家运行这个版本来控制角色、看到画面;所有游戏逻辑要去问服务器,比如“我打中了没?
Host(主机):既是服务器,又是客户端

作为简单的局域网联机,我们不需要专用的服务器,所以我们的电脑就是主机,既作为服务器也作为其中一个客户端

服务器永远只有一个,所以调用ServerRPC的函数实际也只会在唯一的服务器上执行;但是客户端可以有很多个,所以server调用ClientRpc函数会将请求发送给每一个客户端,如果本机是host即包含server和client,也会发送给自己

NGO中数据同步方法1:ServerRpcClientRpc

RPC 是 Remote Procedure Call(远程过程调用) 的缩写

类型谁调用这个方法方法在哪执行(逻辑)用来干嘛
ServerRpc客户端调用服务器执行客户端发请求,请服务器做事(比如验证、修改状态)
ClientRpc服务器调用客户端执行服务器发通知,让所有客户端做某件事(比如动画、UI)

并且可以用自带的特殊参数类,调用 ClientRpc 并指定目标客户端的代码

[ClientRpc]
private void TestClientRpc(ClientRpcParams clientRpcParams) {
    Debug.Log("TestClientRpc");
}

private void Update() {
    if (!IsOwner) return;

    if (Input.GetKeyDown(KeyCode.T)) {
        TestClientRpc(new ClientRpcParams {
            Send = new ClientRpcSendParams {
                TargetClientIds = new List<ulong> { 1 }
            }
        });
    }
}

记巧:XXRPC声明的函数,真正执行在XX端,由另一端调用

如何使用方法?比如一个开门函数:

  • 传参只能传值类型(注意这里允许字符串类型,然而网络变量不允许)
  • 必须要在方法上面加上 [ClientRpc] 或 [ServerRpc] 的 Attribute
  • 你的方法名,后缀必须带 ClientRpc 或者 ServerRpc
  • 调用这个方法的类,必须继承于 NetworkBehaviour,挂的物体必须有 NetworkObject
// 客户端调用这个方法,但实际在服务器执行
[ServerRpc]
void OpenDoorServerRpc()
{
    door.SetActive(true); // 这个门只在服务器上被激活
}

你点击按钮,等于“发送”了一个 ServerRpc 请求服务器收到后,“调用”了 OpenDoorServerRpc 的方法门的状态在服务器上发生变化,服务器再同步给其他客户端

默认情况下,只有服务器有修改Netcode for gameobject状态的权力。所以对于客户端当按下移动键时,客户端的确会在本地尝试移动,客户端不断从服务器接收数据包并将其重置
如果对于制作的高度竞争的游戏,基本规则是永远不该相信客户端,那就由服务器修改数据再同步给客户端。但如果是休闲的合作游戏,就可以让客户端在本地修改数据再同步给服务器。

序列化

NetworkBehaviour

属性名类型说明
IsClientbool当前代码运行的实例是否是客户端(包括Host)。服务端运行时如果启用了客户端功能(如Host),也返回 true
IsHostbool当前实例是否是Host(既是服务器又是客户端)。
IsServerbool当前代码运行的实例是否是服务器。
IsLocalPlayerbool该对象是否是本地玩家(Client 中自己控制的 Player 对象)。常用于只执行本地输入或控制逻辑。
IsOwnerbool该对象是否是由本地客户端拥有的(一般用于玩家控制的对象)。
IsOwnedByServerbool该对象是否是由服务器端拥有的(比如AI、环境元素等)。
IsSpawnedbool该对象是否已经被服务端实例化并同步到网络中(通过 Spawn() 生成)。

胡闹厨房与单机代码修改

跳过盘子相关的代码修改

准备工作

玩家只在连接时被创立,并不是场景初始化就创建

Player.Instance.OnSelectedCounterChanged += Player_OnSelectedCounterChanged;

单人时,Player游戏物体在Scene中,但是多人游戏时我们将Player设为预制体,此时无法再Inspector中拖动引用

[SerializeField] private GameInput gameInput;
//GameInput.Instance替换gameInput

自己:将Dialogue Manager从Player中独立,删除Player预制体上的该组件

大前提:不允许玩家中途加入

处理输入信号 – 移动

只有本地拥有这个对象(Owner)的客户端,才会执行移动和交互逻辑。如果不修改会出现一方输入两方移动在Player脚本中添加一个条件

private void Update() {
    //只有本地拥有这个对象(Owner)的客户端,才会执行移动和交互逻辑。
    if (!IsOwner)
    {
        return;
    }
    HandleMovement();
    HandleInteractions();
}

同步位置

只需要在玩家预制体上添加一个脚本组件。这里用了官网的一个组件代码(对于非服务器处理代码架构需要,否则不用),如果用自带的Network Transform组件,只有host有权将移动同步到其他客户端

using Unity.Netcode.Components;
using UnityEngine;

namespace Unity.Multiplayer.Samples.Utilities.ClientAuthority
{
    /// <summary>
    /// Used for syncing a transform with client side changes. This includes host. Pure server as owner isn't supported
    /// for transforms that'll always be owned by the server.
    /// </summary>
    [DisallowMultipleComponent]
    public class ClientNetworkTransform : NetworkTransform
    {
        /// <summary>
        /// Used to determine who can write to this transform. Owner client only.
        /// This imposes state to the server. This is putting trust on your clients. Make sure no security-sensitive logic here.
        /// </summary>
        protected override bool OnIsServerAuthoritative()
        {
            return false;
        }
    }
}

挂载到Player预制体上,由于没有跳跃,y方向移动,非Z轴旋转,取消勾选同步以提高同步带宽

注意:ClientNetworkTransform 接管了 Transform 的控制和同步,无法在 Inspector 中直接编辑位置。

同步动画

仅需要在挂载Animator的Player游戏子物体上挂载来自官网的代码,组件名为Owner Network Animator

using Unity.Netcode.Components;

public class OwnerNetworkAnimator : NetworkAnimator {
    protected override bool OnIsServerAuthoritative() {
        return false;
    }
}

摄像机

原来在Inspector面板拖入引用两个虚拟摄像机

// cinemachine
[SerializeField] private CinemachineVirtualCamera cinemachine1d;
[SerializeField] private CinemachineVirtualCamera cinemachine3d;

现在需要动态绑定

虚拟相机(CinemachineVirtualCamera通常不需要放到 NetworkManager 的 NetworkPrefab 列表里
虚拟相机只需本地可见,每个玩家只在自己客户端生成和控制自己的虚拟相机,其它玩家无需也看不到你的相机对象

我一开始在想为什么总是多生成一组摄像机,后来Debug出来是因为new PlayerSpawnedEventArgs中参数的初始化函数中创建了两个摄像机对象

PerspectiveManager类新增:

private void OnEnable()
{
    Player.OnLocalPlayerSpawned += HandleLocalPlayerSpawned;
}

private void OnDisable()
{
    Player.OnLocalPlayerSpawned -= HandleLocalPlayerSpawned;
}

private void HandleLocalPlayerSpawned(object sender, PlayerSpawnedEventArgs e)
{
    player = sender as Player;
    playerVisual = e.playerVisual;


    // 从玩家对象中查找虚拟摄像机(例如作为子物体)
    TopCamera = e.Camera3;
    FirstVisualCamera = e.Camera1;

    Debug.Log("摄像机绑定成功!");
}

Delivery Counter(搁置)

if (plateContentsMatchesRecipe) {
    // Player delivered the correct recipe!
    DeliverCorrectRecipeServerRpc();  // 【客户端调用】发送RPC给服务器
    return;
}

[ServerRpc]
private void DeliverCorrectRecipeServerRpc() {
    DeliverCorrectRecipeClientRpc();  // 【服务器调用】广播给所有客户端
}

[ClientRpc]
//客户端执行
private void DeliverCorrectRecipeClientRpc() {
    successfulRecipesAmount++;
    waitingRecipeSOList.RemoveAt(i);

    OnRecipeCompleted?.Invoke(this, EventArgs.Empty);
    OnRecipeSuccess?.Invoke(this, EventArgs.Empty);
}

客户端:DeliverCorrectRecipeServerRpc() // 请求服务器执行逻辑

服务器:DeliverCorrectRecipeClientRpc() // 通知所有客户端执行

所有客户端:执行 DeliverCorrectRecipeClientRpc 逻辑

图示 1:ServerRpc – 客户端调用,服务器执行

  • 比如你(客户端)把盘子交给柜台(表示交付菜品)。
  • 你不能直接判断对不对,你得请求服务器判断是否匹配(你自己不能作弊)。
  • 所以你调用 MyServerRpc()(图中蓝色线),把这个请求发给服务器
  • 服务器收到了请求,就会在服务器这边 执行这个函数
  • 你自己本地不会执行这个函数。

🧩 对应你游戏中的:

csharp复制编辑DeliverCorrectRecipeServerRpc(); // 客户端调用,服务器判断是否配方正确

图示 2:ClientRpc – 服务器调用,客户端执行

  • 现在服务器发现你交的菜是对的,它要通知所有人“某人完成了菜品”。
  • 它调用 MyClientRpc()这个函数不会在服务器上运行
  • 相反,它会广播给所有客户端,让每个客户端都执行这个函数(更新界面、加分等)。

🧩 对应你游戏中的:

csharp复制编辑DeliverCorrectRecipeClientRpc(); // 服务器通知所有客户端更新成功菜品信息

最重要的一句话总结:

ServerRpc 是客户端请求服务器做决定,ClientRpc 是服务器广播给所有客户端更新状态。

Player

多人联机玩家不再是唯一的,但是本地客户端是唯一的,所以初始化静态对象的方法要从原来单例模式中的Awake函数修改为当客户端接入网络时自动调用的函数

public override void OnNetworkSpawn() {
    if (IsOwner) {
        LocalInstance = this;
    }
}

同时其他所有在其他脚本的start函数或者inspector面板引用Player单例都不再适用,因为玩家(本地客户端)单例不再在awake中生成,会造成空引用。所以我们需要为Player设置一个静态事件用于通知该客户端单例生成(我做了一个参数类,因为自己增加了一些功能)

public static event EventHandler<PlayerSpawnedEventArgs> OnLocalPlayerSpawned;

并且在客户端加入网络的函数中触发该事件,并在主菜单的重置

public override void OnNetworkSpawn(){
   if (!IsOwner) return;
   OnLocalPlayerSpawned?.Invoke(this,args); // 广播:本地玩家已生成
}

注意事件本身是静态对象,在场景创建时就有,与对象是否初始化无关,所以可以在player客户端没有初始化之前就绑定事件的响应者:

private void Start() {
    //因为player的单例对象在Awake生命周期函数中声明,为了保证不会空引用所以声明在start周期函数中
    //记得在声明事件外的类去订阅事件哦,这样才能更好的解耦合
    if (Player.LocalInstance != null)
    {
        Player.LocalInstance.OnSelectedCounterChanged += Player_OnSelectedCounterChanged;
    }
    else
    {
        Player.OnLocalPlayerSpawned += Player_OnLocalPlayerSpawned;
    }
}

private void Player_OnLocalPlayerSpawned(object sender, Player.PlayerSpawnedEventArgs e)
{
    if (Player.LocalInstance != null)
    {
        Player.LocalInstance.OnSelectedCounterChanged -= Player_OnSelectedCounterChanged;
        Player.LocalInstance.OnSelectedCounterChanged += Player_OnSelectedCounterChanged;
    }
}

但同时需要注意(重要):

1.所有静态对象在一个进程内是唯一的

2.不同客户端运行在不同的电脑上,或者同一个电脑的不同进程

3.所以在脚本中声明的静态对象有多份,在各个进程中唯一(原来sb的我还想着所有客户端共同同一个静态对象)

所以需要先注销订阅的原因并不是因为各个客户端,他们之间是独立的,原教程只说因为可能会多次触发客户端接入网络的事件,但没说具体原因,我猜是因为在切换场景时可能会重复调用该事件,因为切换场景会销毁物体再创建,此时还是同一个进程,如果回到主界面再进入可能会绑定2次,所以需要先取消绑定,再进行绑定

同时需要将原来音频

KitchenObject(WorldItem)的生成

创建物体并同步,在教程中我是WordItem中含有Create的函数,下面记录修改的步骤

在NetWorkManager中拖入WordItem的预制体

原来:

    public static WorldItem CreateOutSide(Vector3 spawnPosition, ItemSO itemScriptableObject,bool isDisPlayIcon = false)
    {
        Transform worldItemTransform = Instantiate(GameAssets.i.pfWorldItem, spawnPosition, Quaternion.identity);

        WorldItem worldItem = worldItemTransform.GetComponent<WorldItem>();
        worldItem.isDisPlayIcon = isDisPlayIcon;
        worldItem.itemSO = itemScriptableObject;
        worldItem.needMove = false;
        return worldItem;
    }

新建一个KitchGameMutiplayer类,静态单例,在其中创建一个CreateOnIKitchenParrent函数作为原来WordItem中CreateOnIKitchenParrent的回调方法,
将其中的代码也转移过来,并将原来的返回值设为void(原来的返回值没使用过)
这一步是因为{XXXRPC要求函数是非静态函数,为方便改动,不用给每个ItemSO添加脚本引用,声明另外一个类

    public static void CreateOnIKitchenParrent(ItemSO itemScriptableObject,IKitchenObjectParent kitchenObjectParent, bool canReplace, bool isDisPlayIcon = false)
    {
        KitchGameMutiplayer.Instance.CreateOnIKitchenParrent(itemScriptableObject, kitchenObjectParent, isDisPlayIcon, canReplace);

    }
public class KitchGameMutiplayer : NetworkBehaviour
{
    public static KitchGameMutiplayer Instance;
    [SerializeField]private ItemListSO ItemListSO;

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
        }
    }
    public void CreateOnIKitchenParrent(ItemSO itemScriptableObject, IKitchenObjectParent kitchenObjectParent, bool isDisPlayIcon, bool canReplace)
    {
        SpawnKitchenObjectServerRPC(itemScriptableObject, kitchenObjectParent, isDisPlayIcon, canReplace);
    }


    (ItemSO itemScriptableObject, IKitchenObjectParent kitchenObjectParent, bool isDisPlayIcon, bool canReplace)

    [ServerRpc(RequireOwnership = false)]
    public void SpawnKitchenObjectServerRPC(ItemSO itemScriptableObject, IKitchenObjectParent kitchenObjectParent, bool isDisPlayIcon, bool canReplace)
    {
        Transform worldItemTransform = Instantiate(GameAssets.i.pfWorldItem);

        NetworkObject worldItemNetworkObject = worldItemTransform.GetComponent<NetworkObject>();
        worldItemNetworkObject.Spawn(true);

        WorldItem worldItem = worldItemTransform.GetComponent<WorldItem>();
        worldItem.isDisPlayIcon = isDisPlayIcon;
        worldItem.itemSO = itemScriptableObject;
        worldItem.needMove = false;

        worldItem.SetWordItemToKitchenParent(kitchenObjectParent, canReplace);
    }
}

报错信息:

error – SpawnKitchenObjectServerRPC – Don’t know how to serialize IKitchenObjectParent.
RPC parameter types must either implement INetworkSerializeByMemcpy or INetworkSerializable.

Netcode 的 ServerRpc/ClientRpc 参数,只能传递可序列化的内容(INetworkSerializableINetworkSerializeByMemcpy 的类型,或Unity已知的基础类型)。

你尝试在 RPC(ServerRpc/ClientRpc)函数参数里直接传了一个接口类型(IKitchenObjectParent),而接口、普通类引用都无法直接序列化,所以会报这个错

ItemSO 作为 ScriptableObject,本质上是资源数据的“引用”,不是简单值,也无法被Netcode自动序列化。

Unity的ScriptableObject/MonoBehaviour等都无法在网络上直接传递,Netcode要求RPC参数必须是基础类型(int、float、string等)、或实现了 INetworkSerializable 的结构体,或者 NetworkObjectReference

解决方法:
对于ScriptObject
创建另一个XXListscriptObject类,内只有一个XXscriptObject的List,存某只传唯一标识(如ID或名字),而不是直接传ScriptableObject对象

可以给每个 ItemSO 分配一个唯一的ID(int/string),在RPC只传这个ID

而对于组件,接口,游戏物体:

  • NetworkObjectReference 是 Netcode 官方专门用于**在网络上传递网络对象引用(即带 NetworkObject 的物体)**的序列化类型。
  • 这样就可以在RPC里安全地传递“某个GameObject”的引用,而不是接口、脚本或Unity对象本身
  • 可以跨网络“引用”一个物体
    • 你不能直接传 IKitchenObjectParentGameObject,但你可以传它的 NetworkObjectReference
    • 接收端用 TryGet(out NetworkObject) 恢复成 Unity 里的物体,再通过 GetComponent 获取实际组件。
  • 解决Netcode只允许传基础类型/可序列化对象的限制
    • 避免了“xxx不能序列化”报错
    • 官方推荐方法,兼容性最好

给IkitchenParent接口新增一个方法用于获取游戏物体的NetworkObject

public NetworkObject GetNetworkObject();

现在就能完全替换KitchGameMutiplayer 中的Create函数了

public class KitchGameMutiplayer : NetworkBehaviour
{
    public static KitchGameMutiplayer Instance;
    [SerializeField]private ItemListSO ItemListSO;

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
        }
    }
    public void CreateOnIKitchenParrent(ItemSO itemSO, IKitchenObjectParent kitchenObjectParent, bool isDisPlayIcon, bool canReplace)
    {
        SpawnKitchenObjectServerRPC(GetKitchenObjectSOIndex(itemSO), kitchenObjectParent.GetNetworkObject(), isDisPlayIcon, canReplace);
    }


    //(ItemSO itemScriptableObject, IKitchenObjectParent kitchenObjectParent, bool isDisPlayIcon, bool canReplace)

    [ServerRpc(RequireOwnership = false)]
    public void SpawnKitchenObjectServerRPC(int itemSOIndex, NetworkObjectReference kitchenObjectParentNetworkObjectReference, bool isDisPlayIcon, bool canReplace)
    {
        ItemSO itemScriptableObject = GetItemSOFromIndex(itemSOIndex);
        Transform worldItemTransform = Instantiate(GameAssets.i.pfWorldItem);

        NetworkObject worldItemNetworkObject = worldItemTransform.GetComponent<NetworkObject>();
        worldItemNetworkObject.Spawn(true);

        WorldItem worldItem = worldItemTransform.GetComponent<WorldItem>();
        worldItem.isDisPlayIcon = isDisPlayIcon;
        worldItem.itemSO = itemScriptableObject;
        worldItem.needMove = false;

        kitchenObjectParentNetworkObjectReference.TryGet(out worldItemNetworkObject);
        IKitchenObjectParent kichenObjectParent = worldItemNetworkObject.GetComponent<IKitchenObjectParent>();
        worldItem.SetWordItemToKitchenParent(kichenObjectParent, canReplace);
    }

    private int GetKitchenObjectSOIndex(ItemSO itemScriptableObject)
    {
        return ItemListSO.ItemSOList.IndexOf(itemScriptableObject);
    }

    private ItemSO GetItemSOFromIndex(int ItemSOIndex)
    {
        return ItemListSO.ItemSOList[ItemSOIndex];
    }
}

设置父物体

InvalidParentException: Invalid parenting, NetworkObject moved under a non-NetworkObject parent

NGO 确实支持 NetworkObject 之间建立父子关系,但有限制条件:

  • 不能将 NetworkObject 作为另一个“刚刚动态生成还没同步完的” NetworkObject 的子物体,否则会报错或无法同步。
  • 父对象子对象都必须挂有 NetworkObject 组件。
  • 父对象必须在所有客户端都已经存在并被 Spawn,并且是通过 NGO 创建/同步的。
  • 子对象的父变更,必须通过 NGO 的 ChangeParent() 实现,而不是直接用 Unity 的 transform.SetParent。
  • 但是实际上NetcodeForObject限制无法将一个Netcode对象设置为动态创建的Netcode对象的子对象

为什么有这个限制?(原因分析)

  • NGO 必须在所有客户端之间同步“对象关系树”(即父子关系)。
    • 如果一个 NetworkObject 是在本地新生成的、或者还没 Spawn 完成,其他客户端就还没“认识”它,这时候把别的 NetworkObject 挂到它下面,就会丢失父子同步信息
  • 父对象必须是所有客户端都“已知”的网络对象,否则客户端之间会乱套。
  • 父对象如果是刚刚用 Instantiate 创建,还没调用 NetworkObject.Spawn(),那还不属于网络上的“已知对象”,这时候强行挂父会报错。

我们可以换一种思路避免涉及修改父子关系,只是让物体跟随而不必须修改父子关系,新增一个跟随脚本,并替换WorlldItem设置父子关系的代码

public class FollowTransform : MonoBehaviour
{

    private Transform targetTransform;

    public void SetTargetTransform(Transform targetTransform)
    {
        this.targetTransform = targetTransform;
    }

    private void LateUpdate()
    {
        if (targetTransform == null)
        {
            return;
        }
        transform.position = targetTransform.position;
        transform.rotation = targetTransform.rotation;
    }
}
 public void SetWordItemToKitchenParent(IKitchenObjectParent kitchenObjectParent,bool canReplace)
 {
     if (this.kitchenObjectParent != null && canReplace)
     {
         this.kitchenObjectParent.ClearKitchenObject();
     }

     this.kitchenObjectParent = kitchenObjectParent;

     if (kitchenObjectParent.HasKitchenObject())
     {
         Debug.LogError("IKitchenObjectParent already has a KitchenObject!");
     }

     
     //kitchenObjectParent.SetKitchenObject(this);

     //transform.parent = kitchenObjectParent.GetKitchenObjectFollowTransform();
     //transform.localPosition = Vector3.zero;
     //transform.rotation = Quaternion.identity;
     followTransform.SetTargetTransform(kitchenObjectParent.GetKitchenObjectFollowTransform());
 }

重要:

为什么KitchGameMutiplayer没有[clientRPC]的函数难道不需要通知客户端有物体生成了嘛?

物体生成/销毁本身用 NetworkObject 的 Spawn()/Despawn() 就自动同步了,只有你想让“所有客户端都执行某个特定逻辑”(比如屏幕特效、UI提示、音效),而且这个逻辑不是单纯的网络物体生成,才需要用 [ClientRpc],也正因此我们我们上面犯了一个错误

NetworkObject worldItemNetworkObject = worldItemTransform.GetComponent<NetworkObject>();
        worldItemNetworkObject.Spawn(true);
...
   worldItem.SetWordItemToKitchenParent(kichenObjectParent, canReplace);

上面创建的网络对象生成在网络中,自动同步给所有Client,然而下面设置父子关系的代码并不会自动同步给客户端,只有host会执行,因为这个函数是{serverRPC],所以我们的思路是在此处替换为另一个函数,告诉服务器应该改变父对象,再次回顾RequireOwnership = false 让所有客户端都能调用该 ServerRpc,而不仅仅是这个 NetworkObject 的 Owner 客户端。再让服务器通知所有Client执行具体代码

 public void SetWordItemToKitchenParent(IKitchenObjectParent kitchenObjectParent, bool canReplace)
 {
     SetSetWordItemToKitchenParentServerRPC(kitchenObjectParent.GetNetworkObject(), canReplace);
 }

 //private void SetSetWordItemToKitchenParentServerRPC(IKitchenObjectParent kitchenObjectParent, bool canReplace)无法实例化接口
 [ServerRpc(RequireOwnership = false)]
 private void SetSetWordItemToKitchenParentServerRPC(NetworkObjectReference kitchenObjectParentNetworkObjectReference, bool canReplace)
 {
     SetWordItemObjectparentClientRPC(kitchenObjectParentNetworkObjectReference, canReplace);
 }

 [ClientRpc]
 private void SetWordItemObjectparentClientRPC(NetworkObjectReference kitchenObjectParentNetworkObjectReference, bool canReplace)
 {
     kitchenObjectParentNetworkObjectReference.TryGet(out NetworkObject worldItemParentNetworkObject);
     IKitchenObjectParent kichenObjectParent = worldItemParentNetworkObject.GetComponent<IKitchenObjectParent>();

     if (this.kitchenObjectParent != null && canReplace)
     {
         Debug.LogError("我错了!");
         this.kitchenObjectParent.ClearKitchenObject();
     }

     this.kitchenObjectParent = kichenObjectParent;  
     //kitchenObjectParent.SetKitchenObject(this);

     followTransform.SetTargetTransform(kichenObjectParent.GetKitchenObjectFollowTransform());

     if (kitchenObjectParent.HasKitchenObject())
     {
         Debug.LogError("IKitchenObjectParent already has a KitchenObject!");
     }


     //transform.parent = kitchenObjectParent.GetKitchenObjectFollowTransform();
     //transform.localPosition = Vector3.zero;
     //transform.rotation = Quaternion.identity;
 }

我debug了两个小时,客户端的位置一直不同步,和视频里的现象一样,但是代码是一样的,最后发现是WorldItem没有把Mono Behaviour改为NetWork Behaviour,关键是我没改下面的XXRPC也不报错

同步资源台动画(最直观的本地修改联机例子)

销毁物体

创建物体和销毁物体都是只有服务器才可以做到,所以当同步销毁的物体时,客户端调用下函数就会报错

public void DestroySelf() {
    Destroy(gameObject);
}
NotServerException: Destroy a spawned NetworkObject on a non-host client is not valid. Call Destroy or Despawn on the server/host instead.

NetworkObject 的生命周期只能由服务器/主机来决定。客户端只能请求(通过 ServerRpc),不能直接销毁或生成 NetworkObject

在WorldItem中声明静态函数

    //没有必要声明为静态的,这里只是不想让WordItem引用该脚本
    public static void DestroyWorldItem(WorldItem worldItem)
    {
        KitchGameMutiplayer.Instance.DestroyWorldItem(worldItem);
    }

建筑系统

在放置建筑的函数中

NGO常用组件

NetworkManager

📌 Player Prefab

  • 中文:玩家预制体
  • 就是你的“玩家角色”Prefab,联网时每个客户端都会生成这个角色。
  • 要绑定一个预制体,比如一个拥有 NetworkObject 的角色

生成对象

📌 NetworkPrefabs

  • 中文:网络预制体列表
  • 你想通过网络生成的所有物体都要添加到这个列表里

📌 Network Transport

  • 网络传输方式,你要选择一个“传输层”组件,比如:
    • Unity Transport (推荐),需要你先添加 UnityTransport 组件。
    • 没选就不能联网!

📌 Tick Rate

  • 每秒网络更新频率
  • 默认为 30,意思是每秒同步 30 次。越高越流畅,越低越省资源。

📌127.0.0.1 永远代表“我自己这台电脑”,不管在哪台电脑上运行。用这个地址,表示客户端要连接的是 本机运行的服务器

NetworkObject

网络物体标识,所有要在网络中同步的物体都必须挂载这个组件

条件是否显示在编辑器(场景中)
✅ 挂了 NetworkObjectSpawn()✅ 会在所有客户端和服务器中生成并显示
❌ 没有挂 NetworkObject(本地对象)❌ 只在本地客户端存在,不同步
✅ 是 NetworkObject,但还没 Spawn()❌ 不会出现(未注册到网络中)
✅ 是场景中的 NetworkObject,勾选 “Spawn With Scene”✅ 所有客户端都会加载它

数据同步方法2:NetworkTransform & ClientNetworkTransform

用于同步更新Transform

NetworkTransform服务器权威:位置同步只允许服务器发给客户端,客户端不能修改位置

ClientNetworkTransform客户端权威:允许 客户端 控制自己对象的位置,并将位置同步给服务器和其他客户端(这就对应上面所说的客户端修改数据,对于我们非竞技游戏是可以接受的)
注:这个类不包括在核心包中,可以复制代码,或者通过URL在包管理器下载插件

有很多种方式可以同步Transform数据的方法,如果不愿意相信客户端,还可以通过网络变量实现

数据同步方法3:Network Variables 网络变量

NetworkVariable 就是一个可以在服务器和所有客户端之间自动同步的变量,
唯一限制这个变量必须是值类型(int float enum bool struct)

public NetworkVariable<int> health = new NetworkVariable<int>(
    100, // 初始值
    NetworkVariableReadPermission.Everyone,
    NetworkVariableWritePermission.Server
);

可以声明一个结构体来包括想记录的数据,但是,自定义数据类型网络管理器不知道如何网络序列化,使用自定义类型需要实现可序列化接口

public struct MyCustomData : INetworkSerializable {
    public int _int;
    public bool _bool;

    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter {
        serializer.SerializeValue(ref _int);
        serializer.SerializeValue(ref _bool);
    }
}
权限项意义
Everyone所有人都能读/写
Server只有服务器能写(推荐)
Owner只有拥有这个对象的客户端能写

可以看到默认状态下是设置为所有端可读,但只有Server可以写

然而客户端不能写入其他客户端的变量,即使是洛变量。这样做是为了 防止客户端越权篡改别人数据,例如篡改别人的血量、分数等,保障安全性。

条件能否写入 NetworkVariable
客户端尝试写入自己拥有的变量✅ 可以(如果权限是 Owner
客户端尝试写入别人拥有的变量❌ 不行(即使变量是公开的)
服务器✅ 总是可以写入

好处:

自动同步:不用手动调用 Rpc,它会自动广播给需要同步的客户端

自带事件:当 .Value 变化时

private NetworkVariable<int> randomNumber = new NetworkVariable<int>(1, NetworkVariableReadPermission.Everyone);

public override void OnNetworkSpawn() {
    randomNumber.OnValueChanged += (int previousValue, int newValue) => {
        Debug.Log(OwnerClientId + "; randomNumber: " + randomNumber.Value);
    };
}

NetworkAnimator

用于同步更新 Animator

可能需要深入了解的

面对输入延迟,进行客户端预测

面对黑客,采用服务器处理所有代码的架构,会更复杂,需要验证输入。我们现在不用,因为相信客户端,

与建筑系统结合问题:

主机端的TileMap会因客户端的玩家移动而变红更新,但是客户端移动Host端不会更新

两侧建筑的放置都不会更新

生成的物品也不会更新同步

传递的是引用player的脚本,可能会有问题

学习笔记如有侵权,请提醒我,我会马上删除
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇