mcpField
U
unity-multiplayer

unity-multiplayer

github.com/nice-wolf-studio开发4070 次安装6 周前更新
代码仓库

关于 unity-multiplayer

unity-multiplayer 是面向 Unity 联机游戏开发的网络编程参考技能,涵盖 Netcode for GameObjects、网络架构、状态同步与 RPC,以及多人游戏服务和常见实现问题。

主要功能

  1. 01Netcode for GameObjects 配置
  2. 02网络对象与所有权管理
  3. 03NetworkVariables 与 RPC
  4. 04连接审批与场景同步
  5. 05Relay 和 Lobby 工作流
  6. 06多人网络常见反模式指南

适用场景

  1. 01配置主机或客户端-服务器网络
  2. 02同步玩家与游戏状态
  3. 03管理网络对象的所有权和生成
  4. 04通过 Relay 和 Lobby 连接玩家

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/nice-wolf-studio/unity-claude-skills --skill unity-multiplayer

安装后在 Claude Code 中输入/unity-multiplayer确认已加载。

附带文件references/netcode-gameobjects.mdreferences/transport-layer.md

详细介绍

Unity 多人游戏与网络

多人游戏架构概览

Unity 的多人游戏生态由多个层组成:

层级软件包/服务用途
高层Netcode for GameObjects基于 GameObject 的网络逻辑
高层Netcode for Entities基于 DOTS 的网络功能
底层Unity Transport (com.unity.transport)UDP/WebSocket 通信,支持可选的可靠传输、排序和分片
服务Relay通过云中继服务进行 NAT 穿透
服务Lobby匹配和会话发现
服务Sessions SDK玩家组管理
工具Multiplayer Play Mode在编辑器中模拟最多 4 名玩家
工具Multiplayer Tools分析、调试和测试实用工具

拓扑选项:

  • 客户端-服务端(专用): 服务端拥有权威;客户端发送输入,服务端进行验证
  • 客户端-服务端(监听/主机): 一名玩家同时充当服务端和客户端
  • 分布式权威: 基于所有权的权威分布在各个客户端之间

使用 Multiplayer Center(Window > Multiplayer > Multiplayer Center),根据游戏需求获取软件包推荐。

Netcode for GameObjects 设置

安装

通过 Package Manager 安装 com.unity.netcode.gameobjects(v2.10+)。此操作会自动引入 com.unity.transport。

NetworkManager 配置

在场景中的 GameObject 上添加 NetworkManager 组件。它是所有网络功能的单例入口。

using Unity.Netcode;

public class GameLauncher : MonoBehaviour
{
    public void StartAsHost()
    {
        NetworkManager.Singleton.StartHost();
    }

    public void StartAsServer()
    {
        NetworkManager.Singleton.StartServer();
    }

    public void StartAsClient()
    {
        NetworkManager.Singleton.StartClient();
    }

    public void Shutdown()
    {
        NetworkManager.Singleton.Shutdown();
    }
}

NetworkManager 的主要属性:

  • IsServer、IsClient、IsHost -- 执行上下文
  • ConnectedClients -- 已连接客户端的字典
  • ConnectedClientsIds -- 只读客户端 ID 列表
  • LocalClientId -- 本地客户端的 ID
  • SceneManager -- NetworkSceneManager 实例
  • SpawnManager -- NetworkSpawnManager 实例
  • NetworkConfig -- 项目网络配置

NetworkManager 的主要事件: OnClientConnectedCallback、OnClientDisconnectCallback、OnConnectionEvent、OnServerStarted/OnServerStopped、OnClientStarted/OnClientStopped、OnTransportFailure

NetworkObject 和 NetworkBehaviour

NetworkObject

每个联网的 GameObject 都需要 NetworkObject 组件。它提供身份标识、所有权和可见性。

主要属性:

  • NetworkObjectId (ulong) -- 在网络中同步的唯一 ID
  • IsSpawned -- 是否已在网络上生成
  • OwnerClientId -- 当前所有者的客户端 ID
  • IsOwner -- 如果本地玩家拥有此对象,则为 true
  • HasAuthority -- 如果本地实例拥有权威,则为 true

生成(仅限服务端):

// Basic spawn
NetworkObject netObj = Instantiate(prefab).GetComponent<NetworkObject>();
netObj.Spawn();

// Spawn with specific owner
netObj.SpawnWithOwnership(clientId);

// Spawn as player object
netObj.SpawnAsPlayerObject(clientId);

// Despawn
netObj.Despawn(destroy: true);

所有权(仅限服务端):

netObj.ChangeOwnership(newClientId);
netObj.RemoveOwnership();

可见性:

netObj.NetworkShow(clientId);
netObj.NetworkHide(clientId);
bool visible = netObj.IsNetworkVisibleTo(clientId);
NetworkBehaviour

所有联网脚本都继承自 NetworkBehaviour,而不是 MonoBehaviour。

生命周期方法(按顺序):

  1. OnNetworkPreSpawn(ref NetworkManager) -- 任何生成操作之前
  2. OnNetworkSpawn() -- NetworkObject 生成之后;在此处注册处理程序
  3. OnNetworkPostSpawn() -- 所有同级 NetworkBehaviour 生成之后
  4. OnNetworkPreDespawn() -- 取消生成之前
  5. OnNetworkDespawn() -- 取消生成时

所有权回调:

  • OnGainedOwnership() / OnLostOwnership()
  • OnOwnershipChanged(ulong previous, ulong current) -- 在所有客户端上触发
using Unity.Netcode;

public class PlayerController : NetworkBehaviour
{
    public override void OnNetworkSpawn()
    {
        if (IsOwner)
        {
            // Initialize local player controls
            EnableInput();
        }
    }

    void Update()
    {
        if (!IsOwner) return;
        // Only the owner processes input
        HandleMovement();
    }

    public override void OnNetworkDespawn()
    {
        // Cleanup
    }
}

状态检查: IsServer、IsClient、IsHost、IsOwner、IsSpawned、IsLocalPlayer、HasAuthority

NetworkVariables

NetworkVariable<T> 会自动将状态从服务端同步到所有客户端。类型 T 必须是 unmanaged(基元类型、非托管结构体)。

public class PlayerHealth : NetworkBehaviour
{
    public NetworkVariable<int> Health = new NetworkVariable<int>(
        value: 100,
        readPerm: NetworkVariableReadPermission.Everyone,
        writePerm: NetworkVariableWritePermission.Server
    );

    public override void OnNetworkSpawn()
    {
        Health.OnValueChanged += OnHealthChanged;
    }

    public override void OnNetworkDespawn()
    {
        Health.OnValueChanged -= OnHealthChanged;
    }

    private void OnHealthChanged(int oldValue, int newValue)
    {
        Debug.Log($"Health changed: {oldValue} -> {newValue}");
        UpdateHealthUI(newValue);
    }

    // Server-side only (due to WritePerm.Server)
    public void TakeDamage(int amount)
    {
        if (!IsServer) return;
        Health.Value -= amount;
    }
}

写入权限:

  • NetworkVariableWritePermission.Server(默认)-- 只有服务端可以写入
  • NetworkVariableWritePermission.Owner -- 只有所有者可以写入

NetworkList -- 同步列表(T 必须是 unmanaged + IEquatable<T>):

public class Inventory : NetworkBehaviour
{
    public NetworkList<int> Items;

    void Awake()
    {
        Items = new NetworkList<int>();
    }

    public override void OnNetworkSpawn()
    {
        Items.OnListChanged += OnItemsChanged;
    }

    private void OnItemsChanged(NetworkListEvent<int> changeEvent)
    {
        Debug.Log($"List changed: {changeEvent.Type}");
    }
}

RPC(ServerRpc、ClientRpc)

RPC 是服务器与客户端之间的远程过程调用。方法必须位于 NetworkBehaviour 中,并使用带有 SendTo 目标的 [Rpc] 属性。

统一的 Rpc 属性(v2.x)
public class CombatSystem : NetworkBehaviour
{
    // Server executes this when any client calls it
    [Rpc(SendTo.Server)]
    void AttackRpc(int targetId, RpcParams rpcParams = default)
    {
        ulong senderId = rpcParams.Receive.SenderClientId;
        ProcessAttack(senderId, targetId);
    }

    // All clients (and host) execute this
    [Rpc(SendTo.ClientsAndHost)]
    void ShowDamageEffectRpc(Vector3 position, int damage)
    {
        SpawnDamagePopup(position, damage);
    }

    // Only the owner executes this
    [Rpc(SendTo.Owner)]
    void NotifyOwnerRpc(string message)
    {
        Debug.Log(message);
    }

    // Everyone including sender
    [Rpc(SendTo.Everyone)]
    void PlaySoundRpc(int soundId)
    {
        AudioManager.Play(soundId);
    }
}
SendTo 目标
目标描述
Server在服务器上执行;如果在服务器上调用,则在本地执行
NotServer除服务器以外的所有客户端(不包括主机)
Owner仅对象的所有者
NotOwner所有者以外的所有人
Authority客户端-服务器模式下为服务器;分布式权限模式下为所有者
NotAuthority所有非权威实例
ClientsAndHost包括主机在内的所有客户端
Everyone观察者列表中的所有实例
Me仅在本地执行
NotMe发送方以外的所有人
SpecifiedInParams通过 RpcSendParams 在运行时设置目标
旧版属性(仍受支持)
[ServerRpc]
void RequestSpawnServerRpc(ServerRpcParams rpcParams = default)
{
    // Runs on server; only owner can call by default
}

[ClientRpc]
void UpdateUIClientRpc(int score)
{
    // Runs on all clients
}

连接审批

使用 NetworkManager 上的 ConnectionApprovalCallback 验证正在连接的客户端。

// Server-side: register approval callback
NetworkManager.Singleton.ConnectionApprovalCallback = (request, response) =>
{
    string password = System.Text.Encoding.UTF8.GetString(request.Payload);
    response.Approved = (password == "secret");
    response.CreatePlayerObject = response.Approved;
    // Optionally set: response.PlayerPrefabHash, response.Position, response.Rotation
    if (!response.Approved) response.Reason = "Invalid password";
    response.Pending = false; // Signal decision is made
};

// Client-side: set payload before connecting
NetworkManager.Singleton.NetworkConfig.ConnectionData =
    System.Text.Encoding.UTF8.GetBytes("secret");
NetworkManager.Singleton.StartClient();

场景管理

NetworkSceneManager(通过 NetworkManager.Singleton.SceneManager 访问)负责处理同步场景加载。

public class GameSceneManager : NetworkBehaviour
{
    public void LoadGameScene()
    {
        if (!IsServer) return;
        // Server-only: loads scene on all clients
        NetworkManager.Singleton.SceneManager.LoadScene("GameScene", LoadSceneMode.Single);
    }

    public void LoadAdditiveScene()
    {
        if (!IsServer) return;
        NetworkManager.Singleton.SceneManager.LoadScene("Arena", LoadSceneMode.Additive);
    }

    public override void OnNetworkSpawn()
    {
        NetworkManager.Singleton.SceneManager.OnSceneEvent += OnSceneEvent;
    }

    void OnSceneEvent(SceneEvent sceneEvent)
    {
        switch (sceneEvent.SceneEventType)
        {
            case SceneEventType.LoadComplete:
                Debug.Log($"Client {sceneEvent.ClientId} loaded {sceneEvent.SceneName}");
                break;
            case SceneEventType.LoadEventCompleted:
                Debug.Log($"All clients loaded {sceneEvent.SceneName}");
                break;
            case SceneEventType.SynchronizeComplete:
                Debug.Log($"Client {sceneEvent.ClientId} fully synchronized");
                break;
        }
    }
}

场景事件类型: OnLoad、OnUnload、OnSynchronize、OnLoadComplete、OnUnloadComplete、OnLoadEventCompleted、OnUnloadEventCompleted、OnSynchronizeComplete

重要: 不要在场景事件回调中启动新的场景事件。

Unity Multiplayer Services(Relay、Lobby)

完整集成示例请参阅 references/transport-layer.md

Unity Relay

Relay 通过云中继服务器提供 NAT 穿透(无需端口转发)。需要 UGS 身份验证。

主机流程: 创建分配、获取加入代码、配置传输层、启动主机。 客户端流程: 使用代码加入分配、配置传输层、启动客户端。

// Host: create relay and start
Allocation allocation = await RelayService.Instance.CreateAllocationAsync(maxPlayers);
string joinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);
transport.SetRelayServerData(allocation.ToRelayServerData("dtls")); // "dtls" = encrypted UDP
NetworkManager.Singleton.StartHost();

// Client: join relay and connect
JoinAllocation join = await RelayService.Instance.JoinAllocationAsync(joinCode);
transport.SetRelayServerData(join.ToRelayServerData("dtls"));
NetworkManager.Singleton.StartClient();
Unity Lobby

Lobby 提供会话发现和匹配功能。将 Relay 加入代码存储在 Lobby 数据中。

// Create lobby with relay join code
_lobby = await LobbyService.Instance.CreateLobbyAsync(name, maxPlayers, new CreateLobbyOptions {
    Data = new Dictionary<string, DataObject> {
        { "JoinCode", new DataObject(DataObject.VisibilityOptions.Member, relayJoinCode) }
    }
});

// Query available lobbies
QueryResponse response = await Lobbies.Instance.QueryLobbiesAsync(new QueryLobbiesOptions {
    Filters = new List<QueryFilter> {
        new QueryFilter(QueryFilter.FieldOptions.AvailableSlots, "0", QueryFilter.OpOptions.GT)
    }
});

// Join and extract relay code
Lobby lobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId);
string joinCode = lobby.Data["JoinCode"].Value;

// IMPORTANT: Send heartbeats every 15s or lobby expires
await LobbyService.Instance.SendHeartbeatPingAsync(lobby.Id);

常见模式

使用自定义预制件生成玩家(服务器端)
// In a NetworkBehaviour on the server:
NetworkManager.Singleton.OnClientConnectedCallback += (ulong clientId) => {
    GameObject player = Instantiate(playerPrefab);
    player.GetComponent<NetworkObject>().SpawnAsPlayerObject(clientId);
};
所有者权威移动
public class PlayerMovement : NetworkBehaviour
{
    public NetworkVariable<Vector3> Position = new(writePerm: NetworkVariableWritePermission.Owner);
    void Update()
    {
        if (!IsOwner) return;
        // Note: Uses legacy Input for brevity. See unity-input for the new Input System.
        Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
        transform.position += move * Time.deltaTime * 5f;
        Position.Value = transform.position;
    }
}
使用输入 RPC 的服务器权威模式
public class ServerAuthMovement : NetworkBehaviour
{
    [Rpc(SendTo.Server)]
    void MoveRpc(Vector3 input) { transform.position += input * Time.deltaTime * 5f; }
    void Update()
    {
        if (!IsOwner) return;
        MoveRpc(new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"))); // legacy Input; see unity-input
    }
}

反模式

  1. 在没有权限时写入 NetworkVariable -- 只有服务器(或具有 WritePerm.Owner 的所有者)才能写入。客户端写入会被静默忽略。

  2. 忘记在 Update 中检查 IsOwner -- 如果不检查所有者,所有客户端都会运行输入逻辑,导致状态冲突。

  3. 使用 Instantiate 却不调用 Spawn -- 单独使用 Instantiate 创建的对象仅存在于本地。务必对 NetworkObject 调用 Spawn(),使其在网络中可见。

  4. 从客户端代码生成对象 -- 只有服务器才能生成 NetworkObject。客户端必须发送 RPC 来请求生成对象。

  5. 在 RPC 中传输大量数据,而非使用 NetworkVariables -- RPC 是即发即弃的;较晚加入的客户端会错过这些调用。请使用 NetworkVariables 保存持久状态。

  6. 未取消订阅 OnValueChanged -- 在 OnNetworkSpawn 中订阅,在 OnNetworkDespawn 中取消订阅,以防止泄漏。

  7. 在场景事件回调中启动场景事件 -- NetworkSceneManager 禁止这样做;这会导致未定义行为。

  8. 未发送 Lobby 心跳 -- 如果不定期调用 SendHeartbeatPingAsync(每 15–30 秒一次),Lobby 就会过期。

  9. 使用 NetworkVariable 进行频繁的小型更新 -- 对于高频数据(例如位置),优先使用 NetworkTransform 或自定义序列化。

  10. 在 OnNetworkSpawn 之前调用 RPC -- RPC 要求 NetworkObject 已生成。请延迟到 OnNetworkSpawn 后再调用。

API 关键内容速查

类关键成员
NetworkManagerStartHost()、StartServer()、StartClient()、Shutdown()、Singleton、ConnectedClients、LocalClientId、SceneManager、ConnectionApprovalCallback
NetworkObjectSpawn()、SpawnWithOwnership()、SpawnAsPlayerObject()、Despawn()、ChangeOwnership()、NetworkShow()、NetworkHide()、NetworkObjectId、OwnerClientId、IsOwner、HasAuthority
NetworkBehaviourOnNetworkSpawn()、OnNetworkDespawn()、OnGainedOwnership()、OnLostOwnership()、IsServer、IsClient、IsHost、IsOwner、IsSpawned、HasAuthority、RpcTarget
NetworkVariable<T>.Value、OnValueChanged、ReadPerm、WritePerm、CheckDirtyState()
NetworkList<T>Add()、Remove()、Insert()、Clear()、Count、OnListChanged
NetworkSceneManagerLoadScene()、UnloadScene()、OnSceneEvent
[Rpc(SendTo.X)]Server、Owner、NotOwner、ClientsAndHost、Everyone、NotMe、Authority、SpecifiedInParams

相关技能

  • unity-foundations -- Unity 核心概念、GameObject、组件、场景层级
  • unity-scripting -- C# 脚本编写、MonoBehaviour 生命周期、协程
  • unity-physics -- 物理系统;同步物理时使用 NetworkRigidbody

其他资源

评论

支持 Markdown