Unity Mirror 多人同步 基础教程

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的一些特性包括:

  1. 消息处理(Message handlers)
  2. 通用的高性能的序列化(General purpose high performance
  3. serialization)
  4. 分布式对象管理
  5. 状态同步
  6. 网络类,如: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):60100 Hz。		RPG/MMO30 Hz 左右。		慢节奏(策略/回合):110 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 端口号。客户端需要连接这个端口,常用如 777725565。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.011cm。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 编辑器窗口并镜像原始项目的更改来测试多人游戏,而无需构建项目。
👉 注意:克隆的项目不可编辑否则会报错。

特征:

  1. 测试多人游戏,无需构建项目
  2. 用于管理所有项目克隆的 GUI 工具
  3. 受保护的资产不被其他克隆实例修改
  4. 方便的 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 ManagerNetworkManager核心入口,管理服务器/客户端的启动、场景切换、玩家生成等。
Network Manager With ActionsNetworkManager同上,但额外提供 Action 事件回调,方便用委托而不是继承来订阅。
Network AuthenticatorNetworkAuthenticator自定义认证(账号/密码/令牌验证),控制客户端是否能加入。
Network BehaviourNetworkBehaviourMirror 网络对象的基类,带有 OnStartServerOnStartClient 等生命周期函数。
Network Behaviour With ActionsNetworkBehaviourNetworkBehaviour 基础上加了事件委托版本,逻辑更解耦。
Custom Interest ManagementInterestManagement控制对象的可见性(只同步范围内的对象 / 分组广播)。
Network Room ManagerNetworkRoomManager内置房间逻辑(大厅/准备/开始游戏/切换场景)。
Network Room PlayerNetworkRoomPlayer房间里玩家的状态(如准备/未准备、玩家编号),与 Room Manager 配套。
Network DiscoveryNetworkDiscovery局域网房间发现(客户端广播 → 服务器回应)。
Network TransformNetworkTransformReliable(或 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 多人同步 基础教程 完整示例工程

暂时先这样吧,如果实在看不明白就留言,看到我会回复的。希望这个教程对您有帮助!
路漫漫其修远,与君共勉。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/98237.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/98237.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于红尾鹰优化的LSTM深度学习网络模型(RTH-LSTM)的一维时间序列预测算法matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.部分程序 4.算法理论概述 5.完整程序 1.程序功能描述 红尾鹰优化的LSTM&#xff08;RTH-LSTM&#xff09;算法&#xff0c;是将红尾鹰优化算法&#xff08;Red-Tailed Hawk Optimization, RTHO&#xff09;与长短期…

深度学习“调参”黑话手册:学习率、Batch Size、Epoch都是啥?

点击 “AladdinEdu&#xff0c;同学们用得起的【H卡】算力平台”&#xff0c;注册即送-H卡级别算力&#xff0c;80G大显存&#xff0c;按量计费&#xff0c;灵活弹性&#xff0c;顶级配置&#xff0c;学生更享专属优惠。 引言&#xff1a;从"炼丹"到科学&#xff0c;…

【网络实验】-MUX-VLAN

实验拓扑实验要求&#xff1a; 在企业网络中&#xff0c;企业员工和企业客户可以访问企业的服务器&#xff0c;对于企业来说&#xff0c;希望员工之间可以互相交流&#xff0c;但是企业用户之间相互隔离&#xff0c;不能够访问。为了实现所有用户都可以访问企业服务器&#xff…

Java泛型:类型安全的艺术与实践指南

Java泛型&#xff1a;类型安全的艺术与实践指南 前言&#xff1a;一个常见的编译错误 最近在开发中遇到了这样一个编译错误&#xff1a; Required type: Callable<Object> Provided: SalesPitchTask这个看似简单的错误背后&#xff0c;隐藏着Java泛型设计的深层哲学。今天…

UMI企业智脑 2.1.0:智能营销新引擎,图文矩阵引领内容创作新潮流

在数字营销日益激烈的今天&#xff0c;企业如何在信息洪流中脱颖而出&#xff1f;UMI企业智脑 2.1.0 的发布为企业提供了全新的解决方案。这款智能营销工具结合了先进的AI技术与数据驱动策略&#xff0c;帮助企业优化营销流程、提升效率&#xff0c;并通过图文矩阵实现内容创作…

Lustre Ceph GlusterFS NAS 需要挂载在k8s容器上,数据量少,选择哪一个存储较好

在 K8s 容器环境中&#xff0c;数据量 不大的 规模下&#xff0c;Lustre、Ceph、GlusterFS 和 NAS 的选择需结合性能需求、运维成本、扩展性和K8s 适配性综合判断。以下是针对性分析及推荐&#xff1a;一、核心对比与适用场景二、关键决策因素1. 性能需求高并发 / 高吞吐&#…

深入解析 Apache Doris 写入原理:一条数据的“落地之旅”

在日常的数据分析场景中&#xff0c;我们经常会向 Apache Doris 写入大量数据&#xff0c;无论是实时导入、批量导入&#xff0c;还是通过流式写入。但你是否想过&#xff1a;一条数据从客户端发出&#xff0c;到最终稳定落盘&#xff0c;中间到底经历了哪些步骤&#xff1f; …

基于MATLAB的视频动态目标跟踪检测实现方案

一、系统架构设计 视频动态目标跟踪系统包含以下核心模块&#xff1a; 视频输入模块&#xff1a;支持摄像头实时采集或视频文件读取预处理模块&#xff1a;灰度转换、降噪、光照补偿目标检测模块&#xff1a;背景建模、运动区域提取跟踪算法模块&#xff1a;卡尔曼滤波、粒子滤…

【Python】Python文件操作

Python文件操作 文章目录Python文件操作[toc]1.文件的编码2.文件打开、读取&#xff08;r模式&#xff09;、关闭3.文件的写入&#xff08;w模式&#xff09;4.文件的追加写入&#xff08;a模式&#xff09;5.综合案例1.文件的编码 意义&#xff1a;计算机只能识别0和1&#x…

CES Asia的“五年计划”:打造与北美展比肩的科技影响力

在全球科技产业版图中&#xff0c;展会一直是前沿技术展示、行业趋势探讨以及商业合作达成的关键平台。CES Asia&#xff08;亚洲消费电子技术展&#xff09;作为亚洲科技领域的重要展会&#xff0c;近日明确提出其“五年计划”&#xff0c;目标是打造与北美展会比肩的科技影响…

【计算机网络 | 第16篇】DNS域名工作原理

文章目录3.5 域名系统工作原理主机的标识方式&#xff1a;域名 vs IP 地址标识转换机制&#xff1a;DNS系统因特网的域名系统&#xff1a;层次域名空间&#x1f426;‍&#x1f525;顶级域名分类低级域名与管理域名与IP的区别因特网的域名系统&#xff1a;域名服务器&#x1f9…

YASKAWA安川机器人铝材焊接节气之道

在铝材焊接领域&#xff0c;保护气体的合理使用对焊接质量与成本控制至关重要。安川焊接机器人凭借高精度与稳定性成为行业常用设备&#xff0c;而WGFACS节气装置的应用&#xff0c;则为其在铝材焊接过程中实现高效节气提供了创新路径。掌握二者结合的节气之道&#xff0c;对提…

GooseDB,一款实现服务器客户端模式的DuckDB

在网上看到韩国公司开发的一款GooseDB&#xff0c; 官方网站对它的介绍是DuckDB™ 的功能扩展分支&#xff0c;具有服务器/客户端、多会话和并发写入支持&#xff0c;使用 PostgreSQL 有线协议&#xff08;DuckDB™是 DuckDB 基金会的商标&#xff09; 使用也很简单&#xff…

lesson62:JavaScript对象进化:ES2025新特性深度解析与实战指南

目录 一、迭代器辅助方法&#xff1a;对象数据处理的优雅革命 1.1 核心方法与语法 1.2 对象属性处理实战 1.3 性能与兼容性考量 二、JSON模块原生支持&#xff1a;对象加载的范式转变 2.1 静态与动态导入语法 2.2 与传统方案的对比优势 2.3 典型应用场景 三、Set集合增…

设计模式学习笔记(一)

设计模式学习笔记&#xff08;一&#xff09; 一般说设计模式都是指面向对象的设计模式&#xff0c;因为面向对象语言可以借助封装、继承、多态等特性更好的达到复用性、可拓展性、可维护性。 面向对象一般指以类、对象为组织代码的基本单元&#xff0c;并将封装、继承、多态、…

【CSS】一个自适应大小的父元素,如何让子元素的宽高比一直是2:1

父元素是自适应大小的容器&#xff08;比如 width:100%&#xff09;&#xff0c;我们希望子元素 始终保持 2:1 宽高比&#xff08;比如宽 200px → 高 100px&#xff0c;宽 300px → 高 150px&#xff09;。 有几种常见解法&#xff1a;✅ 方法一&#xff1a;CSS aspect-ratio&…

如何搭建redis集群(docker方式非哨兵)

1、redis的配置文件这里要注意&#xff0c;主从的ip不需要我们去设置&#xff0c;只需要设置主从的密码就可以&#xff0c;然后就是protect-mode&#xff0c;我设置的是no&#xff0c;一定注意不能设置主从。客户端要访问&#xff0c;一定要加# 每个节点的 redis.conf 中 clust…

如何学习VBA_3.3.9:利用“搭积木”思想,快速有效地完成你的代码

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的劳动效率&#xff0c;而且可以提高数据处理的准确度。我推出的VBA系列教程共九套和一部VBA汉英手册&#xff0c;现在已经全部完成&#xff0c;希望大家利用、学习。如果您…

JSP程序设计之输入/输出对象 — response对象

response对象1.概述2.实例&#xff1a;response对象方法运用&#xff08;1&#xff09;实例一&#xff1a;页面自动刷新&#xff08;2&#xff09;实例二&#xff1a;实现页面重定向&#xff0c;具体的代码&#xff08;3&#xff09;综合实例&#xff1a;实现登录并记录用户名1…

Redis 事件驱动框架(ae.c_ae.h)深度解析

Redis 事件驱动框架&#xff08;ae.c/ae.h&#xff09;深度解析 之前咱们用 “超市收银员” 的例子&#xff0c;简单看懂了 ae 模块是 Redis 的 “多任务神器”。现在咱们再往深走一层&#xff0c;不用复杂代码&#xff0c;只拆它的 “核心运作逻辑”—— 搞懂它怎么做到 “一个…