Unity Mirror 多人同步 基础教程
Mirror是什么?
Mirror是一款免费的开源的可以用于多人网络联机的一个库,其不仅适用于局域网,也可用于专用的服务器(Dedicate Server)C/S模式,适用于Unity 2019/ 2020 / 2021 /2022 /2023 / 6000。其前身是基于Unet构建的,简化了一些Unet里的api操作,重构并添加了一些新的功能,大部分的概念和Unet是相通的。
Mirror的一些特性包括:
- 消息处理(Message handlers)
- 通用的高性能的序列化(General purpose high performance
- serialization)
- 分布式对象管理
- 状态同步
- 网络类,如:Server、Client、Connection等
Mirror由不同的层构建而成:

Mirror
链接: GitHub Mirror 下载地址
链接: Mirror & ParrelSync & Mirror 模板 ScriptTemplates下载地址
链接: Mirror 官方文档
NetworkManager(网络管理器)
Configuration:配置
Dont Destroy On Load:
是否在切换场景时保持 NetworkManager 不被销毁。
勾选:通常用于只有一个全局 NetworkManager 的项目。
不勾选:如果每个场景都有独立的 NetworkManager。
Run In Background:
是否允许游戏在后台继续运行(比如切出去窗口)。
勾选:保证多人游戏网络不会因应用暂停而断开。

Auto-Start Options:自动启动
Headless Start Mode:
无头模式(服务器构建时)启动行为。
DoNothing:不自动启动。
Auto Start Server:启动时自动作为服务器运行。
Auto Start Client:启动时自动作为客户端运行。
Editor Auto Start:
在 Unity Editor 下是否也应用 Headless Start Mode,方便调试。
Send Rate:
服务器/客户端每秒发送更新的频率。
高速游戏(FPS):60–100 Hz。
RPG/MMO:30 Hz 左右。
慢节奏(策略/回合):1–10 Hz。

Scene Management:场景管理
Offline Scene:
当网络断开/停止时切换到的场景。
Online Scene:
当服务器启动、客户端连接成功后切换的场景。
Offline Scene Load Delay:
从断开到加载 Offline Scene 的延迟(秒),比如显示 “连接丢失” 提示。

NetworkManager 是 Mirror 的核心网络入口,这些参数基本涵盖了 生命周期(配置/启动)→ 场景切换 → 连接设置 → 玩家生成 → 安全 → 同步优化。
Network Info:网络信息
Transport:
传输层组件(Mirror 提供 KCP/Telepathy/其他自定义 Transport)。
Network Address:
客户端连接服务器的 IP 或域名,默认 localhost。
Max Connections:
最大同时连接的客户端数量。
Disconnect Inactive Connections:
是否自动断开不活跃的连接。
Disconnect Inactive Timeout:
不活跃多久(秒)后断开。

Authentication:身份验证
Authenticator:
可选的认证组件(比如用户名/密码验证)。
默认为 None,所有连接直接通过。
可自定义扩展。

Player Object:玩家对象
Player Prefab:
客户端连接时生成的玩家对象(必须带 NetworkIdentity)。
Auto Create Player:
是否在客户端连接时自动生成玩家。
Player Spawn Method:
玩家生成位置的选择方式:
Random:随机选择一个 NetworkStartPosition。
RoundRobin:轮流顺序分配。

Security:安全
Exceptions Disconnect:
如果在处理网络消息时抛出异常,是否立即断开该客户端。
开启:更安全,避免漏洞。
关闭:可能允许客户端继续运行,但有风险。

Snapshot Interpolation:快照插值
Snapshot Settings:
插值参数,用于平滑同步移动(插帧/预测)。
如非必要,不用调整默认就行。

Connection Quality:连接质量
Evaluation Method:
评估网络连接质量的方式:
Simple:基于 RTT 和抖动。
Pragmatic:基于插值的调整。
Evaluation Interval:
多久评估一次连接质量(秒)。

Interpolation:UI 插值调试 UI
Time Interpolation Gui:
是否在 Editor/Dev Build 中启用插值调试 GUI(帮助可视化网络延迟和插值)。
Registered Spawnable Prefabs:
可被网络动态生成的 Prefab 列表。
这里需要把游戏中要通过网络 Spawn 的物体(非玩家)都注册进来。
例如子弹、怪物、掉落物。
点击 Populate Spawnable Prefabs 按钮可自动添加。

KcpTransport(KCP 通信协议)
Transport Configuration:通信配置
Port:
服务器监听的 UDP 端口号。客户端需要连接这个端口,常用如 7777 或 25565。
Dual Mode:
同时支持 IPv4 和 IPv6。
✅ 开启:更通用,推荐。
❌ 关闭:仅支持 IPv4(在部分设备/网络环境下更稳定)。
No Delay:
是否启用 KCP 的 Nodelay 模式,即立即发包而不是等聚合。
开启:延迟更低,适合实时游戏。
关闭:节省带宽,延迟稍高。
Interval (ms):
KCP 的内部刷新周期(单位:毫秒)。
默认 10ms(比 KCP 原始默认 100ms 要快很多)。
越小延迟越低,但 CPU 占用更高。
Timeout (ms):
超时时间,客户端多久没响应就判定掉线。默认 10000ms(10 秒)。
Recv Buffer Size / Send Buffer Size (bytes):
Socket 的收/发缓冲区大小。默认 7MB 左右。
并发高、大流量时要足够大。
操作系统也需要支持这么大的 buffer,否则无效。

Advanced:高级设置
Fast Resend:
丢包重传的激进程度。
0:标准模式。
2:快速模式,丢包后更快重传(推荐实时游戏)。
Receive Window Size / Send Window Size:
接收/发送窗口大小(以包为单位)。默认 4096,代表可以同时缓存/飞行这么多包。
窗口越大,吞吐量越高,但丢包时压力更大。
Max Retransmit:
单个包的最大重传次数,超过就判定连接异常。默认 40。
Maximize Socket Buffer:
是否尝试将 socket 缓冲区设置到系统允许的最大值。
建议开启,在高并发/大消息场景下更稳。

Allowed Max Message Sizes:允许的最大消息大小
这些是只读值,显示在当前窗口设置下:
Reliable Max Message Size:
在“可靠通道”下单个消息的最大字节数。
Unreliable Max Message Size:
在“不可靠通道”下单个消息的最大字节数(一般接近 MTU ~1200 字节)。
👉 提示:即使最大值很大,也推荐把大消息拆分为小消息传输,否则会导致延迟增加。

Debug:调试
Debug Log:
是否打印调试日志。
Statistics GUI:
是否在屏幕上显示统计 GUI(仅限 Editor/Dev Build)。
Statistics Log:
是否定期在控制台输出统计信息(方便无头服务器调试)。

NetworkManagerHUD(网络管理器 HUD)
Offset X / Offset Y:画面偏移
Offset X / Offset Y:
类型:int
作用:控制 HUD 在屏幕上的 水平偏移 / 垂直偏移(像素)。
默认值:0,表示从屏幕左边缘开始绘制。
使用场景:
如果你的游戏左上角有其他 UI(例如血条、菜单按钮),可以通过修改这个值让 HUD 向右移动,避免重叠。

如果脚本搭载,会在视图左上角出现这样的效果,按钮可点击执行相应的方法。
主要方法
Host (Server + Client):
NetworkManager.StartHost()
内部流程:
启动 服务器(StartServer())
启动 本地客户端(StartClient())
用于单机本地测试(既当服务端,又有一个客户端连入)。
Client:
NetworkManager.StartClient()
内部流程:
使用 manager.networkAddress(默认是 "localhost")和端口去连接服务器。
连接成功后会触发 OnClientConnect()。
Server Only:
NetworkManager.StartServer()
内部流程:
仅启动服务器,等待远程客户端连接。
没有本地玩家。

Client Ready:
让客户端向服务器声明“我已准备好”,并生成玩家对象。
Stop Host / Stop Client / Stop Server:
NetworkManager.StopHost();
NetworkManager.StopClient();
NetworkManager.StopServer();
分别关闭 Host、客户端、服务器。

小结:
Host → StartHost()
Client → StartClient()(同时可修改 IP 和端口)
Server Only → StartServer()
Client Ready → NetworkClient.Ready() + AddPlayer()
Stop 系列 → StopHost() / StopClient() / StopServer()
NetworkStartPosition(玩家出生点位置)
如何使用:
1. 放几个点就有几个可选出生位:给场景里多个空物体加上 NetworkStartPosition,就能形成一个出生点池。
2. 朝向也会被用到:玩家会按该 Transform.rotation 生成,摆好面向。
3. 换场景/销毁会自动清理:不必手动管理列表,组件的 OnDestroy 会把自己移除,避免脏引用。
3. 与 PlayerSpawnMethod 联动:在 NetworkManager 里切换 Random / RoundRobin 可改变分配策略(适合大厅或多刷新点地图)。
4. 没有出生点也能生成:若列表为空,Mirror 会在(0,0,0)或默认位置实例化玩家(取决于你的自定义逻辑);通常建议至少放一个 NetworkStartPosition。

NetworkIdentity(网络“身份证”)
Server Only:服务器端
说明:如果勾选,表示这个对象 只会存在于服务器,不会同步到客户端。
用途:
服务器逻辑物体(如路径点、服务端专用的管理对象)。
怪物尸体复活前隐藏、只在服务器运算等。
Visibility:可见性
说明:决定对象是否广播给客户端(可见性覆盖 Interest Management 系统)。
Default → 使用 Interest Management(默认规则,比如 AOI 可见性)。
ForceHidden → 强制对所有客户端不可见(即使理论上在范围内)。
ForceShown → 强制广播给所有客户端(比如比分 UI、全局物体)。
用途:
怪物重生时用 ForceHidden 先隐藏。
全局排行榜、房间管理器等用 ForceShown 始终可见。
如何使用:
1. 所有可联网物体必须挂 NetworkIdentity(玩家、子弹、敌人…)。
2. 服务器专用逻辑对象 → 勾选 Server Only。
3. 全局广播对象 → Visibility = ForceShown。
4. 需要临时隐藏 → Visibility = ForceHidden(如怪物复活)。
5. Prefab 必须有 assetId,不要复制 prefab 时丢失。

NetworkTransformReliable(网络同步器 稳定版)
Target:同步物体
Target:
需要同步的 Transform 对象(一般就是 Player 或附加的子物体)。

Selective Sync:选择性同步
Sync Position:
是否同步位置。
Sync Rotation:
是否同步旋转。
Sync Scale:
是否同步缩放。
👉 如果某些属性不需要频繁同步(比如缩放固定),可以取消勾选节省带宽。

Bandwidth Savings:带宽优化
Only Sync On Change:
只有当值变化超过阈值时才同步(位置变化大于 Position Precision、旋转变化大于 Rotation Sensitivity)。
Compress Rotation:
使用压缩四元数(Smallest-3 压缩),减少数据量。

Interpolation:插值平滑
Interpolate Position / Rotation / Scale:
是否在客户端平滑插值过渡,而不是瞬间跳跃。
✅ 勾选 → 画面流畅,适合角色移动。
❌ 关闭 → 精准、即时,适合子弹/爆炸等瞬时事件。

Coordinate Space:坐标空间
Coordinate Space:
Local → 同步本地坐标(相对于父物体)。
World → 同步全局坐标。

Timeline Offset:时间偏移修正
Timeline Offset:
是否启用时间偏移修正,用于弱网下抵消网络延迟造成的“卡顿”。

Debug:调试
Show Gizmos / Show Overlay / Overlay Color:
调试功能:在场景视图或屏幕上显示插值/同步状态。

Additional Settings:其他设置
Only Sync On Change Correction Multiplier:(在 Inspector 里叫 Only Sync On Change 值)
当启用 “只在变化时同步” 时,用于修正快照时间的倍数,避免物体第一次移动时出现卡顿。
Use Fixed Update:
是否在 FixedUpdate 中应用快照(适合物理物体同步),默认 Update。

Rotation:旋转灵敏度
Rotation Sensitivity:
旋转灵敏度(角度差超过多少度才同步)。默认 0.01。

Precision:位置同步
Position Precision:
位置同步的精度(小数点后保留多少)。默认 0.01 ≈ 1cm。
Scale Precision:
缩放同步的精度,默认 0.01。
👉 值越大,带宽占用越少,但精度也下降。

Sync Settings:同步设置
Sync Direction:
Client To Server → 客户端控制(例如玩家移动)。
Server To Client → 服务器控制(例如怪物 AI)。
Sync Interval:
同步间隔(秒)。0 表示每帧都可能同步。

总结:
这个组件就是 Mirror 官方的 高精度、低带宽版 Transform 同步器:
1. Selective Sync:决定同步哪些属性。
2. Bandwidth Savings:减少带宽消耗(只在变化时发包 + 压缩)。
3. Interpolation:客户端平滑移动,避免抖动。
4. Precision / Sensitivity:控制同步的粒度。
5. Sync Direction:谁来作为“权威端”同步数据。
Unity Mirror 示例
Mirror & ParrelSync 插件以及 ScriptTemplates代码模板导入
Mirror 插件 导入
1. 你解压或者下载之后,直接拉到 Unity Assets 中。

2. 导入之后最好点击一下All 然后点击Import 按钮。

3. 如果想要了解 可以在 Assets/Mirror/Examples 文件夹下选择自己感兴趣的场景进行尝试。

4. 我推荐这个场景,整体功能基本上都有大家可以自己尝试尝试。
场景地址:Assets/Mirror/Examples/TopDownShooter/Scenes/MirrorTopDownShooter

ParrelSync 插件导入
ParrelSync 是一个 Unity 编辑器扩展,允许用户通过打开另一个 Unity 编辑器窗口并镜像原始项目的更改来测试多人游戏,而无需构建项目。
👉 注意:克隆的项目不可编辑否则会报错。
特征:
- 测试多人游戏,无需构建项目
- 用于管理所有项目克隆的 GUI 工具
- 受保护的资产不被其他克隆实例修改
- 方便的 API 可加快测试工作流程
1. 你解压或者下载之后,直接拉到 Unity Assets 中。只不过选择的是:ParrelSync 。

2. 导入成功之后可以在顶部导航栏 点击 ParrelSync->Clones Manager

3. 可以更改自己想要克隆的路径,点至Open In New Editor 就可以打开镜像项目了。


4. 最后就是这样的效果

ScriptTemplates 代码模板导入
1. 你解压或者下载之后选择 ScriptTemplates 文件夹,直接拉到 Unity Assets 中。


2. 导入之后会是这样,导入成功之后要重启编辑器。

3. 成功之后在Assets 中鼠标右键 Create -> Mirror 就可以创建可种各样的代码模板使用了。

ScriptTemplates 代码 模板 作用
| 模板名称 | 基类 | 主要作用 |
|---|---|---|
| Network Manager | NetworkManager | 核心入口,管理服务器/客户端的启动、场景切换、玩家生成等。 |
| Network Manager With Actions | NetworkManager | 同上,但额外提供 Action 事件回调,方便用委托而不是继承来订阅。 |
| Network Authenticator | NetworkAuthenticator | 自定义认证(账号/密码/令牌验证),控制客户端是否能加入。 |
| Network Behaviour | NetworkBehaviour | Mirror 网络对象的基类,带有 OnStartServer、OnStartClient 等生命周期函数。 |
| Network Behaviour With Actions | NetworkBehaviour | 在 NetworkBehaviour 基础上加了事件委托版本,逻辑更解耦。 |
| Custom Interest Management | InterestManagement | 控制对象的可见性(只同步范围内的对象 / 分组广播)。 |
| Network Room Manager | NetworkRoomManager | 内置房间逻辑(大厅/准备/开始游戏/切换场景)。 |
| Network Room Player | NetworkRoomPlayer | 房间里玩家的状态(如准备/未准备、玩家编号),与 Room Manager 配套。 |
| Network Discovery | NetworkDiscovery | 局域网房间发现(客户端广播 → 服务器回应)。 |
| Network Transform | NetworkTransformReliable(或 NetworkTransform) | 同步对象的 Transform(位置、旋转、缩放),带插值和可靠传输。 |
你能用它们干什么?
快速搭建多人联机框架:
Network Manager 负责整体网络。
Network Room Manager + Player 负责大厅、准备、进入游戏。
Network Authenticator 控制谁能加入。
Custom Interest Management 控制谁能看到哪些对象。
同步游戏对象:
Network Behaviour/With Actions → 写自定义联网逻辑(比如血量、技能冷却)。
Network Transform → 同步位置和旋转,保持客户端一致。
扩展局域网/发现功能:
Network Discovery 允许自动发现服务器(无需手输 IP)。
✅ 总结:这些模板就像“起手式”,帮你在写联网代码时不需要每次都从 MonoBehaviour 改成 NetworkBehaviour,再一个个补生命周期。直接选对应的模板,就能快速得到 Mirror 推荐的代码结构。
Mirro 消息发送接收与同步
Mirror 的“消息发送/接收/同步
1. 高层数据同步:SyncVar / SyncList / SyncDictionary / SyncSet(自动同步,有钩子)
2. 远程调用:[Command](Client→Server)、[ClientRpc] / [TargetRpc](Server→Clients/某个Client)
3. 原始消息:NetworkMessage(RegisterHandler + Send,完全自定义协议)
4. Transform 同步:NetworkTransform( Reliable )(位置/旋转/缩放 + 插值)
自定义消息(最灵活、协议自控)
适合:聊天、房间列表、业务事件等。
核心 API:RegisterHandler<T>()、Send(msg)、conn.Send(msg)、NetworkServer.SendToAll(msg)
// ─────────────────────────────────────────────────────────────────────────────
// 项目:Mirror Demo
// 文件:ChatMessages.cs
// 说明:演示 Mirror 的 NetworkMessage 收发(客户端→服务器→广播给所有客户端)
// ─────────────────────────────────────────────────────────────────────────────
using Mirror;
using UnityEngine;
public struct ChatMsg_ZH : NetworkMessage
{
// 这里放要传的字段(必须是 public field,不是属性)
public string _Text;
}
// 挂到你的 NetworkManager 的同一个对象上更方便初始化
public class ChatMessageHub : MonoBehaviour
{
// ───── 服务器端注册 ─────
/// <summary>服务器启动时注册消息处理</summary>
[ServerCallback]
private void OnEnable()
{
// 客户端发来的 ChatMsg
// false=不要求通过 Auth 才能收此消息:contentReference[oaicite:1]{index=1}
NetworkServer.RegisterHandler<ChatMsg>(OnServerChatMsg, false);
}
/// <summary>服务器关闭时注销消息处理</summary>
[ServerCallback]
private void OnDisable()
{
// 模板里也有示例:contentReference[oaicite:2]{index=2}
NetworkServer.UnregisterHandler<ChatMsg>();
}
/// <summary>服务器收到客户端消息 → 回发给所有人</summary>
private void OnServerChatMsg(NetworkConnectionToClient _Conn, ChatMsg _Msg)
{
Debug.Log($"[Server] 收到:{_Msg._Text}");
// 回给所有客户端
NetworkServer.SendToAll(_Msg);
}
// ───── 客户端注册 ─────
/// <summary>客户端启动时注册接收</summary>
private void Start()
{
NetworkClient.RegisterHandler<ChatMsg>(OnClientChatMsg, false); //:contentReference[oaicite:3]{index=3}
}
/// <summary>客户端收到服务器(或其他客户端转发)的消息</summary>
private void OnClientChatMsg(ChatMsg _Msg)
{
Debug.Log($"[Client] 收到:{_Msg._Text}");
}
// ───── 客户端发送 ─────
/// <summary>客户端发消息到服务器</summary>
public void ClientSend(string _Text)
{
if (!NetworkClient.isConnected) return;
//:contentReference[oaicite:4]{index=4}
NetworkClient.Send(new ChatMsg { _Text = _Text });
}
}
要点:
结构体必须是 public struct + public 字段,Mirror 自动序列化。
先 RegisterHandler<T>() 再 Send(),否则会丢。
可搭配 KCP 的可靠/不可靠通道(KcpTransport 层),消息体尽量小且高频时要考虑带宽(你前面已配好 KCP 参数)。
远程调用:Command / Rpc(经典、够用)
适合:权威服模式下的“客户端输入→服务器处理→同步给所有客户端”
using Mirror;
using UnityEngine;
public class MoveAbility_ZH : NetworkBehaviour
{
[SyncVar(hook = nameof(OnSpeedChanged))] // 值改变自动同步,调用钩子
public float _Speed = 3f;
// ───── 客户端输入 → 发到服务器 ─────
/// <summary>客户端请求移动</summary>
/// <param name="_Dir">移动方向(已归一化)</param>
[Command] // Client→Server
private void CmdMove(Vector3 _Dir)
{
if (!isServer) return;
// 服务器权威地修改位置(示例:简单位移)
transform.position += _Dir * _Speed * Time.fixedDeltaTime;
// 广播给所有客户端做一些即时效果
RpcOnMoveFx(transform.position);
}
// ───── 服务器广播 → 客户端执行 ─────
/// <summary>移动效果(仅客户端执行)</summary>
[ClientRpc] // Server→All Clients
private void RpcOnMoveFx(Vector3 _NewPos)
{
// 仅做特效/音效,位置同步可交给 SyncVar 或 NetworkTransform
// Debug.DrawLine(oldPos, _NewPos, Color.green, 0.1f);
}
// ───── SyncVar 钩子 ─────
private void OnSpeedChanged(float _Old, float _New)
{
// 本地 UI 刷新
}
// ───── 本地采集输入 ─────
private void Update()
{
if (!hasAuthority) return; // 仅本地玩家采集输入
Vector3 _Dir = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")).normalized;
if (_Dir != Vector3.zero)
{
// 向服务器发命令
CmdMove(_Dir);
}
}
}
要点:
[Command] 只能由拥有对象 authority 的客户端调用;服务端执行方法体。
[ClientRpc] 由服务端调用、所有客户端执行;如只给某个玩家:用 [TargetRpc](参数首个是 NetworkConnectionToClient)。
小状态(数值/开关)优先用 SyncVar,大范围连续状态(位置)交给 NetworkTransform。
高层自动同步:SyncVar / SyncList…
适合:数值状态、装备表、队伍列表等。
using Mirror;
using UnityEngine;
/// <summary>
/// 同步生命与物品列表的示例
/// </summary>
public class StatsAndBag_ZH : NetworkBehaviour
{
// 数值:改一次→自动同步给观察者
[SyncVar(hook = nameof(OnHpChanged))]
public int _Hp = 100;
// 列表:增删改→逐项同步
public readonly SyncList<string> _Items = new SyncList<string>();
[Server]
public void ServerTakeDamage(int _Value)
{
_Hp = Mathf.Max(0, _Hp - _Value); // 赋值会触发同步 + 钩子
}
private void OnHpChanged(int _Old, int _New)
{
// 刷 UI、播放受击等
}
private void Awake()
{
// 监听同步列表事件
_Items.Callback += (_Op, _Index, _OldItem, _NewItem) =>
{
// 根据 _Op(Add/Remove/Insert/Set)刷新 UI
};
}
}
要点:
SyncVar 适合小而离散的数据;SyncList 适合集合数据。
只有服务器改动的值才会被同步(默认权威)。客户端想改 → 用 Command 请求服务器。
如何选择
1. 玩家输入/交互:Command 上行 → 服务器改状态 → SyncVar/ClientRpc 下发
2. 属性数值:SyncVar + hook
3. 集合/背包:SyncList / SyncDictionary
4. Transform:NetworkTransformReliable(或自定义 NetworkTransformBase)
5. 杂项业务事件(聊天/房间/公告):NetworkMessage(Register + Send)
6. 筛可见性/降低带宽:自定义 InterestManagement 限制 Observer
Mirro UGUI 网络控制
代码里的方法映射
1. Start Host → OnClickStartHost() → NetworkManager.StartHost()(禁用三键防连点)。
2. Start Client → OnClickStartClient():读取 _AddressInputField 与 _PortInputField,设置 networkAddress 和 KcpTransport.Port → StartClient()。
3. Start Server → OnClickStartServer():设置端口 → StartServer()(可选:切换到 online 场景的示例协程已给出,默认为注释)。
4. Stop → StopButtons():根据当前状态调用 StopHost() / StopClient() / StopServer()。
UGUI 控制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Mirror;
using UnityEngine.UI;
using Mirror.BouncyCastle.Bcpg.OpenPgp;
using Newtonsoft.Json.Serialization;
using UnityEngine.SceneManagement;
[AddComponentMenu("NetHUD/NetworkManagerHUD_ZH")]
public class NetworkManagerHUD_ZH : MonoBehaviour
{
NetworkManager _Manager;
//开启按钮组
public GameObject _StartButonsGroup;
//停止按钮组
public GameObject _StopButtonsGroup;
//显示状态按钮
public Text _StatusText;
//创建Host
public Button _StartHostButton;
//创建 client
public Button _StartClientButton;
//IP地址输入框
public InputField _AddressInputField;
//端口输入框
public InputField _PortInputField;
//创建服务器
public Button _StartServerButton;
//停止Host
public Button _StopHostButton;
// ───── 单例防重 + 常驻 ─────
private static NetworkManagerHUD_ZH _Instance;
private void Awake()
{
// 防止切换场景后出现第二个 HUD
if (_Instance != null && _Instance != this)
{
Destroy(gameObject);
return;
}
_Instance = this;
// 关键:切场景不销毁
DontDestroyOnLoad(gameObject);
}
void Start()
{
//获取组件
_Manager = NetworkManager.singleton ?? FindObjectOfType<NetworkManager>();
// 先清理,防止因重复绑定导致一个点击触发两次
_StartHostButton.onClick.RemoveAllListeners();
_StartClientButton.onClick.RemoveAllListeners();
_StartServerButton.onClick.RemoveAllListeners();
_StopHostButton.onClick.RemoveAllListeners();
_StartHostButton.onClick.AddListener(OnClickStartHost);
_StartClientButton.onClick.AddListener(OnClickStartClient);
_StartServerButton.onClick.AddListener(OnClickStartServer);
_StopHostButton.onClick.AddListener(StopButtons);
}
void Update()
{
// UI 状态刷新
StatusLabels();
bool _IsHost = NetworkServer.active && NetworkClient.active;
bool _IsServer = NetworkServer.active && !NetworkClient.active;
bool _IsClient = NetworkClient.isConnected && !NetworkServer.active;
//根据状态显示按钮
if (!_IsHost && !_IsServer && !_IsClient)
{
// 如果我们还没有连接,则允许更改地址
if (!NetworkClient.active)
{
// 未连接
_Manager.networkAddress = _AddressInputField.text;
//只有当我们有端口传输时才显示端口字段
//我们不能在address字段中使用“IP:PORT”,因为只有这个字段
//支持IPV4:PORT。
//对于IPV6:PORT,这可能会误导,因为IPV6包含“:”:
// 2001:0db8: 0000:0000:0000: ff00: 0042:8329
if (Transport.active is PortTransport portTransport)
{
// 如果有人试图输入非数字字符,请使用TryParse
if (ushort.TryParse(_PortInputField.text, out ushort port))
{
portTransport.Port = port;
}
// 状态显示为空
_StatusText.text = "";
}
}
else
{
// 正在连接中
_StatusText.text = ($"Connecting to {_Manager.networkAddress}..");
}
_StartButonsGroup.SetActive(true);
_StopButtonsGroup.SetActive(false);
}
else
{
_StartButonsGroup.SetActive(false);
_StopButtonsGroup.SetActive(true);
}
}
private void OnEnable()
{
// 有些项目把 NetworkManager 放在玩法场景里,切场景后需要重新拿引用
UnityEngine.SceneManagement.SceneManager.activeSceneChanged += OnActiveSceneChanged;
}
private void OnDisable()
{
// 解绑事件
UnityEngine.SceneManagement.SceneManager.activeSceneChanged -= OnActiveSceneChanged;
}
/// <summary>
/// 活动场景已更改
/// </summary>
/// <param name="oldS"></param>
/// <param name="newS"></param>
private void OnActiveSceneChanged(Scene oldS, UnityEngine.SceneManagement.Scene newS)
{
// 场景切换后,重新拿一次 NetworkManager / Transport 等(以防丢引用)
if (_Manager == null)
{
_Manager = FindObjectOfType<NetworkManager>();
}
// 这里一般不需要重新绑按钮,因为按钮在本对象上,随着 HUD 常驻一起在
// 如果你的按钮是在场景里的别的对象,需要在这里重新查找并绑定
}
/// <summary>
/// 点击创建 Host
/// </summary>
public void OnClickStartHost()
{
// 禁止连点
_StartHostButton.interactable = false;
_StartClientButton.interactable = false;
_StartServerButton.interactable = false;
// 设置地址和端口
_Manager.StartHost();
//// 切换场景 onlineScene 要设置为空
//StartCoroutine(Co_SwitchOnlineSceneOnce("MyScene"));
//if (!string.IsNullOrWhiteSpace(_Manager.onlineScene))
//{
// _Manager.ServerChangeScene(_Manager.onlineScene);
//}
}
/// <summary>
/// 点击创建 Client
/// </summary>
public void OnClickStartClient()
{
// 设置地址
_Manager.networkAddress = _AddressInputField.text;
// 设置地址和端口
if (Transport.active is PortTransport portTransport &&
ushort.TryParse(_PortInputField.text, out ushort port))
{
portTransport.Port = port;
}
// 启动客户端
_Manager.StartClient();
}
/// <summary>
/// 点击创建 Server
/// </summary>
public void OnClickStartServer()
{
//if (int.TryParse(_PortInputField.text, out int port))
//{
// _Manager.GetComponent<TelepathyTransport>().port = (ushort)port;
//}
//_Manager.StartServer();
// 设置地址和端口
if (Transport.active is PortTransport portTransport &&
ushort.TryParse(_PortInputField.text, out ushort port))
{
portTransport.Port = port;
}
// 启动服务器
_Manager.StartServer();
// 切换场景 onlineScene 要设置为空
//StartCoroutine(Co_SwitchOnlineSceneOnce("MyScene"));
//if (!string.IsNullOrWhiteSpace(_Manager.onlineScene))
//{
// _Manager.ServerChangeScene(_Manager.onlineScene);
//}
}
/// <summary>
/// 停止按钮方法
/// </summary>
public void StopButtons()
{
// 如果同时是服务器和客户端(Host)
if (NetworkServer.active && NetworkClient.isConnected)
{
_Manager.StopHost();
print("停止主机");
}
// 停止客户端(如果处于客户端模式)
else if (NetworkClient.isConnected)
{
_Manager.StopClient();
Debug.Log("停止客户端");
}
// 停止服务器(如果处于服务器模式)
else if (NetworkServer.active)
{
_Manager.StopServer();
print("停止服务器");
}
}
/// <summary>
/// UI 状态刷新方法
/// </summary>
private void StatusLabels()
{
// 主机模式
if (NetworkServer.active && NetworkClient.active)
{
// 主机模式
_StatusText.text=($"<b>Host</b>: running via {Transport.active}");
}
else if (NetworkServer.active)
{
// 仅服务器端
_StatusText.text = ($"<b>Server</b>: running via {Transport.active}");
}
else if (NetworkClient.isConnected)
{
// 仅限客户端
_StatusText.text = ($"<b>Client</b>: connected to {_Manager.networkAddress} via {Transport.active}");
}
}
private IEnumerator Co_SwitchOnlineSceneOnce(string _SceneName)
{
// 等到服务器真正启动 & 不在加载中
yield return new WaitUntil(() => NetworkServer.active && !NetworkServer.isLoadingScene);
// 再检查当前场景是否已经是你想去的那个
if (SceneManager.GetActiveScene().name != _SceneName)
{
_Manager.ServerChangeScene(_SceneName);
//_Manager.onlineScene = _SceneName;
}
}
}
脚本搭载
1. 注意物体搭载附加

2. Canvas 自己创建就行,可以按照自己的风格进行处理。

常见坑与排查
1. 按钮没反应:确认 Button 的 onClick 没被别的脚本覆盖;此脚本里已 RemoveAllListeners() 然后重新绑定,避免重复触发。
2. 端口不生效:确保当前激活的传输层是实现了 PortTransport 的(如 KcpTransport),并且输入的是数字(脚本用 ushort.TryParse 做了校验)。
3. 切场景后 HUD 重复:脚本已有“单例防重”逻辑;如果你又在新场景放了一个 HUD,会被自动销毁保留第一个。
4. 连外网失败:服务器需要开放 UDP 端口;客户端地址要填公网 IP,或者配合 NetworkDiscovery 做局域网发现。
Mirro 场景切换功能
场景编排器
using System.Collections;
using System.Collections.Generic;
using Mirror;
using UnityEngine;
using UnityEngine.SceneManagement;
/// <summary>
/// 场景编排器(Scene Orchestrator)
/// 功能:
/// 1. 服务器权威管理 Additive 子场景的加载与卸载;
/// 2. 使用 SyncList<string> 同步子场景状态到所有客户端;
/// 3. 客户端(含晚加入)会根据列表自动对齐场景加载状态。
/// </summary>
public class SceneOrchestrator_ZH : NetworkBehaviour
{
// ───── 同步列表 ─────
// 当前应加载的子场景列表(服务器写入,客户端跟随)
public readonly SyncList<string> _LoadedAdditives = new SyncList<string>();
// ───── 本地状态防抖 ─────
// 记录正在加载的子场景,避免重复调用
private readonly HashSet<string> _Loading = new HashSet<string>();
// 记录正在卸载的子场景,避免重复调用
private readonly HashSet<string> _Unloading = new HashSet<string>();
// ───── Server 端 API ─────
#region Server API
/// <summary>
/// 服务器端:加载一组 Additive 场景(已加载/正在加载的会自动跳过)
/// </summary>
[Server]
public void ServerLoadAdditivesOnce(IEnumerable<string> _Names)
{
StartCoroutine(Co_ServerLoadAdditivesOnce(_Names));
}
/// <summary>
/// 服务器端:切换到新的 Additive 集合(方案1:清空后再加载)
/// </summary>
[Server]
public void ServerSwitchAdditiveSet(IEnumerable<string> _NewSet)
{
StartCoroutine(Co_ServerResetAndApply(_NewSet));
}
/// <summary>
/// 协程:清空旧列表 → 直接加载新集合(避免卸载无效场景报错)
/// </summary>
private IEnumerator Co_ServerResetAndApply(IEnumerable<string> _NewSet)
{
// 清空同步列表(客户端收到 CLEAR 事件,会卸掉所有 Additive)
_LoadedAdditives.Clear();
yield return null; // 给客户端一帧时间处理
// 直接加载目标集合
yield return Co_ServerLoadAdditivesOnce(_NewSet);
}
/// <summary>
/// 协程:仅加载缺失的 Additive 场景
/// </summary>
private IEnumerator Co_ServerLoadAdditivesOnce(IEnumerable<string> _SceneNames)
{
// 遍历请求的场景名
foreach (var _Name in _SceneNames)
{
// 跳过空名、已加载的、正在加载的
if (string.IsNullOrWhiteSpace(_Name)) continue;
if (_LoadedAdditives.Contains(_Name)) continue;
if (_Loading.Contains(_Name)) continue;
// 加载场景
_Loading.Add(_Name);
// 注意:这里不需要检查场景是否存在于 Build Settings 中
var _Op = SceneManager.LoadSceneAsync(_Name, LoadSceneMode.Additive);
while (!_Op.isDone) yield return null;
_Loading.Remove(_Name);
// 更新同步列表
if (!_LoadedAdditives.Contains(_Name))
{
_LoadedAdditives.Add(_Name); // 同步到客户端
}
}
}
#endregion
// ───── Client 端同步逻辑 ─────
#region Client Sync
/// <summary>
/// 客户端启动时:做一次全集对齐,并注册列表回调
/// </summary>
public override void OnStartClient()
{
// 保留基类调用,确保 Mirror 内部逻辑不丢失
base.OnStartClient();
StartCoroutine(Co_ClientApplyFullList()); // 晚加入对齐
// 注册列表变化回调
_LoadedAdditives.Callback += OnLoadedAdditivesChanged;
}
/// <summary>
/// 客户端关闭时:移除列表回调
/// </summary>
public override void OnStopClient()
{
// 移除列表变化回调
_LoadedAdditives.Callback -= OnLoadedAdditivesChanged;
base.OnStopClient();
}
/// <summary>
/// 同步列表变化时的回调
/// </summary>
private void OnLoadedAdditivesChanged(SyncList<string>.Operation _Op, int _Index, string _OldItem, string _NewItem)
{
// 根据操作类型处理
switch (_Op)
{
case SyncList<string>.Operation.OP_ADD:
// 新增场景
if (!string.IsNullOrEmpty(_NewItem))
{
StartCoroutine(Co_ClientEnsureLoaded(_NewItem));
}
break;
case SyncList<string>.Operation.OP_REMOVEAT:
// 移除场景
if (!string.IsNullOrEmpty(_OldItem))
{
StartCoroutine(Co_ClientEnsureUnloaded(_OldItem));
}
break;
case SyncList<string>.Operation.OP_CLEAR:
// 清空列表
StartCoroutine(Co_ClientUnloadAll());
break;
}
}
/// <summary>
/// 客户端:全集对齐(卸掉多余的,加载缺的)
/// </summary>
private IEnumerator Co_ClientApplyFullList()
{
// 卸掉本地多余的(根场景除外)
for (int _i = 0; _i < SceneManager.sceneCount; ++_i)
{
// 跳过根场景
var _Sc = SceneManager.GetSceneAt(_i);
if (_Sc == SceneManager.GetActiveScene()) continue;
// 如果不在同步列表里,就卸掉
if (!_LoadedAdditives.Contains(_Sc.name))
{
yield return Co_ClientEnsureUnloaded(_Sc.name);
}
}
// 加载缺失的
foreach (var _Name in _LoadedAdditives)
{
// 跳过空名
yield return Co_ClientEnsureLoaded(_Name);
}
}
/// <summary>
/// 客户端:确保场景已加载
/// </summary>
private IEnumerator Co_ClientEnsureLoaded(string _SceneName)
{
// 跳过空名
if (string.IsNullOrWhiteSpace(_SceneName)) yield break;
// 跳过已加载的和正在加载的
var _Sc = SceneManager.GetSceneByName(_SceneName);
if (_Sc.IsValid() && _Sc.isLoaded) yield break;
if (_Loading.Contains(_SceneName)) yield break;
// 加载场景
_Loading.Add(_SceneName);
var _Op = SceneManager.LoadSceneAsync(_SceneName, LoadSceneMode.Additive);
// 注意:这里不需要检查场景是否存在于 Build Settings 中
while (!_Op.isDone) yield return null;
_Loading.Remove(_SceneName);
}
/// <summary>
/// 客户端:确保场景已卸载
/// </summary>
private IEnumerator Co_ClientEnsureUnloaded(string _SceneName)
{
// 跳过空名
if (string.IsNullOrWhiteSpace(_SceneName)) yield break;
// 跳过未加载的和正在卸载的
var _Sc = SceneManager.GetSceneByName(_SceneName);
if (!_Sc.IsValid() || !_Sc.isLoaded) yield break;
if (_Unloading.Contains(_SceneName)) yield break;
// 卸载场景
_Unloading.Add(_SceneName);
var _Op = SceneManager.UnloadSceneAsync(_SceneName);
// 注意:这里不需要检查场景是否存在于 Build Settings 中
while (_Op != null && !_Op.isDone) yield return null;
_Unloading.Remove(_SceneName);
}
/// <summary>
/// 客户端:卸载所有非根场景
/// </summary>
private IEnumerator Co_ClientUnloadAll()
{
// 遍历所有场景,卸掉非根场景
for (int i = 0; i < SceneManager.sceneCount; ++i)
{
// 跳过根场景
var _Sc = SceneManager.GetSceneAt(i);
if (_Sc == SceneManager.GetActiveScene()) continue;
// 卸掉场景
yield return Co_ClientEnsureUnloaded(_Sc.name);
}
}
#endregion
}
自定义 NetworkManager
using Mirror;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.SceneManagement;
using System.IO;
/// <summary>
/// 自定义 NetworkManager:
/// 1. 扩展 Mirror 自带的场景切换逻辑;
/// 2. 在根场景切换完成后,服务器权威地加载指定的 Additive 子场景;
/// 3. 通过 SceneOrchestrator_ZH 同步给所有客户端,保证晚加入客户端也能正确对齐。
/// </summary>
public class CustomNetworkManager_ZH : NetworkManager
{
// ───── 预制体引用 ─────
[Header("Orchestrator 预制体(已在 Spawnable Prefabs 中注册)")]
public SceneOrchestrator_ZH _OrchestratorPrefab; // 用于管理 Additive 子场景的网络对象预制体
// ───── 根场景与其对应的 Additive 集合映射 ─────
[Header("根场景 → Additive 集合映射")]
public List<string> _AdditivesForMyScene = new List<string> { "Add", "GameList" }; // 当根场景是 MyScene 时要加载的子场景
public List<string> _AdditivesForMyotherScene = new List<string> { "Add", "GameList" }; // 当根场景是 MyOtherScene 时要加载的子场景
// 服务器侧持有的 orchestrator 实例(单例)
private SceneOrchestrator_ZH _ServerOrchestrator;
// ───── 玩家生成防重 ─────
/// <summary>
/// 重写 Mirror 的 OnServerAddPlayer,避免重复给同一个连接添加玩家。
/// </summary>
public override void OnServerAddPlayer(NetworkConnectionToClient _Conn)
{
if (_Conn.identity != null)
{
Debug.LogWarning($"[Server] 添加玩家操作被忽略(连接已存在玩家对象) connId={_Conn.connectionId}");
return;
}
base.OnServerAddPlayer(_Conn);
}
// ───── 场景切换钩子 ─────
/// <summary>
/// 当服务器端完成根场景切换时调用。
/// Mirror 会在 ServerChangeScene → FinishLoadScene → OnServerSceneChanged 顺序触发。
/// </summary>
public override void OnServerSceneChanged(string _SceneName)
{
Debug.Log($"[Server] OnServerSceneChanged -> {_SceneName}");
// 保留基类调用,确保 Mirror 内部逻辑不丢失
base.OnServerSceneChanged(_SceneName);
if (!NetworkServer.active) return;
// 开启协程,等根场景完全切换完成后再装配 Additive
StartCoroutine(Co_PostSceneChanged(_SceneName));
}
// ───── 协程:根场景切换完成后再加载 Additive 集 ─────
/// <summary>
/// 根场景切换后的后处理逻辑:
/// 1. 等待场景完全切换完成;
/// 2. 规范化根场景名(去除路径和后缀);
/// 3. 如果 orchestrator 不存在,则生成并 Spawn;
/// 4. 按映射选择要加载的 Additive 集,并调用 orchestrator 同步加载。
/// </summary>
/// <param name="_ScenePathOrName">传入的场景路径或名字(Mirror 传的可能是完整路径)</param>
private IEnumerator Co_PostSceneChanged(string _ScenePathOrName)
{
// 等待 Mirror 把根场景切换完毕
yield return new WaitUntil(() => !NetworkServer.isLoadingScene);
yield return null; // 再等一帧更稳
// 从路径提取出纯场景名
string _RootName = Path.GetFileNameWithoutExtension(_ScenePathOrName);
Debug.Log($"[Server] RootSceneName 规范化后 = {_RootName}");
// 如果 orchestrator 还没生成,就在服务器端实例化并 Spawn
if (_ServerOrchestrator == null)
{
_ServerOrchestrator = Instantiate(_OrchestratorPrefab);
DontDestroyOnLoad(_ServerOrchestrator.gameObject); // 保持跨场景不销毁
NetworkServer.Spawn(_ServerOrchestrator.gameObject); // 广播给所有客户端
}
// 按根场景名选择要加载的 Additive 集
List<string> _Set = null;
if (_RootName == "MyScene") _Set = _AdditivesForMyScene;
else if (_RootName == "MyOtherScene" || _RootName == "MyotherScene") _Set = _AdditivesForMyotherScene;
else _Set = new List<string>(); // 未配置的根场景 → 不加载任何 Additive
// 调用 orchestrator 执行子场景加载(服务器权威,客户端跟随)
_ServerOrchestrator.ServerSwitchAdditiveSet(_Set);
Debug.Log($"[Server] Additive 集装配完成:[{string.Join(", ", _Set)}]");
}
// ───── 工具函数:校验子场景是否在 Build Settings 中 ─────
/// <summary>
/// 检查 Additive 场景是否都已加入 Build Settings。
/// 避免运行时报 “场景未找到”。
/// </summary>
/// <param name="_Names">要校验的子场景列表</param>
private bool CheckScenesInBuild(List<string> _Names)
{
// 空列表直接通过
if (_Names == null) return true;
// 遍历检查每个场景名
for (int i = 0; i < _Names.Count; i++)
{
// 跳过空白项
var _N = _Names[i];
if (string.IsNullOrWhiteSpace(_N)) continue;
// 查找是否存在
bool _Exists = false;
// 遍历 Build Settings 里的场景
for (int _Bi = 0; _Bi < SceneManager.sceneCountInBuildSettings; _Bi++)
{
var _Path = SceneUtility.GetScenePathByBuildIndex(_Bi);
var _NameOnly = System.IO.Path.GetFileNameWithoutExtension(_Path);
if (_NameOnly == _N) { _Exists = true; break; }
}
// 报错并返回
if (!_Exists)
{
Debug.LogError($"[BuildSettings] 缺少场景:{_N}");
return false;
}
}
return true;
}
}
场景管理
using Mirror;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
/// <summary>
/// 场景管理脚本:
/// - 管理 UI 文本显示(子弹数量、消息内容);
/// - 负责调用玩家脚本的消息接口;
/// - 提供按钮触发的场景切换逻辑。
/// </summary>
public class SceneScript_ZH : NetworkBehaviour
{
[Header("UI 引用")]
public Text _BulletText; // 显示子弹数量
public Text _MessageText; // 显示消息文本
[Header("玩家引用")]
public PlayerController_ZH _PlayerController; // 玩家脚本引用
[SyncVar(hook = nameof(OnStatusTextChanged))]
public string _StatusText; // 同步消息内容(带钩子)
/// <summary>
/// 当 _StatusText 发生变化时调用,刷新 UI。
/// </summary>
private void OnStatusTextChanged(string _Old, string _NewStr)
{
_MessageText.text = _StatusText;
}
/// <summary>
/// 按钮:发送消息
/// </summary>
public void OnSendMessageButton()
{
if (_PlayerController != null)
{
_PlayerController.CmdSendPlayerMessage();
}
}
/// <summary>
/// 按钮:切换场景(仅服务器可操作)
/// </summary>
public void ChangeSceneButton()
{
// 检查服务器是否已启动,是独立服务器还是作为主机服务器。
if (!NetworkServer.active)
{
Debug.Log("只有服务器/主机可以切换场景");
return;
}
// 检查是否正在切换场景中
if (NetworkServer.isLoadingScene)
{
Debug.Log("正在切换场景中,忽略重复请求");
return;
}
// 决定下一个场景
string _Cur = SceneManager.GetActiveScene().name;
string _NextRoot = (_Cur == "MyScene") ? "MyOtherScene" : "MyScene";
// 如果当前场景就是目标场景,则不切换
if (_Cur == _NextRoot) return;
// 切换场景
NetworkManager.singleton.ServerChangeScene(_NextRoot);
Debug.Log($"切根场景到:{_NextRoot}");
}
}
脚本搭载以及运行
1. 自定义 NetworkManager 搭载:

2. 注意 场景编排器预制体的创建以及切换场景和附加场景名称添加。

3. 点击创建 Host But 创建房间

4. 房间创建的时候 会在CustomNetworkManager_ZH 脚本中自动执行 OnServerSceneChanged 方法。
然后会执行Co_PostSceneChanged 协程方法,按映射加载 Additive集并调用 orchestrator同步加载。

5. 点击 Change Scene 按钮,调用 SceneScript_ZH.ChangeSceneButton() 方法,进行游戏场景切换。

6. 如果切换成功之后,会根据场景名称进行加载附加场景集。
在 CustomNetworkManager_ZH代码中,根场景 → Additive 集合映射。

7. 执行顺序会在 Console 窗口中进行显示。

链接: Unity Mirror 多人同步 基础教程 完整示例工程
暂时先这样吧,如果实在看不明白就留言,看到我会回复的。希望这个教程对您有帮助!
路漫漫其修远,与君共勉。
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_43925843/article/details/151752879



