尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity多人FPS开发:从网络同步到服务器权威架构实战

Unity多人FPS开发:从网络同步到服务器权威架构实战 1. 项目概述为什么选择Unity开发多人FPS如果你对游戏开发感兴趣尤其是想做一个能和朋友们一起“突突突”的射击游戏那么Unity绝对是一个绕不开的选项。我入行十多年从单机小游戏做到大型多人在线项目Unity的灵活性和强大的网络生态让我在多人FPS这个领域少走了很多弯路。很多人一听到“多人联机”、“网络同步”就觉得头大觉得这是大厂高级工程师才能碰的东西。其实不然用对方法和工具从零搭建一个可玩性不错的多人射击游戏并没有想象中那么遥不可及。这个项目标题“5步掌握Unity多人FPS开发”听起来像是个速成教程但我的本意并非让你囫囵吞枣。这“5步”更像是一个清晰的路线图帮你把庞大的工程拆解成几个可执行、可验证的阶段。从搭建基础的单人射击原型到引入网络框架实现多人联机再到处理棘手的延迟补偿和反作弊逻辑每一步都有其核心目标和必须攻克的难点。最终你得到的不仅是一个能跑起来的Demo更是一套理解多人游戏底层逻辑的思维框架。无论你是刚学完C#基础的学生还是想从单机转向联网开发的独立开发者跟着这个思路走一遍都能对Unity多人游戏开发有一个扎实的、体系化的认知。2. 核心思路与架构设计如何规划你的多人FPS在动手写第一行代码之前我们必须先想清楚整个游戏的架构。多人FPS和单机FPS最大的区别在于“状态”的归属和同步。在单机游戏中所有逻辑都在本地计算你说一不二。但在多人游戏中一个玩家的开枪动作、命中判定、位置移动都需要及时、准确地告知所有其他玩家同时还要保证游戏的公平性比如不能因为网络延迟就让某些玩家“开挂”。2.1 客户端-服务器C/S架构是基石对于严肃的多人FPS客户端-服务器Client-Server架构是唯一的选择而不是点对点P2P。在C/S架构中有一个权威的服务器Server作为“裁判”。所有客户端的操作移动、开枪都发送到服务器由服务器进行验证、计算比如计算子弹是否命中然后将结果广播给所有客户端。这样做有几个关键好处防作弊核心逻辑和判定在服务器客户端只是输入和表现层极大增加了作弊难度。一致性服务器是唯一的事实来源避免了不同客户端之间状态不一致的问题。可扩展性更容易管理玩家连接、房间匹配和游戏逻辑。对于独立开发者或小型团队我们通常采用“监听服务器Listen Server”模式。即其中一个玩家同时兼任客户端和服务器。这省去了单独部署专用服务器的成本和复杂度非常适合原型开发和局域网游戏。Unity的主流网络框架如Mirror、Netcode for GameObjects (NGO)都对此有很好的支持。2.2 网络框架选型Mirror vs Netcode for GameObjects这是早期最重要的技术决策之一。目前Unity生态中两个最主流的免费网络框架是Mirror和Unity官方推出的Netcode for GameObjects。Mirror可以看作是早期非常流行的UNET的高性能、社区维护版。它成熟、稳定拥有庞大的社区和丰富的插件生态比如用于房间管理的Mirror.Discovery用于中继服务器的Steamworks.Net。它的API直观学习曲线相对平缓文档和教程资源极其丰富。如果你搜索“Unity 多人游戏教程”十有八九会找到基于Mirror的。Netcode for GameObjects (NGO)Unity官方力推的新一代网络解决方案与Unity服务如Unity Gaming Services深度集成旨在提供更平滑的云端部署和在线服务体验。它采用了最新的网络传输层性能有潜力更好并且背靠官方长期支持有保障。我的选择与理由对于这个从零开始的项目我强烈推荐从Mirror开始。原因有三第一社区资源无敌。你遇到的几乎任何问题都能在论坛、GitHub或视频教程中找到答案。第二API设计对新手更友好能让你快速理解网络同步的基本概念如[Command],[ClientRpc],[SyncVar]。第三它的“监听服务器”模式开箱即用调试方便。等你对多人游戏的核心机制了然于胸后再研究NGO或更底层的Transport Layer会容易得多。注意无论选择哪个框架一旦开始尽量不要中途更换。它们的编程模型和概念有较大差异迁移成本很高。2.3 游戏核心循环与状态同步策略一个FPS游戏的核心循环可以简化为输入 - 逻辑处理 - 渲染。在多人环境下这个循环变得复杂客户端预测Client-side Prediction玩家按下移动键角色不能等到服务器确认后才移动那样延迟感会让人崩溃。所以客户端需要“预测”自己的移动立刻在本地表现出来。服务器权威Server Authority服务器收到移动指令后在权威的游戏状态下执行并计算最终位置。状态同步与调和Reconciliation服务器将权威状态位置、血量等定期同步给客户端。如果客户端的预测位置与服务器发回的权威位置有差异客户端需要进行“调和”平滑地修正自己的位置。对于射击游戏还有一个更关键的模块射击判定。这里必须采用“服务器权威判定”。即客户端开枪时只向服务器发送“我在某个时刻朝某个方向开枪了”的信息。服务器根据收到信息时的游戏状态考虑网络延迟需要进行回溯计算子弹是否命中目标。这能有效防止“延迟开枪”Lag Shoot等不公平现象。3. 第一步搭建基础的单人FPS原型在引入网络之前我们必须先有一个功能完整的单人FPS原型。这一步的所有代码和预制体之后都能被网络框架复用。目标是实现角色移动、视角控制、武器系统、射击与伤害、简单的敌人AI。3.1 角色控制器第一人称移动与视角不要使用Unity标准的CharacterController组件来做第一人称控制器它的移动手感通常不符合FPS的高要求。我们采用更灵活的方式使用Rigidbody或直接操作Transform并自己处理输入。using UnityEngine; public class FPSController : MonoBehaviour { public float walkSpeed 5f; public float runSpeed 10f; public float jumpForce 5f; public float mouseSensitivity 2f; public Transform cameraTransform; // 摄像机应作为角色的子物体 private float verticalRotation 0f; private float verticalVelocity 0f; private CharacterController characterController; private bool isGrounded; void Start() { characterController GetComponentCharacterController(); Cursor.lockState CursorLockMode.Locked; // 锁定鼠标到屏幕中心 } void Update() { // 1. 处理鼠标视角旋转 float mouseX Input.GetAxis(Mouse X) * mouseSensitivity; float mouseY Input.GetAxis(Mouse Y) * mouseSensitivity; verticalRotation - mouseY; // 注意是减号因为鼠标Y向上是正但我们要向上看需要绕X轴负向旋转 verticalRotation Mathf.Clamp(verticalRotation, -90f, 90f); // 限制上下视角 cameraTransform.localRotation Quaternion.Euler(verticalRotation, 0, 0); transform.Rotate(0, mouseX, 0); // 左右旋转整个角色 // 2. 处理移动 float speed Input.GetKey(KeyCode.LeftShift) ? runSpeed : walkSpeed; float moveX Input.GetAxis(Horizontal) * speed; float moveZ Input.GetAxis(Vertical) * speed; Vector3 movement transform.right * moveX transform.forward * moveZ; // 3. 处理跳跃与重力 if (characterController.isGrounded) { verticalVelocity -0.5f; // 一个小的向下力确保贴地 if (Input.GetButtonDown(Jump)) { verticalVelocity jumpForce; } } else { verticalVelocity Physics.gravity.y * Time.deltaTime; // 应用重力 } movement.y verticalVelocity; characterController.Move(movement * Time.deltaTime); } }实操心得视角旋转的代码要放在Update中而移动逻辑特别是涉及CharacterController.Move的最好放在FixedUpdate中以保证与物理系统的同步避免因帧率波动导致移动速度变化。上面的代码做了简化实际项目中应将输入采集和逻辑执行分离。3.2 武器系统射线检测与伤害计算FPS游戏通常使用射线检测Raycast来模拟子弹。当玩家开枪时从摄像机中心发射一条射线检测击中的物体。public class Weapon : MonoBehaviour { public float damage 10f; public float range 100f; public Camera fpsCam; public ParticleSystem muzzleFlash; public GameObject impactEffect; void Update() { if (Input.GetButtonDown(Fire1)) { Shoot(); } } void Shoot() { muzzleFlash.Play(); // 播放枪口火焰特效 RaycastHit hit; if (Physics.Raycast(fpsCam.transform.position, fpsCam.transform.forward, out hit, range)) { // 击中物体 Debug.Log(hit.transform.name); // 实例击打效果如弹孔 GameObject impactGO Instantiate(impactEffect, hit.point, Quaternion.LookRotation(hit.normal)); Destroy(impactGO, 2f); // 尝试对击中的目标造成伤害 Target target hit.transform.GetComponentTarget(); if (target ! null) { target.TakeDamage(damage); } } } }注意事项在单人原型中这样的伤害计算是没问题的。但记住在多人版本中这段Shoot和TakeDamage的逻辑必须移到服务器端去执行。客户端只负责播放动画、音效和发射射线的视觉效果如曳光弹真正的命中判定由服务器权威进行。3.3 敌人AI与生命值系统创建一个简单的敌人它有一个生命值组件当生命值降到0以下时死亡。同时可以给它添加一个最简单的AI当玩家进入一定范围时朝向玩家并移动。public class EnemyHealth : MonoBehaviour { public float health 50f; public void TakeDamage(float amount) { health - amount; if (health 0f) { Die(); } } void Die() { // 播放死亡动画销毁物体或设置为非激活 Destroy(gameObject); } }至此一个基础的单人FPS原型就完成了。你可以移动、环顾四周、开枪射击并消灭敌人。接下来我们将把这个单人游戏改造成多人游戏。4. 第二步引入Mirror网络框架并同步玩家现在进入核心环节让多个玩家出现在同一个世界里。首先通过Unity的Package Manager或Asset Store安装Mirror。然后我们需要创建网络化的玩家预制体。4.1 创建网络玩家预制体在场景中创建一个空物体命名为NetworkManager。为其添加NetworkManager和NetworkManagerHUD组件。NetworkManager是Mirror的核心管理器负责玩家生成、场景切换等。NetworkManagerHUD会提供一个简单的UI方便我们测试主机/连接。创建你的玩家角色预制体例如PlayerPrefab。它必须挂载NetworkIdentity组件勾选Local Player Authority这是Mirror识别网络对象的基础。在NetworkManager组件的Player Prefab槽中拖入你的PlayerPrefab。4.2 同步玩家移动与旋转玩家的移动和视角旋转需要从本地客户端同步到服务器再由服务器同步给其他客户端。我们需要修改之前的FPSController脚本。using Mirror; using UnityEngine; public class NetworkFPSController : NetworkBehaviour { public float walkSpeed 5f; public float mouseSensitivity 2f; public Transform cameraTransform; private CharacterController characterController; private float verticalRotation 0f; void Start() { characterController GetComponentCharacterController(); // 关键只有本地玩家才能控制这个角色和摄像机 if (isLocalPlayer) { cameraTransform.gameObject.SetActive(true); // 启用本地玩家的摄像机 Cursor.lockState CursorLockMode.Locked; } else { // 禁用其他玩家角色的摄像机/音频监听器等 cameraTransform.gameObject.SetActive(false); // 可能还需要禁用控制脚本但NetworkBehaviour会处理 } } void Update() { // 只有本地玩家才能处理输入 if (!isLocalPlayer) return; // 处理视角旋转只在本地生效不需要同步 float mouseX Input.GetAxis(Mouse X) * mouseSensitivity; float mouseY Input.GetAxis(Mouse Y) * mouseSensitivity; verticalRotation - mouseY; verticalRotation Mathf.Clamp(verticalRotation, -90f, 90f); cameraTransform.localRotation Quaternion.Euler(verticalRotation, 0, 0); transform.Rotate(0, mouseX, 0); // 处理移动需要同步到服务器 float moveX Input.GetAxis(Horizontal); float moveZ Input.GetAxis(Vertical); Vector3 movement (transform.right * moveX transform.forward * moveZ).normalized * walkSpeed; characterController.SimpleMove(movement); // SimpleMove会自动处理重力 // 由于移动是连续的我们通过一个[Command]来同步位置和旋转 // 但更高效的做法是使用NetworkTransform组件或者同步速度矢量 } }上面的代码处理了本地控制但移动并没有同步。为了同步位置最简单的方法是使用Mirror提供的NetworkTransform组件。将它添加到你的PlayerPrefab上它会自动同步物体的位置、旋转和缩放。但是对于FPS游戏直接使用NetworkTransform同步玩家位置会有问题它会导致其他玩家看到的角色移动不流畅因为网络更新频率低于本地渲染帧率。因此我们需要实现客户端预测和服务器调和。这是一个高级话题但Mirror社区有成熟的解决方案比如使用CharacterController配合自定义的网络脚本来同步速度或输入指令而不是直接同步位置。对于入门项目可以暂时使用NetworkTransform并将同步间隔调小同时设置适当的插值Interpolation来平滑运动。4.3 同步玩家动画与状态玩家的动画 idle, run, jump 也需要同步。为玩家预制体添加NetworkAnimator组件并将其Animator引用拖入。NetworkAnimator会自动同步Animator的参数如Speed,IsGrounded和触发器。你需要设置好Animator Controller并确保在NetworkFPSController脚本中设置这些动画参数。NetworkAnimator会负责将这些参数的变化同步给所有客户端。5. 第三步实现权威的服务器端射击与伤害系统这是多人FPS最核心、最易出错的部分。我们必须确保开枪和伤害判定由服务器说了算。5.1 重构武器系统客户端发起服务器判定客户端检测开枪输入播放本地特效枪口火焰、上膛动画然后向服务器发送一个[Command]。服务器收到[Command]后执行权威的射线检测计算命中。如果命中则在服务器端调用受伤目标的[ClientRpc]来同步伤害效果并通知被击中的客户端。using Mirror; using UnityEngine; public class NetworkWeapon : NetworkBehaviour { public float damage 10f; public float range 100f; public ParticleSystem muzzleFlash; public GameObject impactEffectPrefab; [Command] // 这个函数在客户端调用但在服务器上运行 public void CmdShoot(Vector3 shotOrigin, Vector3 shotDirection) { // 服务器端权威射击判定 RaycastHit hit; if (Physics.Raycast(shotOrigin, shotDirection, out hit, range)) { // 击中物体 Debug.Log($Server: Hit {hit.transform.name}); // 在所有人客户端生成击打效果弹孔、火花 RpcSpawnImpactEffect(hit.point, hit.normal); // 尝试造成伤害 NetworkIdentity hitIdentity hit.transform.GetComponentNetworkIdentity(); if (hitIdentity ! null) { // 找到被击中物体的网络标识并调用其上的一个[Command]或直接修改其SyncVar // 这里假设被击中的物体有一个名为Target的脚本上面有[Server]方法TakeDamage EnemyHealth enemyHealth hit.transform.GetComponentEnemyHealth(); if (enemyHealth ! null) { enemyHealth.TakeDamage(damage); // 这个方法必须在服务器端有权限执行 } } } } [ClientRpc] // 这个函数在服务器调用在所有客户端运行 void RpcSpawnImpactEffect(Vector3 position, Vector3 normal) { // 在所有客户端实例化击打效果 GameObject impact Instantiate(impactEffectPrefab, position, Quaternion.LookRotation(normal)); Destroy(impact, 2f); } // 本地客户端调用 public void LocalShoot() { if (!isLocalPlayer) return; // 播放本地特效只有自己能看到更流畅的反馈 muzzleFlash.Play(); // 播放开枪音效... // 向服务器发送射击指令传递射击原点摄像机位置和方向 CmdShoot(Camera.main.transform.position, Camera.main.transform.forward); } void Update() { if (!isLocalPlayer) return; if (Input.GetButtonDown(Fire1)) { LocalShoot(); } } }关键点解析[Command]前缀为Cmd。由客户端调用但实际在拥有该物体权威的服务器上执行。用于执行需要服务器验证的关键操作如射击、使用技能、拾取物品。[ClientRpc]前缀为Rpc。由服务器调用在所有客户端或指定的目标客户端上执行。用于同步视觉效果、音效等非权威但需要大家看到的状态。参数传递CmdShoot传递了shotOrigin和shotDirection。这是因为服务器不能信任客户端传来的“我击中了谁”的结果它必须根据客户端声称的“开枪位置和方向”在自己的权威游戏世界里重新进行射线检测。这能防止客户端篡改命中结果。5.2 实现网络化的生命值与死亡系统敌人的生命值EnemyHealth脚本也需要网络化。我们需要使用[SyncVar]来同步生命值并在生命值变化或死亡时做出网络响应。using Mirror; using UnityEngine; public class NetworkEnemyHealth : NetworkBehaviour { [SyncVar(hook nameof(OnHealthChanged))] // SyncVar同步变量hook在值变化时调用指定函数 public float health 50f; public void TakeDamage(float amount) { // 这个方法应该只能被服务器调用 if (!isServer) return; health - amount; if (health 0f) { Die(); } } void OnHealthChanged(float oldHealth, float newHealth) { // 在所有客户端上当health同步变量发生变化时这个函数会被调用 // 可以在这里更新血条UI等 Debug.Log($Health changed from {oldHealth} to {newHealth}); } [Server] // 确保这个方法只在服务器端执行 void Die() { // 服务器通知所有客户端播放死亡动画或效果 RpcDie(); // 服务器稍后销毁这个网络对象 Invoke(nameof(DestroySelf), 2f); } [ClientRpc] void RpcDie() { // 在所有客户端上播放死亡动画、音效 GetComponentAnimator().SetTrigger(Die); // 禁用碰撞体、AI脚本等 GetComponentCollider().enabled false; } [Server] void DestroySelf() { NetworkServer.Destroy(gameObject); } }注意事项[SyncVar]的更新不是每帧发生的它有固定的同步速率。对于需要即时反馈的UI如血条使用hook回调是标准的做法。对于敌人的死亡通常由服务器触发一个[ClientRpc]来播放死亡动画然后由服务器在延迟后销毁网络对象确保所有客户端都看到了死亡效果。6. 第四步处理延迟、预测与反作弊基础多人游戏无法避免网络延迟Ping。100ms的延迟在快节奏的FPS中已经非常明显。我们需要一些策略来提升手感并建立基础的公平环境。6.1 延迟补偿服务器端回溯Lag Compensation这是FPS游戏中最关键的延迟处理技术。当服务器收到客户端“我在时间T开枪了”的消息时客户端的游戏世界已经是TLatency时间之后的状态了。如果服务器直接用当前时间做射线检测子弹可能会打不中在客户端看来明明瞄准了的敌人。解决方案服务器进行回溯。服务器需要记录所有玩家过去一段时间比如1秒内的位置和旋转快照。当收到射击命令时服务器会“时光倒流”到客户端开枪的那个时间点T在那个历史状态的世界里进行射线检测。这样就能与客户端当时的视角保持一致。实现思路概念在服务器上为每个玩家维护一个包含时间戳和状态位置、旋转的队列。当服务器收到CmdShoot(T, origin, direction)时它从队列中找到时间T附近的状态数据。服务器将所有其他玩家的位置和旋转“回滚”到时间T的状态。在回滚后的状态下执行射线检测。检测完毕后恢复所有玩家的状态。Mirror本身不提供开箱即用的高级延迟补偿但你可以自己实现状态快照和回滚逻辑或者寻找社区插件。对于入门项目可以暂时不实现完整的回滚但必须理解这个概念。一个简化的方案是服务器在判定时将射线检测的起点稍微向射击方向的反方向移动一点根据平均延迟估算作为一种近似的补偿。6.2 客户端预测与调和Client-side Prediction Reconciliation对于玩家自身的移动我们采用了客户端预测立刻移动。但服务器可能因为延迟或碰撞等原因否定了客户端的某些移动。这时就需要调和。调和过程客户端发送移动指令给服务器并记录下这个指令和本地预测后的状态。服务器处理指令计算权威状态然后广播给客户端。客户端收到服务器的权威状态后与自己预测的状态对比。如果有差异客户端不是简单地“瞬移”到服务器位置那样会卡顿而是计算一个修正量并平滑地比如用插值将角色移动到权威位置同时重新应用服务器确认后尚未处理的客户端输入。这是一个复杂的话题通常需要同步的是输入指令序列而不是最终位置。Mirror的NetworkTransform组件对于简单的物体同步足够但对于需要精准预测和调和的玩家移动许多开发者会选择自己实现基于指令的同步或者使用像KCP这样的可靠UDP传输层进行更底层的控制。6.3 基础反作弊设计思路完全杜绝作弊是不可能的但可以提高门槛。服务器权威这是最重要的防线。所有关键逻辑移动验证、伤害计算、经济系统必须在服务器执行。客户端只是“视图”。输入验证服务器检查客户端发送的输入是否合理。例如移动速度是否超过最大值开枪频率是否过高是否在不可能的位置开枪如穿墙。状态验证服务器定期检查客户端报告的状态如位置是否与服务器计算的状态在合理误差范围内。如果偏差过大可以强制纠正或踢出玩家。信息隐藏不要将不必要的游戏状态发送给客户端。例如不应该让客户端知道地图上所有物品的位置或者墙后所有敌人的位置。这可以通过服务器只同步玩家视野内的信息来实现虽然实现成本高。对于我们的项目首要任务是确保射击判定和伤害计算绝对在服务器端这是最有效的一步。7. 第五步游戏状态管理、UI与测试部署最后一步我们将游戏组装起来添加一些必要的系统并让它能真正运行起来。7.1 游戏状态管理房间、匹配与回合一个完整的FPS游戏需要管理游戏流程。我们可以利用Mirror的NetworkRoomManager作为起点。它提供了现成的房间管理、玩家就绪、开始游戏等功能。创建房间场景和游戏场景NetworkRoomManager需要两个场景一个用于玩家大厅Room Scene一个用于实际游戏Game Scene。配置NetworkRoomManager创建一个空物体添加NetworkRoomManager组件。将你的PlayerPrefab拖入Room Player Prefab和Game Player Prefab槽通常可以用同一个。设置好房间场景和游戏场景。自定义房间逻辑你可以继承NetworkRoomManager来添加自定义逻辑比如团队选择、地图投票、回合倒计时等。游戏状态同步使用一个单独的、带有NetworkIdentity的GameManager对象来同步全局游戏状态如当前回合、剩余时间、队伍分数等。使用[SyncVar]或自定义网络消息来同步这些信息。7.2 网络化用户界面UI游戏UI如血量条、弹药数、计分板也需要同步。玩家自身状态如血量和弹药可以通过玩家脚本上的[SyncVar]配合hook来更新本地UI。全局状态如计分板可以通过GameManager上的[SyncVar]或[ClientRpc]来更新所有客户端的UI。关键技巧UI更新应该放在OnHealthChanged这类hook函数中或者通过订阅[SyncVar]变化的事件而不是在Update里不断查询。这更高效。7.3 测试与调试本地测试在Unity编辑器中你可以通过NetworkManagerHUD同时启动多个客户端和一个服务器选择“Host”模式。这是最快速的调试方式。构建测试将游戏构建成可执行文件.exe。运行一个实例作为主机Host再运行一个或多个实例作为客户端Client输入主机的局域网IP地址进行连接。测试局域网联机。调试工具Mirror在运行时提供了简单的网络信息统计。你也可以使用Unity的Profiler查看网络流量和对象生成情况。7.4 部署与后续优化部署对于监听服务器模式一个玩家创建房间其他人加入即可。对于小型团队这足够了。如果你需要专用的、24小时运行的服务器就需要将服务器部分单独构建为一个“Headless”版本无图形界面部署到云服务器上。性能优化网络流量减少同步频率只同步变化的数据。使用压缩技术。兴趣管理AOI只同步玩家附近的其他实体和状态。Mirror有NetworkProximityChecker组件可以作为简单实现。客户端性能使用遮挡剔除Occlusion Culling、LODLevel of Detail来减轻渲染压力。8. 常见问题与排查技巧实录在实际开发中你一定会遇到各种网络同步问题。这里记录一些典型坑点和解决方法。问题现象可能原因排查与解决思路其他玩家角色移动卡顿、瞬移1.NetworkTransform同步频率过低或插值设置不当。2. 没有使用客户端预测或预测/调和算法有问题。1. 检查NetworkTransform的sendInterval尝试调小如0.05s。确保插值Interpolation已开启。2. 对于玩家移动考虑实现基于输入指令的同步而非直接同步位置。开枪后自己看到命中但服务器没判定伤害1. 射击判定逻辑仍在客户端未使用[Command]发送到服务器。2. 服务器端的射线检测条件如LayerMask与客户端不同。3. 没有进行延迟补偿服务器检测时目标已移动。1. 确保Shoot逻辑核心在[Command]方法中。2. 在服务器和客户端的射线检测中打印日志对比检测结果。3. 实现或简化实现服务器端回溯。敌人死亡动画只在部分客户端播放1. 死亡逻辑如Destroy只在某个客户端调用未通过网络同步。2. 播放动画的代码放在了只有本地客户端执行的块中如if(isLocalPlayer)。1. 确保死亡由服务器触发[Server]并通过[ClientRpc]通知所有客户端播放动画。2. 销毁对象使用NetworkServer.Destroy()。[Command]或[ClientRpc]不执行1. 函数命名未遵循CmdXxx/RpcXxx规范。2. 调用[Command]的脚本所在的物体没有NetworkIdentity或该NetworkIdentity没有分配给玩家。3. 从客户端调用的[Command]其所在的物体不是该客户端拥有的hasAuthority为false。1. 检查函数命名。2. 确保脚本挂在有NetworkIdentity的物体上并且该物体是玩家预制体或其子物体。3.[Command]只能从该客户端拥有权威的物体上调用。连接失败错误提示1. 网络地址或端口错误。2. 防火墙/路由器阻止了连接。3.NetworkManager中的玩家预制体未正确分配。1. 确认IP和端口。局域网连接使用本地IP。2. 在路由器设置端口转发对于公网连接或关闭防火墙测试。3. 检查NetworkManager面板的所有预制体槽位。一个宝贵的调试习惯大量使用Debug.Log并在日志中明确标出是[Server]还是[Client]输出的以及是哪个玩家。这能帮你理清逻辑的执行流。Mirror也提供了详细的日志选项可以在NetworkManager中开启。走到这里你已经完成了一个具备核心功能的多人FPS游戏框架。从单人原型到多人同步从客户端表现到服务器权威这套流程覆盖了最关键的环节。记住多人游戏开发是一个深水区充满了细节和挑战。不要指望第一个版本就完美无缺。先让它跑起来再逐步迭代优化网络同步手感添加更多的功能如多种武器、技能、地图交互。最关键的是理解了“服务器是上帝客户端只是信徒”这一核心原则并在每一次功能开发中都贯彻它。
返回列表