Unity Mirror多人游戏开发:免费方案全流程实战指南

📅 2026/7/13 8:40:32
Unity Mirror多人游戏开发:免费方案全流程实战指南
1. 项目概述为什么我们需要一个免费的 Mirror 多人游戏教程如果你正在 Unity 里折腾多人游戏大概率听说过 Mirror。它作为 Unity 早期官方网络解决方案 UNET 的社区继承者以其相对友好、免费开源的特点成为了很多独立开发者和中小团队进入多人游戏领域的首选。但说实话Mirror 的官方文档和社区教程对于新手来说常常是“一看就会一用就废”。概念讲得云里雾里示例项目要么过于简单要么依赖一堆付费服务想从零开始搭一个能稳定跑起来的多人游戏 Demo中间隔着无数个坑。这就是我动手整理这个“亲测免费”教程项目文档的初衷。它不是一个简单的功能列表翻译而是我基于多个实际项目踩坑、调试、优化后梳理出的一套从环境配置到核心功能实现再到上线前测试的完整实操指南。所有用到的工具和服务都确保是免费或开源方案让你在不花一分钱的情况下就能搭建起一个可运行、可扩展的多人游戏原型。无论是想做一款和朋友联机的小游戏还是为你的作品探索多人玩法这份文档都能给你一个扎实的起点。2. 核心思路与免费方案选型在开始敲代码之前理清架构思路和选择正确的免费工具链至关重要。多人游戏的核心是网络通信而 Mirror 作为高层网络库需要底层传输和连接服务的支持。2.1 网络架构客户端-服务器C/S与权威服务器Mirror 主要支持客户端-服务器模型。在这个模型里有一个或多个客户端玩家连接到一个中央服务器。服务器是游戏状态的“权威”它接收所有客户端的输入进行逻辑计算和验证然后将结果同步给所有客户端。这能有效防止作弊是绝大多数竞技和合作游戏的选择。注意Mirror 也支持点对点P2P模式但对于新手和大多数需要稳定状态的游戏C/S 模型是更稳妥、更主流的选择。本教程将完全基于 C/S 模型展开。2.2 免费工具链全景图要实现一个免费的 Mirror 多人游戏你需要组合以下几样东西Unity 与 Mirror基础环境。Unity 个人版免费Mirror 在 Asset Store 或 GitHub 上免费。传输层Transport负责底层数据包的收发。Mirror 自带 KCP 和 Telepathy但我们将使用更现代的Unity Transport Package (UTP)。它是 Unity 官方推出的高性能网络层免费且与 Unity 服务集成更好。网络地址转换NAT穿透与连接中继这是家庭网络环境下联机的最大障碍。玩家通常处于路由器后没有公网 IP无法直接相互连接。我们需要一个服务来帮助建立连接。免费选择NGONetcode for GameObjectsLobby 与 Relay实验性Unity 官方提供的 Netcode for GameObjects 套件中包含 Lobby 和 Relay 服务目前有免费的额度。虽然它主要适配 NGO但其 Relay 服务基于标准的 UTP经过适当配置可以被 Mirror 使用。这是我们实现免费联机的关键。替代选择自建信令服务器Signaling Server如果你懂一些后端可以用 Node.js、PythonFastAPI等免费工具在阿里云、腾讯云的免费额度服务器或 Railway、Render 等免费托管平台上搭建一个简单的信令服务器来实现连接交换。这更灵活但需要额外的开发工作。匹配与大厅可选让玩家找到彼此并组成房间。可以使用 Unity 的Lobby 服务免费额度或者用上述自建信令服务器实现简单的房间列表。本教程的核心路线我们将采用Unity UTP Unity Relay实验性接入的方案。这条路线的优势是完全在 Unity 生态内无需额外服务器知识利用官方免费额度快速验证玩法。我会详细说明如何让 Mirror 与这些服务“握手”。2.3 项目结构设计一个清晰的项目结构能让后续开发事半功倍。我建议的文件夹结构如下Assets/ ├── 3rdParty/ # 存放 Mirror、UTP 等插件 ├── Scripts/ │ ├── Network/ # 网络相关核心脚本 │ │ ├── Managers/ # NetworkManager 扩展 │ │ ├── Messages/ # 自定义网络消息 │ │ └── Components/# 网络身份、同步脚本 │ ├── Gameplay/ # 游戏逻辑脚本 │ └── UI/ # 用户界面脚本 ├── Scenes/ │ ├── 00_Bootstrap.unity # 初始化场景 │ ├── 01_Menu.unity # 主菜单场景 │ └── 02_Game.unity # 游戏主场景 ├── Prefabs/ # 预制体 └── Resources/ # 配置等资源3. 环境准备与核心包安装工欲善其事必先利其器。这一步我们确保所有必要的软件和包都正确安装。3.1 Unity 编辑器版本选择推荐使用Unity 2022.3 LTS或更新版本。LTS长期支持版本更稳定社区资源也更丰富。确保在安装时勾选Windows/Mac IL2CPP Build Support和Linux Build Support如果你需要发布到Linux服务器。3.2 安装必要的 Unity 包我们将通过 Unity 的 Package Manager 安装所有依赖。打开 Package Manager (Window Package Manager)将来源切换到 “Unity Registry”。安装 Unity Transport (UTP)点击 “” 号选择 “Add package by name...”。输入com.unity.transport并安装。建议选择 1.x.x 稳定版本。安装 Netcode for GameObjects (NGO)同样 “Add package by name...”。输入com.unity.netcode.gameobjects并安装。我们主要用它附带的 Relay 和 Lobby 服务。安装必要的依赖包com.unity.services.coreUnity 核心服务com.unity.services.authentication身份验证服务com.unity.services.relayRelay 服务com.unity.services.lobbyLobby 服务 通常安装 NGO 后这些依赖会自动引入。如果没有请手动按上述名称添加。3.3 导入 Mirror 网络库Mirror 可以通过多种方式导入最稳定的是从 Asset Store 下载。打开 Unity Asset Store (Window Asset Store)。搜索 “Mirror Networking”。点击 “Add to My Assets”然后回到 Unity在 Package Manager 中切换到 “My Assets” 标签页找到 Mirror 并点击 “Download” 然后 “Import”。导入时务必注意Mirror 自带的默认传输层如 KCP Transport会与我们即将使用的 UTP 冲突。导入后我们先不处理后续会进行配置替换。3.4 配置 Unity 项目与服务这是让免费 Relay 服务工作的关键一步。启用 Unity 服务打开 Unity Services 窗口 (Window General Services)。如果你没有项目需要创建一个。使用 Unity 账号登录后可以关联一个项目可以是新创建的。在 Services 窗口中找到Relay和Lobby服务并点击 “Enable”。这会将你的项目与 Unity 的后台服务关联起来。获取项目 ID 和认证配置服务启用后Unity 会自动生成配置。你可以在Edit Project Settings Services下查看Organization ID和Project ID。这些信息会在代码中用于初始化服务。4. 构建基础网络管理器桥接 Mirror 与 UTP/Relay现在进入核心编码环节。我们需要创建一个自定义的 NetworkManager让它既能驱动 Mirror 的网络逻辑又能使用 UTP 进行传输并调用 Unity Relay 服务。4.1 创建自定义 UTP Transport 适配器Mirror 需要通过一个实现了Transport抽象类的组件来发送和接收数据。我们需要创建一个桥接器让 Mirror 能使用 UTP。在Assets/Scripts/Network/下创建脚本CustomUtpTransport.cs。让它继承自Mirror.Transport。核心任务是封装 UTP 的NetworkDriver和NetworkPipeline。你需要处理连接建立、数据发送/接收、事件轮询等。using UnityEngine; using Unity.Networking.Transport; using Unity.Networking.Transport.Utilities; using Mirror; using System; using System.Collections.Generic; public class CustomUtpTransport : Transport { private NetworkDriver m_Driver; private NetworkPipeline m_Pipeline; private Dictionaryint, NetworkConnection m_Connections new Dictionaryint, NetworkConnection(); // 实现 Transport 抽象类的所有必要方法 // - ClientConnect(string address) // - ServerStart() // - ClientDisconnect() // - ServerStop() // - ServerSend(int connectionId, ArraySegmentbyte segment, int channelId) // - ClientSend(ArraySegmentbyte segment, int channelId) // - ServerGetClientAddress(int connectionId) // - Shutdown() // - 以及 Client/Server Active 等属性。 // 在 LateUpdate 或一个独立的 MonoBehaviour 更新循环中调用 m_Driver.ScheduleUpdate().Complete() 来轮询网络事件。 private void Update() { if (!m_Driver.IsCreated) return; m_Driver.ScheduleUpdate().Complete(); // ... 处理连接、数据事件并调用 Mirror 的对应回调如 OnClientConnected.Invoke() } // 关键处理 Relay 分配的数据。 public void SetRelayServerData(string ipv4Address, ushort port, byte[] allocationBytes, byte[] key, byte[] connectionData) { // 使用 UTP 的 RelayUtility 解析 allocationBytes并配置 m_Driver 连接到 Relay 服务器。 var relayServerData RelayUtility.ParseAllocationData(allocationBytes); // ... 配置网络端点 (NetworkEndpoint) } }实操心得直接实现一个完整的、稳定的 UTP Transport 工作量不小。一个更快捷的路径是参考或直接使用 Unity 官方提供的Unity.Netcode.Adapter.UTP包如果可用或者寻找社区已经写好的 Mirror-UTP 适配器。但为了理解原理自己尝试实现核心部分是非常有价值的。4.2 创建集成 Relay 的自定义 NetworkManager这是整个网络系统的指挥中心。在Assets/Scripts/Network/Managers/下创建脚本CustomNetworkManager.cs。让它继承自Mirror.NetworkManager。这样你就拥有了 Mirror 所有的基础管理功能。添加 Unity 服务初始化和 Relay 分配/加入的逻辑。using UnityEngine; using Mirror; using Unity.Services.Core; using Unity.Services.Authentication; using Unity.Services.Relay; using Unity.Services.Relay.Models; using System.Threading.Tasks; public class CustomNetworkManager : NetworkManager { [Header(Relay Settings)] public bool useRelay true; private string m_JoinCode; public override async void Start() { base.Start(); if (useRelay) { await InitializeUnityServices(); } } private async Task InitializeUnityServices() { try { await UnityServices.InitializeAsync(); await AuthenticationService.Instance.SignInAnonymouslyAsync(); Debug.Log($Signed in. Player ID: {AuthenticationService.Instance.PlayerId}); } catch (System.Exception e) { Debug.LogError($Unity Services init failed: {e}); } } // 主机分配一个 Relay 服务器并获取加入码 public async void StartHostWithRelay(int maxPlayers 4) { try { Allocation allocation await RelayService.Instance.CreateAllocationAsync(maxPlayers); m_JoinCode await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId); Debug.Log($Relay Join Code: {m_JoinCode}); // 将 Relay 服务器数据allocation配置给我们的 CustomUtpTransport CustomUtpTransport transport GetComponentCustomUtpTransport(); if (transport ! null) { transport.SetRelayServerData(allocation); } // 启动 Mirror 主机 StartHost(); } catch (RelayServiceException e) { Debug.LogError($Relay allocation failed: {e.Message}); } } // 客户端使用加入码连接到 Relay 服务器 public async void StartClientWithRelay(string joinCode) { try { JoinAllocation joinAllocation await RelayService.Instance.JoinAllocationAsync(joinCode); // 将连接数据配置给 CustomUtpTransport CustomUtpTransport transport GetComponentCustomUtpTransport(); if (transport ! null) { transport.SetRelayServerData(joinAllocation); } // 启动 Mirror 客户端地址和端口由 Relay 数据决定 networkAddress relay; // 这是一个占位符实际连接由 transport 内部处理 StartClient(); } catch (RelayServiceException e) { Debug.LogError($Relay join failed: {e.Message}); } } }4.3 配置场景中的 GameObject在Scenes/00_Bootstrap.unity中创建一个空的 GameObject命名为 “NetworkManager”。将CustomNetworkManager脚本挂载上去。将CustomUtpTransport脚本或你采用的 UTP Transport 组件也挂载上去。在CustomNetworkManager组件的Transport字段中拖入CustomUtpTransport组件替换掉默认的 KCP Transport。配置好玩家预制体Player Prefab、房间玩家预制体Room Player Prefab等 Mirror 标准设置。5. 实现核心多人游戏功能有了网络管理器我们就可以开始实现具体的游戏功能了。这里涵盖几个最核心的模块。5.1 玩家生成与同步玩家预制体创建一个代表玩家的预制体如Prefabs/NetworkPlayer.prefab。上面必须挂载NetworkIdentity组件。这是 Mirror 识别网络对象的基础。玩家生成在CustomNetworkManager中将Player Prefab字段指向这个预制体。当客户端连接成功后Mirror 会自动在服务器上生成该玩家并同步到所有客户端。同步变换在玩家预制体上添加NetworkTransform组件。它可以自动同步物体的位置、旋转和缩放。重要参数Sync Position/Rotation/Scale: 勾选需要同步的项。Sync Interval: 同步频率秒。0.05秒20次/秒是实时动作游戏的常用值回合制可以设高。Interpolation客户端的插值平滑减少抖动。同步自定义变量使用[SyncVar]属性。当标记了[SyncVar]的变量在服务器端发生变化时Mirror 会自动将新值同步给所有客户端。public class PlayerHealth : NetworkBehaviour { [SyncVar(hook nameof(OnHealthChanged))] public int currentHealth 100; void OnHealthChanged(int oldValue, int newValue) { // 当 health 在客户端同步后会调用此方法。在这里更新血条UI。 Debug.Log($Health changed from {oldValue} to {newValue}); } [Server] // 这个属性表明此方法只能在服务器端调用 public void TakeDamage(int damage) { if (!isServer) return; // 双重保险 currentHealth - damage; if (currentHealth 0) Die(); } }5.2 远程过程调用RPCRPC 用于在客户端和服务器之间调用特定的方法。[Command]: 在客户端调用在服务器上执行。用于发送玩家输入如攻击指令。[Command] void CmdFire() { // 在服务器上生成子弹确保逻辑权威 GameObject bullet Instantiate(bulletPrefab, firePosition.position, firePosition.rotation); NetworkServer.Spawn(bullet, connectionToClient); // 关联生成权限 bullet.GetComponentRigidbody().velocity firePosition.forward * bulletSpeed; }[ClientRpc]: 在服务器调用在所有客户端或排除发送者执行。用于广播事件如播放全屏特效、更新游戏状态。[ClientRpc] void RpcPlayerDied(string playerName) { // 在所有客户端屏幕上显示死亡信息 Debug.Log(${playerName} has been eliminated!); }[TargetRpc]: 在服务器调用在指定的单个客户端执行。用于发送私密信息如分配角色、发送个人任务。注意事项RPC 方法名有约定俗成的前缀Cmd, Rpc, Target以方便识别。参数可以是基本类型、Unity 内置类型Vector3, Quaternion或定义了序列化的自定义结构体。避免传递大量数据。5.3 大厅与房间管理利用 Unity 的 Lobby 服务免费额度可以方便地创建和管理游戏房间。创建 Lobby 管理器新建一个LobbyManager脚本用于处理与 Lobby API 的交互。using Unity.Services.Lobby; using Unity.Services.Lobby.Models; public class LobbyManager : MonoBehaviour { private Lobby m_CurrentLobby; private float m_LobbyHeartbeatTimer; public async Task CreateLobby(string lobbyName, int maxPlayers) { try { CreateLobbyOptions options new CreateLobbyOptions { IsPrivate false, Data new Dictionarystring, DataObject { { GameMode, new DataObject(DataObject.VisibilityOptions.Public, Deathmatch) } } }; m_CurrentLobby await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, options); Debug.Log($Lobby created: {m_CurrentLobby.Name}, Code: {m_CurrentLobby.LobbyCode}); // 创建成功后调用 NetworkManager 的 StartHostWithRelay FindObjectOfTypeCustomNetworkManager().StartHostWithRelay(maxPlayers); } catch (LobbyServiceException e) { Debug.LogError(e); } } public async Task JoinLobbyByCode(string lobbyCode) { try { m_CurrentLobby await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode); // 加入大厅后获取 Relay 加入码假设主机已将其存为大厅数据 string relayJoinCode m_CurrentLobby.Data[RelayJoinCode].Value; FindObjectOfTypeCustomNetworkManager().StartClientWithRelay(relayJoinCode); } catch (LobbyServiceException e) { Debug.LogError(e); } } void Update() { // 定期发送心跳保持大厅活跃 if (m_CurrentLobby ! null m_CurrentLobby.HostId AuthenticationService.Instance.PlayerId) { m_LobbyHeartbeatTimer - Time.deltaTime; if (m_LobbyHeartbeatTimer 0) { m_LobbyHeartbeatTimer 15f; // Lobby 默认15秒超时 LobbyService.Instance.SendHeartbeatPingAsync(m_CurrentLobby.Id); } } } }UI 集成创建大厅 UI包含创建房间、输入房间号加入、刷新房间列表等功能。房间列表可以通过LobbyService.Instance.QueryLobbiesAsync()获取。5.4 游戏状态同步与权威计算这是防止作弊和保证游戏一致性的核心。黄金法则所有关键的游戏逻辑判断必须在服务器端进行。伤害计算客户端发送“我攻击了”的 Command服务器收到后计算伤害修改目标的[SyncVar]血量然后通过 Rpc 播放受击特效。物品拾取客户端发送拾取 Command服务器检查物品是否存在、玩家是否在范围内然后销毁物品网络对象NetworkServer.Destroy并将物品添加到玩家库存同步变量。物理交互对于需要精确同步的物理对象如足球为其添加NetworkTransform并设置为服务器端控制。客户端只发送输入服务器进行物理模拟并同步结果。6. 测试、调试与部署6.1 在编辑器中模拟多人测试Unity 编辑器本身支持以主机模式运行但测试客户端连接需要多个实例。ParrelSync这是最推荐的免费工具。它是一个 Unity 编辑器扩展可以一键创建并运行项目的克隆副本完美模拟多个客户端。在 Asset Store 搜索 “ParrelSync” 并导入。安装后在工具栏会出现 “ParrelSync” 菜单通过它创建克隆并打开你就可以在一个编辑器里运行主机另一个里运行客户端了。构建独立客户端通过File Build Settings构建出独立的.exe文件。运行多个副本进行测试。这更接近最终发布状态。6.2 关键调试技巧Mirror 的 Network Monitor在Window Analysis Network Monitor中打开。它可以实时显示网络消息、RPC 调用、同步变量变化等是排查网络问题不可或缺的工具。日志区分在代码中使用Debug.Log时用isServer和isClient来标记日志来源。Debug.Log($[{(isServer?Server:Client)}] Player connected.);断点与单步调试在 Visual Studio 或 Rider 中可以附加到多个 Unity 编辑器进程进行调试。在 Command 和 Rpc 方法中设置断点观察执行流程。6.3 构建独立服务器Headless Build对于稍正式的项目你可能会需要一个独立的、不带图形界面的专用服务器。在 Build Settings 中勾选“Server Build”选项旧版本 Unity 可能需要在 “Target Platform” 处选择 “Linux/Windows Server”。构建出的程序将不包含任何图形渲染组件。你可以在命令行中运行它通常需要指定端口等参数。./MyGameServer.exe -port 7777 -batchmode -nographics在CustomNetworkManager的Start()方法中可以通过Application.isBatchMode或SystemInfo.graphicsDeviceType GraphicsDeviceType.Null来判断是否运行在无头模式并自动以服务器模式启动。6.4 部署到免费云主机可选如果你想让朋友通过互联网连接需要一个有公网 IP 的服务器。一些云服务商提供免费额度Oracle Cloud提供永久免费的 ARM 或 AMD 虚拟机性能足够运行一个小型游戏服务器。Google Cloud Run / AWS App Runner对于无状态或可以快速启动的服务器可以考虑容器化的 Serverless 服务有免费使用额度。游戏服务器托管对于更专业的需求可以考虑使用Unity Game Server Hosting (Multiplay)或Amazon GameLift它们有免费 tier 或按需计费但配置更复杂。部署时将你的独立服务器构建文件上传到云主机运行即可。确保在云主机的安全组/防火墙中开放你游戏使用的 UDP/TCP 端口。7. 常见问题与避坑指南实录在实际开发中你会遇到各种各样的问题。这里记录了一些高频坑点和解决方案。7.1 连接与断开问题问题现象可能原因排查步骤与解决方案客户端无法连接到主机/Relay1. 防火墙/路由器未开放端口。2. Relay 服务未启用或初始化失败。3. UTP Transport 配置错误。4. 主机不是公网IP且未使用Relay。1. 检查 Unity Services 窗口确认 Relay 服务已启用绿色勾。检查控制台有无认证错误。2. 在CustomNetworkManager的InitializeUnityServices方法中添加更详细的日志和try-catch。3. 暂时关闭防火墙和路由器的 UPnP 进行测试。4.务必使用 ParrelSync 或构建独立客户端在本地网络测试排除网络环境问题。连接后立即断开1. 玩家预制体Player Prefab未正确分配或缺少NetworkIdentity。2. 场景中存在于多个NetworkManager。3. 同步频率过高或网络消息过大。1. 确认NetworkManager中的Player Prefab字段已赋值且预制体根物体有NetworkIdentity。2. 确保整个游戏运行时只有一个激活的NetworkManager实例通常用 DontDestroyOnLoad。3. 检查NetworkTransform的Sync Interval适当调低。使用 Network Monitor 观察消息量。只有主机能看到其他玩家移动NetworkTransform的权限问题。非权威的客户端上NetworkTransform默认会覆盖本地位置。确保玩家的移动逻辑在服务器端执行通过 Command或者确保NetworkTransform的Client Authority在适当场景下被正确处理。对于完全由服务器控制的物体不要勾选Client Authority。7.2 同步与状态不同步问题[SyncVar]不更新确保变量只在服务器端修改。修改后Mirror 会自动同步。检查 hook 方法是否被正确调用。注意[SyncVar]对属性的支持有限最好用在公共字段上。物体生成位置不对在服务器端生成网络物体Instantiate后必须调用NetworkServer.Spawn()并传入生成的 GameObject。生成时的位置和旋转以服务器端Instantiate时的参数为准。RPC 调用不执行检查方法名是否有Cmd/Rpc/Target前缀。检查方法是否在继承了NetworkBehaviour的类中。检查调用者是否具有正确的权限[Command]必须由拥有NetworkIdentity的客户端对象调用。使用 Network Monitor 查看 RPC 是否被发送和接收。7.3 性能优化要点序列化优化自定义[SyncVar]结构体或 RPC 参数时实现IMessageBase接口进行手动序列化可以减少数据大小。网络可见性Network Visibility使用NetworkProximityChecker组件或自定义的NetworkBehaviour重写OnCheckObserver方法只将物体同步给一定范围内的玩家大幅减少不必要的网络流量。压缩UTP 支持数据压缩。可以在 Transport 配置中启用。减少同步频率不是所有数据都需要每帧同步。对于变化缓慢的状态如玩家分数可以每秒同步一次甚至只在变化时同步。7.4 Relay 服务限额与成本Unity Relay 和 Lobby 的免费额度通常是每月一定量的活跃玩家分钟数对于原型开发和中小型游戏初期完全足够。但务必在 Unity Dashboard 中监控使用量。如果预计会超量需要提前规划优化游戏减少每个玩家的连接时间。考虑在玩家达到一定规模后迁移到自建信令服务器 专用服务器或社区 Relay 方案如 Steamworks P2P。了解 Unity 的付费阶梯。整个流程走下来你会发现用 Mirror 配合免费的 Unity 服务搭建多人游戏虽然初期配置有些繁琐但一旦跑通后续的功能开发会顺畅很多。这套方案最大的价值在于它让你在几乎零成本的情况下就能验证多人游戏的核心玩法和网络架构把精力集中在游戏性本身。记住多人游戏开发早测试、多测试、在不同网络环境下测试是唯一真理。先从最小的可玩原型开始逐步增加功能祝你的多人游戏项目顺利起步。