Unity 多人联网:Mirror 与 Netcode for GameObjects(NGO)
- 核心模型相同:服务器权威 + 状态同步,客户端发请求、服务器改真相再下发。
选型与共同点
| 维度 | Mirror | NGO (Netcode for GameObjects) |
|---|
| 来源 | 社区(原 UNET 分支) | Unity 官方包 |
| 包名 | 导入 Mirror 资源 / UPM | com.unity.netcode.gameobjects |
| 网络实体 | NetworkIdentity | NetworkObject |
| 行为脚本基类 | NetworkBehaviour | NetworkBehaviour |
| 总控 | NetworkManager(勿挂在 NetworkIdentity 上) | NetworkManager(全场景唯一) |
| 状态同步变量 | [SyncVar] | NetworkVariable<T> |
| 远程调用 | [Command] [ClientRpc] [TargetRpc] | [Rpc(SendTo.Server)] 等 |
| Transform 同步 | NetworkTransform(Mirror 组件) | NetworkTransform(NGO 组件) |
| 传输层 | Telepathy / KCP 等(常 TCP+UDP 组合) | Unity Transport(UTP,偏 UDP) |
| 公网联机 | 需自建 / Relay 第三方 | Unity Relay + Lobby 官方链路 |
| 学习资料 | 项目内 Mirror 笔记、社区多 | 官方文档、Multiplayer Tools |
共同原则
- 每个联网对象有权威端(通常是 Server)。
- 客户端只发指令(RPC),玩法判定在服务器。
- 持久状态用同步变量(SyncVar / NetworkVariable),持续位置用 NetworkTransform。
- 场景中通常只有一个激活的
NetworkManager。
flowchart LR
subgraph 共同模型
C[Client 输入/UI] -->|RPC| S[Server 权威逻辑]
S -->|SyncVar / NetworkVariable / NetworkTransform| C
S -->|同步| C2[其他 Client]
end
客户端 / 服务器 / Host
Mirror 与 NGO 都不是固定两个 exe,而是通过打包方式 + 启动 API 区分角色。
| 形态 | 说明 | Mirror | NGO |
|---|
| 客户端 | 渲染、输入、发 RPC;不做权威判定 | StartClient() | StartClient() |
| 专用服务器 | 无头、无渲染;权威逻辑 | StartServer() + Headless Build | StartServer() + Dedicated Server Build |
| Host | 同进程兼 Server + Client | StartHost() | StartHost() |
| 能力 | Mirror | NGO |
|---|
| 是否服务器 | isServer | IsServer(Host 也为 true) |
| 是否客户端 | isClient | IsClient(Host 也为 true) |
| 是否 Host | isServer && isClient | IsHost |
| 是否本地拥有者 | isOwned / hasAuthority(版本用语) | IsOwner |
| 已入网 | netIdentity != null 等 | IsSpawned |
Host 注意:写逻辑时用 IsServer / isServer 做玩法判定,不要只用 IsClient,否则 Host 会漏逻辑。
连接是如何建立的
通用流程(二者逻辑一致)

Mirror:传输与握手(你的笔记要点)
- 传输层:Mirror 通过 Transport 抽象使用 TCP / UDP(默认封装,一般不直接写 Socket)。
- TCP:连接、鉴权、关键可靠消息。
- UDP:位置等实时数据(上层可加重传)。
- 服务器:
StartServer() / StartHost() → 绑定 0.0.0.0:端口 → 监听。 - 客户端:
StartClient(address, port) → 向目标 IP 发起连接。 - TCP 三次握手:SYN → SYN+ACK → ACK(Mirror Transport 自动完成)。
- 应用层握手:版本协商、可选身份验证 → 成功则同步房间/玩家列表。
- NetworkManager:连接全流程入口;回调如
OnServerConnect、OnClientConnected。
| Host vs 专服 | 区别 |
|---|
| Headless Server | 独立进程监听;客户端连 IP:Port |
| Host | 无独立服程序;StartHost() 本地同时开服+客;其他客户端连 Host 的局域网 IP |
Mirror.NetworkManager 注意
- 每场景仅一个激活的 NetworkManager(单例)。
- 不要把 NetworkManager 挂在带
NetworkIdentity 的对象上(场景加载时可能被禁用)。 - 一个节点树只允许一个
NetworkIdentity,挂在父节点;父链上不能再有 Identity。
NGO:传输与鉴权
| 项 | 说明 |
|---|
| Unity Transport | 默认 UTP(UDP),在 NetworkManager 选 Transport |
| 连接 | StartServer / StartHost / StartClient,地址端口在 Transport / NetworkConfig |
| 鉴权 | ConnectionApproval = true + ConnectionApprovalCallback |
| 公网 | Unity Relay,客户端无需主机公网 IP |
| 断开 | NetworkManager.Singleton.Shutdown() |
核心对照表
组件
| 作用 | Mirror | NGO |
|---|
| 可联网物体 | NetworkIdentity | NetworkObject |
| 网络脚本 | NetworkBehaviour | NetworkBehaviour |
| 位置旋转同步 | NetworkTransform | NetworkTransform |
| 玩家预制体 | NetworkManager → Player Prefab | Network Config → Player Prefab |
| 动态预制体列表 | Registered Spawnable Prefabs | Network Prefabs List |
修饰符 / 执行上下文(Mirror 独有语法)
| Mirror | 含义 |
|---|
[Server] | 仅服务器执行;客户端调用会警告 |
[ServerCallback] | 仅服务器执行;客户端调用不警告(常用于 Update 里包一层) |
[Client] | 仅客户端执行 |
[ClientCallback] | 仅客户端执行;服务器调用不警告 |
NGO 无上述特性标签,用 if (IsServer) / if (IsClient) / if (IsOwner) 分支代替。
RPC 对照
| 方向 | Mirror | NGO (2.x) |
|---|
| 客户端 → 服务器 | [Command],方法名建议 Cmd 开头 | [Rpc(SendTo.Server)] |
| 服务器 → 所有客户端 | [ClientRpc] | [Rpc(SendTo.Clients)] 或 SendTo.ClientsAndHost |
| 服务器 → 指定客户端 | [TargetRpc] | Rpc + RpcParams 指定 ClientId |
| 调用限制 | Command 不能从 Server 调 | Server RPC 须 Client/Owner 发起;ClientRpc 须 Server 发起 |
1
2
3
4
5
6
7
8
9
10
11
12
13
| // Mirror
[Command]
void CmdFire() { }
[ClientRpc]
void RpcOnHit(int dmg) { }
// NGO
[Rpc(SendTo.Server)]
void CmdFireRpc() { }
[Rpc(SendTo.ClientsAndHost)]
void RpcOnHitRpc(int dmg) { }
|
同步变量对照
| Mirror [SyncVar] | NGO NetworkVariable<T> |
|---|
| 谁改 | 仅 Server 改字段 | 常用 WritePermission.Server |
| 客户端 | 只读,自动同步 | .Value 读,Server 写 |
| 变化回调 | [SyncVar(hook = nameof(OnHpChanged))] | OnValueChanged += ... |
| Hook 执行端 | 在客户端,收到服务器同步后触发 | 各端订阅,在值变化时触发 |
| UI 更新 | 在 hook 里改 UI(客户端) | 在 OnValueChanged 里改 UI |
为什么本地直接改 UI 不会同步给别人?
UI 只是本机表现;要让所有人看到名字/颜色,必须 Command/RPC → 服务器改 SyncVar/NetworkVariable → 自动同步到所有客户端 → hook/OnValueChanged 更新 UI。
名字与颜色同步(通用流程)

Mirror 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| [SyncVar(hook = nameof(OnNameChanged))]
string playerName;
[Command]
void CmdSetName(string name)
{
playerName = name; // 仅服务器赋值
}
void OnNameChanged(string oldVal, string newVal)
{
// 在客户端执行:更新 TMP/UI
nameText.text = newVal;
}
|
NGO 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public NetworkVariable<FixedString64Bytes> PlayerName = new(
default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
[Rpc(SendTo.Server)]
void SubmitNameRpc(FixedString64Bytes name)
{
PlayerName.Value = name;
}
public override void OnNetworkSpawn()
{
PlayerName.OnValueChanged += (_, newVal) => nameText.text = newVal.ToString();
}
|
场景与生成
| 主题 | Mirror | NGO |
|---|
| 离线场景 | Offline Scene(断线后加载) | 自行用 SceneManager + Shutdown 处理 |
| 在线场景 | Online Scene(连接后加载) | NetworkSceneManager 或手动加载 |
| 切场景 | NetworkManager.singleton.ServerChangeScene(name) | NetworkSceneManager.LoadScene |
| 玩家生成 | 在 Online Scene 生成,不在 Offline Scene | Server Spawn Player Prefab |
| 跨场景保留 | NetworkManager / 玩家上 DontDestroyOnLoad | 同上 |
Mirror 生成注意:玩家默认在 Online Scene 生成;跨场景不消失需 DontDestroyOnLoad(通常挂在 Manager 或 Player 根上)。
NGO 生成
1
2
3
4
5
| if (IsServer)
{
var go = Instantiate(prefab, pos, rot);
go.GetComponent<NetworkObject>().Spawn();
}
|
VR / Pico 实战
共同结构(Mirror / NGO 均适用)
| 点 | 说明 |
|---|
| Pico 大空间 | 录制地图的原点、正方向 → Unity 场景原点与前进轴 |
| XR Rig | 只驱动本地相机/手柄,不要把整个 Rig 当网络根同步 |
| 推荐层级 | NetworkPlayer(Identity/Object + Transform 同步)→ 子物体 LocalXRRig(仅 Owner 启用) |
| 移动 | Owner 采集输入 → Command/Server RPC → 服务器校验 → 改权威 Transform |
NGO:Owner → Server RPC
1
2
3
4
5
6
7
8
9
10
11
12
| void Update()
{
if (!IsOwner) return;
SubmitMoveRpc(ComputeTarget());
}
[Rpc(SendTo.Server)]
void SubmitMoveRpc(Vector3 pos)
{
if (!ValidateMove(pos)) return;
transform.position = pos;
}
|
Mirror:Command 等价
1
2
3
4
5
6
7
8
9
10
11
12
| void Update()
{
if (!isOwned) return;
CmdMove(ComputeTarget());
}
[Command]
void CmdMove(Vector3 pos)
{
if (!ValidateMove(pos)) return;
transform.position = pos;
}
|
Mirror 项目脚本架构(你的 VR 工程)
| 脚本 | 职责 |
|---|
| VRNetworkManager | 总控:初始化联网/断网场景,Discovery、重连;管理 Host/Client |
| VRNetworkDiscovery | 局域网发现(广播/响应房间) |
| VRNetworkLobbyUI | 大厅入口:监听发现事件,按配置自动发现或手动进房 |
| VRNetworkReconnect | 断线重连 |
| VRNetworkPlayer | 玩家可见性、XR Rig 挂接 |
| NetworkMessages | 自定义消息数据结构(若用 Message 系 API) |
对应到 NGO 时:Manager → NetworkManager + 自定义 Lobby;Discovery → Relay/Lobby 或局域网广播自写;Reconnect → 监听 OnClientDisconnectCallback 后重连;Player → NetworkObject + Owner 启用 XR。
NGO 快速上手与示例
1. 配置 NetworkManager
- 添加
NetworkManager(全场景仅一个)

- Transport:Unity Transport

- 指定 Player Prefab(含
NetworkObject) - Inspector Start Host 本机调试

2. NetworkManagerUI
1
2
3
| ServerButton.onClick.AddListener(() => NetworkManager.Singleton.StartServer());
HostButton.onClick.AddListener(() => NetworkManager.Singleton.StartHost());
ClientButton.onClick.AddListener(() => NetworkManager.Singleton.StartClient());
|
3. HelloWorldPlayer(NetworkVariable)
见原 NGO 笔记;要点:Server 写 Position.Value,OnValueChanged 更新 Transform。
4. 同步方案选型
| 需求 | Mirror | NGO |
|---|
| 位置旋转 | NetworkTransform | NetworkTransform |
| 血量分数等 | [SyncVar] | NetworkVariable |
| 一次性事件 | [ClientRpc] / [Command] | [Rpc(SendTo.*)] |
调试、部署与常见坑
部署
| 场景 | Mirror | NGO |
|---|
| 本机 | localhost | 127.0.0.1 |
| 局域网 | ipconfig 查 IPv4,客户端连主机 IP:端口 | 同上 |
| 专服 | Dedicated Server / Headless,-batchmode -nographics | Server Build + 命令行 -server |
常见坑(两边通用)
| 现象 | 原因 |
|---|
| RPC 不执行 | 未 Spawn / 调用端不对(Client 调了 ClientRpc) |
| 别人看不到我 | 未 Spawn、预制体未注册 |
| 位置回弹 | 客户端和服务器同时改 Transform |
| 变量不同步 | 客户端直接改 SyncVar / NetworkVariable |
| VR 头身分离 | Rig 与 Network Player 根节点混用 |
| 现象 | Mirror 特有 | NGO 特有 |
|---|
| Manager 失效 | Manager 挂在 NetworkIdentity 上 | 多个 NetworkManager |
| 玩家不在线场景 | 未设 Online Scene | Prefab 未进 Network Prefabs |
调试:Mirror 可多开 Build + Editor;NGO 用 Multiplayer Play Mode + Multiplayer Tools Profiler。
Mirror 实现细节
社区 Unity 多人方案(UNET 分支)。模型:服务器权威 + 状态同步。
目录
核心原则
| 原则 | 说明 |
|---|
| 客户端 | 渲染、输入、发 Command;不做权威玩法判定 |
| 服务器 | 校验、改 SyncVar / Transform,再同步给所有客户端 |
| 权威 | 谁改服务器上的“真相”,所有人看到一致结果 |
| 单例 Manager | 每场景仅一个激活的 NetworkManager |
flowchart LR
C[Client] -->|Command| S[Server]
S -->|SyncVar / ClientRpc / NetworkTransform| C
S --> C2[其他 Client]
客户端 / 服务器 / Host
Mirror 没有固定的“客户端 exe / 服务器 exe”两套工程,靠打包方式 + 启动 API 区分角色。
客户端打包
- 可独立运行的游戏本体
- 负责:输入、渲染、音效
- 向服务器同步本地状态(位置、操作),接收服务器下发的全局状态
- 不参与权威判定(血量、胜负等由服务器算)
服务器形态
1. 无头专用服务器(Headless Server)
| 项 | 说明 |
|---|
| 形态 | 无图形界面的独立可执行程序 |
| 内容 | 仅网络逻辑 + 权威玩法;不加载渲染/UI |
| 职责 | 管理连接、鉴权、碰撞/得分/血量、全局状态同步 |
| 打包 | Build Settings 勾选平台 + Mirror Headless Server |
2. Host(客户端兼服务器)
| 项 | 说明 |
|---|
| 形态 | 复用同一客户端程序,运行时 StartHost() |
| Host 端 | 既渲染操作,又在本地跑服务端逻辑 |
| 其他客户端 | 连 Host 的 IP:Port |
| 本地通信 | Host 的 Client 与内置 Server 走进程内通道,延迟更低 |
| 适用 | 原型、局域网、小规模房间 |
启动 API
1
2
3
| NetworkManager.singleton.StartServer(); // 专服
NetworkManager.singleton.StartHost(); // 房主
NetworkManager.singleton.StartClient(); // 加入(地址端口在 Transport / Manager 配置)
|
常用判断
| 属性 | 含义 |
|---|
isServer | 服务器逻辑为真(Host 也为 true) |
isClient | 客户端为真(Host 也为 true) |
isServer && isClient | Host |
isOwned | 本机拥有该 NetworkIdentity(读输入、发 Command) |
玩法逻辑用 isServer 判断,不要只用 isClient,否则 Host 会漏执行。
连接建立流程
传输层(Mirror Transport)
| 协议 | 用途 |
|---|
| TCP | 可靠连接、鉴权、关键状态消息 |
| UDP | 位置等实时数据(上层可加重传) |
开发者一般不直接写 Socket,通过 Mirror Transport 配置即可。
步骤概览
sequenceDiagram
participant S as Server
participant C as Client
S->>S: 1. 绑定 0.0.0.0:端口,StartServer/Host 监听
C->>S: 2. StartClient,指定 IP:Port
Note over C,S: 3. TCP 三次握手(Transport 自动)
C->>S: 4. 应用层握手(版本、可选鉴权)
S->>C: 5. 连接成功,同步房间/玩家列表
S->>C: 6. Spawn Player 等初始状态
- 服务器启动:绑定 IP(常用
0.0.0.0)+ 端口(如 7777),防火墙放行。 - 客户端连接:知晓服务器地址(局域网 IPv4 / 公网 IP 或域名 + 端口映射)。
- TCP 三次握手:SYN → SYN+ACK → ACK。
- Mirror 应用层握手:协议版本、可选密码/防作弊;失败则断开。
- 回调:
OnServerConnect / OnClientConnected 等做业务逻辑。
| 场景 | 连谁 |
|---|
| 专服 | 服务器公网/内网 IP:Port |
| Host | Host 机器局域网 IP:Port |
| 本机测试 | localhost |
NetworkManager
所有连接、生成、场景切换的入口。
必须遵守
- 每场景只有一个激活的 NetworkManager(单例)。
- 不要挂在带
NetworkIdentity 的 GameObject 上(加载场景时可能被禁用)。 - 一棵物体树只能有一个
NetworkIdentity,挂在父节点;父链上不能再有 Identity。
常用配置
| 项 | 说明 |
|---|
| Transport | Telepathy / KCP 等 |
| Network Address / Port | 默认连接地址与端口 |
| Player Prefab | 带 NetworkIdentity 的玩家预制体 |
| Offline Scene | 未连接时场景 |
| Online Scene | 连接后自动加载的场景 |
| Registered Spawnable Prefabs | 可网络生成的预制体列表 |
常用回调(自定义继承 NetworkManager)
OnServerConnect / OnServerDisconnectOnClientConnect / OnClientDisconnectOnServerAddPlayer / OnServerReady
RPC 与特性标签
远程过程调用(RPC)
| 特性 | 方向 | 命名建议 | 调用限制 |
|---|
[Command] | 客户端 → 服务器 | Cmd 开头 | 不能从服务器调用 |
[ClientRpc] | 服务器 → 所有客户端 | Rpc 开头 | 必须服务器调用 |
[TargetRpc] | 服务器 → 指定客户端 | TargetRpc 开头 | 须服务器调用 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| [Command]
void CmdFire(Vector3 dir)
{
// 仅在服务器执行:校验、扣弹药、伤害
}
[ClientRpc]
void RpcPlayMuzzleFx()
{
// 各客户端播特效
}
[TargetRpc]
void TargetRpcShowMessage(string msg)
{
// 仅目标客户端显示
}
|
执行上下文标签(Mirror 独有)
| 特性 | 说明 |
|---|
[Server] | 仅服务器执行;客户端调用警告 |
[ServerCallback] | 仅服务器执行;客户端调用不警告(适合 Update 里包一层) |
[Client] | 仅客户端执行;服务器调用警告 |
[ClientCallback] | 仅客户端执行;服务器调用不警告 |
1
2
3
4
5
| void Update()
{
if (!isServer) return; // 或 [ServerCallback] 包一层
SimulateAI();
}
|
SyncVar 与 UI 同步
SyncVar 规则
- 标记需要同步的字段,仅服务器可改值
- 客户端自动收到更新
[SyncVar(hook = nameof(OnXxxChanged))]:值变化时调用 hook
重要:hook 在客户端收到同步后执行(用于更新 UI),不是在服务器上执行。
名字 / 颜色同步流程
sequenceDiagram
participant O as Owner Client
participant S as Server
participant A as All Clients
O->>S: CmdSetName / CmdSetColor
S->>S: 校验,写入 SyncVar
S-->>A: 自动同步
A->>A: hook 更新 TMP/UI
[Command]
void CmdSetName(string name)
{
playerName = name; // 只在服务器改
}
void OnNameChanged(string oldVal, string newVal)
{
nameText.text = newVal; // 客户端更新 UI
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#### 为什么本地直接改 UI 别人看不到?
UI 只是本机显示。必须走:**Command → 服务器改 SyncVar → 同步 → hook 改 UI**。
#### 与 NGO 对照
| Mirror | NGO |
|--------|-----|
| `[SyncVar]` | `NetworkVariable<T>` |
| `hook` | `OnValueChanged` |
---
### 场景管理
| 配置 | 作用 |
|------|------|
| **Offline Scene** | 断开连接后自动加载 |
| **Online Scene** | 连接成功后自动加载 |
| **ServerChangeScene** | 服务器切换场景并同步给客户端 |
```csharp
NetworkManager.singleton.ServerChangeScene("GameScene");
|
玩家生成
- 玩家默认在 Online Scene 中生成,不在 Offline Scene
- 跨场景不销毁:对 NetworkManager 或 Player 根节点使用
DontDestroyOnLoad
网络生成
1
2
| // 仅服务器
NetworkServer.Spawn(go);
|
预制体须在 NetworkManager 的 Spawnable Prefabs 中注册。
VR 工程脚本
| 脚本 | 职责 |
|---|
| VRNetworkManager | 总控:联网/断网场景、Discovery、重连;Host/Client |
| VRNetworkDiscovery | 局域网广播发现房间 |
| VRNetworkLobbyUI | 大厅入口:监听发现、进房 |
| VRNetworkReconnect | 断线重连 |
| VRNetworkPlayer | 玩家可见性、XR Rig 挂接 |
| NetworkMessages | 自定义消息数据结构 |
VR 推荐结构
1
2
| NetworkPlayer (NetworkIdentity + NetworkTransform)
└── LocalXRRig(仅 isOwned 时启用,不同步 Rig 根)
|
1
2
3
4
5
6
7
8
9
10
11
12
| void Update()
{
if (!isOwned) return;
CmdMove(ComputeTargetFromXR());
}
[Command]
void CmdMove(Vector3 worldPos)
{
if (!ValidateMove(worldPos)) return;
transform.position = worldPos; // 或由 NetworkTransform 同步
}
|
Rig 只驱动本地相机;网络根节点用 Player + NetworkTransform。
调试与部署
| 场景 | 做法 |
|---|
| 本机 | localhost + 同一端口 |
| 局域网 | 主机 ipconfig 查 IPv4,客户端连 192.168.x.x:端口 |
| 专服 | Dedicated Server / Headless,-batchmode -nographics |
| 双开测试 | 一个 Build + 一个 Editor,或两个 Build |
常见坑
| 现象 | 原因 / 处理 |
|---|
| Command 不执行 | 未 Spawn、从服务器调了 Command、无 Identity |
| SyncVar 不同步 | 在客户端直接改字段 |
| hook 没触发 UI | hook 写错方法名;或以为在服务器执行 |
| Manager 失效 | Manager 挂在 NetworkIdentity 上 |
| 别人看不到物体 | 未 NetworkServer.Spawn、预制体未注册 |
| 位置回弹 | 客户端和服务器同时改 Transform |
| VR 头身分离 | 同步了 XR Rig 而非 Player 根 |
Netcode for GameObjects 实现细节
核心原则
| 原则 | 说明 |
|---|
| 客户端 → 服务器 | 客户端发 RPC 或请求;不要直接在客户端改“全局权威”状态 |
| 服务器 → 客户端 | 服务器(或权威端)改状态后,用 NetworkVariable / NetworkTransform / ClientRpc 下发 |
| 单一 NetworkManager | 全场景仅一个;Inspector 调试可配合 Multiplayer Tools(Package Manager) |
| 预制体必须登记 | 动态 Spawn 的预制体要在 NetworkManager 的 Network Prefabs 列表中 |
一句话记忆
谁权威,谁改 Transform / NetworkVariable;其他人只读或做表现层插值。
RPC 对照(Mirror / 旧 NGO → 新 API)
| 旧关键字 | 新版 SendTo | 方向 | 命名习惯(可选) |
|---|
[Command] | SendTo.Server | 客户端 → 服务器 | CmdXxx |
[ClientRpc] | SendTo.Clients | 服务器 → 所有客户端(不含纯 Server 进程时视版本而定) | RpcXxx |
[TargetRpc] | SendTo 指定客户端 | 服务器 → 单个客户端 | TargetRpcXxx |
| — | SendTo.ClientsAndHost | 服务器 → 客户端 + Host 上的本地玩家 | 广播常用 |
| — | SendTo.Server + RpcParams | 可指定目标 ClientId | 私聊、踢人等 |
1
2
3
4
5
| [Rpc(SendTo.Server)]
void CmdFireRpc() { }
[Rpc(SendTo.ClientsAndHost)]
void RpcOnHitRpc(int damage) { }
|
启动模式与常用 API
| 模式 | API | 谁跑逻辑 | 典型用途 |
|---|
| Dedicated Server | StartServer() | 仅服务器有完整模拟;无本地玩家画面(或无头) | 正式服、多房间 |
| Host | StartHost() | 同一进程既是 Server 又是 Client | 开发调试、房主开房 |
| Client | StartClient() | 只接收同步 + 发 RPC | 加入房间的玩家 |
1
2
3
4
5
| // 常用入口
NetworkManager.Singleton.StartServer();
NetworkManager.Singleton.StartHost();
NetworkManager.Singleton.StartClient();
NetworkManager.Singleton.Shutdown(); // 断开并清理网络对象
|
NetworkBehaviour 里判断“我是谁”
| 属性 | 含义 | 典型用法 |
|---|
IsServer | 当前实例在服务器逻辑上为真(Host 也为 true) | 扣血、刷怪、判定胜负 |
IsClient | 当前是客户端(Host 也为 true) | 播本地特效、UI |
IsHost | 既是 Server 又是 Client | 仅 Host 才有的 UI |
IsOwner | 本客户端拥有该 NetworkObject | 读输入、发移动 RPC |
IsSpawned | 已完成网络生成 | 避免 OnNetworkSpawn 前访问网络 API |
IsServer && !IsClient → 专用服务器;IsServer && IsClient → Host。
生命周期(常用重写)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public class MyNetObj : NetworkBehaviour
{
public override void OnNetworkSpawn()
{
// 已 Spawn,可安全用 NetworkVariable / RPC
if (IsOwner) { /* 绑定输入 */ }
someVar.OnValueChanged += OnSomeVarChanged;
}
public override void OnNetworkDespawn()
{
someVar.OnValueChanged -= OnSomeVarChanged;
}
public override void OnGainedOwnership() { } // 所有权转移给你
public override void OnLostOwnership() { } // 失去所有权
}
|
flowchart TB
subgraph 权威
S[Server / 权威端]
end
subgraph 非权威
C1[Client A - Owner]
C2[Client B]
end
C1 -->|SendTo.Server RPC| S
S -->|NetworkVariable / NetworkTransform| C1
S -->|同步| C2
核心组件
| 组件 | 作用 |
|---|
| NetworkManager | 连接、Spawn Player、Prefabs 列表、NetworkConfig |
| NetworkObject | 可被复制到各端的实体;有 NetworkObjectId |
| NetworkBehaviour | 挂在带 NetworkObject 的对象上;提供 RPC、NetworkVariable |
| NetworkTransform | 同步 position / rotation / scale(含插值) |
| Unity Transport | 底层 UDP(UTP),配置 IP、端口 |
层级要求:NetworkBehaviour 必须与 NetworkObject 在同一 GameObject,或在其子物体上(子物体上的脚本仍属该 NetworkObject)。
Pico 大空间 / VR
| 点 | 说明 |
|---|
| 地图原点 / 正方向 | Pico 录制时标定的原点、朝向 → 映射到 Unity 场景原点与前进方向(常与 +Z 对齐,以项目文档为准) |
| VR Rig | 头显、手柄父节点只驱动本地相机;不要把 Rig 根节点当网络 Player 直接同步 |
| 推荐结构 | 网络 Player(NetworkObject + NetworkTransform)← 子节点 Local XR Rig(仅 Owner 启用) |
| 坐标系 | 大空间定位 SDK 给出的位姿需转换到 Unity 世界空间后再写入权威 Transform |
VR 玩家移动推荐范式(Owner 输入 → Server 权威)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| public class NetworkedVRPlayer : NetworkBehaviour
{
[SerializeField] Transform xrCamera; // 仅 Owner 本地引用
void Update()
{
if (!IsOwner) return;
// 采集本帧目标(例:摇杆 / 传送点)
Vector3 target = ComputeTargetFromInput();
SubmitMoveRpc(target);
}
[Rpc(SendTo.Server)]
void SubmitMoveRpc(Vector3 worldPosition)
{
// 服务器校验(距离、冷却、是否在 NavMesh 上)
if (!ValidateMove(worldPosition)) return;
transform.position = worldPosition;
// 若挂了 NetworkTransform,由组件自动同步;否则写 NetworkVariable
}
bool ValidateMove(Vector3 pos) => Vector3.Distance(transform.position, pos) < 5f;
}
|
客户端—服务器快速启动
1. 配置 NetworkManager
- 添加
NetworkManager(全场景仅一个)

- Transport 选 Unity Transport

- Network Config 中指定 Player Prefab(带
NetworkObject) - Inspector 点 Start Host 做本机联调

检查清单
2. NetworkManagerUI(可选)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;
public class NetworkManagerUI : MonoBehaviour
{
[SerializeField] private Button ServerButton;
[SerializeField] private Button HostButton;
[SerializeField] private Button ClientButton;
void Awake()
{
ServerButton.onClick.AddListener(() => NetworkManager.Singleton.StartServer());
HostButton.onClick.AddListener(() => NetworkManager.Singleton.StartHost());
ClientButton.onClick.AddListener(() => NetworkManager.Singleton.StartClient());
}
}
|
3. Player 预制体示例
预制体必须有 NetworkObject。示例:HelloWorldPlayer、RpcTest。
HelloWorldPlayer — NetworkVariable 同步位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| using Unity.Netcode;
using UnityEngine;
namespace HelloWorld
{
public class HelloWorldPlayer : NetworkBehaviour
{
// 默认:Server 写,所有人读
public NetworkVariable<Vector3> Position = new NetworkVariable<Vector3>(
Vector3.zero,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
public override void OnNetworkSpawn()
{
Position.OnValueChanged += (_, newVal) => transform.position = newVal;
if (IsOwner) Move();
}
public override void OnNetworkDespawn()
{
Position.OnValueChanged -= (_, newVal) => transform.position = newVal;
}
public void Move() => SubmitPositionRequestRpc();
[Rpc(SendTo.Server)]
private void SubmitPositionRequestRpc(RpcParams rpcParams = default)
{
var randomPosition = GetRandomPositionOnPlane();
transform.position = randomPosition;
Position.Value = randomPosition; // 仅 Server 可写
}
static Vector3 GetRandomPositionOnPlane()
{
return new Vector3(Random.Range(-3f, 3f), 1f, Random.Range(-3f, 3f));
}
}
}
|
生产环境更推荐用 OnValueChanged 更新 Transform,而不是每帧在 Update 里赋值(减少无谓逻辑)。
RpcTest — RPC 链式调用
public class RpcTest : NetworkBehaviour
{
public override void OnNetworkSpawn()
{
if (!IsServer && IsOwner)
ServerOnlyRpc(0, NetworkObjectId);
}
[Rpc(SendTo.ClientsAndHost)]
private void ClientAndHostRpc(int value, ulong sourceNetworkObjectId)
{
Debug.Log($"Client Received RPC #{value} on #{sourceNetworkObjectId}");
if (IsOwner)
ServerOnlyRpc(value + 1, sourceNetworkObjectId);
}
[Rpc(SendTo.Server)]
private void ServerOnlyRpc(int value, ulong sourceNetworkObjectId)
{
Debug.Log($"Server Received RPC #{value} on #{sourceNetworkObjectId}");
ClientAndHostRpc(value, sourceNetworkObjectId);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
##### 4. NetworkTransform 测试
```csharp
using System;
using Unity.Netcode;
using UnityEngine;
public class NetworkTransformTest : NetworkBehaviour
{
private void Update()
{
if (!IsServer) return;
float theta = Time.frameCount / 10.0f;
transform.position = new Vector3(
(float)Math.Cos(theta), 0f, (float)Math.Sin(theta));
}
}
|
同步方案选型
| 方式 | 优点 | 缺点 | 适用 |
|---|
| NetworkTransform | 自动插值、省代码 | 带宽随物体数量增 | 角色、可拾取物、大部分动态物体 |
| NetworkVariable | 精确、可订阅变化 | 需自己处理表现 | 血量、弹药、枚举状态、回合数 |
| RPC | 瞬时事件、无需持久状态 | 不保证顺序、不能当存档 | 开枪音效、一次性技能、聊天 |
| 手动 RPC + Transform | 完全自定义 | 易出 desync、要写插值 | 特殊同步规则 |
flowchart TD
A[需要同步什么?] --> B{持续 Transform?}
B -->|是| C[NetworkTransform]
B -->|否| D{离散状态?}
D -->|是| E[NetworkVariable]
D -->|否| F[一次性事件 → RPC]
NetworkVariable 详解
1
2
3
4
5
6
7
8
9
| public NetworkVariable<int> Health = new NetworkVariable<int>(
100,
NetworkVariableReadPermission.Everyone, // 谁可读
NetworkVariableWritePermission.Server); // 谁可写(常用 Server)
public NetworkVariable<float> Charge = new NetworkVariable<float>(
0f,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Owner); // 仅 Owner 可写(仍经服务器转发)
|
| 写权限 | 说明 |
|---|
Server | 只有服务器改 .Value(推荐:玩法真相在服) |
Owner | 拥有者客户端可发起写,实际仍由 NGO 同步规则处理 |
订阅变化(UI、动画触发):
1
2
3
4
| Health.OnValueChanged += (oldVal, newVal) =>
{
Debug.Log($"HP {oldVal} -> {newVal}");
};
|
支持类型:基础值、Vector3、Quaternion、实现 INetworkSerializable 的自定义结构体等(见官方列表)。
注意
- 不要在
OnNetworkSpawn 之前读写 .Value - 高频数据(每帧坐标)优先
NetworkTransform,不要滥用 NetworkVariable<Vector3> - 客户端不要用
NetworkVariable 做“仅本地”数据,用普通字段即可
RPC 详解
编写规则
| 规则 | 说明 |
|---|
| 声明位置 | 必须在 NetworkBehaviour 子类中 |
| 方法签名 | 通常 void,参数需为 NGO 支持的序列化类型 |
| 调用时机 | 仅在 IsSpawned == true 之后 |
| 谁可发起 | SendTo.Server 一般由 Client/Owner 调;SendTo.Clients* 必须由 Server 调 |
| 安全 | 所有玩法判定在 Server RPC 内做,不要相信客户端传来的伤害、坐标 |
ServerRpc 内改状态 → 自动同步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| [Rpc(SendTo.Server)]
void CmdTakeDamageRpc(int damage, RpcParams p = default)
{
if (!IsServer) return;
int hp = Mathf.Max(0, Health.Value - damage);
Health.Value = hp;
if (hp == 0) RpcPlayDeathFxRpc();
}
[Rpc(SendTo.ClientsAndHost)]
void RpcPlayDeathFxRpc()
{
// 各端播死亡特效(不含玩法判定)
}
|
| 设置(概念) | 说明 |
|---|
| Server Authority | 只有服务器改 Transform 会同步(NPC、物理球) |
| Owner Authority | Owner 客户端驱动,适合第一人称 / VR 本地移动(仍需 Server 校验时用 RPC 改服) |
| 插值 | 远端客户端平滑;本地 Owner 可关闭或减小插值避免“拖泥带水” |
第一人称 / VR:常见组合 = Owner 读输入 + SendTo.Server 移动 RPC + NetworkTransform(Server 权威)或 Owner 权威 + 服务端校验。
生成与销毁
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 仅服务器(或 Host 的服务器侧)可 Spawn
if (IsServer)
{
var go = Instantiate(prefab, pos, rot);
var netObj = go.GetComponent<NetworkObject>();
netObj.Spawn(); // 默认把 Player 的 connection 作为 owner 时可 SpawnWithOwnership
// netObj.SpawnWithOwnership(clientId);
}
// 销毁
if (IsServer)
netObj.Despawn(); // 推荐:走网络销毁
// Destroy(go) 需知会网络层,否则易留幽灵对象
|
| API | 说明 |
|---|
Spawn() | 创建网络实例,分配 NetworkObjectId |
Despawn() | 网络层销毁,各端 OnNetworkDespawn |
ChangeOwnership(clientId) | 转移 Owner(载具上下车、观战切换) |
TrySetParent(parentNetObj) | 网络父子关系(武器挂点等) |
连接与鉴权
NetworkConfig 常用项
| 项 | 说明 |
|---|
ConnectionData / Address | IP、端口 |
PlayerPrefab | 玩家自动 Spawn 的预制体 |
ConnectionApproval | 是否启用连接审批回调 |
MaxConnections | 最大连接数 |
TickRate | 模拟 tick(影响同步频率) |
Connection Approval 示例骨架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // NetworkManager 上挂脚本,或在自定义 Manager 里订阅
void Start()
{
NetworkManager.Singleton.ConnectionApprovalCallback = ApprovalCheck;
NetworkManager.Singleton.NetworkConfig.ConnectionApproval = true;
}
void ApprovalCheck(
NetworkManager.ConnectionApprovalRequest request,
NetworkManager.ConnectionApprovalResponse response)
{
string payload = System.Text.Encoding.UTF8.GetString(request.Payload);
bool approved = payload == "expected_token";
response.Approved = approved;
response.CreatePlayerObject = approved;
response.Pending = false;
}
|
客户端连接时可通过 NetworkManager.Singleton.NetworkConfig.ConnectionData 传入自定义字节(版本号、房间密码等)。
Unity Relay(简表)
| 步骤 | 说明 |
|---|
| 安装 | com.unity.services.relay、com.unity.services.authentication |
| 流程 | 登录 → 创建/加入 Allocation → 把 Relay 数据写入 Transport → StartHost / StartClient |
| 用途 | 无公网 IP 联机、Pico / 移动端房间 |
命令行启动(Dedicated Server)
1
2
3
4
5
6
7
8
| void Start()
{
var args = System.Environment.GetCommandLineArgs();
if (System.Array.IndexOf(args, "-server") >= 0)
NetworkManager.Singleton.StartServer();
else if (System.Array.IndexOf(args, "-client") >= 0)
NetworkManager.Singleton.StartClient();
}
|
构建 Dedicated Server 时选 Server Build,无头运行加 -batchmode -nographics(平台相关)。
场景与预制体
| 主题 | 说明 |
|---|
| Network Prefabs List | 所有会被 Spawn 的预制体必须注册,否则客户端缺资源 |
| 场景内 NetworkObject | 场景中预先放的 NetworkObject 需在加载时由服务器同步生成(使用 NGO 场景管理或手动 Spawn) |
| DontDestroyOnLoad | NetworkManager 常放启动场景并 DontDestroyOnLoad |
| Additive 场景 | 使用 NetworkSceneManager 做网络化场景加载(大空间分块可选) |
调试与常见坑
| 现象 | 可能原因 |
|---|
| RPC 不执行 | 未 Spawn、方法不是 NetworkBehaviour、Server RPC 在纯 Client 调 |
| 客户端看不到物体 | 预制体未进 Network Prefabs、Server 未 Spawn |
| 位置抖动 / 回弹 | Client 与 Server 同时改 Transform;应只权威端写 |
| Host 正常、专服异常 | 用了仅 Client 的逻辑,未用 IsServer 区分 |
| VR 身体与头分离 | Rig 与 Network Player 根节点混用;应用子节点分离 |
| 变量不同步 | 写权限不是 Server 却在客户端改 .Value |
调试工具
- Multiplayer Play Mode:编辑器内多实例
- Network Profiler / Stats:Package Manager → Multiplayer Tools
- 日志:
NetworkLog.LogInfo、连接事件 OnClientConnectedCallback
网络概念
| 主题 | 描述 |
|---|
| 权威(Authority) | 谁负责该对象的“真相”状态;状态同步由权威端产生 |
| 所有权(Ownership) | 哪个 ClientId 是该 NetworkObject 的 Owner;影响谁可发部分 RPC、Owner 写变量 |
| 网络拓扑 | Dedicated Server、Listen Server(Host)、纯 P2P(NGO 以服为中心) |
配置
连接配置
- NetworkManager → Network Config:地址、端口、Player Prefab、Tick、连接数。
- Connection Approval:
ConnectionApproval = true 时必须实现 ConnectionApprovalCallback,否则连接会被拒。
传输(Transport)
- Unity Transport(UTP):UDP;可配
ConnectionData 的 Address、Port。 - 局域网:
127.0.0.1 + 同一端口;真机需本机局域网 IP。
Unity Relay
- NGO + Relay:房间码 / Join Code 交换 Allocation 数据,客户端无需知道主机真实 IP。
命令行参数
-server、-client、-ip、-port 等由启动脚本解析并写入 UnityTransport。