9. 网络5v5对战
2026/4/15大约 4 分钟
9. 网络5v5对战:和朋友一起开黑
9.1 网络架构选择
MOBA 游戏有两种常见的网络架构:
| 架构 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 状态同步 | 服务器计算所有逻辑,把结果发给客户端 | 作弊难、权威性高 | 服务器压力大、延迟高 |
| 帧同步 | 每个客户端自己算,只同步操作指令 | 响应快、服务器压力小 | 容易作弊、需确定性计算 |
我们的简化版使用 状态同步 方案,因为 Godot 的高层网络 API 更适合这种模式。
为什么选状态同步?
对于教学项目来说,状态同步更直观、更容易调试。帧同步虽然性能好,但要求每一帧的计算结果完全确定(浮点数误差都会导致不同步),实现难度太高。
9.2 网络管理器
C#
// NetworkManager.cs
using Godot;
public partial class NetworkManager : Node
{
private const int DefaultPort = 7777;
private const int MaxPlayers = 10; // 5v5
[Signal]
public delegate void PlayerConnectedEventHandler(int peerId);
[Signal]
public delegate void PlayerDisconnectedEventHandler(int peerId);
[Signal]
public delegate void GameStartedEventHandler();
private bool _isServer = false;
// 创建服务器(房主调用)
public Error CreateServer()
{
var peer = new ENetMultiplayerPeer();
var error = peer.CreateServer(DefaultPort, MaxPlayers);
if (error != Error.Ok)
{
GD.PrintErr($"创建服务器失败: {error}");
return error;
}
Multiplayer.MultiplayerPeer = peer;
_isServer = true;
Multiplayer.PeerConnected += (id) =>
{
GD.Print($"玩家 {id} 已连接");
EmitSignal(SignalName.PlayerConnected, id);
};
Multiplayer.PeerDisconnected += (id) =>
{
GD.Print($"玩家 {id} 已断开");
EmitSignal(SignalName.PlayerDisconnected, id);
};
GD.Print("服务器创建成功,等待玩家加入...");
return Error.Ok;
}
// 加入服务器(其他玩家调用)
public Error JoinServer(string address)
{
var peer = new ENetMultiplayerPeer();
var error = peer.CreateClient(address, DefaultPort);
if (error != Error.Ok)
{
GD.PrintErr($"连接服务器失败: {error}");
return error;
}
Multiplayer.MultiplayerPeer = peer;
_isServer = false;
GD.Print($"正在连接 {address}...");
return Error.Ok;
}
// 断开连接
public void Disconnect()
{
Multiplayer.MultiplayerPeer = null;
_isServer = false;
}
public bool IsServer => _isServer;
public bool IsConnected => Multiplayer.MultiplayerPeer != null &&
Multiplayer.MultiplayerPeer.GetConnectionStatus() !=
MultiplayerPeer.ConnectionStatus.Disconnected;
}GDScript
# network_manager.gd
extends Node
const DEFAULT_PORT: int = 7777
const MAX_PLAYERS: int = 10 # 5v5
signal player_connected(peer_id: int)
signal player_disconnected(peer_id: int)
signal game_started
var is_server: bool = false
# 创建服务器(房主调用)
func create_server() -> Error:
var peer = ENetMultiplayerPeer.new()
var error = peer.create_server(DEFAULT_PORT, MAX_PLAYERS)
if error != Error.OK:
push_error("创建服务器失败: %s" % error)
return error
multiplayer.multiplayer_peer = peer
is_server = true
multiplayer.peer_connected.connect(func(id):
print("玩家 %d 已连接" % id)
player_connected.emit(id)
)
multiplayer.peer_disconnected.connect(func(id):
print("玩家 %d 已断开" % id)
player_disconnected.emit(id)
)
print("服务器创建成功,等待玩家加入...")
return Error.OK
# 加入服务器
func join_server(address: String) -> Error:
var peer = ENetMultiplayerPeer.new()
var error = peer.create_client(address, DEFAULT_PORT)
if error != Error.OK:
push_error("连接服务器失败: %s" % error)
return error
multiplayer.multiplayer_peer = peer
is_server = false
print("正在连接 %s..." % address)
return Error.OK
# 断开连接
func disconnect_peer():
multiplayer.multiplayer_peer = null
is_server = false
func get_is_connected() -> bool:
return multiplayer.multiplayer_peer != null and \
multiplayer.multiplayer_peer.get_connection_status() != \
MultiplayerPeer.CONNECTION_STATUS_DISCONNECTED9.3 状态同步
关键数据需要通过网络同步到所有客户端:
| 数据 | 同步方式 | 频率 |
|---|---|---|
| 英雄位置 | @warning_ignore("unused_signal") + RPC | 每帧 |
| 英雄生命/蓝量 | RPC | 变化时 |
| 技能释放 | RPC(所有人) | 释放时 |
| 小兵生成 | RPC(服务器→客户端) | 每波 |
| 防御塔状态 | RPC | 变化时 |
| 金币/经验 | RPC(服务器→客户端) | 变化时 |
C#
// SyncManager.cs - 同步英雄状态
using Godot;
public partial class SyncManager : Node
{
// 服务器每帧同步所有英雄位置
public override void _Process(double delta)
{
if (!Multiplayer.IsServer()) return;
foreach (var node in GetTree().GetNodesInGroup("player_heroes"))
{
var hero = node as HeroBase;
if (hero == null) continue;
Rpc("SyncHeroPosition", hero.Name,
hero.GlobalPosition.X, hero.GlobalPosition.Y, hero.GlobalPosition.Z);
}
}
[Rpc(MultiplayerApi.RpcMode.AnyPeer, CallLocal = false, TransferMode = MultiplayerPeer.TransferModeEnum.Unreliable)]
private void SyncHeroPosition(string heroName, float x, float y, float z)
{
var hero = GetNodeOrNull<Node3D>($"/root/Main/World/Heroes/{heroName}");
if (hero != null && !hero.IsMultiplayerAuthority())
{
hero.GlobalPosition = new Vector3(x, y, z);
}
}
// 同步技能释放(由施法者调用)
public void BroadcastSkillCast(string heroName, int skillSlot,
float targetX, float targetY, float targetZ)
{
Rpc("OnSkillCast", heroName, skillSlot, targetX, targetY, targetZ);
}
[Rpc(MultiplayerApi.RpcMode.AnyPeer, CallLocal = true)]
private void OnSkillCast(string heroName, int skillSlot,
float targetX, float targetY, float targetZ)
{
var hero = GetNodeOrNull<HeroBase>($"/root/Main/World/Heroes/{heroName}");
if (hero != null)
{
var targetPos = new Vector3(targetX, targetY, targetZ);
hero.GetNode<SkillManager>("SkillManager").CastSkill(skillSlot, targetPos, null);
}
}
}GDScript
# sync_manager.gd
extends Node
# 服务器每帧同步所有英雄位置
func _process(delta):
if not multiplayer.is_server():
return
for node in get_tree().get_nodes_in_group("player_heroes"):
var hero = node as Node3D
if hero == null:
continue
sync_hero_position.rpc(hero.name,
hero.global_position.x, hero.global_position.y, hero.global_position.z)
@rpc("any_peer", call_local = false, unreliable = true)
func sync_hero_position(hero_name: String, x: float, y: float, z: float):
var hero = get_node_or_null("/root/Main/World/Heroes/%s" % hero_name)
if hero and not hero.is_multiplayer_authority():
hero.global_position = Vector3(x, y, z)
# 同步技能释放
func broadcast_skill_cast(hero_name: String, skill_slot: int,
target_x: float, target_y: float, target_z: float):
on_skill_cast.rpc(hero_name, skill_slot, target_x, target_y, target_z)
@rpc("any_peer", call_local = true)
func on_skill_cast(hero_name: String, skill_slot: int,
target_x: float, target_y: float, target_z: float):
var hero = get_node_or_null("/root/Main/World/Heroes/%s" % hero_name) as HeroBase
if hero:
var target_pos = Vector3(target_x, target_y, target_z)
hero.get_node("SkillManager").cast_skill(skill_slot, target_pos, null)9.4 断线重连
MOBA 游戏中断线重连非常重要,没有人想因为网络波动就输掉一局游戏:
C#
// 断线重连逻辑(简化版)
public partial class ReconnectHandler : Node
{
private Dictionary<int, float> _disconnectedPlayers = new();
public override void _Process(double delta)
{
if (!Multiplayer.IsServer()) return;
// 检测断线的玩家
foreach (var peerId in Multiplayer.GetPeers())
{
// 如果长时间没收到心跳包,标记为断线
// 保留60秒的重连窗口
}
// 清理超时的断线玩家
var timedOut = new List<int>();
foreach (var kvp in _disconnectedPlayers)
{
kvp.Value -= (float)delta;
if (kvp.Value <= 0)
timedOut.Add(kvp.Key);
}
foreach (var id in timedOut)
_disconnectedPlayers.Remove(id);
}
}GDScript
# reconnect_handler.gd
extends Node
var disconnected_players: Dictionary = {} # peer_id -> 剩余时间
func _process(delta):
if not multiplayer.is_server():
return
# 检测断线玩家,保留60秒重连窗口
var timed_out = []
for peer_id in disconnected_players:
disconnected_players[peer_id] -= delta
if disconnected_players[peer_id] <= 0:
timed_out.append(peer_id)
for id in timed_out:
disconnected_players.erase(id)章节导航
| 上一章 | 下一章 |
|---|---|
| ← 8. AI队友与敌方 | 10. UI打磨 → |
