尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity多人联机开发:Photon PUN2核心原理与高丝滑同步实战

Unity多人联机开发:Photon PUN2核心原理与高丝滑同步实战 1. 先搞清楚 Photon 联机到底能解决什么问题如果你在 Unity 里做联机功能还在纠结用 Mirror、Netcode for GameObjects 还是自己写 Socket那 Photon 是一个绕不开的选项。它最核心的价值不是“能做联机”而是帮你把网络同步、房间管理、玩家匹配这些底层脏活累活都包了让你能更专注于游戏逻辑本身。很多人一听到“联机”就觉得是“延迟高、卡顿、不同步”。Photon 的 PUN 2Photon Unity Networking方案主打的就是在常规网络环境下实现相对高丝滑、低延迟的体验。这里的“高丝滑”不是指零延迟而是指在正确的架构和参数下能有效掩盖网络波动让玩家的操作反馈和物体运动看起来连贯自然。它适合谁两类人最该看一是独立开发者或小团队没精力从传输协议开始造轮子二是已经用过 Unity 自带的 UNET已废弃或其他方案被同步问题折磨过想找个更成熟、文档更全的替代品。如果你只是做个本地双人同屏游戏那用不上它但如果你想做的是多人在线、实时对抗或合作比如类《Among Us》、《恐鬼症》这样的游戏Photon 能帮你省下大量时间。最关键的一点是Photon 采用了一种托管服务器Cloud和可自建服务器On-Premise结合的架构。对于学习和原型开发你可以直接用它的免费云服务不用自己租服务器等项目成熟了再考虑迁移到自己的服务器上以获得完全的控制权和更低的成本。这种灵活性是很多纯云服务或纯自建方案比不了的。2. 环境准备别在第一步就卡住在动手写代码之前先把环境理顺。很多“联机失败”的问题根源都在环境配置这一步。2.1 Unity 版本与 Photon SDK 获取首先你的 Unity 版本最好在 2019.4 LTS 或更新版本。太老的版本可能会遇到一些兼容性问题虽然 Photon 支持较旧的版本但为了减少不必要的麻烦用稳定的 LTS 版本是更稳妥的选择。获取 Photon PUN 2 有两种主流方式通过 Unity Asset Store推荐新手在 Unity 编辑器内打开 Asset Store搜索 “PUN 2”。找到后点击下载并导入到你的项目。这是最无脑的方式Unity 会自动处理一些依赖。通过 Photon 官网下载去 Photon Engine 官网注册一个账号免费在 Dashboard 里创建一个新应用选择 “PUN 2” 类型。然后就可以下载一个PhotonUnityNetworking.unitypackage包手动导入到你的 Unity 项目。注意无论哪种方式导入后 Unity 可能会要求你重启编辑器。这是正常现象因为 PUN 2 会安装一些编辑器扩展。2.2 最重要的配置App ID这是连接 Photon 云服务的“钥匙”。没有它一切免谈。导入 PUN 2 后Unity 菜单栏会多出一个Window - Photon Unity Networking - PUN Wizard。打开这个向导窗口它会自动检测你是否需要设置 App ID。你需要回到 Photon 官网的 Dashboard找到你创建的应用复制它的App ID。将App ID粘贴到 PUN Wizard 窗口的对应位置然后点击 “Setup Project”。完成这一步后你的项目里会生成一个PhotonServerSettings文件通常在Resources文件夹下。这个文件保存了你的 App ID 和其他连接设置。请务必把这个文件加入版本管理如 Git但注意不要公开你的 App ID。2.3 基础场景搭建在开始写网络逻辑前先搭一个最简单的测试场景创建一个新的 Unity 场景。在场景中创建一个空物体命名为NetworkManager。给这个空物体挂载一个脚本比如叫GameManager。这个脚本将负责处理网络连接的生命周期。再创建一个简单的玩家预制体Prefab比如就是一个 Cube 或 Capsule上面挂载一个脚本比如叫PlayerController。这个预制体就是每个玩家控制的对象。环境到这里就基本就绪了。核心就是Unity项目 导入的PUN2包 正确的App ID。如果这一步完成后编辑器没有报错PUN Wizard 显示绿色对勾那就可以进入下一步了。3. 核心流程从连接到房间一步步跑通联机功能可以拆解成几个明确的阶段连接服务器、加入或创建房间、实例化玩家、同步状态。我们按顺序来。3.1 连接 Photon 云服务器在GameManager脚本里我们首先要处理连接。通常会在游戏启动时比如在Start方法里进行连接。using Photon.Pun; using Photon.Realtime; using UnityEngine; public class GameManager : MonoBehaviourPunCallbacks { private void Start() { // 1. 连接到Photon服务器。使用PhotonNetwork.ConnectUsingSettings()是最简单的方式。 // 它会自动读取PhotonServerSettings里的配置。 PhotonNetwork.ConnectUsingSettings(); Debug.Log(正在尝试连接至Photon服务器...); } // 2. 重写回调方法处理连接成功后的逻辑 public override void OnConnectedToMaster() { Debug.Log(已成功连接至Photon服务器。); // 连接成功后通常立即尝试加入一个随机房间或大厅 // 这里我们先加入默认大厅Lobby以便获取房间列表 PhotonNetwork.JoinLobby(); } public override void OnJoinedLobby() { Debug.Log(已加入大厅。); // 进入大厅后可以创建或加入房间 CreateOrJoinRoom(); } }关键点MonoBehaviourPunCallbacks这是 PUN 2 提供的基类里面预定义了很多网络事件回调的虚方法。我们通过重写override这些方法来响应网络事件比如连接成功、加入房间等。这比用委托或事件监听更清晰。ConnectUsingSettings()这个方法封装了连接所需的所有参数是最省事的连接方式。回调是异步的网络操作需要时间所以代码不会在ConnectUsingSettings()那里等待而是立即返回。真正的连接结果会在OnConnectedToMaster或OnDisconnected这些回调里通知你。写联机逻辑一定要习惯这种异步思维。3.2 创建或加入房间房间Room是玩家进行具体游戏会话的场所。我们接着在GameManager里写房间逻辑。public class GameManager : MonoBehaviourPunCallbacks { // ... 之前的连接代码 ... private void CreateOrJoinRoom() { // 定义房间选项 RoomOptions roomOptions new RoomOptions(); roomOptions.MaxPlayers 4; // 设置房间最大人数 roomOptions.IsVisible true; // 房间是否在大厅列表中可见 roomOptions.IsOpen true; // 房间是否可加入 // 尝试加入一个名为“TestRoom”的房间如果不存在则创建它 PhotonNetwork.JoinOrCreateRoom(TestRoom, roomOptions, TypedLobby.Default); } // 3. 重写加入房间成功的回调 public override void OnJoinedRoom() { Debug.Log($成功加入房间: {PhotonNetwork.CurrentRoom.Name}); Debug.Log($当前房间玩家数: {PhotonNetwork.CurrentRoom.PlayerCount}); // 加入房间后最重要的一步生成玩家角色 SpawnPlayer(); } }关键点RoomOptions这里可以配置房间的各种属性比如最大人数、是否公开、自定义属性等。MaxPlayers一定要设默认是0。JoinOrCreateRoom这是一个非常方便的方法省去了先查询房间是否存在再决定的逻辑。OnJoinedRoom这是玩家进入游戏房间的确认信号。在这里生成玩家对象是最合适的时机。3.3 生成并同步玩家角色这是实现“联机”视觉效果的核心。每个玩家都需要在网络上有一个代表自己的对象。首先确保你之前创建的玩家预制体比如叫PlayerPrefab已经被 PUN 识别。在Resources文件夹如果没有就创建一个内放入你的玩家预制体。打开PhotonServerSettings文件你之前配置App ID的那个。在PUN设置部分找到Player Prefab字段将你的PlayerPrefab拖拽赋值进去。或者你也可以在代码中动态指定。然后编写生成玩家的代码public class GameManager : MonoBehaviourPunCallbacks { [SerializeField] private GameObject playerPrefab; // 也可以通过PhotonServerSettings设置 [SerializeField] private Transform[] spawnPoints; // 一组出生点 // ... 之前的房间代码 ... private void SpawnPlayer() { if (playerPrefab null) { Debug.LogError(PlayerPrefab未分配); return; } // 选择一个出生点简单起见这里按进入房间的顺序选 int spawnIndex PhotonNetwork.CurrentRoom.PlayerCount - 1; Vector3 spawnPosition spawnPoints[spawnIndex % spawnPoints.Length].position; // 关键方法使用PhotonNetwork.Instantiate来生成网络对象 PhotonNetwork.Instantiate(playerPrefab.name, spawnPosition, Quaternion.identity); } }关键点PhotonNetwork.Instantiate这是生成网络游戏对象的唯一正确方式。不要用GameObject.Instantiate。这个方法会在所有连入房间的客户端上都生成一个该预制体的实例并且为这个实例分配一个唯一的网络 IDPhotonView用于后续的同步。所有权Ownership调用PhotonNetwork.Instantiate的客户端默认是这个生成对象的“所有者”。所有者有权限调用该对象上标记为[PunRPC]的方法并控制某些同步逻辑。这个概念对后续实现玩家控制很重要。3.4 实现玩家移动同步现在每个客户端都有了玩家对象但移动还不同步。我们需要在PlayerController脚本中处理。PUN 2 提供了几种同步方式最常用的是通过PhotonView组件观察Observing和序列化Serializing数据。为玩家预制体添加PhotonView组件这是每个网络对象的“身份证”和“数据同步器”。编写同步脚本using Photon.Pun; using UnityEngine; public class PlayerController : MonoBehaviourPun, IPunObservable { private float moveSpeed 5f; private Vector3 networkPosition; // 用于插值同步的位置 private Quaternion networkRotation; // 用于插值同步的旋转 private float lerpSmoothing 10f; // 插值平滑系数 private void Update() { // 关键判断只有本地玩家这个客户端控制的玩家才处理输入 if (photonView.IsMine) { HandleLocalInput(); } else { // 对于其他玩家的对象进行插值平滑以减少网络抖动带来的卡顿 transform.position Vector3.Lerp(transform.position, networkPosition, Time.deltaTime * lerpSmoothing); transform.rotation Quaternion.Lerp(transform.rotation, networkRotation, Time.deltaTime * lerpSmoothing); } } private void HandleLocalInput() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 moveDir new Vector3(h, 0, v).normalized; transform.Translate(moveDir * moveSpeed * Time.deltaTime); } // 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(); } } }关键点photonView.IsMine这是最重要的属性之一。用于判断当前脚本所在的游戏对象是不是由本客户端控制的。自己的角色自己处理输入别人的角色则接收网络同步过来的数据。IPunObservable接口当你需要自定义同步哪些数据比如位置、旋转、血量、状态等时就需要实现这个接口。在OnPhotonSerializeView方法里用stream.IsWriting判断是发送方还是接收方然后对应地进行SendNext和ReceiveNext。插值Lerp直接使用接收到的网络位置会显得非常生硬和抖动。通过Vector3.Lerp和Quaternion.Lerp在当前帧位置和目标网络位置之间进行平滑过渡是实现“丝滑”观感的关键技巧。lerpSmoothing系数可以调整平滑程度值越大跟得越快但可能产生“滑步”值越小越平滑但延迟感可能更明显。发送频率OnPhotonSerializeView的调用频率很高默认约10次/秒。对于移动同步这通常足够了。你可以在PhotonView组件上调整Synchronization为Unreliable或UnreliableOnChange来优化带宽但一般Reliable Delta Compressed默认对初学者更友好。至此一个最基础的、可运行的 Photon 联机 Demo 就完成了。你应该能实现两个客户端连接同一个房间并看到彼此的角色在移动。4. 实现“高丝滑低延迟”的关键调优基础功能跑通只是第一步。要让体验变得“丝滑”还需要在以下几个地方下功夫。很多卡顿和延迟问题不是 Photon 的锅而是参数没调对。4.1 网络参数的精细调整在PhotonServerSettings或代码中可以调整一些影响性能和流畅度的核心参数。发送速率Send Rate与序列化速率Serialization RateSend RatePhoton 向服务器发送数据的频率默认约30次/秒。对于快节奏动作游戏可以提高到 40-50。但注意提高它会增加带宽和服务器负载。Serialization RateOnPhotonSerializeView被调用的频率默认约10次/秒。这个频率决定了你自定义同步数据的发送频率。对于位置同步10次/秒结合插值通常够用。你可以通过PhotonNetwork.SerializationRate在代码中动态调整。建议不要盲目调高。先在默认值下测试如果发现移动“跳帧”或延迟感明显再尝试微调Serialization Rate。Send Rate一般保持默认即可。网络延迟补偿与预测插值Interpolation上面已经用了这是处理位置同步延迟和抖动的标准方法。确保为远程玩家的对象启用。外推Extrapolation在接收到的位置数据基础上根据最后已知的速度和方向预测下一帧的位置。这能进一步减少延迟感但预测错误会导致对象“回弹”。PUN 2 没有内置简单的外推需要自己基于速度向量实现复杂度较高新手慎用。命令缓冲与执行对于射击等需要高反应速度的操作可以采用客户端预测服务器校验回滚Server Reconciliation的方式。这属于高级话题PUN 2 本身不直接提供需要自己设计网络命令RPC和状态管理逻辑。4.2 优化同步数据量网络带宽是有限的同步的数据越少、越小延迟通常就越低。只同步必要的数据在OnPhotonSerializeView里只SendNext那些真正会变化且对游戏逻辑关键的数据。比如玩家的颜色如果不变化就不需要每帧同步。使用合适的压缩PhotonStream本身会对基础类型int,float,Vector3等进行一定压缩。对于自定义结构体可以考虑手动压缩。例如将旋转从Quaternion4个float转换为short或byte表示的欧拉角如果精度允许。利用PhotonTransformView等组件PUN 2 自带了一些标准同步组件如PhotonTransformView、PhotonAnimatorView。它们经过优化通常比自己手写IPunObservable更高效。对于标准的移动和动画同步优先考虑使用这些组件在 Inspector 里勾选需要同步的选项即可。状态同步 vs. 输入同步这是两种思路。状态同步同步结果位置、血量。上面例子就是。简单直观但带宽消耗随对象数量增加而增加。输入同步同步原因按键、摇杆方向。服务器和所有客户端收到相同的输入在相同的逻辑下计算出相同的结果。这能极大减少同步数据量只传几个按键状态但对游戏逻辑的确定性要求极高且所有客户端的物理模拟必须完全一致。对于小团队先从状态同步开始更稳妥。4.3 房间与匹配策略优化房间管理也会影响体验。区域选择Region在PhotonServerSettings中可以设置优先连接的区域如asia,eu,us等。让玩家连接到地理上最近的服务器是降低物理延迟最有效的方法。最大玩家数在RoomOptions中设置合理的MaxPlayers。一个房间人越多需要同步的数据就越多服务器广播的压力越大。根据你的游戏设计来定。自定义房间属性与匹配使用RoomOptions.CustomRoomProperties可以给房间打标签如地图、模式、等级限制。然后玩家可以使用OpJoinRandomRoom的重载方法传入期望的属性来匹配房间这比让玩家自己在大厅列表里找要快得多体验更好。4.4 客户端性能与渲染优化网络流畅了客户端卡顿也会破坏“丝滑”感。玩家角色面数面数规范这就是输入材料里提到的“unity pc游戏面数规范”。一个网络游戏中同屏显示的玩家模型面数必须严格控制。一个数万面的精致角色在本地单机可能没问题但10个这样的角色同时出现在网络游戏里对GPU是巨大压力。通常实时网络游戏的玩家角色面数建议在5,000 - 15,000 三角面以内并配合LOD多细节层次。Draw Call 合并确保玩家角色使用的材质和贴图尽可能共享以减少 Draw Call。可以使用 Unity 的 Static Batching 或 GPU Instancing对于相同的网格和材质。摄像机裁剪与视锥体剔除确保摄像机不会渲染视野外的玩家对象。对于网络对象可能需要结合OnBecameVisible/OnBecameInvisible或手动管理来停止同步远离视线的玩家数据高级优化。5. 常见问题排查与避坑指南即使按照教程做了还是会遇到各种问题。下面是我在项目里和社区里最常见的一些坑。5.1 连接与房间问题问题无法连接一直停在“Connecting...”或提示超时。排查顺序检查 App ID确认PhotonServerSettings里的 App ID 是否正确且没有过期免费版长期有效但需偶尔登录官网确认。检查网络是否有防火墙或代理阻止了 Unity 编辑器访问互联网可以尝试关闭防火墙或换个网络环境。检查区域确认设置的 Region 是可用的。如果不确定可以设置为“”空字符串或“best”让 Photon 自动选择最佳区域。查看日志在PhotonServerSettings中将Logging级别设为Full。运行游戏在 Console 窗口查看详细的连接日志错误信息通常会在这里给出线索。问题能连接但加入房间失败。排查顺序房间名冲突JoinOrCreateRoom使用的房间名是否包含了特殊字符或过长房间名是字符串键。房间已满检查RoomOptions.MaxPlayers是否设置得太小而房间已满。房间不可见/不可加入检查RoomOptions.IsVisible和IsOpen。自定义属性匹配失败如果使用了OpJoinRandomRoom并带有匹配属性确保属性名和类型完全匹配。5.2 同步与表现问题问题我能看到其他玩家但他们移动起来一卡一卡的不丝滑。排查顺序确认插值已启用检查远程玩家对象的PlayerController脚本是否执行了Vector3.Lerp那段代码。调整平滑系数尝试增大lerpSmoothing值比如从10调到20让远程玩家跟得更紧。但注意调得过高会放大网络抖动。检查网络状况在PhotonNetwork.NetworkStatisticsEnabled设为true后可以打印PhotonNetwork.NetworkStatistics查看丢包率和往返时间RTT。高延迟或高丢包是卡顿的元凶。降低同步频率如果带宽有限可以尝试适当降低PhotonNetwork.SerializationRate但要以牺牲流畅度为代价需权衡。问题我的角色移动其他玩家看到的位置有延迟。这是正常现象由网络传输的物理延迟Ping值决定。我们优化的目标是让这个延迟看起来不明显通过插值、预测而不是消除它。确保你的游戏设计能容忍 100-200ms 的延迟。对比测试在同一台电脑上开两个 Unity 编辑器实例用不同的项目端口运行模拟“零”延迟环境。如果这样还延迟那就是代码逻辑问题了。5.3 高级功能与部署问题问题我想用 Photon Server 自己部署而不是用 Cloud 服务。Photon 提供了Photon Server软件供自己部署On-Premise。你需要从官网下载 Server 端程序在 Windows 或 Linux 服务器上运行并修改客户端PhotonServerSettings中的Server Address和Port指向你自己的服务器 IP。这涉及到服务器运维、网络配置端口转发等更复杂的知识建议先彻底玩转 Cloud 版本再尝试。问题如何实现游戏内聊天、同步非玩家对象如子弹、道具聊天可以通过PhotonNetwork.RaiseEvent发送自定义事件来实现。这是一种更灵活的低级通信方式。子弹/道具和生成玩家一样使用PhotonNetwork.Instantiate生成一个带有PhotonView的预制体。谁生成谁通常就是所有者。同步逻辑和玩家类似可能生命周期更短击中后PhotonNetwork.Destroy。问题打包成 exe 后无法联机。检查构建设置确保在File - Build Settings - Player Settings - Other Settings中.NET版本与开发时一致如.NET Standard 2.1或.NET Framework。检查防火墙打包后的 exe 可能被系统防火墙拦截。需要在防火墙中为你的游戏 exe 添加出入站规则。确认App ID确保打包时PhotonServerSettings文件在Resources文件夹里被正确包含在构建中并且 App ID 无误。最后也是最实在的建议不要一开始就追求完美的丝滑体验。先用最简单的方块和基础同步把整个连接-加入-移动的流程跑通。然后一个一个地引入优化点加上插值、调整同步频率、优化模型面数、测试不同网络环境。每做一步改动都对比测试一下效果。网络游戏的调试是一个持续的过程理解底层原理状态同步、权威服务器、延迟补偿比记住某个特定参数值更重要。当你踩过这些坑之后再回头看 Photon 的文档和社区你会发现很多问题都已经有了成熟的解决方案。
返回列表