这次我们来看一个在 Unity 游戏开发中实现高丝滑、低延迟联机功能的实战方案Photon Unity Networking (PUN)。对于独立开发者和小型团队来说自己搭建一套稳定、高效的网络同步框架门槛极高而 Photon 提供了一套成熟的云端托管服务与 SDK能让你快速为游戏注入多人联机能力。本文将聚焦于如何利用 Photon PUN 实现流畅的实时同步从环境配置、核心概念到实战编码与优化带你走通从零到联机的完整流程。如果你正在开发一款需要实时对战、合作闯关或大型多人在线体验的 Unity 游戏并且希望避免复杂的服务器运维那么 Photon 是一个值得重点评估的选择。它的核心优势在于提供了托管式的网络后端开发者只需关注客户端的游戏逻辑。本文将详细拆解 Photon PUN 的工作流并通过一个简单的玩家移动同步示例演示如何达成“高丝滑、低延迟”的联机效果。1. 核心能力速览在深入代码之前我们先快速了解 Photon PUN 的核心特性和能力边界这有助于判断它是否适合你的项目。能力项说明服务类型托管式云端网络服务SaaS提供房间管理、玩家匹配、消息转发等后端功能。网络模型权威服务器Authoritative Server模型。Photon Cloud 作为中继服务器可配置为客户端主导或服务器校验逻辑。主要SDKPhoton Unity Networking (PUN 2)最常用面向状态同步。Photon Fusion新一代SDK提供更精确的输入同步和预测回滚适合竞技游戏。开发门槛中等偏低。无需自建物理服务器Unity 集成度高API 设计相对直观。延迟表现依赖区域和网络质量。Photon 在全球有多个数据中心Region选择离玩家最近区域可显著降低延迟。通常在同区域下可做到 50-150ms 的往返延迟。连接方式支持 TCP/UDP。PUN 默认使用可靠的 UDP在延迟和可靠性间取得平衡。收费模式提供免费档CCU 20适用于开发和测试。正式上线需根据并发用户数CCU购买套餐。适合场景实时对战游戏如 MOBA、FPS、合作游戏、棋牌类、聊天室、大型房间制游戏。不适合场景超大规模无房间 MMO需自研或专用解决方案、对网络代码有极致控制需求的项目。2. 适用场景与使用边界Photon PUN 并非万能明确其适用边界能帮助你做出更合适的技术选型。它非常适合以下场景中小型团队或独立开发者无需投入运维成本快速验证联机玩法。房间制游戏如《Among Us》、《恐鬼症》等玩家创建或加入特定房间进行游戏。实时动作游戏需要同步玩家位置、旋转、动画状态等。回合制或状态同步游戏如棋牌、策略游戏同步游戏状态即可。需要注意的边界与限制服务器逻辑限制Photon Cloud 本身不运行你的游戏逻辑纯中继。复杂的游戏规则校验需要在客户端实现或使用 Photon Server自托管或 Fusion 的NetworkedObject状态权威。反作弊由于主要逻辑在客户端反作弊是薄弱环节。重要逻辑如伤害计算应做服务器校验。数据流量成本频繁同步大量数据如大量实体位置会产生流量需优化同步频率和内容。区域选择如果玩家分布在全球需要处理区域选择逻辑或使用 Photon 的“最佳区域”功能。版权与合规确保你的游戏内容符合 Photon 服务条款特别是涉及用户生成内容UGC时。3. 环境准备与前置条件开始编码前请确保你的开发环境已就绪。3.1 硬件与操作系统操作系统Windows 10/11 或 macOS。Unity 和 Photon 开发对此无特殊要求。硬件无特殊要求普通开发机即可。但建议保证网络连接稳定。3.2 软件环境Unity Hub Unity Editor建议使用 Unity 2021.3 LTS 或 2022.3 LTS 等长期支持版本。确保已安装。Photon 账户前往 Photon Engine 官网 注册一个免费账户。这是获取 App ID 的必需步骤。3.3 创建新 Unity 项目打开 Unity Hub点击“New Project”。选择核心模板如 3D Core。为项目命名例如PhotonNetworkDemo选择存储位置然后创建项目。4. 安装 Photon PUN 2 并配置这是将 Photon 集成到项目中的关键步骤。4.1 通过 Unity Package Manager 安装在 Unity Editor 中点击顶部菜单Window-Package Manager。在 Package Manager 窗口左上角点击“”按钮选择Add package from git URL...。输入 Photon PUN 2 的 Git URLhttps://github.com/ExitGames/PhotonUnityNetworking.git#upm点击“Add”。Unity 会下载并导入 PUN 2 包及其依赖项。4.2 配置 Photon 应用设置获取并设置 App ID登录你的 Photon Engine 仪表板。点击“Create a New App”。应用类型选择Photon PUN。应用名称输入你的游戏名称如MySmoothGame。创建后在应用管理页面找到App ID并复制它。回到 Unity 项目。安装 PUN 2 后首次导入通常会弹出 “Photon Unity Networking Setup Wizard” 窗口。如果没有可以通过菜单Window-Photon Unity Networking-PUN Wizard打开。在向导窗口中将复制的App ID粘贴到Photon App Id字段。点击 “Setup Project” 按钮。这会在你的项目Assets文件夹下创建Photon文件夹和必要的资源。4.3 重要文件说明配置完成后项目中会生成关键文件Assets/Photon/PhotonUnityNetworking/Resources/PhotonServerSettings.asset存放你的 App ID 和区域设置。务必将其加入版本控制忽略列表如 .gitignore避免泄露你的 App ID。5. 核心概念与工作流理解在写代码前理解 Photon PUN 的几个核心概念至关重要。5.1 核心组件PhotonView网络视野组件。任何需要通过网络同步的 GameObject 都必须挂载它。它赋予物体一个唯一的网络 ID并决定同步哪些数据。PhotonNetwork静态类提供连接、房间、玩家管理等核心 API 的入口。例如PhotonNetwork.ConnectUsingSettings()。IPunObservable接口。挂在有PhotonView的物体上用于自定义序列化你要同步的变量如位置、血量。5.2 基本工作流连接与大厅客户端调用PhotonNetwork.ConnectUsingSettings()连接到 Photon Cloud。成功后进入全局大厅Lobby。创建或加入房间玩家可以创建CreateRoom一个特定名称的房间或随机加入JoinRandomRoom一个可用房间或加入指定房间JoinRoom。房间内同步所有加入同一房间的玩家构成一个对等网络。通过PhotonView组件利用 RPC远程过程调用或通过IPunObservable接口同步变量来同步游戏状态。离开与断开玩家离开房间LeaveRoom或断开连接。5.3 数据同步方式RPCRemote Procedure Call 用于调用特定方法。例如玩家开枪时调用一个RPC方法在所有客户端播放开枪动画和音效。[PunRPC] void PlayerShootRPC(int shooterId) { // 在所有客户端上执行开枪逻辑 }通过 PhotonView 观察Observing组件 可以自动同步Transform组件的位置、旋转。配置简单但流量可能较大。通过 IPunObservable 接口自定义同步 最灵活高效的方式。你可以精确控制同步哪些变量如Vector3 position,float health以及同步的频率。public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { if (stream.IsWriting) { // 本地玩家发送数据 stream.SendNext(transform.position); stream.SendNext(health); } else { // 远程玩家接收数据 transform.position (Vector3)stream.ReceiveNext(); health (float)stream.ReceiveNext(); } }6. 实战实现一个高丝滑的玩家同步我们来创建一个简单的场景实现多个玩家控制立方体移动并追求“高丝滑”的同步效果。6.1 场景搭建在场景中创建一个平面Plane作为地面。创建一个立方体Cube预制体命名为PlayerPrefab。为PlayerPrefab添加一个PhotonView组件。在PhotonView组件的Observed Components列表中默认已经添加了Transform (Synchronize)。为了更好的控制我们先移除它。我们将使用自定义同步。6.2 创建网络管理器创建一个空的 GameObject命名为NetworkManager并为其附加一个新的 C# 脚本GameNetworkManager.cs。using UnityEngine; using Photon.Pun; using Photon.Realtime; using System.Collections.Generic; public class GameNetworkManager : MonoBehaviourPunCallbacks { [SerializeField] private GameObject playerPrefab; // 在Inspector中拖入PlayerPrefab [SerializeField] private Transform[] spawnPoints; // 出生点数组 void Start() { // 连接到Photon服务器使用PhotonServerSettings中的配置 PhotonNetwork.ConnectUsingSettings(); Debug.Log(正在连接到Photon服务器...); } public override void OnConnectedToMaster() { Debug.Log(已连接到Photon服务器主节点。); // 加入默认大厅 PhotonNetwork.JoinLobby(); } public override void OnJoinedLobby() { Debug.Log(已进入大厅。); // 创建一个房间房间名随机最大4人 RoomOptions roomOptions new RoomOptions(); roomOptions.MaxPlayers 4; PhotonNetwork.JoinOrCreateRoom(TestRoom, roomOptions, TypedLobby.Default); } public override void OnJoinedRoom() { Debug.Log($已加入房间: {PhotonNetwork.CurrentRoom.Name}); SpawnPlayer(); } void SpawnPlayer() { if (playerPrefab null) { Debug.LogError(PlayerPrefab 未分配); return; } // 选择一个出生点简单轮询 int spawnIndex PhotonNetwork.CurrentRoom.PlayerCount - 1; Vector3 spawnPos spawnPoints.Length 0 ? spawnPoints[spawnIndex % spawnPoints.Length].position : Vector3.zero; // 实例化玩家角色。PhotonNetwork.Instantiate 会在所有客户端生成这个物体。 GameObject myPlayer PhotonNetwork.Instantiate(playerPrefab.name, spawnPos, Quaternion.identity); Debug.Log(玩家已生成。); } }将PlayerPrefab和几个空的 GameObject作为出生点拖拽到NetworkManager的脚本组件对应字段中。6.3 创建玩家控制与同步脚本为PlayerPrefab创建一个 C# 脚本SmoothPlayerController.cs。这个脚本将处理本地输入、网络同步并实现插值Interpolation以达到丝滑效果。using UnityEngine; using Photon.Pun; public class SmoothPlayerController : MonoBehaviourPun, IPunObservable { [Header(Movement Settings)] public float moveSpeed 5f; public float turnSpeed 180f; [Header(Network Smoothing)] public float interpolationSmoothing 10f; // 位置插值平滑系数 public float rotationSmoothing 10f; // 旋转插值平滑系数 private Vector3 networkPosition; // 从网络接收到的目标位置 private Quaternion networkRotation; // 从网络接收到的目标旋转 private CharacterController characterController; // 使用CharacterController处理移动和碰撞 private Camera playerCamera; void Start() { characterController GetComponentCharacterController(); if (characterController null) { characterController gameObject.AddComponentCharacterController(); characterController.height 2.0f; characterController.radius 0.5f; } // 如果是本地控制的玩家设置主摄像机跟随 if (photonView.IsMine) { playerCamera Camera.main; if (playerCamera ! null) { // 创建一个简单的摄像机跟随脚本这里简化处理 playerCamera.transform.SetParent(transform); playerCamera.transform.localPosition new Vector3(0, 2, -5); playerCamera.transform.LookAt(transform); } // 初始化网络位置为当前位置避免初始跳跃 networkPosition transform.position; networkRotation transform.rotation; } else { // 如果是远程玩家禁用本地控制组件如本脚本中的输入处理部分将由网络数据驱动 // 我们通过photonView.IsMine在Update中区分逻辑 } } void Update() { if (photonView.IsMine) { // --- 本地玩家处理输入并发送数据 --- HandleLocalInput(); } else { // --- 远程玩家根据接收到的网络数据平滑移动 --- SmoothlyUpdatePositionAndRotation(); } } void HandleLocalInput() { float horizontal Input.GetAxis(Horizontal); float vertical Input.GetAxis(Vertical); Vector3 moveDirection new Vector3(horizontal, 0, vertical).normalized; if (moveDirection.magnitude 0.1f) { // 计算移动和旋转 float targetAngle Mathf.Atan2(moveDirection.x, moveDirection.z) * Mathf.Rad2Deg; float angle Mathf.SmoothDampAngle(transform.eulerAngles.y, targetAngle, ref turnVelocity, 0.1f); transform.rotation Quaternion.Euler(0, angle, 0); Vector3 moveVector Quaternion.Euler(0, targetAngle, 0) * Vector3.forward; characterController.SimpleMove(moveVector * moveSpeed); } } private float turnVelocity 0; // 用于SmoothDampAngle的引用变量 void SmoothlyUpdatePositionAndRotation() { // 使用 Lerp 或 Slerp 平滑地过渡到网络目标位置和旋转 // 这是实现“丝滑”的关键避免了瞬移 if (Vector3.Distance(transform.position, networkPosition) 0.1f) { transform.position Vector3.Lerp(transform.position, networkPosition, Time.deltaTime * interpolationSmoothing); } transform.rotation Quaternion.Slerp(transform.rotation, networkRotation, Time.deltaTime * rotationSmoothing); } // --- IPunObservable 接口实现 --- public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { if (stream.IsWriting) { // 本地玩家发送位置和旋转数据 stream.SendNext(transform.position); stream.SendNext(transform.rotation); } else { // 远程玩家接收位置和旋转数据并更新目标值 networkPosition (Vector3)stream.ReceiveNext(); networkRotation (Quaternion)stream.ReceiveNext(); } } }将此脚本附加到PlayerPrefab上。同时确保PlayerPrefab的PhotonView组件中Observed Components列表里添加了这个SmoothPlayerController脚本因为其实现了IPunObservable。6.4 配置 PhotonView选中PlayerPrefab确保其PhotonView组件设置如下Ownership Transfer: 通常设为Fixed所有权不转移或Takeover可请求转移。本例中设为Fixed。Observed Components: 列表应包含SmoothPlayerController (Script)。确保Transform不在列表中因为我们通过脚本自定义同步。Synchronization: 保持为Unreliable On Change或Reliable。对于频繁的位置更新Unreliable效率更高能容忍少量丢包。7. 运行测试与效果验证7.1 单机测试模拟多玩家在 Unity Editor 中点击播放按钮。观察 Console 窗口日志应显示连接、加入房间、生成玩家成功。你可以用 WASD 控制生成的立方体移动。目前只有一个客户端。7.2 多客户端联机测试实现“联机”这是验证同步效果的关键。构建独立客户端点击菜单File-Build Settings。添加当前场景选择目标平台如 PC, Mac Linux Standalone。点击Build And Run将游戏构建到一个新文件夹如Builds/Client1并运行。这是第一个客户端。在 Unity Editor 中运行不要关闭刚才构建的客户端。回到 Unity Editor再次点击播放按钮。这是第二个客户端。观察同步在 Unity Editor客户端2中移动你的角色。切换到构建的独立客户端客户端1窗口你应该能看到客户端2的角色在平滑地移动而不是瞬移。反之亦然在独立客户端中移动在 Editor 中观察。效果验证点连接成功两个客户端 Console 都显示加入同一房间。玩家生成每个客户端场景中应有两个立方体代表两个玩家。输入控制隔离每个客户端只能控制自己生成的玩家photonView.IsMine为 true 的那个。丝滑同步远程玩家的移动是平滑插值的没有明显的卡顿或跳跃在良好网络下。你可以尝试在SmoothPlayerController中调整interpolationSmoothing和rotationSmoothing参数来优化手感。8. 性能优化与降低延迟的关键技巧“高丝滑低延迟”是目标以下是实现它的核心优化策略8.1 网络流量优化降低同步频率不要在Update()中每帧都调用RPC或发送大量数据。对于位置同步可以在FixedUpdate()中处理并考虑设置一个发送速率如每秒 10-20 次。压缩数据使用PhotonNetwork.SerializationRate控制全局序列化频率。在OnPhotonSerializeView中只同步必要的、变化的数据。例如如果玩家静止可以减少甚至跳过位置发送。对Vector3或Quaternion进行精度压缩如使用Half精度或自定义压缩。// 示例压缩位置简单版将float转为short public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { if (stream.IsWriting) { short x (short)(transform.position.x * 100); // 放大100倍保留两位小数精度 short z (short)(transform.position.z * 100); stream.SendNext(x); stream.SendNext(z); } else { short x (short)stream.ReceiveNext(); short z (short)stream.ReceiveNext(); networkPosition new Vector3(x / 100f, transform.position.y, z / 100f); } }选择正确的可靠性频繁更新的数据如位置使用Unreliable或Unreliable On Change关键事件如玩家死亡、得分使用Reliable。8.2 客户端预测与插值实现“丝滑”本文的示例已经实现了基础的插值Lerp/Slerp。更高级的方案包括状态插值如示例所示存储网络最新状态并在本地平滑过渡。预测Prediction对于本地玩家的输入可以立即应用移动客户端预测然后等待服务器权威状态进行校正。这需要更复杂的逻辑PUN 基础版支持有限可考虑 Photon Fusion。延迟补偿在射击等游戏中服务器可能需要根据玩家的延迟Ping来回溯计算命中判定。8.3 Photon 服务器区域选择在PhotonServerSettings中可以设置固定的Fixed Region如asiaeuus。为了最佳体验可以在游戏启动时让玩家选择或使用PhotonNetwork.ConnectToBestCloudServer()让 SDK 自动选择 ping 最低的区域。8.4 使用 Photon Fusion 以获得更佳体验如果你的项目对网络同步要求极高如格斗、赛车、FPS强烈建议评估Photon Fusion。Fusion 提供了状态权威State Authority明确的服务器或客户端权威模式。输入权威Input Authority精确的输入同步和预测回滚Prediction Reconciliation。网络物理Networked Physics更好的物理对象同步。更细粒度的控制对网络 tick、插值、预测有更深层的控制。从 PUN 迁移到 Fusion 有一定学习成本但对于追求极致同步的项目是值得的。9. 常见问题与排查方法在开发过程中你可能会遇到以下问题问题现象可能原因排查方式解决方案无法连接到 Photon 服务器1. 网络问题防火墙、代理。2. App ID 错误或未设置。3. Photon 服务临时故障。1. 检查 Unity Console 错误信息。2. 确认PhotonServerSettings.asset中的 App ID 正确。3. 访问 Photon 官网状态页。1. 检查网络连接关闭可能冲突的代理。2. 重新复制粘贴 App ID 并运行 PUN Wizard。3. 等待服务恢复或联系支持。玩家预制体没有在其它客户端生成1. 预制体未放在Resources文件夹中。2. 预制体名称与实例化时使用的字符串不匹配。3.PhotonView组件未正确附加或配置。1. 检查预制体路径。2. 确认PhotonNetwork.Instantiate的第一个参数是预制体在Resources下的路径不含后缀。3. 检查预制体上的PhotonView。1. 确保玩家预制体位于Resources文件夹内或其子文件夹。2. 使用playerPrefab.name或硬编码的正确路径字符串。3. 确保PhotonView的Observed Components设置正确。远程玩家移动卡顿、瞬移1. 网络延迟高或丢包。2. 同步频率过高或过低。3. 未使用插值Interpolation。4. 在Update中直接设置transform.position。1. 使用PhotonNetwork.NetworkingClient.LoadBalancingPeer.RoundTripTime查看 Ping 值。2. 检查数据发送频率。3. 确认远程玩家脚本使用了插值逻辑。4. 审查同步代码。1. 选择正确的服务器区域。2. 优化同步频率和数据量。3. 实现并调整插值平滑系数。4. 远程玩家避免直接赋值位置应平滑过渡到目标位置。只有房主Master Client能看到效果某些逻辑只在PhotonNetwork.IsMasterClient为 true 时执行且未通过网络同步。检查代码中是否有仅由 Master Client 执行的生成、销毁或状态变更逻辑。使用RPC或通过PhotonView同步状态确保所有客户端执行相同的核心游戏逻辑。或者使用 Photon Fusion 的NetworkedObject。构建后运行报错或无法连接1.PhotonServerSettings未包含在构建中。2. 脚本在构建时被优化掉。1. 确认PhotonServerSettings.asset文件在Resources文件夹内。2. 检查构建日志。1. 确保Photon/PhotonUnityNetworking/Resources目录被打包。2. 检查 Player Settings 中的 Scripting Define Symbols确保PHOTON_UNITY_NETWORKING等符号存在。10. 最佳实践与项目建议预制体与资源管理所有需要通过PhotonNetwork.Instantiate生成的网络对象其预制体必须放在Resources文件夹下。使用PhotonView池频繁实例化/销毁网络对象会产生开销。对于子弹、特效等使用PhotonNetwork.PrefabPool进行对象池管理。房间属性与玩家自定义属性善用Room.CustomProperties和Player.CustomProperties来同步房间状态如游戏阶段、地图和玩家数据如准备状态、队伍而不是自己发消息。断开与重连处理实现OnDisconnected回调给玩家友好的提示并尝试自动重连。Photon 有自动重连机制但需要妥善处理重连后的状态恢复。安全性考虑客户端发送的数据不可信。对于关键逻辑如伤害计算、物品购买尽量由 Master Client 或未来迁移到 Fusion 的服务器端逻辑进行校验。测试策略本地多实例测试使用 Unity Editor 和多个构建的客户端进行基础功能测试。跨网络测试邀请朋友在不同网络环境下进行真实延迟和丢包测试。压力测试使用 Photon 的负载测试工具或模拟大量客户端观察性能表现。通过以上步骤你应该已经成功搭建了一个基于 Photon PUN 的 Unity 联机 demo并理解了实现平滑同步的核心原理。Photon 的强大在于它抽象了复杂的网络底层让开发者能更专注于游戏逻辑本身。从这个小 demo 出发你可以逐步扩展房间管理、游戏状态同步、角色动画同步、战斗系统等更复杂的功能。记住网络编程是一个“细节决定体验”的领域不断测试、测量Ping、丢包率、流量和优化是达成“高丝滑低延迟”目标的唯一路径。