Unity 2022 LTS + Photon Fusion 2:手把手教你搭建第一个多人联机Demo(含完整代码)
Unity 2022 LTS + Photon Fusion 2:30分钟打造多人联机射击原型
当你在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-cecil@1.10.2
2. 搭建基础联机框架
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.AddComponent<NetworkRunner>(); _runner.ProvideInput = true; await _runner.StartGame(new StartGameArgs() { GameMode = mode, SessionName = "QuickDemoRoom", SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>() }); } // 实现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选项勾选 - 移除子对象上的碰撞体,使用父对象的CharacterController
3.2 玩家移动脚本
创建PlayerMovement.cs:
using Fusion; using UnityEngine; public class PlayerMovement : NetworkBehaviour { private NetworkCharacterController _cc; [Networked] private Angle _yaw { get; set; } private void Awake() { _cc = GetComponent<NetworkCharacterController>(); } 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)) GetComponent<PlayerShooter>().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.cs:
using 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.GetComponent<Bullet>().Init(transform.forward); }); } } }5. 玩家生成与同步
回到NetworkManager完善玩家生成逻辑:
[SerializeField] private NetworkPrefabRef playerPrefab; private Dictionary<PlayerRef, NetworkObject> spawnedPlayers = new Dictionary<PlayerRef, 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;插值设置
对移动对象调整NetworkTransform:GetComponent<NetworkTransform>().InterpolationDataSource = InterpolationDataSources.Snapshots;带宽优化
在NetworkInputData中使用位域压缩:public byte inputByte; // 每位代表一个按键状态
这个原型虽然简单,但已经包含了Fusion最核心的功能模块。你可以在此基础上继续扩展:
- 添加命中检测与分数系统
- 实现房间列表与匹配机制
- 加入更复杂的物理交互
- 优化网络预测算法参数
