关于 unity-multiplayer
unity-multiplayer 是面向 Unity 联机游戏开发的网络编程参考技能,涵盖 Netcode for GameObjects、网络架构、状态同步与 RPC,以及多人游戏服务和常见实现问题。
主要功能
- 01Netcode for GameObjects 配置
- 02网络对象与所有权管理
- 03NetworkVariables 与 RPC
- 04连接审批与场景同步
- 05Relay 和 Lobby 工作流
- 06多人网络常见反模式指南
适用场景
- 01配置主机或客户端-服务器网络
- 02同步玩家与游戏状态
- 03管理网络对象的所有权和生成
- 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-- 本地客户端的 IDSceneManager--NetworkSceneManager实例SpawnManager--NetworkSpawnManager实例NetworkConfig-- 项目网络配置
NetworkManager 的主要事件: OnClientConnectedCallback、OnClientDisconnectCallback、OnConnectionEvent、OnServerStarted/OnServerStopped、OnClientStarted/OnClientStopped、OnTransportFailure
NetworkObject 和 NetworkBehaviour
NetworkObject
每个联网的 GameObject 都需要 NetworkObject 组件。它提供身份标识、所有权和可见性。
主要属性:
NetworkObjectId(ulong) -- 在网络中同步的唯一 IDIsSpawned-- 是否已在网络上生成OwnerClientId-- 当前所有者的客户端 IDIsOwner-- 如果本地玩家拥有此对象,则为 trueHasAuthority-- 如果本地实例拥有权威,则为 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。
生命周期方法(按顺序):
OnNetworkPreSpawn(ref NetworkManager)-- 任何生成操作之前OnNetworkSpawn()-- NetworkObject 生成之后;在此处注册处理程序OnNetworkPostSpawn()-- 所有同级 NetworkBehaviour 生成之后OnNetworkPreDespawn()-- 取消生成之前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
}
}
反模式
-
在没有权限时写入 NetworkVariable -- 只有服务器(或具有
WritePerm.Owner的所有者)才能写入。客户端写入会被静默忽略。 -
忘记在 Update 中检查
IsOwner-- 如果不检查所有者,所有客户端都会运行输入逻辑,导致状态冲突。 -
使用
Instantiate却不调用Spawn-- 单独使用Instantiate创建的对象仅存在于本地。务必对NetworkObject调用Spawn(),使其在网络中可见。 -
从客户端代码生成对象 -- 只有服务器才能生成 NetworkObject。客户端必须发送 RPC 来请求生成对象。
-
在 RPC 中传输大量数据,而非使用 NetworkVariables -- RPC 是即发即弃的;较晚加入的客户端会错过这些调用。请使用 NetworkVariables 保存持久状态。
-
未取消订阅 OnValueChanged -- 在
OnNetworkSpawn中订阅,在OnNetworkDespawn中取消订阅,以防止泄漏。 -
在场景事件回调中启动场景事件 --
NetworkSceneManager禁止这样做;这会导致未定义行为。 -
未发送 Lobby 心跳 -- 如果不定期调用
SendHeartbeatPingAsync(每 15–30 秒一次),Lobby 就会过期。 -
使用
NetworkVariable进行频繁的小型更新 -- 对于高频数据(例如位置),优先使用NetworkTransform或自定义序列化。 -
在
OnNetworkSpawn之前调用 RPC -- RPC 要求 NetworkObject 已生成。请延迟到OnNetworkSpawn后再调用。
API 关键内容速查
| 类 | 关键成员 |
|---|---|
NetworkManager | StartHost()、StartServer()、StartClient()、Shutdown()、Singleton、ConnectedClients、LocalClientId、SceneManager、ConnectionApprovalCallback |
NetworkObject | Spawn()、SpawnWithOwnership()、SpawnAsPlayerObject()、Despawn()、ChangeOwnership()、NetworkShow()、NetworkHide()、NetworkObjectId、OwnerClientId、IsOwner、HasAuthority |
NetworkBehaviour | OnNetworkSpawn()、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 |
NetworkSceneManager | LoadScene()、UnloadScene()、OnSceneEvent |
[Rpc(SendTo.X)] | Server、Owner、NotOwner、ClientsAndHost、Everyone、NotMe、Authority、SpecifiedInParams |
相关技能
- unity-foundations -- Unity 核心概念、GameObject、组件、场景层级
- unity-scripting -- C# 脚本编写、MonoBehaviour 生命周期、协程
- unity-physics -- 物理系统;同步物理时使用
NetworkRigidbody