1. 项目概述:为什么从Mirror的Basic示例开始?
如果你正在Unity里折腾多人联机,并且听说了Mirror这个网络框架,那么恭喜你,你大概率找对了方向。Mirror作为UNET的现代化、社区驱动的继任者,以其简洁的API和强大的功能,成为了Unity开发者构建多人游戏的首选之一。但很多朋友在刚接触时,面对官方文档和一堆示例项目,常常感到无从下手,不知道从哪里开始才能真正“跑起来”一个属于自己的网络游戏。
这正是“Basic示例”存在的意义。它不是一个功能繁杂的演示,而是一个最精简、最核心的“骨架”。这个示例剥离了所有花哨的图形、复杂的游戏逻辑和高级的网络特性,只保留了建立一个可工作的客户端-服务器(C/S)架构所必需的最少代码和组件。通过剖析这个示例,你能清晰地看到Mirror框架下,一个网络游戏是如何启动、如何连接、如何同步一个最简单的游戏状态(比如玩家移动)的。这就像学开车,你得先知道方向盘、油门、刹车在哪,而不是一上来就去研究漂移。掌握了这个“骨架”,你才能有底气去添加“肌肉”(游戏逻辑)和“皮肤”(美术资源),最终构建出健壮的多人游戏。
对于Unity初学者,或者从单机转向联网的开发者,直接去啃Mirror的完整API或者研究那些带Relay、UTP传输的复杂示例,很容易被细节淹没。而Basic示例,就是你踏入Mirror世界最坚实、最不会踩坑的第一步。接下来,我会带你把这个示例从里到外彻底拆解一遍。
2. 核心组件与工作流拆解
一个Mirror网络应用,无论简单还是复杂,其核心都围绕着几个关键组件和一套固定的通信流程。Basic示例完美地展示了这套最小可行架构。
2.1 核心组件:NetworkManager与NetworkIdentity
NetworkManager是整个网络游戏的“大脑”和“调度中心”。在Basic示例的场景中,你一定会找到一个挂载了NetworkManager组件的GameObject(通常就叫“NetworkManager”)。这个组件负责管理网络会话的生命周期:启动服务器、停止服务器、允许客户端连接、处理断开连接、管理玩家预制体的生成等。你可以把它理解为一个公司的前台+HR+IT部门,负责接待(连接)、入职(生成玩家)、离职(销毁玩家)和基础设备管理(场景加载)。
在Inspector面板中,你会看到NetworkManager有几个关键配置:
- Network Info: 设置服务器地址和端口。在开发时,我们通常用“localhost”或“127.0.0.1”表示本地机器。
- Player Prefab: 这是最重要的设置之一。它指定了当一个新客户端成功连接到服务器时,服务器将为这个客户端在游戏中生成的代表物,也就是“玩家角色”。这个预制体必须带有
NetworkIdentity组件。 - Spawnable Prefabs: 一个预制体列表。只有在这个列表中的预制体,才允许在网络上通过代码动态生成(Spawn)。你的玩家预制体通常也会放在这里。
NetworkIdentity是网络对象的“身份证”。任何需要在网络上存在、并且状态需要同步的GameObject(比如玩家、怪物、宝箱),都必须挂载这个组件。它赋予了这个对象一个在网络世界中唯一的标识(NetId)。Basic示例中的玩家预制体(Player)上就一定有这个组件。NetworkIdentity会与NetworkBehaviour脚本(后面会讲)协同工作,决定哪些脚本上的哪些变量或方法需要在网络上同步或调用。
2.2 核心工作流:从启动到同步
Basic示例展示了一个最经典的工作流,理解这个流程至关重要:
- 服务器启动:通过调用
NetworkManager.singleton.StartServer(),Unity程序会作为一个独立的服务器进程运行。此时,它开始监听指定端口,等待客户端连接。服务器拥有游戏的“权威”状态。 - 客户端启动与连接:在另一个Unity编辑器实例或构建出的客户端程序中,调用
NetworkManager.singleton.StartClient()。客户端会尝试连接到NetworkManager中配置的服务器地址和端口。 - 玩家生成:连接成功后,服务器会检查NetworkManager中设置的
Player Prefab。然后,它在服务器端实例化这个预制体,接着通过网络将这个实例化命令以及该对象的初始状态(由NetworkIdentity和相关的NetworkBehaviour决定)发送给对应的客户端,并在该客户端的场景中也生成一个相同的对象。至此,该客户端在游戏中有了一个受其控制的“化身”。 - 权威与同步:服务器是状态的权威。例如,在Basic示例中,玩家的移动逻辑写在挂载了
NetworkBehaviour的脚本里。客户端检测输入(如WASD),但不直接修改本地玩家对象的位置。相反,它调用一个用[Command]属性标记的方法,将这个移动意图“告诉”服务器。 - 命令(Command)执行:服务器收到这个
[Command]后,在服务器端对应的玩家对象上执行移动逻辑,计算新的位置。 - 状态同步:服务器计算出的新位置,通过
NetworkTransform组件或自定义的同步变量(用[SyncVar]标记),自动同步到所有客户端(包括操作者自己的客户端)。这样,所有玩家看到的该玩家位置都是一致的,且由服务器验证,防止作弊。
注意:这里有一个关键点,
[Command]方法默认是从客户端实例调用,在服务器实例上运行。而[ClientRpc]方法则是从服务器实例调用,在所有客户端实例上运行。Basic示例通常用[Command]来处理玩家输入。
3. Basic示例场景与脚本深度解析
让我们打开Mirror包中的Basic示例场景(通常路径为Assets/Mirror/Examples/Basic/Scenes/)。你会看到一个非常简洁的场景:一个平面作为地面,一个立方体作为玩家预制体,一个UI画布,以及最重要的NetworkManager GameObject。
3.1 场景布局与UI交互
场景中的UI通常包含几个简单的按钮:“Host (Server + Client)”, “Server Only”, “Client”, “Stop”。这些按钮绑定了NetworkManagerHUD组件或自定义的UI脚本,它们底层调用的就是NetworkManager.singleton的StartHost(),StartServer(),StartClient(),StopHost()等方法。
- Host:最常用的开发模式。它同时启动了服务器和一个本地客户端,并让这个客户端连接到本地服务器。你一个人就可以测试完整的客户端-服务器交互。
- Server Only:仅启动纯服务器进程,不连接任何客户端。通常用于部署专用服务器。
- Client:启动一个纯客户端,并尝试连接到指定的服务器地址。
- Stop:停止所有的网络活动。
3.2 玩家预制体与核心脚本
找到玩家预制体(比如叫“Player”),它通常包含:
- NetworkIdentity:如前所述,这是必须的。
- NetworkTransform:Mirror提供的一个组件,用于自动同步GameObject的位置(Position)、旋转(Rotation)和缩放(Scale)。在Basic示例中,它负责将服务器计算出的玩家新位置同步给所有客户端。你可以在组件上选择同步哪些属性,以及同步的频率(同步间隔),这对优化网络流量很重要。
- 一个自定义的
NetworkBehaviour脚本:例如PlayerController.cs。这是游戏逻辑的核心。
让我们深入这个PlayerController.cs脚本:
using UnityEngine; using Mirror; public class PlayerController : NetworkBehaviour { public float moveSpeed = 5f; void Update() { // 关键点:只有本地玩家(自己控制的这个对象)才处理输入 if (!isLocalPlayer) return; float moveX = Input.GetAxis("Horizontal") * moveSpeed * Time.deltaTime; float moveZ = Input.GetAxis("Vertical") * moveSpeed * Time.deltaTime; // 将移动意图以命令形式发送给服务器 CmdMove(moveX, moveZ); } [Command] void CmdMove(float x, float z) { // 服务器端执行实际的移动 // 注意:服务器端的这个脚本实例在移动“服务器权威”的玩家对象 transform.Translate(x, 0, z); } }逐行解析与避坑指南:
public class PlayerController : NetworkBehaviour:网络逻辑脚本必须继承自NetworkBehaviour,而不是普通的MonoBehaviour。只有这样,它才能使用isLocalPlayer,[Command],[ClientRpc],[SyncVar]等网络专属特性。if (!isLocalPlayer) return;:这是网络游戏脚本中最重要的一行代码之一,也是新手最容易忽略导致诡异Bug的地方。isLocalPlayer是NetworkBehaviour提供的一个属性,用于判断当前脚本实例所依附的游戏对象,是不是属于本客户端控制的玩家。- 对于操作者A的客户端,场景中会有两个玩家对象:一个是A自己控制的(
isLocalPlayer为 true),另一个是服务器同步过来的、代表玩家B的对象(isLocalPlayer为 false)。 - 这行代码确保了:输入处理(如
Input.GetAxis)、摄像机跟随、本地UI更新等逻辑,只会在“属于自己的”那个玩家对象上执行。否则,玩家A按W键,可能会导致场景里所有玩家对象(包括B的角色)都向前移动,这显然是错误的。
- 对于操作者A的客户端,场景中会有两个玩家对象:一个是A自己控制的(
CmdMove(moveX, moveZ);:客户端计算出移动向量后,并不直接调用transform.Translate,而是调用一个以Cmd为前缀、并标记了[Command]属性的方法。这个方法会将参数通过网络发送给服务器。[Command] void CmdMove(...):在服务器端,这个方法会在服务器上对应的那个玩家对象的PlayerController脚本实例上执行。在这里执行transform.Translate,修改的是服务器权威的玩家位置。- 位置同步如何发生?脚本里并没有显式地同步位置。这是因为
NetworkTransform组件在后台工作。它定期(根据设置的同步间隔)检测服务器端对象Transform的变化,然后将变化量压缩后发送给所有客户端。客户端收到后,再应用到本地的对应对象上。所以,移动的逻辑是“客户端发起请求 -> 服务器执行并改变状态 -> NetworkTransform自动同步状态到所有客户端”。
实操心得:在编写任何网络游戏逻辑时,要时刻在脑中区分“这个代码在谁(服务器还是客户端)的哪个对象(本地玩家对象还是远程玩家对象)上运行”。善用
isLocalPlayer,isServer,isClient这几个属性来做条件判断,是写出正确网络代码的基础。
4. 从零构建你自己的Basic示例
理解了原理,最好的巩固方式就是自己动手做一遍。我们抛开Mirror包自带的示例,从头创建一个。
4.1 环境准备与项目设置
- 创建新项目:打开Unity Hub,创建一个新的3D核心模板项目。
- 安装Mirror:最推荐的方式是通过Unity的Package Manager从Git URL安装,这样可以获得最新版本。打开
Window -> Package Manager,点击左上角“+”号,选择“Add package from git URL”,输入:https://github.com/MirrorNetworking/Mirror.git。等待安装完成。这种方式比从Asset Store导入更干净,也更容易更新。 - 验证安装:安装完成后,在菜单栏中看到“Mirror”选项,即表示安装成功。
4.2 构建最小化网络场景
- 创建场景与基础物件:新建一个场景,创建一个Plane(地面)并重置位置,稍微缩放一下作为地板。创建一个Directional Light(方向光)。
- 创建NetworkManager:在Hierarchy中右键 -> Create Empty,重命名为“NetworkManager”。选中它,在Inspector中点击“Add Component”,搜索并添加
Network Manager。 - 配置NetworkManager:暂时保持默认设置。我们稍后会来配置Player Prefab。
4.3 创建并配置玩家预制体
- 创建玩家对象:在场景中创建一个Cube,重命名为“Player”。将其Y轴位置设为0.5,使其刚好站在地面上。你可以给它加个颜色材质以便区分。
- 添加网络身份:选中Player对象,点击“Add Component”,搜索并添加
Network Identity。 - 添加网络变换:继续添加
Network Transform组件。保持默认设置,它会同步位置和旋转。 - 创建控制脚本:在Project窗口中创建一个C#脚本,命名为
SimplePlayerController。将上面解析过的代码复制进去,并挂载到Player对象上。 - 制作预制体:将Hierarchy中的Player对象拖入Project窗口的Assets文件夹,创建一个预制体。创建好后,可以删除场景中的Player对象(因为我们之后会通过网络动态生成它)。
- 关联预制体:回到场景中的NetworkManager对象,在
Network Manager组件的Player Prefab槽中,拖入刚刚创建的Player预制体。同时,点击Spawnable Prefabs列表下方的“+”号,也将Player预制体添加进去。
4.4 创建简易控制UI
- 创建UI:右键Hierarchy -> UI -> Canvas。然后右键Canvas -> UI -> Button,创建四个按钮。
- 排列并重命名按钮:将四个按钮分别命名为“Btn_Host”, “Btn_Server”, “Btn_Client”, “Btn_Stop”,并修改其Text子对象的内容为“Host”, “Server”, “Client”, “Stop”。
- 编写UI控制脚本:创建一个C#脚本,命名为
SimpleNetworkHUD,挂载到Canvas上。
using UnityEngine; using UnityEngine.UI; using Mirror; public class SimpleNetworkHUD : MonoBehaviour { public Button hostButton; public Button serverButton; public Button clientButton; public Button stopButton; void Start() { // 为按钮绑定点击事件 hostButton.onClick.AddListener(() => NetworkManager.singleton.StartHost()); serverButton.onClick.AddListener(() => NetworkManager.singleton.StartServer()); clientButton.onClick.AddListener(() => NetworkManager.singleton.StartClient()); stopButton.onClick.AddListener(() => NetworkManager.singleton.StopHost()); } void Update() { // 根据网络状态更新按钮的交互状态,提升用户体验 bool isNetworkActive = NetworkServer.active || NetworkClient.active; hostButton.interactable = !isNetworkActive; serverButton.interactable = !isNetworkActive; clientButton.interactable = !isNetworkActive; stopButton.interactable = isNetworkActive; } }- 脚本绑定:在Canvas的
SimpleNetworkHUD组件上,将四个按钮拖拽到对应的公开字段中。
4.5 运行与测试
- 保存场景。
- 进入Host模式测试:点击Play运行。点击“Host”按钮。你会立刻在Game视图中看到一个立方体(玩家)生成。使用WASD键移动,观察是否正常。
- 测试客户端连接:
- 首先,停止播放。
- 打开
File -> Build Settings,将当前场景加入构建列表,点击“Build And Run”,将项目构建为一个独立的可执行文件(例如MyGame.exe),放在一个文件夹里。运行它,这个就是“客户端”。 - 回到Unity编辑器,再次点击Play,但这次点击“Server Only”按钮。此时编辑器作为纯服务器运行。
- 在刚才构建出的客户端程序里,点击“Client”按钮。客户端会尝试连接到
localhost(即本机)。 - 连接成功后,你会在Unity编辑器(服务器)和客户端程序中各看到一个立方体。在客户端里用WASD移动,观察两个窗口中的立方体是否同步移动。
如果一切顺利,恭喜你!你已经成功从零搭建了一个最基础的Mirror网络应用框架。这个过程虽然简单,但涵盖了Mirror最核心的90%的概念。
5. 常见问题排查与进阶调试技巧
即使按照步骤操作,也难免会遇到问题。下面是一些在开发Basic示例乃至更复杂项目时的高频问题及解决方法。
5.1 连接失败类问题
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 客户端无法连接到服务器,提示“Connection Failed”或超时。 | 1. 服务器未启动。 2. 防火墙/杀毒软件阻止了端口。 3. NetworkManager中的地址或端口错误。 4. 客户端和服务器使用的传输层(Transport)不匹配。 | 1.确认服务器已启动:检查Unity编辑器或服务器程序的控制台是否有成功启动的日志。 2.检查地址端口:确保客户端NetworkManager的“Network Address”和“Port”与服务器监听的一致。本地测试用 localhost或127.0.0.1,端口默认7777。3.检查防火墙:临时关闭防火墙测试,或为你的Unity编辑器/构建程序添加入站规则,允许其通过指定端口(如7777)通信。 4.检查传输组件:确保服务器和客户端的NetworkManager GameObject上挂载的 Transport组件是同一个类型(如默认的Telepathy Transport或KCP Transport)。 |
| 连接成功,但玩家预制体没有生成。 | 1. Player Prefab未正确赋值或未添加到Spawnable Prefabs列表。 2. Player Prefab上缺少 NetworkIdentity组件。3. 预制体在Resources文件夹或其他特殊路径下。 | 1.检查预制体:双击确认预制体已正确保存,且根物体上有NetworkIdentity组件。2.检查NetworkManager配置:确保Player Prefab槽位拖入了正确的预制体,并且该预制体也在Spawnable Prefabs列表中。 3.检查控制台错误:Unity编辑器控制台通常会给出明确的错误信息,如“Player Prefab must have a NetworkIdentity”。 |
| 多个客户端连接后,移动控制混乱(按一个键所有角色都动)。 | 玩家控制脚本中没有检查isLocalPlayer。 | 修改控制脚本:在Update()或任何处理输入、摄像机跟随的逻辑开头,务必加上if (!isLocalPlayer) return;。这是网络游戏编程的“铁律”。 |
5.2 同步与逻辑类问题
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 玩家移动卡顿、跳跃或不平滑。 | 1. 网络延迟(Latency)和丢包(Packet Loss)的自然现象。 2. NetworkTransform的同步频率过低。3. 在客户端直接修改了应由服务器同步的 Transform。 | 1.理解网络特性:完全平滑的同步在网络游戏中是不可能的。Mirror的NetworkTransform默认会进行插值(Interpolation),在收到位置更新后平滑过渡,以掩盖网络延迟。卡顿可能是网络本身问题。2.调整同步率:尝试提高 NetworkTransform组件上的Sync Interval(如从0.1f改为0.05f),但注意这会增加带宽消耗。3.确保权威:移动逻辑必须在 [Command]方法中,在服务器端执行。客户端只发送指令。 |
[Command]或[ClientRpc]方法没有被调用。 | 1. 方法命名不符合约定(CmdXXX, RpcXXX)。 2. 方法不是 public void类型。3. 从错误的上下文调用(如从服务器调用一个只能由客户端调用的 [Command])。4. 参数类型不被序列化支持。 | 1.检查命名和签名:[Command]方法必须以Cmd开头,[ClientRpc]以Rpc开头。它们必须是public void。2.检查调用者: [Command]只能从继承了NetworkBehaviour的、且isLocalPlayer为true的对象上调用。[ClientRpc]只能从服务器端调用。3.检查参数:参数必须是Mirror支持的基本类型或网络类型(如 NetworkIdentity)。自定义类需要做额外处理。 |
| 非玩家对象(如子弹、道具)无法在网络间生成。 | 1. 该对象的预制体没有添加到NetworkManager的Spawnable Prefabs列表。2. 生成时没有使用 NetworkServer.Spawn()方法。 | 1.注册预制体:确保所有需要动态生成的网络预制体都在Spawnable Prefabs列表中。2.使用正确的生成方法:在服务器端代码中,使用 GameObject bullet = Instantiate(bulletPrefab, ...);后,必须调用NetworkServer.Spawn(bullet);才能将其同步到所有客户端。 |
5.3 进阶调试技巧
- 善用日志:Mirror有详细的日志系统。在菜单栏选择
Mirror -> Log Settings,可以调整不同类别(Info, Warning, Error)的日志级别。在开发阶段,可以全部打开,便于追踪网络事件(连接、断开、生成、销毁等)。 - 使用Network Monitor:这是一个强大的内置调试工具。在Play模式下,打开
Window -> Analysis -> Network Monitor。你可以实时看到所有网络消息的流量、类型、大小,甚至可以查看具体消息的内容。这对于理解同步频率、排查RPC调用问题至关重要。 - 模拟恶劣网络环境:在
Transport组件(如KCP Transport)上,通常有模拟延迟(Latency)和丢包(Packet Loss)的参数。在开发时主动开启并设置一个较高的值,可以测试你的游戏在糟糕网络下的表现,并优化你的同步策略和插值参数。 - 序列化与反序列化:当你使用
[SyncVar]同步自定义结构体(struct)或类(class)时,需要为这个类型实现自定义的序列化方法。如果同步数据不正确,首先检查这里。Mirror的文档有详细示例。
6. 从Basic到进阶:下一步可以做什么?
当你牢牢掌握了Basic示例的所有细节后,你的Mirror之旅才算真正开始。这里有一些明确的方向,可以让你基于这个“骨架”添砖加瓦:
扩展玩家状态:使用
[SyncVar]属性来同步玩家的生命值、弹药量、分数等。[SyncVar]会在变量变化时自动同步给所有客户端。public class PlayerState : NetworkBehaviour { [SyncVar] public int health = 100; [Server] // 这个属性表示此方法仅在服务器端可调用 public void TakeDamage(int amount) { health -= amount; // SyncVar health的变化会自动同步 } }实现非玩家对象的网络交互:比如创建一个“可拾取物品”预制体,带有
NetworkIdentity。当玩家碰撞时,在服务器端销毁该物品(NetworkServer.Destroy(item)),并增加玩家分数(通过[Command]调用服务器方法修改玩家的[SyncVar]分数)。房间与匹配:Basic示例是直连IP的。下一步可以集成Mirror的
Matchmaking组件或第三方服务(如Unity的Relay、Photon Fusion等)来实现大厅、房间列表和自动匹配功能。更换传输层:Mirror支持多种底层传输协议。默认的KCP或Telepathy适合大多数情况。但对于需要WebGL支持的项目,你可能需要集成
WebSockets Transport。对于追求更低延迟和可靠性的项目,可以研究Ignorance(基于ENET)或LiteNetLib Transport。优化与安全:
- 网络变量压缩:对于位置、旋转等浮点数,考虑使用
[SyncVar]的hook进行压缩,减少带宽。 - 预测与回滚:对于快节奏动作游戏,研究客户端预测(Client-side Prediction)和服务器协调(Server Reconciliation)来改善操作手感。
- 反作弊:牢记“服务器是权威的”。所有关键逻辑(如伤害计算、物品掉落)都必须在服务器端进行,客户端只发送意图。对客户端发来的数据进行合理性校验(如移动速度是否超限)。
- 网络变量压缩:对于位置、旋转等浮点数,考虑使用
掌握Basic示例,就像拿到了打开Mirror大门的钥匙。它教给你的不是某个具体的游戏功能,而是Mirror框架最根本的思维方式和工作原理。当你理解了客户端与服务器的界限、命令与RPC的流向、身份与权限的区分之后,再去实现任何复杂的网络功能,都只是将这些基础概念进行组合和扩展而已。