7. 多人系统与动态难度
2026/4/14大约 4 分钟
7. 多人系统与动态难度
7.1 多人游戏的关键问题
支持4人同玩,需要解决以下问题:
| 问题 | 解决方案 |
|---|---|
| 玩家随时加入/离开 | GameManager 维护活跃玩家列表,动态增减 |
| 人数变化影响难度 | DifficultyManager 实时响应玩家数变化 |
| 多个玩家的血条显示 | HUD 根据活跃玩家动态显示/隐藏血条 |
| 摄像机跟随多人 | 摄像机跟踪所有玩家的中心点 |
| 全员倒地判定 | GameManager 轮询玩家状态,全倒触发游戏结束 |
7.2 玩家管理器
C
using Godot;
using System.Collections.Generic;
/// <summary>
/// 玩家管理器——负责生成玩家、处理加入/离开
/// </summary>
public partial class PlayerManager : Node
{
[Export] public PackedScene WarriorScene { get; set; }
[Export] public PackedScene MageScene { get; set; }
[Export] public PackedScene RogueScene { get; set; }
[Export] public PackedScene PaladinScene { get; set; }
// 出生点位置(4个玩家各自的出生位置)
[Export] public Node2D[] SpawnPoints { get; set; }
private readonly Dictionary<int, PlayerBase> _players = new();
/// <summary>
/// 玩家选择角色并加入游戏
/// jobIndex: 0=战士 1=法师 2=盗贼 3=圣骑士
/// </summary>
public void SpawnPlayer(int playerId, int jobIndex)
{
if (_players.ContainsKey(playerId)) return;
PackedScene scene = jobIndex switch
{
0 => WarriorScene,
1 => MageScene,
2 => RogueScene,
3 => PaladinScene,
_ => WarriorScene
};
var player = scene.Instantiate<PlayerBase>();
player.PlayerId = playerId;
// 加入 players 组(敌人AI通过这个组找玩家)
player.AddToGroup("players");
// 设置出生位置
int idx = playerId - 1;
if (SpawnPoints != null && idx < SpawnPoints.Length)
player.GlobalPosition = SpawnPoints[idx].GlobalPosition;
GetParent().AddChild(player);
_players[playerId] = player;
GameManager.Instance?.AddPlayer(playerId);
}
/// <summary>
/// 玩家离开游戏
/// </summary>
public void RemovePlayer(int playerId)
{
if (!_players.TryGetValue(playerId, out var player)) return;
player.QueueFree();
_players.Remove(playerId);
GameManager.Instance?.RemovePlayer(playerId);
}
/// <summary>
/// 检查是否全员倒地(触发游戏结束)
/// </summary>
public bool IsAllPlayersDead()
{
if (_players.Count == 0) return false;
foreach (var p in _players.Values)
{
if (p.IsAlive) return false;
}
return true;
}
public override void _Process(double delta)
{
if (IsAllPlayersDead())
GameManager.Instance?.ChangeState(DnfGameState.GameOver);
}
}GDScript
extends Node
class_name PlayerManager
## 玩家管理器——负责生成玩家、处理加入/离开
@export var warrior_scene: PackedScene
@export var mage_scene: PackedScene
@export var rogue_scene: PackedScene
@export var paladin_scene: PackedScene
@export var spawn_points: Array[Node2D]
var _players: Dictionary = {} # playerId -> PlayerBase
## 玩家选择角色并加入游戏
## job_index: 0=战士 1=法师 2=盗贼 3=圣骑士
func spawn_player(player_id: int, job_index: int) -> void:
if player_id in _players:
return
var scenes: Array[PackedScene] = [warrior_scene, mage_scene, rogue_scene, paladin_scene]
var scene: PackedScene = scenes[clampi(job_index, 0, 3)]
var player: PlayerBase = scene.instantiate()
player.player_id = player_id
player.add_to_group("players")
var idx: int = player_id - 1
if idx < spawn_points.size():
player.global_position = spawn_points[idx].global_position
get_parent().add_child(player)
_players[player_id] = player
if GameManager.instance:
GameManager.instance.add_player(player_id)
## 玩家离开游戏
func remove_player(player_id: int) -> void:
if not player_id in _players:
return
_players[player_id].queue_free()
_players.erase(player_id)
if GameManager.instance:
GameManager.instance.remove_player(player_id)
## 检查是否全员倒地
func is_all_players_dead() -> bool:
if _players.is_empty():
return false
for p in _players.values():
if p.is_alive:
return false
return true
func _process(_delta: float) -> void:
if is_all_players_dead():
if GameManager.instance:
GameManager.instance.change_state(DnfGameState.GAME_OVER)7.3 动态难度实时响应
当玩家人数变化时,难度需要立即调整。通过监听 GameManager 的信号实现:
C
using Godot;
/// <summary>
/// 关卡控制器——监听玩家人数变化,实时调整难度
/// </summary>
public partial class StageController : Node
{
private int _lastPlayerCount = 0;
public override void _Ready()
{
// 监听玩家人数变化信号
if (GameManager.Instance != null)
GameManager.Instance.PlayerCountChanged += OnPlayerCountChanged;
}
private void OnPlayerCountChanged(int newCount)
{
if (newCount == _lastPlayerCount) return;
bool playerIncreased = newCount > _lastPlayerCount;
_lastPlayerCount = newCount;
if (playerIncreased)
{
// 玩家增加:后续生成的怪物会更多更强(已有怪物不变)
GD.Print($"玩家增加到 {newCount} 人,后续难度提升");
}
else
{
// 玩家减少:后续生成的怪物会减少变弱
GD.Print($"玩家减少到 {newCount} 人,后续难度降低");
}
// 通知HUD更新血条显示
UpdateHudForPlayerCount(newCount);
}
private void UpdateHudForPlayerCount(int count)
{
// 找到HUD节点,显示/隐藏对应玩家的血条
var hud = GetTree().GetFirstNodeInGroup("hud");
hud?.Call("UpdatePlayerCount", count);
}
}GDScript
extends Node
class_name StageController
## 关卡控制器——监听玩家人数变化,实时调整难度
var _last_player_count: int = 0
func _ready() -> void:
if GameManager.instance:
GameManager.instance.player_count_changed.connect(_on_player_count_changed)
func _on_player_count_changed(new_count: int) -> void:
if new_count == _last_player_count:
return
var player_increased: bool = new_count > _last_player_count
_last_player_count = new_count
if player_increased:
print("玩家增加到 %d 人,后续难度提升" % new_count)
else:
print("玩家减少到 %d 人,后续难度降低" % new_count)
_update_hud_for_player_count(new_count)
func _update_hud_for_player_count(count: int) -> void:
var hud: Node = get_tree().get_first_node_in_group("hud")
if hud:
hud.call("update_player_count", count)7.4 同屏多人的注意事项
玩家之间不互相碰撞
4个玩家挤在一起会导致卡住,需要把玩家放在不同的碰撞层:
物理层设置(项目设置 → 物理 → 2D → 层名称):
- 层1: players 玩家自身
- 层2: enemies 敌人
- 层3: terrain 地形
- 层4: player_attack 玩家攻击判定
- 层5: enemy_attack 敌人攻击判定
玩家节点碰撞配置:
- collision_layer = players(我是玩家)
- collision_mask = terrain(我只和地形碰撞,不和其他玩家碰撞)屏幕边界限制
当玩家走得太散,摄像机无法兼顾所有人时,需要限制玩家移动范围:
摄像机视野宽度 = 1280像素
最左玩家和最右玩家的距离不能超过 1280 - 200 = 1080像素
超过时,落后的玩家不能继续往后走(被屏幕边界推着走)7.5 本章小结
| 概念 | 说明 |
|---|---|
| PlayerManager | 负责生成和销毁玩家,维护玩家列表 |
| players 组 | 敌人AI通过这个组查找攻击目标 |
| 动态难度响应 | 监听 PlayerCountChanged 信号,实时更新 |
| 碰撞层设置 | 玩家之间不碰撞,只和地形碰撞 |
| 全员倒地 | 检测到全部倒地时触发 GameOver |
下一章我们将设计Boss战,让每关的高潮时刻充满震撼感。
