Unity Mirror 多人同步 基础教程
- Mirror
- NetworkManager(网络管理器)
- Configuration:配置
- Auto-Start Options:自动启动
- Scene Management:场景管理
- Network Info:网络信息
- Authentication:身份验证
- Player Object:玩家对象
- Security:安全
- Snapshot Interpolation:快照插值
- Connection Quality:连接质量
- Interpolation:UI 插值调试 UI
- KcpTransport(KCP 通信协议)
- Transport Configuration:通信配置
- Advanced:高级设置
- Allowed Max Message Sizes:允许的最大消息大小
- Debug:调试
- NetworkManagerHUD(网络管理器 HUD)
- Offset X / Offset Y:画面偏移
- 主要方法
- NetworkStartPosition(玩家出生点位置)
- NetworkIdentity(网络“身份证”)
- Server Only:服务器端
- Visibility:可见性
- NetworkTransformReliable(网络同步器 稳定版)
- Target:同步物体
- Selective Sync:选择性同步
- Bandwidth Savings:带宽优化
- Interpolation:插值平滑
- Coordinate Space:坐标空间
- Timeline Offset:时间偏移修正
- Debug:调试
- Additional Settings:其他设置
- Rotation:旋转灵敏度
- Precision:位置同步
- Sync Settings:同步设置
- Unity Mirror 示例
- Mirror & ParrelSync 插件以及 ScriptTemplates代码模板导入
- Mirror 插件 导入
- ParrelSync 插件导入
- ScriptTemplates 代码模板导入
- ScriptTemplates 代码 模板 作用
- 你能用它们干什么?
- Mirro 消息发送接收与同步
- Mirror 的“消息发送/接收/同步
- 自定义消息(最灵活、协议自控)
- 远程调用:Command / Rpc(经典、够用)
- 高层自动同步:SyncVar / SyncList…
- 如何选择
- Mirro UGUI 网络控制
- 代码里的方法映射
- UGUI 控制代码
- 脚本搭载
- 常见坑与排查
- Mirro 场景切换功能
- 场景编排器
- 自定义 NetworkManager
- 场景管理
- 脚本搭载以及运行
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下载地址
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→Serverprivate void CmdMove(Vector3 _Dir){if (!isServer) return;// 服务器权威地修改位置(示例:简单位移)transform.position += _Dir * _Speed * Time.fixedDeltaTime;// 广播给所有客户端做一些即时效果RpcOnMoveFx(transform.position);}// ───── 服务器广播 → 客户端执行 ─────/// <summary>移动效果(仅客户端执行)</summary>[ClientRpc] // Server→All Clientsprivate 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;//创建Hostpublic Button _StartHostButton;//创建 clientpublic Button _StartClientButton;//IP地址输入框public InputField _AddressInputField;//端口输入框public InputField _PortInputField;//创建服务器public Button _StartServerButton;//停止Hostpublic Button _StopHostButton;// ───── 单例防重 + 常驻 ─────private static NetworkManagerHUD_ZH _Instance;private void Awake(){// 防止切换场景后出现第二个 HUDif (_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:8329if (Transport.active is PortTransport portTransport){// 如果有人试图输入非数字字符,请使用TryParseif (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;// 开启协程,等根场景完全切换完成后再装配 AdditiveStartCoroutine(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 还没生成,就在服务器端实例化并 Spawnif (_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 多人同步 基础教程 完整示例工程
暂时先这样吧,如果实在看不明白就留言,看到我会回复的。希望这个教程对您有帮助!
路漫漫其修远,与君共勉。