Unity NetCode 从入门到精通:多人游戏网络开发完全指南
## 引言:为什么需要NetCode?
在游戏开发中,多人游戏的网络同步一直是最大的挑战之一。Unity NetCode是Unity官方推出的网络同步解决方案,它解决了:
- **多人同步问题**:如何让玩家在不同客户端上看到一致的游戏状态
- **延迟补偿**:如何处理网络延迟带来的操作延迟
- **状态同步**:如何高效地同步游戏世界状态
- **模拟预测**:如何在本地预测玩家操作,减少延迟感
"NetCode的目标是让多人游戏开发和单人游戏开发一样简单。"
---
## 一、Unity NetCode基础:快速上手
### 1.1 环境搭建
**Unity版本要求**:2020.3 LTS或更高版本
**安装方式**:
1. 打开Package Manager
2. 点击"+" → "Add package from git URL"
3. 输入:`com.unity.netcode.gameobjects@1.0.0`(或最新版本号)
### 1.2 第一个多人游戏
**步骤1:创建NetworkManager**
```csharp
using Unity.Netcode;
public class CustomNetworkManager : NetworkManager
{
public static CustomNetworkManager Instance => singleton as CustomNetworkManager;
protected override void Awake()
{
base.Awake();
// 配置网络设置
NetworkConfig.MaxNumberOfPlayers = 8;
NetworkConfig.EnableSceneManagement = true;
NetworkConfig.PrefabHashGenerator = null; // 使用默认哈希生成
}
public override void OnClientConnected()
{
base.OnClientConnected();
Debug.Log("客户端已连接");
}
public override void OnServerStarted()
{
base.OnServerStarted();
Debug.Log("服务器已启动");
// 生成网络对象
SpawnPlayer();
}
[ServerRpc]
public void SpawnPlayerServerRpc()
{
if (IsServer)
{
SpawnPlayer();
}
}
private void SpawnPlayer()
{
// 在服务器上生成玩家对象
var playerObject = Instantiate(Resources.Load("Player"));
// 网络生成
playerObject.GetComponent().Spawn();
}
}
```
**步骤2:创建NetworkBehaviour**
```csharp
using Unity.Netcode;
public class PlayerController : NetworkBehaviour
{
[SerializeField] private float _moveSpeed = 5f;
// 同步玩家位置(使用NetworkVariable)
private NetworkVariable _networkPosition = new NetworkVariable();
// 玩家输入数据结构
private struct PlayerInput : INetworkSerializable
{
public Vector2 MoveDirection;
public bool Jump;
public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref MoveDirection);
serializer.SerializeValue(ref Jump);
}
}
private PlayerInput _lastInput;
private void Update()
{
// 只有本地玩家才能控制输入
if (IsLocalPlayer)
{
ReadInput();
SendInputToServer();
MovePlayer();
}
else
{
// 同步其他玩家的位置
SyncPosition();
}
}
private void ReadInput()
{
_lastInput.MoveDirection = new Vector2(
Input.GetAxis("Horizontal"),
Input.GetAxis("Vertical")
);
_lastInput.Jump = Input.GetKeyDown(KeyCode.Space);
}
[ServerRpc]
private void SendInputToServerServerRpc(PlayerInput input)
{
// 在服务器上应用输入
ApplyInput(input);
}
private void SendInputToServer()
{
if (IsLocalPlayer)
{
SendInputToServerServerRpc(_lastInput);
}
}
private void ApplyInput(PlayerInput input)
{
Vector3 moveDir = new Vector3(input.MoveDirection.x, 0, input.MoveDirection.y);
transform.Translate(moveDir * _moveSpeed * Time.deltaTime);
if (input.Jump)
{
// 跳跃逻辑
}
// 更新网络变量
_networkPosition.Value = transform.position;
}
private void MovePlayer()
{
// 本地预测移动
Vector3 moveDir = new Vector3(
_lastInput.MoveDirection.x, 0, _lastInput.MoveDirection.y
);
transform.Translate(moveDir * _moveSpeed * Time.deltaTime);
}
private void SyncPosition()
{
// 平滑同步其他玩家位置
transform.position = Vector3.Lerp(
transform.position,
_networkPosition.Value,
Time.deltaTime * 10f
);
}
}
```
**步骤3:配置预制体**
1. 创建玩家预制体,添加NetworkObject组件
2. 在NetworkManager中注册该预制体
3. 为需要同步的对象添加NetworkBehaviour组件
---
## 二、核心概念:NetCode的工作原理
### 2.1 服务器-客户端模型
NetCode采用权威服务器模型:
```
服务器 (Server):权威的游戏状态,处理所有逻辑
↓ ↓ ↓
客户端 (Client):本地模拟,只向服务器发送输入
```
**客户端类型**:
- **Host**:既是服务器又是客户端
- **Dedicated Server**:专用服务器,不渲染画面
- **Client**:普通客户端
### 2.2 NetworkVariable:同步变量
NetworkVariable是NetCode同步数据的核心:
```csharp
// 定义同步变量
public NetworkVariable Score = new NetworkVariable(0);
public NetworkVariable PlayerName = new NetworkVariable("Player");
public NetworkVariable Position = new NetworkVariable();
// 带同步权限的变量
public NetworkVariable PlayerHealth = new NetworkVariable(100,
NetworkVariableWritePermission.ServerOnly);
// 变量变更回调
public NetworkVariable Money = new NetworkVariable(0);
private void OnEnable()
{
Money.OnValueChanged += OnMoneyChanged;
}
private void OnMoneyChanged(float previousValue, float newValue)
{
Debug.Log($"Money changed from {previousValue} to {newValue}");
}
```
**NetworkVariable特性**:
- **权限控制**:ServerOnly, OwnerOnly, Everyone
- **回调机制**:OnValueChanged
- **自动同步**:仅在值变化时同步
### 2.3 Rpc:远程过程调用
NetCode支持三种Rpc调用:
#### 1. ServerRpc:客户端调用服务器
```csharp
// 客户端调用服务器
[ServerRpc]
public void TakeDamageServerRpc(float damage, ulong targetId)
{
// 只有服务器会执行这个方法
if (IsServer)
{
var target = NetworkManager.Singleton.SpawnManager.SpawnedObjects[targetId];
var health = target.GetComponent();
health.TakeDamage(damage);
}
}
// 客户端调用
player.TakeDamageServerRpc(10f, targetObject.NetworkObjectId);
```
#### 2. ClientRpc:服务器调用客户端
```csharp
// 服务器调用所有客户端
[ClientRpc]
public void PlayExplosionClientRpc(Vector3 position)
{
// 所有客户端执行这个方法
if (IsClient)
{
var explosion = Instantiate(explosionPrefab, position, Quaternion.identity);
Destroy(explosion, 2f);
}
}
// 服务器调用
explosion.PlayExplosionClientRpc(transform.position);
```
#### 3. ClientRpcTarget:指定目标客户端
```csharp
[ClientRpc]
public void ShowChatMessageClientRpc(string message, ClientRpcParams clientRpcParams = default)
{
// 只在指定客户端执行
Debug.Log("收到聊天消息: " + message);
}
// 向特定客户端发送消息
var clientParams = new ClientRpcParams
{
Send = new ClientRpcSendParams
{
TargetClientIds = new ulong[] { targetClientId }
}
};
chatManager.ShowChatMessageClientRpc("你好", clientParams);
// 向所有客户端发送,除了发送者
[ClientRpc]
public void NotifyPlayerJoinedClientRpc(ulong playerId)
{
// 逻辑
}
// 服务器调用时指定跳过发送者
NotifyPlayerJoinedClientRpc(playerId, new ClientRpcParams
{
Send = new ClientRpcSendParams
{
TargetClientIds = NetworkManager.Singleton.ConnectedClientsIds
.Where(id => id != playerId)
.ToArray()
}
});
```
---
## 三、高级同步技术
### 3.1 网络对象生成与销毁
```csharp
// 生成网络对象
var prefab = Resources.Load("Enemy");
var enemy = Instantiate(prefab, spawnPosition, Quaternion.identity);
// 生成并设置拥有者
var enemyNetworkObject = enemy.GetComponent();
enemyNetworkObject.Spawn(); // 默认由服务器拥有
// 生成并让客户端拥有
public void SpawnForClient(ulong clientId)
{
var player = Instantiate(playerPrefab);
var networkObject = player.GetComponent();
// 让特定客户端拥有该对象
networkObject.SpawnAsPlayerObject(clientId);
}
// 销毁网络对象
public void DestroyEnemy(ulong enemyId)
{
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(enemyId, out var enemy))
{
// 网络销毁
enemy.GetComponent().Despawn();
// 或立即销毁
enemy.GetComponent().Despawn(true);
}
}
```
### 3.2 场景管理
**网络场景切换**:
```csharp
// 服务器切换场景
NetworkManager.Singleton.SceneManager.LoadScene(
"BattleScene",
LoadSceneMode.Single
);
// 客户端场景切换完成回调
public override void OnSceneEvent(SceneEvent sceneEvent)
{
base.OnSceneEvent(sceneEvent);
if (sceneEvent.SceneEventType == SceneEventType.LoadComplete)
{
Debug.Log("场景加载完成: " + sceneEvent.SceneName);
if (IsLocalPlayer)
{
// 初始化客户端
}
}
}
```
**场景同步配置**:
```csharp
// 在NetworkManager中配置
protected override void Awake()
{
base.Awake();
NetworkConfig.SceneManagerHandler = new NetworkSceneManagerHandler(
enableSceneManagement: true,
sceneManagerInstance: null, // 使用默认场景管理器
sceneEventBufferSize: 32
);
}
```
### 3.3 预测与回滚
NetCode支持**客户端预测**和**服务器修正**:
```csharp
public class PredictedPlayerController : NetworkBehaviour
{
[SerializeField] private float _moveSpeed = 5f;
// 服务器权威位置
public NetworkVariable ServerPosition = new NetworkVariable();
// 本地预测状态
private Vector3 _predictedPosition;
private Queue _inputHistory = new Queue();
private struct PlayerInput : INetworkSerializable
{
public Vector2 MoveInput;
public uint Tick; // 游戏帧号
public void NetworkSerialize(BufferSerializer serializer)
{
serializer.SerializeValue(ref MoveInput);
serializer.SerializeValue(ref Tick);
}
}
public override void OnNetworkSpawn()
{
if (IsOwner)
{
_predictedPosition = transform.position;
}
}
private void Update()
{
if (IsOwner)
{
var input = new PlayerInput
{
MoveInput = new Vector2(
Input.GetAxis("Horizontal"),
Input.GetAxis("Vertical")
),
Tick = (uint)NetworkManager.ServerTime.Tick
};
// 本地预测
ApplyInputLocally(input);
// 保存输入历史
_inputHistory.Enqueue(input);
// 发送输入到服务器
SendInputToServer(input);
// 服务器位置修正
CorrectPosition();
}
else
{
// 非拥有者客户端平滑同步
transform.position = Vector3.Lerp(
transform.position,
ServerPosition.Value,
Time.deltaTime * 10f
);
}
}
private void ApplyInputLocally(PlayerInput input)
{
Vector3 moveDir = new Vector3(input.MoveInput.x, 0, input.MoveInput.y);
_predictedPosition += moveDir * _moveSpeed * Time.deltaTime;
transform.position = _predictedPosition;
}
[ServerRpc]
private void SendInputToServerServerRpc(PlayerInput input)
{
if (IsServer)
{
// 在服务器上应用输入
ApplyInputOnServer(input);
// 广播服务器位置给所有客户端
ServerPosition.Value = transform.position;
}
}
private void ApplyInputOnServer(PlayerInput input)
{
Vector3 moveDir = new Vector3(input.MoveInput.x, 0, input.MoveInput.y);
transform.position += moveDir * _moveSpeed * Time.deltaTime;
}
private void CorrectPosition()
{
// 比较预测位置和服务器位置
float error = Vector3.Distance(_predictedPosition, ServerPosition.Value);
if (error > 0.1f) // 误差超过阈值时修正
{
_predictedPosition = ServerPosition.Value;
transform.position = _predictedPosition;
// 清空输入历史,重同步
_inputHistory.Clear();
Debug.Log("位置修正");
}
}
}
```
---
## 四、性能优化:构建高效的多人游戏
### 4.1 带宽优化
**减少网络传输**:
```csharp
// 优化1:压缩数据
[Serializable]
public struct CompressedVector3
{
public short x, y, z;
public CompressedVector3(Vector3 v)
{
x = (short)(v.x * 1000);
y = (short)(v.y * 1000);
z = (short)(v.z * 1000);
}
public static explicit operator Vector3(CompressedVector3 cv)
{
return new Vector3(
cv.x / 1000f,
cv.y / 1000f,
cv.z / 1000f
);
}
}
// 优化2:使用Delta压缩
public NetworkVariable Position = new NetworkVariable();
private Vector3 _lastSentPosition;
private void Update()
{
if (IsServer)
{
if (Vector3.Distance(Position.Value, _lastSentPosition) > 0.01f)
{
// 只有位置变化超过阈值时才更新
_lastSentPosition = Position.Value;
Position.Value = Position.Value;
}
}
}
```
**NetworkVariable优化**:
```csharp
// 自定义序列化类型
public struct PlayerData : INetworkSerializable
{
public int Health;
public int Score;
public Vector3 Position;
public float Rotation;
public void NetworkSerialize(BufferSerializer serializer)
{
// 手动控制序列化顺序和格式
serializer.SerializeValue(ref Health);
serializer.SerializeValue(ref Score);
// 压缩位置
var compressedPos = new CompressedVector3(Position);
serializer.SerializeValue(ref compressedPos);
// 压缩旋转(仅存储Y轴,范围0-360)
short compressedRot = (short)(Rotation * 100);
serializer.SerializeValue(ref compressedRot);
}
}
// 使用自定义类型
public NetworkVariable PlayerState = new NetworkVariable();
```
### 4.2 延迟补偿
**使用快照插值**:
```csharp
public class SnapshotInterpolator
{
private struct Snapshot
{
public Vector3 Position;
public Quaternion Rotation;
public float TimeStamp;
}
private Queue _snapshots = new Queue();
private int _maxSnapshots = 10;
private float _interpolationTime = 0.1f; // 100ms延迟
public void AddSnapshot(Vector3 position, Quaternion rotation)
{
var snapshot = new Snapshot
{
Position = position,
Rotation = rotation,
TimeStamp = Time.time
};
_snapshots.Enqueue(snapshot);
// 限制快照数量
if (_snapshots.Count > _maxSnapshots)
{
_snapshots.Dequeue();
}
}
public bool TryGetInterpolatedTransform(out Vector3 position, out Quaternion rotation)
{
position = Vector3.zero;
rotation = Quaternion.identity;
if (_snapshots.Count < 2)
{
return false;
}
// 找到需要插值的快照
float targetTime = Time.time - _interpolationTime;
Snapshot? previousSnapshot = null;
Snapshot? nextSnapshot = null;
foreach (var snapshot in _snapshots)
{
if (snapshot.TimeStamp <= targetTime)
{
previousSnapshot = snapshot;
}
else if (snapshot.TimeStamp >= targetTime && nextSnapshot == null)
{
nextSnapshot = snapshot;
break;
}
}
if (previousSnapshot != null && nextSnapshot != null)
{
// 计算插值比例
float deltaTime = nextSnapshot.Value.TimeStamp - previousSnapshot.Value.TimeStamp;
float t = (targetTime - previousSnapshot.Value.TimeStamp) / deltaTime;
t = Mathf.Clamp01(t);
// 插值计算
position = Vector3.Lerp(
previousSnapshot.Value.Position,
nextSnapshot.Value.Position,
t
);
rotation = Quaternion.Slerp(
previousSnapshot.Value.Rotation,
nextSnapshot.Value.Rotation,
t
);
return true;
}
return false;
}
}
```
### 4.3 物理同步优化
**服务器端物理**:
```csharp
public class ServerPhysicsController : NetworkBehaviour
{
private Rigidbody _rigidbody;
private Vector3 _serverPosition;
private Quaternion _serverRotation;
private void Awake()
{
_rigidbody = GetComponent();
}
public override void OnNetworkSpawn()
{
if (IsServer)
{
// 服务器拥有物理控制权
_rigidbody.isKinematic = false;
}
else
{
// 客户端使用运动学物理,仅显示
_rigidbody.isKinematic = true;
}
}
[ServerRpc]
public void SetVelocityServerRpc(Vector3 velocity)
{
if (IsServer)
{
_rigidbody.velocity = velocity;
}
}
private void FixedUpdate()
{
if (IsServer)
{
// 服务器定期同步状态
if (Time.frameCount % 5 == 0) // 每5帧同步一次
{
SyncPositionClientRpc(_rigidbody.position, _rigidbody.rotation);
}
}
}
[ClientRpc]
public void SyncPositionClientRpc(Vector3 position, Quaternion rotation)
{
if (!IsServer)
{
// 客户端平滑同步
_rigidbody.MovePosition(Vector3.Lerp(
_rigidbody.position,
position,
Time.fixedDeltaTime * 5f
));
_rigidbody.MoveRotation(Quaternion.Slerp(
_rigidbody.rotation,
rotation,
Time.fixedDeltaTime * 5f
));
}
}
}
```
---
## 五、NetCode的底层原理
### 5.1 网络传输协议
NetCode使用UDP作为传输协议:
```
应用层 (Application Layer)
↓
NetCode 序列化 / 反序列化
↓
传输层 (UDP)
↓
网络层 (IP)
↓
数据链路层
```
**传输特性**:
- **不可靠传输**:不需要确认,快但可能丢失
- **不可靠有序传输**:按顺序发送,但可能丢失
- **可靠传输**:需要确认,确保送达但可能延迟
- **可靠碎片传输**:大消息分片传输
### 5.2 数据序列化
NetCode的序列化系统:
```csharp
// 自定义序列化
public struct MyCustomData : INetworkSerializable
{
public int Value1;
public float Value2;
public Vector3 Value3;
public string Value4;
public void NetworkSerialize(BufferSerializer serializer)
{
// 序列化顺序非常重要
serializer.SerializeValue(ref Value1);
serializer.SerializeValue(ref Value2);
serializer.SerializeValue(ref Value3);
serializer.SerializeValue(ref Value4);
}
}
// 使用自定义数据
public class MyNetworkBehaviour : NetworkBehaviour
{
public NetworkVariable CustomData = new NetworkVariable();
[ServerRpc]
public void SendDataServerRpc(MyCustomData data)
{
// 服务器处理
}
}
```
**序列化优化技巧**:
- 使用固定大小的数据类型
- 避免序列化引用类型
- 压缩浮点数和向量
- 增量序列化(仅序列化变化部分)
### 5.3 网络帧同步
NetCode的帧同步机制:
```
服务器更新频率 60Hz
↓
客户端输入采样频率 60Hz
↓
输入每10帧发送一次
↓
服务器累积输入后处理
↓
服务器发送状态更新
↓
客户端插值显示
```
**帧同步的时间管理**:
```csharp
public class NetworkTimeManager
{
public static NetworkTimeManager Instance { get; private set; }
// 服务器时间
public float ServerTime => NetworkManager.Singleton.ServerTime.Time;
public uint ServerTick => NetworkManager.Singleton.ServerTime.Tick;
// 本地时间
public float LocalTime => Time.time;
// 网络延迟
public float NetworkDelay {
get {
if (NetworkManager.Singleton.IsConnectedClient)
{
return NetworkManager.Singleton.NetworkClient.Ping.RoundTripTime / 1000f;
}
return 0f;
}
}
private void Awake()
{
Instance = this;
}
// 计算预测时间
public float GetPredictedTime()
{
return ServerTime + NetworkDelay;
}
// 计算插值比例
public float GetInterpolationFactor(float targetTime)
{
float delta = targetTime - ServerTime;
return Mathf.Clamp01(delta / NetworkDelay);
}
}
```
---
## 六、实战项目:创建一个多人射击游戏
### 6.1 游戏架构设计
**服务器端架构**:
```csharp
public class GameServer : NetworkBehaviour
{
private Dictionary _players = new Dictionary();
private List _gameEvents = new List();
private struct PlayerData
{
public GameObject PlayerObject;
public int Score;
public int Kills;
public int Deaths;
}
private struct GameEvent
{
public enum EventType { PlayerJoined, PlayerLeft, PlayerKilled, ScoreUpdated }
public EventType Type;
public ulong PlayerId;
public ulong TargetId;
public int Value;
}
public override void OnNetworkSpawn()
{
if (IsServer)
{
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnected;
}
}
private void OnClientConnected(ulong clientId)
{
Debug.Log("客户端已连接: " + clientId);
// 创建玩家数据
var playerData = new PlayerData
{
PlayerObject = null,
Score = 0,
Kills = 0,
Deaths = 0
};
_players.Add(clientId, playerData);
// 记录事件
_gameEvents.Add(new GameEvent
{
Type = GameEvent.EventType.PlayerJoined,
PlayerId = clientId
});
}
private void OnClientDisconnected(ulong clientId)
{
Debug.Log("客户端已断开: " + clientId);
if (_players.TryGetValue(clientId, out var playerData))
{
if (playerData.PlayerObject != null)
{
playerData.PlayerObject.GetComponent().Despawn(true);
}
_players.Remove(clientId);
// 记录事件
_gameEvents.Add(new GameEvent
{
Type = GameEvent.EventType.PlayerLeft,
PlayerId = clientId
});
}
}
[ServerRpc]
public void PlayerKilledServerRpc(ulong killerId, ulong victimId)
{
if (IsServer)
{
// 更新分数
if (_players.TryGetValue(killerId, out var killerData))
{
killerData.Score += 100;
killerData.Kills++;
_players[killerId] = killerData;
}
if (_players.TryGetValue(victimId, out var victimData))
{
victimData.Deaths++;
_players[victimId] = victimData;
}
// 通知所有客户端
NotifyPlayerKilledClientRpc(killerId, victimId);
// 记录事件
_gameEvents.Add(new GameEvent
{
Type = GameEvent.EventType.PlayerKilled,
PlayerId = killerId,
TargetId = victimId
});
}
}
[ClientRpc]
public void NotifyPlayerKilledClientRpc(ulong killerId, ulong victimId)
{
Debug.Log($"玩家 {killerId} 击杀了玩家 {victimId}");
// 播放击杀特效
SpawnKillEffect(killerId, victimId);
}
private void SpawnKillEffect(ulong killerId, ulong victimId)
{
// 在客户端生成击杀特效
}
}
```
### 6.2 玩家同步系统
```csharp
public class PlayerNetwork : NetworkBehaviour
{
// 网络变量
public NetworkVariable Stats = new NetworkVariable();
public NetworkVariable State = new NetworkVariable();
// 数据结构
public struct PlayerStats : INetworkSerializable
{
public int Health;
public int MaxHealth;
public int Score;
public int Ammo;
public void NetworkSerialize(BufferSerializer serializer)
{
serializer.SerializeValue(ref Health);
serializer.SerializeValue(ref MaxHealth);
serializer.SerializeValue(ref Score);
serializer.SerializeValue(ref Ammo);
}
}
public struct PlayerState : INetworkSerializable
{
public Vector3 Position;
public Vector3 Velocity;
public Quaternion Rotation;
public bool IsAlive;
public void NetworkSerialize(BufferSerializer serializer)
{
// 压缩序列化
var compressedPos = new CompressedVector3(Position);
serializer.SerializeValue(ref compressedPos);
var compressedVel = new CompressedVector3(Velocity);
serializer.SerializeValue(ref compressedVel);
// 仅序列化Y轴旋转
float yRot = Rotation.eulerAngles.y;
short compressedRot = (short)(yRot * 100);
serializer.SerializeValue(ref compressedRot);
serializer.SerializeValue(ref IsAlive);
}
}
// 输入处理
private struct PlayerInput : INetworkSerializable
{
public Vector2 MoveInput;
public Vector2 LookInput;
public bool Fire;
public bool Reload;
public bool Jump;
public void NetworkSerialize(BufferSerializer serializer)
{
serializer.SerializeValue(ref MoveInput);
serializer.SerializeValue(ref LookInput);
serializer.SerializeValue(ref Fire);
serializer.SerializeValue(ref Reload);
serializer.SerializeValue(ref Jump);
}
}
private PlayerInput _currentInput;
private void Update()
{
if (IsOwner)
{
ReadInput();
SendInputToServer();
LocalPrediction();
}
else
{
RemoteInterpolation();
}
}
private void ReadInput()
{
_currentInput = new PlayerInput
{
MoveInput = new Vector2(
Input.GetAxis("Horizontal"),
Input.GetAxis("Vertical")
),
LookInput = new Vector2(
Input.GetAxis("Mouse X"),
Input.GetAxis("Mouse Y")
),
Fire = Input.GetMouseButton(0),
Reload = Input.GetKeyDown(KeyCode.R),
Jump = Input.GetKeyDown(KeyCode.Space)
};
}
[ServerRpc]
private void SendInputServerRpc(PlayerInput input)
{
if (IsServer)
{
ServerProcessInput(input);
}
}
private void SendInputToServer()
{
if (IsOwner)
{
SendInputServerRpc(_currentInput);
}
}
private void ServerProcessInput(PlayerInput input)
{
// 服务器处理输入
PlayerController controller = GetComponent();
controller.Move(input.MoveInput);
controller.Look(input.LookInput);
if (input.Fire)
{
controller.Fire();
}
if (input.Reload)
{
controller.Reload();
}
if (input.Jump)
{
controller.Jump();
}
// 更新网络状态
UpdateNetworkState();
}
private void LocalPrediction()
{
// 本地预测移动
PlayerController controller = GetComponent();
controller.LocalPredict(_currentInput);
}
private void RemoteInterpolation()
{
// 远程插值显示
PlayerController controller = GetComponent();
controller.RemoteInterpolate(State.Value);
}
[Server]
private void UpdateNetworkState()
{
if (IsServer)
{
State.Value = new PlayerState
{
Position = transform.position,
Velocity = GetComponent().velocity,
Rotation = transform.rotation,
IsAlive = Stats.Value.Health > 0
};
}
}
}
```
---
## 七、常见问题与解决方案
### 7.1 常见问题
**Q1:客户端与服务器状态不一致**
- **原因**:本地预测错误,服务器修正
- **解决方案**:增加补偿逻辑,使用快照插值
**Q2:网络卡顿**
- **原因**:带宽不足或同步频率过高
- **解决方案**:优化数据压缩,降低同步频率
**Q3:RPC调用失败**
- **原因**:权限问题或调用时机错误
- **解决方案**:确保只在正确的时机调用RPC,使用权限检查
**Q4:预制体哈希不匹配**
- **原因**:客户端与服务器的预制体版本不同
- **解决方案**:使用预先生成的哈希,保持预制体一致
### 7.2 调试技巧
**使用NetCode调试器**:
```csharp
// 启用调试日志
public class NetworkDebugger : NetworkBehaviour
{
private void OnEnable()
{
// 监听网络事件
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnected;
NetworkManager.Singleton.OnServerStarted += OnServerStarted;
NetworkManager.Singleton.OnServerStopped += OnServerStopped;
}
private void OnClientConnected(ulong clientId)
{
Debug.Log($"客户端 {clientId} 已连接");
}
private void OnClientDisconnected(ulong clientId)
{
Debug.Log($"客户端 {clientId} 已断开");
}
private void OnServerStarted()
{
Debug.Log("服务器已启动");
}
private void OnServerStopped()
{
Debug.Log("服务器已停止");
}
// 显示网络统计
private void OnGUI()
{
if (NetworkManager.Singleton.IsConnectedClient)
{
int ping = NetworkManager.Singleton.NetworkClient.Ping.AveragePing;
float roundTripTime = NetworkManager.Singleton.NetworkClient.Ping.RoundTripTime;
GUILayout.Label($"Ping: {ping}ms");
GUILayout.Label($"延迟: {roundTripTime}ms");
GUILayout.Label($"发送速率: {NetworkManager.Singleton.NetworkClient.NetworkMetric.SendRate}bps");
GUILayout.Label($"接收速率: {NetworkManager.Singleton.NetworkClient.NetworkMetric.ReceiveRate}bps");
}
}
}
```
---
## 八、总结与进阶
### 8.1 NetCode的优势
**选择NetCode的原因**:
- **官方支持**:Unity官方推出,长期维护
- **高性能**:优化的网络传输和序列化
- **易于使用**:简单的API,降低多人游戏门槛
- **预测与回滚**:内置预测和回滚支持
- **扩展性**:可扩展的同步系统
### 8.2 进阶学习路径
**精通NetCode的步骤**:
1. **基础掌握**:熟悉NetworkVariable和Rpc
2. **预测与回滚**:理解客户端预测和服务器修正
3. **性能优化**:学习带宽优化和延迟补偿
4. **状态同步**:掌握快照插值和时间同步
5. **多人模式**:构建完整的多人游戏模式
### 8.3 未来发展
**NetCode的发展方向**:
- 支持更多网络协议
- 更强大的预测系统
- 更好的状态同步
- 内置的反作弊系统
- 云服务集成
---
## 结语:开启多人游戏开发之旅
Unity NetCode让多人游戏开发变得更加简单和高效。从简单的同步到复杂的预测回滚,NetCode提供了一套完整的解决方案。
记住,多人游戏开发的关键在于:
- **状态同步**:确保所有客户端看到一致的世界
- **延迟补偿**:减少网络延迟带来的影响
- **性能优化**:让游戏在各种网络条件下流畅运行
现在,你已经掌握了Unity NetCode的核心原理和使用方法,是时候开始你的多人游戏开发之旅了!