Unity 2022 LTS Photon Fusion 230分钟打造多人联机射击原型当你在Unity 2022 LTS中首次打开Photon Fusion 2的文档时可能会被那些状态同步、客户端预测等术语吓到。但别担心我们今天要做的不是理论研究而是直接动手创建一个能让你和好友实时互射小球的多人游戏原型。这个过程中你会自然理解Fusion的核心机制。1. 环境准备与基础配置在开始编码前我们需要确保开发环境正确配置。打开Unity 2022 LTS建议使用2022.3.9f1版本创建一个新的3D核心项目。关键配置步骤Photon账号注册访问Photon官网注册账号在Dashboard中创建新应用选择Fusion类型。记下生成的App ID这相当于连接Photon服务的通行证。项目设置调整在Edit Project Settings Editor中将Asset Serialization模式改为Force Text。这是Fusion生成网络代码的必要设置。SDK导入下载最新Fusion SDK后通过Assets Import Package Custom Package导入。导入完成后会自动弹出Fusion Hub向导粘贴之前获取的App ID。提示如果导入后出现Mono Cecil缺失错误可通过Package Manager添加com.unity.nuget.mono-cecil1.10.22. 搭建基础联机框架2.1 创建网络管理器首先创建一个空对象命名为NetworkManager并挂载新建的NetworkManager.cs脚本using Fusion; using UnityEngine; public class NetworkManager : MonoBehaviour, INetworkRunnerCallbacks { private NetworkRunner _runner; public async void StartGame(GameMode mode) { _runner gameObject.AddComponentNetworkRunner(); _runner.ProvideInput true; await _runner.StartGame(new StartGameArgs() { GameMode mode, SessionName QuickDemoRoom, SceneManager gameObject.AddComponentNetworkSceneManagerDefault() }); } // 实现INetworkRunnerCallbacks接口方法 public void OnPlayerJoined(NetworkRunner runner, PlayerRef player) { /* 后续实现 */ } public void OnPlayerLeft(NetworkRunner runner, PlayerRef player) { /* 后续实现 */ } // ... 其他回调方法暂留空 }2.2 添加简易UI控制为快速测试我们添加简单的GUI按钮private void OnGUI() { if (_runner null) { if (GUI.Button(new Rect(0,0,200,40), Host)) StartGame(GameMode.Host); if (GUI.Button(new Rect(0,40,200,40), Join)) StartGame(GameMode.Client); } }此时运行游戏点击Host/Join按钮应该能看到控制台日志但还没有实际游戏内容。3. 实现玩家角色与移动3.1 创建玩家预制体新建空对象命名为PlayerPrefab添加组件NetworkObject核心网络标识NetworkCharacterController带预测的移动控制添加子对象Capsule作为视觉表现关键配置确保NetworkObject的Player Object选项勾选移除子对象上的碰撞体使用父对象的CharacterController3.2 玩家移动脚本创建PlayerMovement.csusing Fusion; using UnityEngine; public class PlayerMovement : NetworkBehaviour { private NetworkCharacterController _cc; [Networked] private Angle _yaw { get; set; } private void Awake() { _cc GetComponentNetworkCharacterController(); } public override void FixedUpdateNetwork() { if (GetInput(out NetworkInputData input)) { Vector3 moveDir Vector3.zero; if (input.IsDown(NetworkInputData.BUTTON_FORWARD)) moveDir Vector3.forward; if (input.IsDown(NetworkInputData.BUTTON_BACK)) moveDir Vector3.back; if (input.IsDown(NetworkInputData.BUTTON_LEFT)) moveDir Vector3.left; if (input.IsDown(NetworkInputData.BUTTON_RIGHT)) moveDir Vector3.right; _cc.Move(5 * transform.rotation * moveDir * Runner.DeltaTime); if (input.IsDown(NetworkInputData.BUTTON_FIRE)) GetComponentPlayerShooter().Fire(); } } }3.3 输入系统配置创建NetworkInputData.cs定义输入结构using Fusion; using UnityEngine; public struct NetworkInputData : INetworkInput { public const int BUTTON_FORWARD 1; public const int BUTTON_BACK 1 1; public const int BUTTON_LEFT 1 2; public const int BUTTON_RIGHT 1 3; public const int BUTTON_FIRE 1 4; public NetworkButtons buttons; }在NetworkManager中实现输入收集public void OnInput(NetworkRunner runner, NetworkInput input) { var data new NetworkInputData(); data.buttons.Set(NetworkInputData.BUTTON_FORWARD, Input.GetKey(KeyCode.W)); data.buttons.Set(NetworkInputData.BUTTON_BACK, Input.GetKey(KeyCode.S)); data.buttons.Set(NetworkInputData.BUTTON_LEFT, Input.GetKey(KeyCode.A)); data.buttons.Set(NetworkInputData.BUTTON_RIGHT, Input.GetKey(KeyCode.D)); data.buttons.Set(NetworkInputData.BUTTON_FIRE, Input.GetMouseButton(0)); input.Set(data); }4. 实现射击系统4.1 创建子弹预制体新建Sphere对象命名为BulletPrefab添加组件NetworkObjectNetworkTransform同步位置新建Bullet.cs脚本Bullet.cs核心代码using Fusion; using UnityEngine; public class Bullet : NetworkBehaviour { [Networked] private TickTimer life { get; set; } [Networked] private Vector3 direction { get; set; } public void Init(Vector3 fireDirection) { direction fireDirection; life TickTimer.CreateFromSeconds(Runner, 2f); } public override void FixedUpdateNetwork() { if (life.Expired(Runner)) { Runner.Despawn(Object); return; } transform.position direction * 10 * Runner.DeltaTime; } }4.2 玩家射击脚本创建PlayerShooter.csusing Fusion; using UnityEngine; public class PlayerShooter : NetworkBehaviour { [SerializeField] private NetworkPrefabRef bulletPrefab; [Networked] private TickTimer cooldown { get; set; } public void Fire() { if (cooldown.ExpiredOrNotRunning(Runner)) { cooldown TickTimer.CreateFromSeconds(Runner, 0.3f); Runner.Spawn(bulletPrefab, transform.position transform.forward, Quaternion.LookRotation(transform.forward), Object.InputAuthority, (runner, obj) { obj.GetComponentBullet().Init(transform.forward); }); } } }5. 玩家生成与同步回到NetworkManager完善玩家生成逻辑[SerializeField] private NetworkPrefabRef playerPrefab; private DictionaryPlayerRef, NetworkObject spawnedPlayers new DictionaryPlayerRef, NetworkObject(); public void OnPlayerJoined(NetworkRunner runner, PlayerRef player) { if (runner.IsServer) { Vector3 spawnPos new Vector3((player.RawEncoded % 10) * 2 - 10, 0, 0); NetworkObject playerObj runner.Spawn(playerPrefab, spawnPos, Quaternion.identity, player); spawnedPlayers.Add(player, playerObj); } } public void OnPlayerLeft(NetworkRunner runner, PlayerRef player) { if (spawnedPlayers.TryGetValue(player, out NetworkObject playerObj)) { runner.Despawn(playerObj); spawnedPlayers.Remove(player); } }6. 场景布置与最终试创建地面Plane对象为玩家和子弹添加简单材质区分确保所有预制体都已拖入Resources文件夹在NetworkManager中分配playerPrefab和bulletPrefab引用测试流程构建项目并运行两个实例一个实例点击Host另一个点击Join使用WASD移动鼠标左键射击观察子弹的同步效果和碰撞7. 性能优化技巧网络压缩在NetworkRunner组件上调整_runner.Config.Compression NetworkProjectConfig.CompressionType.LZ4;插值设置对移动对象调整NetworkTransformGetComponentNetworkTransform().InterpolationDataSource InterpolationDataSources.Snapshots;带宽优化在NetworkInputData中使用位域压缩public byte inputByte; // 每位代表一个按键状态这个原型虽然简单但已经包含了Fusion最核心的功能模块。你可以在此基础上继续扩展添加命中检测与分数系统实现房间列表与匹配机制加入更复杂的物理交互优化网络预测算法参数