1. 项目概述为什么选择Mirror与Pong如果你正在Unity里折腾网络联机大概率听说过Mirror这个名字。它不是一个全新的发明而是基于Unity早期官方网络方案UNET的一个社区维护分支。但正是这个“分支”在UNET被官方逐渐放弃后凭借其开源、活跃、以及对开发者极其友好的设计成为了Unity社区里中小型多人游戏开发的事实标准。我第一次接触Mirror是在做一个需要快速验证玩法的多人原型时当时被UNET的复杂性和文档缺失搞得焦头烂额切换到Mirror后那种“原来可以这么简单”的感觉至今记忆犹新。那么为什么我们要通过一个“Pong”乒乓球游戏来学习Mirror呢Pong是电子游戏的鼻祖之一规则极简两个挡板一个球谁没接到球谁丢分。但恰恰是这种极简让它成为了理解网络游戏核心概念的绝佳载体。在一个Pong游戏里你需要处理玩家的输入同步挡板移动、权威状态同步球的位置和速度、简单的游戏逻辑得分判定以及最基本的玩家匹配与连接管理。麻雀虽小五脏俱全。通过复现一个网络版的Pong你几乎能摸到所有中小型实时对战游戏比如一些休闲竞技、io类游戏的技术骨架。这比一上来就啃一个复杂的MMO或FPS示例要友好得多你能清晰地看到每一行代码是如何影响网络行为的而不会被庞大的业务逻辑淹没。Mirror的设计哲学就是“让网络编程对游戏开发者更友好”。它抽象了底层的传输协议你可以选TCP、WebSockets等提供了一套基于消息和远程过程调用RPC的高层API。你不用直接去操作Socket字节流而是像调用本地函数一样让函数在另一个玩家的客户端上执行。这种心智模型的转变大大降低了入门门槛。接下来我们就从零开始拆解如何用Mirror搭建一个稳定、可玩的网络Pong游戏。2. 环境准备与项目初始化2.1 Unity版本与Mirror导入首先确保你使用的Unity版本是2019.4 LTS或更高版本推荐2021/2022 LTS这是Mirror稳定支持的基础。打开Unity Hub创建一个新的3D核心模板项目命名为“MirrorPongDemo”。导入Mirror主要有两种方式我强烈推荐第一种通过Unity的Package Manager从Git URL添加。打开Window - Package Manager点击左上角的“”号选择“Add package from git URL...”。在弹出的输入框中粘贴Mirror在GitHub上的官方仓库地址https://github.com/MirrorNetworking/Mirror.git。点击“Add”Unity就会自动下载并导入Mirror的最新稳定版。这种方式能确保你获取到的是纯净的、未经修改的官方代码也便于后续更新。注意有时直接使用Git URL可能会因为网络问题失败。备选方案是从Mirror的GitHub Releases页面下载最新的.unitypackage文件然后通过Assets - Import Package - Custom Package进行导入。导入后检查你的项目面板应该会出现一个名为“Mirror”的文件夹。导入成功后你会在菜单栏看到一个新的“Mirror”菜单项。这是Mirror提供的便捷工具入口后续创建网络管理器等组件会用到。2.2 核心场景与基础对象搭建我们的Pong场景非常简单。首先删除场景中自带的默认摄像机Main Camera和方向光Directional Light我们将创建更适合2D游戏的摄像机。创建场景基础结构在场景中创建一个空物体命名为“GameScene”。这个物体将作为所有游戏逻辑对象的根节点方便管理。然后在它下面创建三个子空物体分别命名为“Environment”环境、“Network”网络、“UI”界面。设置2D摄像机在“Environment”下创建一个新的摄像机重命名为“GameCamera”。将其Projection投影设置为“Orthographic”正交Size大小设置为5。这样我们就得到了一个适合2D游戏的视图。将摄像机位置设为(0, 0, -10)确保它能完整看到游戏区域。创建游戏区域在“Environment”下创建一个Cube重命名为“Wall_Top”将其Scale设置为(20, 1, 1)位置设为(0, 4.5, 0)。这将是球场的上边界。同理创建“Wall_Bottom”位置设为(0, -4.5, 0)。再创建两个侧边墙“Wall_Left”和“Wall_Right”Scale设为(1, 10, 1)位置分别设为(-9.5, 0, 0)和(9.5, 0, 0)。为这四个墙体添加一个简单的材质比如灰色以便区分。创建球和挡板预制体球 (Ball)创建一个Sphere重命名为“Ball”Scale设为(0.5, 0.5, 0.5)。为其添加一个Rigidbody组件并勾选Is Kinematic我们将用代码控制其运动而非物理引擎。然后将其从场景中拖入Project面板的Assets文件夹创建一个预制体。创建后可以删除场景中的实例。挡板 (Paddle)创建一个Cube重命名为“Paddle”Scale设为(0.5, 2, 1)。同样添加Rigidbody并勾选Is Kinematic。也将其制作为预制体。至此我们的静态场景和基础预制体就准备好了。接下来我们将进入Mirror网络框架的核心配置环节。3. Mirror网络框架核心配置解析3.1 Network Manager与玩家预制体Network Manager是Mirror单例模式的核心管理器它负责处理整个网络生命周期的连接、断开、场景切换和玩家生成。在“Network”空物体下右键选择Create Empty然后从菜单栏选择Mirror - NetworkManager。这个操作会自动为该物体添加NetworkManager组件以及其依赖的KcpTransport或SimpleWebTransport等传输组件。选中刚创建的NetworkManager物体我们重点配置几个属性Don‘t Destroy On Load默认勾选确保切换场景时网络连接不被销毁。Player Prefab这是最重要的设置之一。它定义了当玩家连接到服务器时服务器为这个玩家生成的、代表其游戏内实体的对象。我们先留空等创建好玩家控制的挡板对象后再来指定。Network Address与Network Port服务器的地址和端口。在本地测试时地址通常是“localhost”或“127.0.0.1”。接下来创建玩家预制体。将之前做好的“Paddle”预制体拖入场景重命名为“PlayerPaddle”。我们需要让它具备网络身份。为其添加NetworkIdentity组件这是Mirror中任何网络对象的“身份证”组件会有一个唯一的NetId。然后添加NetworkTransform组件。这个组件是Mirror的魔法之一它自动帮你同步物体的位置、旋转和缩放。对于我们的挡板只需要同步Y轴位置。在NetworkTransform组件中将Sync Position勾选Sync Scale和Sync Rotation可以取消。为了更精细的控制可以将Client Authority客户端权威取消勾选因为我们希望服务器的位置是权威的。现在将这个配置好的“PlayerPaddle”物体从场景中拖回Project面板更新或创建一个新的预制体。然后将这个新的“PlayerPaddle”预制体拖拽到NetworkManager的Player Prefab槽位中。3.2 网络身份NetworkIdentity与组件同步NetworkIdentity是网络对象的基石。任何需要在网络上存在的GameObject都必须挂载它。它有几个关键属性Server Only如果勾选该对象只存在于服务器端客户端不会生成。适合纯服务器逻辑对象。Local Player Authority如果勾选这个对象对应的客户端玩家对其有控制权其上的某些组件如带有[Command]属性的函数可以由该客户端调用。我们的玩家挡板就需要这个权限以便将本地输入转化为服务器指令。Network Sync Mode通常为Observers即所有观察者其他客户端都能看到。NetworkTransform是基于状态同步的组件。它定期可配置的发送间隔将物体的Transform信息从拥有者服务器或权威客户端发送给其他客户端。这里有一个重要的性能权衡发送频率越高同步越平滑但网络流量越大。对于Pong中的挡板由于移动是连续的我们可以设置一个较高的频率如每秒15-30次。而对于球它的运动更快速且可预测我们甚至可以考虑使用更低的频率或者采用不同的同步策略如只同步速度和方向客户端自行预测轨迹。3.3 传输层选择与基础配置Mirror支持多种底层传输协议。在NetworkManager物体上你可以看到默认添加的可能是KcpTransport或TelepathyTransport。对于Pong这类实时性要求高、但数据量小的游戏KcpTransport是一个很好的选择。KCP是一个在UDP基础上实现可靠、有序数据传输的协议比纯TCP延迟更低。在KcpTransport组件中你可以调整Port监听的端口号需要和NetworkManager中的设置一致。Dual Mode是否同时支持IPv4和IPv6。No Delay/Interval控制数据发送的延迟和间隔影响响应速度和带宽。对于快节奏游戏可以启用No Delay并设置较小的Interval如10ms。配置完成后你的基础网络框架就搭建好了。NetworkManager提供了两个最常用的方法StartHost()启动主机同时作为服务器和客户端、StartServer()仅启动服务器、StartClient()作为客户端连接。我们通常会创建一个简单的UI用按钮来触发这些方法。4. Pong游戏核心逻辑实现4.1 玩家输入与挡板移动同步首先为“PlayerPaddle”预制体创建一个C#脚本命名为PaddleController。这个脚本需要处理本地输入并将移动指令发送到服务器。using Mirror; using UnityEngine; public class PaddleController : NetworkBehaviour { public float moveSpeed 10f; private float verticalInput; void Update() { // 只有本地玩家控制的挡板才处理输入 if (!isLocalPlayer) return; verticalInput Input.GetAxis(Vertical); // 如果按下了按键则调用Command if (verticalInput ! 0) { CmdMovePaddle(verticalInput); } } [Command] void CmdMovePaddle(float input) { // 在服务器端执行移动逻辑 Vector3 newPosition transform.position Vector3.up * input * moveSpeed * Time.deltaTime; // 简单的边界限制假设挡板在Y轴范围-4到4之间移动 newPosition.y Mathf.Clamp(newPosition.y, -4f, 4f); transform.position newPosition; } }关键点解析NetworkBehaviour所有Mirror网络脚本都必须继承自此类而不是MonoBehaviour。isLocalPlayer这是一个属性用于判断当前脚本实例是否属于本地玩家控制的网络对象。我们只在本地玩家的客户端上捕获输入。[Command]属性这是一个Mirror特性。带有[Command]的函数可以从客户端调用但实际执行逻辑在服务器上。这是Mirror实现客户端预测和服务器权威的关键。函数名必须以Cmd前缀开头。在CmdMovePaddle中我们在服务器端计算新的挡板位置并直接赋值。由于这个物体挂载了NetworkTransform服务器位置的变化会自动同步到所有客户端。实操心得很多新手会疑惑为什么不在Update里直接移动然后靠NetworkTransform同步那样做会导致客户端预测不同步因为输入是在客户端处理的直接移动会看到本地挡板立即响应但服务器端的权威位置可能因为网络延迟而不同步造成“回弹”或“拉扯”。通过[Command]将移动逻辑放在服务器确保了所有客户端看到的挡板位置都来自同一个权威来源虽然本地输入反馈有微小延迟但保证了公平性。对于Pong这种简单游戏这个延迟是可以接受的。4.2 球的运动逻辑与网络同步球的逻辑更复杂一些因为它不由任何玩家直接控制而是由服务器进行权威模拟。为“Ball”预制体创建脚本BallController。首先为球添加NetworkIdentity并确保Local Player Authority为false球由服务器控制。同样添加NetworkTransform用于位置同步。using Mirror; using UnityEngine; public class BallController : NetworkBehaviour { public float initialSpeed 5f; private Vector2 currentDirection; private Rigidbody rb; public override void OnStartServer() { base.OnStartServer(); rb GetComponentRigidbody(); // 服务器初始化球的方向和速度 ResetBall(); } [Server] void ResetBall() { // 将球重置到中心 transform.position Vector3.zero; // 随机一个初始方向略偏向某一侧增加随机性 float randomAngle Random.Range(-45f, 45f) * Mathf.Deg2Rad; currentDirection new Vector2(Mathf.Cos(randomAngle), Mathf.Sin(randomAngle)).normalized; // 应用速度 rb.velocity currentDirection * initialSpeed; } void FixedUpdate() { // 只在服务器端运行物理模拟 if (!isServer) return; // 服务器持续更新速度方向用于碰撞计算等 currentDirection rb.velocity.normalized; } [ServerCallback] void OnCollisionEnter(Collision collision) { // 只在服务器端处理碰撞 if (collision.gameObject.CompareTag(Paddle)) { // 碰撞挡板反弹并可能根据碰撞点改变角度 Vector2 normal collision.contacts[0].normal; Vector2 reflectDir Vector2.Reflect(currentDirection, normal); // 可以加入一点随机扰动防止死循环 reflectDir new Vector2(Random.Range(-0.1f, 0.1f), Random.Range(-0.1f, 0.1f)); rb.velocity reflectDir.normalized * initialSpeed; currentDirection reflectDir.normalized; } else if (collision.gameObject.CompareTag(Wall)) { // 碰撞上下墙反转Y方向 Vector2 newDir new Vector2(currentDirection.x, -currentDirection.y); rb.velocity newDir.normalized * initialSpeed; currentDirection newDir.normalized; } else if (collision.gameObject.CompareTag(Goal)) { // 碰撞左右边界球门得分逻辑 HandleScore(collision.gameObject.name); ResetBall(); } } [Server] void HandleScore(string goalName) { // 这里触发得分事件可以通过RPC通知所有客户端更新UI // 例如RpcUpdateScore(goalName.Contains(Left) ? 1 : 0); Debug.Log($球进入了 {goalName}得分); } }关键点解析isServer判断当前是否在服务器端。球的物理模拟和碰撞检测必须只在服务器进行以保证所有客户端看到的状态一致。[Server]属性标记一个函数只能在服务器端调用。如果客户端尝试调用会被忽略。[ServerCallback]用于Unity生命周期函数如OnCollisionEnter告诉Mirror这个函数只在服务器端有意义。OnStartServer()这是一个网络生命周期函数当该对象在服务器端生成时调用。这是初始化服务器端逻辑如设置球的初始速度的理想位置。同步策略球的位置通过NetworkTransform同步。由于球的运动是连续的且由服务器权威计算所有客户端都会平滑地插值到服务器发来的最新位置。虽然客户端看到球的位置会有轻微的延迟取决于网络往返时间和发送频率但因为运动是可预测的NetworkTransform的插值功能可以很好地掩盖这种延迟让球的运动看起来是平滑的。4.3 游戏状态管理得分与胜负判定游戏状态如比分、游戏是否开始/结束也需要在网络上同步。我们创建一个全局的游戏管理器GameManager并挂载到场景中的一个空物体上例如放在“Network”下。这个管理器也需要是一个网络对象。using Mirror; using UnityEngine; using UnityEngine.UI; public class GameManager : NetworkBehaviour { // 同步变量当值改变时会自动同步到所有客户端 [SyncVar(hook nameof(OnScoreChanged))] private int player1Score 0; [SyncVar(hook nameof(OnScoreChanged))] private int player2Score 0; [SyncVar] private bool isGameActive false; public Text scoreText; // UI文本引用 public override void OnStartServer() { base.OnStartServer(); // 服务器初始化游戏状态 ResetGame(); } [Server] public void PlayerScored(int playerIndex) { if (playerIndex 1) player1Score; else if (playerIndex 2) player2Score; // 检查胜利条件 if (player1Score 5 || player2Score 5) { EndGame(); } } [Server] void ResetGame() { player1Score 0; player2Score 0; isGameActive true; // 通知所有客户端游戏开始 RpcGameStarted(); } [Server] void EndGame() { isGameActive false; RpcGameEnded(player1Score player2Score ? 1 : 2); } // 客户端得分更新时的回调 void OnScoreChanged(int oldValue, int newValue) { UpdateScoreUI(); } void UpdateScoreUI() { if (scoreText ! null) scoreText.text ${player1Score} : {player2Score}; } [ClientRpc] void RpcGameStarted() { Debug.Log(游戏开始); UpdateScoreUI(); } [ClientRpc] void RpcGameEnded(int winnerIndex) { Debug.Log($游戏结束玩家 {winnerIndex} 获胜); // 这里可以显示胜利UI } }关键点解析[SyncVar]这是Mirror中用于同步简单变量如int, float, string, 某些结构体的特性。当标记了[SyncVar]的变量在服务器端被修改时其新值会自动发送给所有客户端。这是实现游戏状态同步最高效的方式之一。Hook钩子[SyncVar(hook nameof(OnScoreChanged))]中的hook参数指定了一个方法名。当这个SyncVar的值在客户端发生变化时会自动调用这个钩子方法。这是更新UI如比分板的完美时机。[ClientRpc]与[Command]相反[ClientRpc]标记的函数在服务器端调用但在所有客户端执行。用于广播事件如游戏开始、结束、播放全局特效等。函数名必须以Rpc前缀开头。服务器权威所有修改游戏核心状态如加分、判定胜负的逻辑都必须放在服务器端用[Server]保护起来。客户端只能通过发送[Command]来请求服务器执行这些操作。现在我们需要修改BallController中的HandleScore方法让它调用GameManager的PlayerScored方法。这涉及到网络对象间的通信。一种常见做法是使用NetworkServer.FindObjectOfTypeGameManager()在服务器端查找管理器或者通过单例模式、依赖注入等方式获取引用。5. 网络调试与性能优化要点5.1 利用Mirror内置的HUD与统计信息Mirror提供了一个非常实用的网络HUD平视显示器。在NetworkManager物体上你可以添加NetworkManagerHUD组件。运行游戏后屏幕上会出现一个简单的UI允许你选择“Host主机”、“Client客户端”、“Server仅服务器”模式并输入服务器地址。这在开发初期快速测试连接时极其方便。此外添加NetworkStatistics组件可以实时显示网络流量数据包括每秒发送/接收的字节数、消息数量等。这对于评估游戏网络负载、发现异常流量峰值至关重要。例如如果你的Pong游戏每秒数据量超过了几KB那就需要检查是否同步了不必要的数据或者NetworkTransform的发送频率设置过高。5.2 权威性与防作弊浅析在Pong这样简单的游戏中作弊似乎不是大问题但建立正确的权威模型是开发任何多人游戏的基础。我们的架构是典型的“服务器权威”输入客户端发送[Command]到服务器。逻辑与状态服务器执行所有核心游戏逻辑挡板移动计算、球体运动、碰撞检测、得分判定。同步服务器通过[SyncVar]和NetworkTransform将状态同步给所有客户端。这种模式下客户端只是一个“输入采集器”和“状态渲染器”。一个恶意客户端即使发送了伪造的“超快移动”指令服务器也会按照合理的速度moveSpeed * Time.deltaTime来执行移动并检查边界。它无法直接修改球的位置或自己的分数因为这些状态只在服务器端被修改。注意事项服务器权威并非万能。它增加了服务器负载并引入了输入延迟。对于要求极致反应速度的游戏如格斗、FPS常采用“客户端预测服务器校正”的混合模式。但对于Pong服务器权威完全足够且实现简单、逻辑清晰。5.3 同步频率与网络带宽优化在Inspector中选中“Ball”和“PlayerPaddle”预制体上的NetworkTransform组件你可以看到Sync Interval同步间隔参数。默认可能是0.1秒每秒10次。对于挡板这个频率可以接受。但对于快速移动的球0.1秒的间隔可能导致客户端看到的球位置跳跃感明显。优化建议球的NetworkTransform将Sync Interval降低到0.05秒每秒20次或0.033秒每秒30次。同时确保Interpolation插值功能开启这样客户端会在收到两个更新包之间平滑过渡球的位置使运动更流畅。压缩NetworkTransform默认同步的是完整的Vector3位置。对于2D游戏Z轴可能不变。你可以通过继承NetworkTransform编写自定义组件只同步X和Y或者使用更小的数据类型如Half来减少数据量。不过对于Pong数据量本身很小这一步通常不是必须的。状态同步 vs 事件同步我们使用了[SyncVar]同步比分。比分变化是低频事件。如果是一个高频变化的状态比如球的旋转虽然我们没用用[SyncVar]可能产生大量同步消息。这时可以考虑用[ClientRpc]事件来同步变化或者只在变化超过某个阈值时才同步。5.4 断线重连与场景处理Mirror的NetworkManager内置了离线场景和在线场景的管理。你可以在NetworkManager组件中设置Offline Scene和Online Scene。当服务器停止或客户端断开时会自动切换回离线场景通常是大厅或主菜单。当连接建立时会自动加载在线场景游戏场景。对于Pong我们需要处理玩家中途加入的情况。默认情况下NetworkManager会在玩家连接时为其生成Player Prefab。但在一个正在进行的对局中新玩家加入应该处于观战状态还是等待下一局这需要额外的逻辑。你可以通过重写NetworkManager的OnServerAddPlayer方法或者使用PlayerSpawner系统来更精细地控制玩家生成的位置和状态。6. 构建、部署与测试实战6.1 多实例本地测试Unity编辑器本身支持以多实例模式运行这是测试网络功能最快捷的方式。在编辑器中打开游戏场景。确保NetworkManager的Player Prefab已正确设置。运行第一个编辑器实例点击Play按钮。在游戏视图弹出的NetworkManagerHUD上点击“Host”。这样你就同时启动了服务器和一个客户端。不要停止第一个实例回到Unity编辑器再次点击Play按钮。Unity会打开第二个游戏实例。在第二个实例的HUD上点击“Client”地址填写“localhost”。现在你应该能看到两个窗口每个窗口里有两个挡板一个是本地控制的一个是远程同步过来的球在中间运动。你可以分别操作两个窗口测试移动和得分同步。6.2 独立构建与局域网测试本地多实例测试通过后下一步是构建独立的可执行文件进行局域网测试。打开File - Build Settings。将当前游戏场景添加到“Scenes In Build”中。选择目标平台如Windows、Mac、Linux。点击“Build”生成一个可执行文件例如MirrorPong.exe。将生成的可执行文件复制到另一台在同一局域网内的电脑上。在一台电脑上运行程序并点击“Host”或“Server”。在另一台电脑上运行程序在客户端模式的地址栏中输入第一台电脑的局域网IP地址如192.168.1.100然后点击“Client”。如果连接成功你们就可以进行真正的局域网对战了。这个过程能暴露一些在单机多实例测试中不明显的问题比如防火墙设置、NAT穿透等。6.3 常见连接问题排查连接失败超时检查防火墙确保构建的程序的入站规则被允许或者临时关闭防火墙测试。检查IP地址和端口确认客户端输入的服务器IP地址正确且端口未被其他程序占用。检查传输协议确保服务器和客户端使用相同的传输组件如都是KcpTransport。连接成功但玩家看不到彼此检查Player Prefab确认NetworkManager上指定的预制体正确挂载了NetworkIdentity和NetworkTransform。检查生成位置玩家预制体可能在场景外或重叠。可以在NetworkManager的OnServerAddPlayer中自定义生成逻辑。检查权限确认玩家预制体上控制移动的脚本中使用了if (!isLocalPlayer) return;来区分本地和远程对象否则输入控制会混乱。运动卡顿或抖动调整NetworkTransform同步间隔尝试降低同步间隔如从0.1s改为0.05s。开启插值确保NetworkTransform的Interpolate选项被勾选。检查帧率确保服务器和客户端的运行帧率稳定。可以在Quality Settings中关闭垂直同步VSync并将目标帧率设为-1不限制或一个较高值进行测试。7. 从Pong出发Mirror进阶思路完成基础的Pong你已经掌握了Mirror最核心的[Command]、[ClientRpc]、[SyncVar]、NetworkTransform以及NetworkManager的使用。但Mirror的能力远不止于此。你可以基于这个Pong项目进行扩展深入学习更高级的特性网络场景物体Network Scene Objects我们目前通过NetworkManager动态生成玩家。但场景中静态存在的球和墙也可以通过给它们添加NetworkIdentity并勾选Scene Object使其成为网络场景物体。这样它们会在所有客户端加载场景时自动存在无需服务器动态生成。自定义消息Network Messages当[Command]和[ClientRpc]不能满足需求时例如需要发送自定义结构的数据包可以使用NetworkMessage系统进行底层通信。兴趣管理Interest Management对于大型场景不是所有客户端都需要知道所有物体的状态。Mirror支持基于距离、场景分区等的兴趣管理系统可以大幅减少网络流量。断线预测与状态快照对于更复杂的游戏可以研究如何将服务器的权威状态以快照形式发送给客户端客户端在两次快照之间进行预测和插值以提供更流畅的体验。这个Pong示例就像一张地图带你走进了Mirror网络开发的大门。里面的每一行代码、每一个设计选择都是为了解决网络游戏中最本质的问题如何在延迟和不可靠的网络环境下让所有玩家看到一个一致、公平、可玩的虚拟世界。当你理解了Pong中的同步、权威和RPC再去探索更复杂的项目你会发现核心思想是相通的只是规模和细节上的扩展。