元某人也元某人也

Unity 多人联网:Mirror 与 NGO

Unity 多人联网:Mirror 与 Netcode for GameObjects 对比与实践。

本文目录

Unity 多人联网:Mirror 与 Netcode for GameObjects(NGO)

  • 核心模型相同:服务器权威 + 状态同步,客户端发请求、服务器改真相再下发。

选型与共同点

维度MirrorNGO (Netcode for GameObjects)
来源社区(原 UNET 分支)Unity 官方包
包名导入 Mirror 资源 / UPMcom.unity.netcode.gameobjects
网络实体NetworkIdentityNetworkObject
行为脚本基类NetworkBehaviourNetworkBehaviour
总控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

共同原则

  1. 每个联网对象有权威端(通常是 Server)。
  2. 客户端只发指令(RPC),玩法判定在服务器。
  3. 持久状态用同步变量(SyncVar / NetworkVariable),持续位置用 NetworkTransform。
  4. 场景中通常只有一个激活的 NetworkManager。
  flowchart LR
    subgraph 共同模型
        C[Client 输入/UI] -->|RPC| S[Server 权威逻辑]
        S -->|SyncVar / NetworkVariable / NetworkTransform| C
        S -->|同步| C2[其他 Client]
    end

客户端 / 服务器 / Host

Mirror 与 NGO 都不是固定两个 exe,而是通过打包方式 + 启动 API 区分角色。

形态说明MirrorNGO
客户端渲染、输入、发 RPC;不做权威判定StartClient()StartClient()
专用服务器无头、无渲染;权威逻辑StartServer() + Headless BuildStartServer() + Dedicated Server Build
Host同进程兼 Server + ClientStartHost()StartHost()
能力MirrorNGO
是否服务器isServerIsServer(Host 也为 true)
是否客户端isClientIsClient(Host 也为 true)
是否 HostisServer && isClientIsHost
是否本地拥有者isOwned / hasAuthority(版本用语)IsOwner
已入网netIdentity != null 等IsSpawned

Host 注意:写逻辑时用 IsServer / isServer 做玩法判定,不要只用 IsClient,否则 Host 会漏逻辑。


连接是如何建立的

通用流程(二者逻辑一致)

image-20260520092635825

Mirror:传输与握手(你的笔记要点)

  1. 传输层:Mirror 通过 Transport 抽象使用 TCP / UDP(默认封装,一般不直接写 Socket)。
    • TCP:连接、鉴权、关键可靠消息。
    • UDP:位置等实时数据(上层可加重传)。
  2. 服务器:StartServer() / StartHost() → 绑定 0.0.0.0:端口 → 监听。
  3. 客户端:StartClient(address, port) → 向目标 IP 发起连接。
  4. TCP 三次握手:SYN → SYN+ACK → ACK(Mirror Transport 自动完成)。
  5. 应用层握手:版本协商、可选身份验证 → 成功则同步房间/玩家列表。
  6. NetworkManager:连接全流程入口;回调如 OnServerConnect、OnClientConnected。
Host vs 专服区别
Headless Server独立进程监听;客户端连 IP:Port
Host无独立服程序;StartHost() 本地同时开服+客;其他客户端连 Host 的局域网 IP

Mirror.NetworkManager 注意

  1. 每场景仅一个激活的 NetworkManager(单例)。
  2. 不要把 NetworkManager 挂在带 NetworkIdentity 的对象上(场景加载时可能被禁用)。
  3. 一个节点树只允许一个 NetworkIdentity,挂在父节点;父链上不能再有 Identity。

NGO:传输与鉴权

项说明
Unity Transport默认 UTP(UDP),在 NetworkManager 选 Transport
连接StartServer / StartHost / StartClient,地址端口在 Transport / NetworkConfig
鉴权ConnectionApproval = true + ConnectionApprovalCallback
公网Unity Relay,客户端无需主机公网 IP
断开NetworkManager.Singleton.Shutdown()

核心对照表

组件

作用MirrorNGO
可联网物体NetworkIdentityNetworkObject
网络脚本NetworkBehaviourNetworkBehaviour
位置旋转同步NetworkTransformNetworkTransform
玩家预制体NetworkManager → Player PrefabNetwork Config → Player Prefab
动态预制体列表Registered Spawnable PrefabsNetwork Prefabs List

修饰符 / 执行上下文(Mirror 独有语法)

Mirror含义
[Server]仅服务器执行;客户端调用会警告
[ServerCallback]仅服务器执行;客户端调用不警告(常用于 Update 里包一层)
[Client]仅客户端执行
[ClientCallback]仅客户端执行;服务器调用不警告

NGO 无上述特性标签,用 if (IsServer) / if (IsClient) / if (IsOwner) 分支代替。

RPC 对照

方向MirrorNGO (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。

名字与颜色同步(通用流程)

image-20260520092652946

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();
}

场景与生成

主题MirrorNGO
离线场景Offline Scene(断线后加载)自行用 SceneManager + Shutdown 处理
在线场景Online Scene(连接后加载)NetworkSceneManager 或手动加载
切场景NetworkManager.singleton.ServerChangeScene(name)NetworkSceneManager.LoadScene
玩家生成在 Online Scene 生成,不在 Offline SceneServer 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

  1. 添加 NetworkManager(全场景仅一个)
    image-20260408210526625
  2. Transport:Unity Transport
    image-20260408210601297
  3. 指定 Player Prefab(含 NetworkObject)
  4. Inspector Start Host 本机调试
    image-20260408211025268

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. 同步方案选型

需求MirrorNGO
位置旋转NetworkTransformNetworkTransform
血量分数等[SyncVar]NetworkVariable
一次性事件[ClientRpc] / [Command][Rpc(SendTo.*)]

调试、部署与常见坑

部署

场景MirrorNGO
本机localhost127.0.0.1
局域网ipconfig 查 IPv4,客户端连主机 IP:端口同上
专服Dedicated Server / Headless,-batchmode -nographicsServer Build + 命令行 -server

常见坑(两边通用)

现象原因
RPC 不执行未 Spawn / 调用端不对(Client 调了 ClientRpc)
别人看不到我未 Spawn、预制体未注册
位置回弹客户端和服务器同时改 Transform
变量不同步客户端直接改 SyncVar / NetworkVariable
VR 头身分离Rig 与 Network Player 根节点混用
现象Mirror 特有NGO 特有
Manager 失效Manager 挂在 NetworkIdentity 上多个 NetworkManager
玩家不在线场景未设 Online ScenePrefab 未进 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 && isClientHost
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 等初始状态
  1. 服务器启动:绑定 IP(常用 0.0.0.0)+ 端口(如 7777),防火墙放行。
  2. 客户端连接:知晓服务器地址(局域网 IPv4 / 公网 IP 或域名 + 端口映射)。
  3. TCP 三次握手:SYN → SYN+ACK → ACK。
  4. Mirror 应用层握手:协议版本、可选密码/防作弊;失败则断开。
  5. 回调:OnServerConnect / OnClientConnected 等做业务逻辑。
场景连谁
专服服务器公网/内网 IP:Port
HostHost 机器局域网 IP:Port
本机测试localhost

NetworkManager

所有连接、生成、场景切换的入口。

必须遵守
  1. 每场景只有一个激活的 NetworkManager(单例)。
  2. 不要挂在带 NetworkIdentity 的 GameObject 上(加载场景时可能被禁用)。
  3. 一棵物体树只能有一个 NetworkIdentity,挂在父节点;父链上不能再有 Identity。
常用配置
项说明
TransportTelepathy / KCP 等
Network Address / Port默认连接地址与端口
Player Prefab带 NetworkIdentity 的玩家预制体
Offline Scene未连接时场景
Online Scene连接后自动加载的场景
Registered Spawnable Prefabs可网络生成的预制体列表
常用回调(自定义继承 NetworkManager)
  • OnServerConnect / OnServerDisconnect
  • OnClientConnect / OnClientDisconnect
  • OnServerAddPlayer / 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 没触发 UIhook 写错方法名;或以为在服务器执行
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 ServerStartServer()仅服务器有完整模拟;无本地玩家画面(或无头)正式服、多房间
HostStartHost()同一进程既是 Server 又是 Client开发调试、房主开房
ClientStartClient()只接收同步 + 发 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

  1. 添加 NetworkManager(全场景仅一个)
    image-20260408210526625
  2. Transport 选 Unity Transport
    image-20260408210601297
  3. Network Config 中指定 Player Prefab(带 NetworkObject)
  4. Inspector 点 Start Host 做本机联调
    image-20260408211025268

检查清单

  • Player Prefab 已加入 Network Prefabs(若用默认列表)
  • 场景中无重复 NetworkManager
  • 防火墙放行 UDP 端口(默认见 Transport)

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()
{
  // 各端播死亡特效(不含玩法判定)
}

NetworkTransform 与移动

设置(概念)说明
Server Authority只有服务器改 Transform 会同步(NPC、物理球)
Owner AuthorityOwner 客户端驱动,适合第一人称 / 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 / AddressIP、端口
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)
DontDestroyOnLoadNetworkManager 常放启动场景并 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。
仅用于交流学习,记录学习心得