6. 波次系统
2026/4/14大约 9 分钟
6. 保卫萝卜——波次系统
什么是波次系统?
想象一下看电影:电影不会一开始就把所有情节一股脑儿倒给你,而是分"一场一场"的,每场之间还有过渡。波次系统也是一样的道理——怪物不是一次性全部涌来,而是分成一波一波的,每波之间给玩家喘息和调整的时间。
波次系统就像是游戏的"节奏控制器":
| 阶段 | 比喻 | 玩家在做什么 |
|---|---|---|
| 波次间歇 | 课间休息 | 调整塔的布局、攒金币 |
| 波次预告 | 上课铃响 | 屏幕提示"第N波即将来临" |
| 怪物出场 | 上课了 | 怪物按照间隔一个一个出现 |
| 波次结束 | 下课了 | 所有怪物被消灭,进入间歇 |
波次配置数据
每一波有哪些怪物、多少只、间隔多久——这些信息需要一种"格式"来记录。我们用资源文件来存储。
波次数据资源
C
using Godot;
using System.Collections.Generic;
/// <summary>
/// 单个波次的配置数据 —— 一波怪物的"出场表"
/// </summary>
[GlobalClass]
public partial class WaveData : Resource
{
/// <summary>波次编号</summary>
[Export] public int WaveNumber { get; set; } = 1;
/// <summary>怪物出场列表</summary>
/// 每个条目定义了:出什么怪、出几只、间隔多久
[Export] public WaveEntry[] Entries { get; set; } = {};
/// <summary>是否为 Boss 波次</summary>
[Export] public bool IsBossWave { get; set; } = false;
/// <summary>Boss 数据(Boss 波次专用)</summary>
[Export] public MonsterData BossData { get; set; }
/// <summary>Boss 出场延迟(秒)</summary>
[Export] public float BossSpawnDelay { get; set; } = 5.0f;
/// <summary>波次开始前的预告时间(秒)</summary>
[Export] public float WarningDuration { get; set; } = 3.0f;
/// <summary>波次描述(给玩家看的提示)</summary>
[Export] public string Description { get; set; } = "普通波次";
}
/// <summary>
/// 波次条目 —— 一个怪物组的出场配置
/// </summary>
[GlobalClass]
public partial class WaveEntry : Resource
{
/// <summary>怪物数据</summary>
[Export] public MonsterData Monster { get; set; }
/// <summary>数量</summary>
[Export] public int Count { get; set; } = 5;
/// <summary>每只怪物之间的间隔(秒)</summary>
[Export] public float SpawnInterval { get; set; } = 1.0f;
/// <summary>第一个怪物出场的延迟(秒)</summary>
[Export] public float StartDelay { get; set; } = 0.0f;
}GDScript
class_name WaveData
extends Resource
## 单个波次的配置数据 —— 一波怪物的"出场表"
## 波次编号
@export var wave_number: int = 1
## 怪物出场列表
@export var entries: Array[WaveEntry] = []
## 是否为 Boss 波次
@export var is_boss_wave: bool = false
## Boss 数据
@export var boss_data: MonsterData = null
## Boss 出场延迟(秒)
@export var boss_spawn_delay: float = 5.0
## 波次预告时间(秒)
@export var warning_duration: float = 3.0
## 波次描述
@export var description: String = "普通波次"波次配置示例
下面是一个关卡中前5波的配置示例:
| 波次 | 怪物组成 | 数量 | 间隔 | Boss |
|---|---|---|---|---|
| 第1波 | 普通小怪 | 5只 | 2秒 | 无 |
| 第2波 | 普通小怪 | 8只 | 1.5秒 | 无 |
| 第3波 | 普通小怪 + 快速小怪 | 5+3只 | 1.2秒 | 无 |
| 第4波 | 普通小怪 + 坦克小怪 | 6+2只 | 1.5秒 | 无 |
| 第5波 | 混合 | 10只 | 1秒 | 1只Boss |
波次管理器实现
波次管理器负责按照配置数据"按剧本出场"——什么时间出什么怪,出多少只,都由它来控制。
C
using Godot;
using System.Collections.Generic;
/// <summary>
/// 波次管理器 —— 控制怪物出场的"导演"
/// 按照配置的波次数据,按时间顺序释放怪物
/// </summary>
public partial class WaveManager : Node
{
// ===== 信号 =====
/// <summary>波次开始时发送</summary>
[Signal]
public delegate void WaveStartedEventHandler(int waveNumber);
/// <summary>波次结束(所有怪物被消灭)时发送</summary>
[Signal]
public delegate void WaveCompletedEventHandler(int waveNumber);
/// <summary>Boss 出场时发送</summary>
[Signal]
public delegate void BossSpawnedEventHandler();
/// <summary>波次预告时发送</summary>
[Signal]
public delegate void WaveWarningEventHandler(
int waveNumber, float countdown);
// ===== 导出属性 =====
/// <summary>所有波次配置</summary>
[Export] public WaveData[] Waves { get; set; }
/// <summary>怪物生成器引用</summary>
[Export] public MonsterSpawner Spawner { get; set; }
// ===== 运行时状态 =====
private int _currentWaveIndex = -1;
private bool _isWaveActive = false;
private int _monstersRemaining;
private int _monstersSpawned;
private Timer _spawnTimer;
private int _currentEntryIndex;
private int _currentEntrySpawned;
/// <summary>当前波次编号</summary>
public int CurrentWaveNumber =>
_currentWaveIndex >= 0 ? _currentWaveIndex + 1 : 0;
/// <summary>当前波次是否进行中</summary>
public bool IsWaveActive => _isWaveActive;
public override void _Ready()
{
// 创建生成计时器
_spawnTimer = new Timer
{
OneShot = true,
Autostart = false
};
_spawnTimer.Timeout += OnSpawnTimerTimeout;
AddChild(_spawnTimer);
// 监听怪物死亡和到达终点事件
// 通过怪物生成器间接监听
}
/// <summary>
/// 开始下一波
/// 由 GameManager 调用
/// </summary>
public void StartNextWave()
{
_currentWaveIndex++;
if (_currentWaveIndex >= Waves.Length)
{
GD.Print("所有波次已完成!");
return;
}
var waveData = Waves[_currentWaveIndex];
StartWave(waveData);
}
/// <summary>
/// 开始指定波次
/// </summary>
private async void StartWave(WaveData data)
{
_isWaveActive = true;
_monstersSpawned = 0;
_monstersRemaining = 0;
_currentEntryIndex = 0;
_currentEntrySpawned = 0;
// 计算本波总怪物数
foreach (var entry in data.Entries)
{
_monstersRemaining += entry.Count;
}
if (data.IsBossWave && data.BossData != null)
{
_monstersRemaining++;
}
// 发送波次预告
EmitSignal(SignalName.WaveStarted, data.WaveNumber);
// 等待预告时间
float warningTime = data.WarningDuration;
for (float t = warningTime; t > 0; t -= 0.5f)
{
EmitSignal(SignalName.WaveWarning,
data.WaveNumber, t);
await ToSignal(GetTree().CreateTimer(0.5f),
Timer.SignalName.Timeout);
}
// 开始按条目生成怪物
if (data.Entries.Length > 0)
{
StartEntrySpawn(data.Entries[0]);
}
// 如果是 Boss 波,延迟生成 Boss
if (data.IsBossWave && data.BossData != null)
{
StartBossSpawn(data);
}
}
/// <summary>
/// 按条目生成怪物
/// </summary>
private void StartEntrySpawn(WaveEntry entry)
{
_currentEntrySpawned = 0;
// 第一个怪物的延迟
if (entry.StartDelay > 0)
{
_spawnTimer.Start(entry.StartDelay);
}
else
{
SpawnNextMonster(entry);
}
}
/// <summary>
/// 生成计时器超时回调
/// </summary>
private void OnSpawnTimerTimeout()
{
if (!_isWaveActive) return;
if (_currentWaveIndex >= Waves.Length) return;
var waveData = Waves[_currentWaveIndex];
if (_currentEntryIndex >= waveData.Entries.Length) return;
var currentEntry = waveData.Entries[_currentEntryIndex];
SpawnNextMonster(currentEntry);
}
/// <summary>
/// 生成下一个怪物
/// </summary>
private void SpawnNextMonster(WaveEntry entry)
{
if (_currentEntrySpawned >= entry.Count)
{
// 当前条目的怪物已全部生成,进入下一个条目
AdvanceToNextEntry();
return;
}
// 生成怪物
Spawner.SpawnMonster(entry.Monster, 0);
_currentEntrySpawned++;
_monstersSpawned++;
// 设置下一个怪物的间隔
if (_currentEntrySpawned < entry.Count)
{
_spawnTimer.Start(entry.SpawnInterval);
}
else
{
// 当前条目完成,延迟后进入下一个条目
AdvanceToNextEntry();
}
}
/// <summary>
/// 推进到下一个出场条目
/// </summary>
private void AdvanceToNextEntry()
{
_currentEntryIndex++;
if (_currentWaveIndex >= Waves.Length) return;
var waveData = Waves[_currentWaveIndex];
if (_currentEntryIndex < waveData.Entries.Length)
{
// 还有下一个条目
var nextEntry = waveData.Entries[_currentEntryIndex];
_spawnTimer.Start(nextEntry.StartDelay > 0
? nextEntry.StartDelay
: nextEntry.SpawnInterval);
}
// 否则等待所有怪物被消灭
}
/// <summary>
/// Boss 延迟生成
/// </summary>
private async void StartBossSpawn(WaveData data)
{
await ToSignal(GetTree().CreateTimer(data.BossSpawnDelay),
Timer.SignalName.Timeout);
if (!_isWaveActive) return;
Spawner.SpawnMonster(data.BossData, 0);
_monstersSpawned++;
EmitSignal(SignalName.BossSpawned);
}
/// <summary>
/// 通知怪物死亡(由 MonsterSpawner 调用)
/// </summary>
public void NotifyMonsterKilled()
{
_monstersRemaining--;
// 检查是否所有怪物都被消灭且都已生成
if (_monstersRemaining <= 0
&& AllMonstersSpawned())
{
CompleteCurrentWave();
}
}
/// <summary>
/// 通知怪物到达终点(由 MonsterSpawner 调用)
/// </summary>
public void NotifyMonsterReachedEnd()
{
_monstersRemaining--;
if (_monstersRemaining <= 0
&& AllMonstersSpawned())
{
CompleteCurrentWave();
}
}
/// <summary>
/// 所有怪物是否都已生成
/// </summary>
private bool AllMonstersSpawned()
{
if (_currentWaveIndex >= Waves.Length) return true;
var waveData = Waves[_currentWaveIndex];
// 所有条目都已处理完
return _currentEntryIndex >= waveData.Entries.Length
&& (_currentEntryIndex == 0
|| _currentEntrySpawned >=
waveData.Entries[
Mathf.Min(_currentEntryIndex - 1,
waveData.Entries.Length - 1)].Count);
}
/// <summary>
/// 完成当前波次
/// </summary>
private void CompleteCurrentWave()
{
_isWaveActive = false;
int waveNum = CurrentWaveNumber;
EmitSignal(SignalName.WaveCompleted, waveNum);
GameManager.Instance.OnWaveComplete();
}
/// <summary>
/// 跳过当前波次(调试用)
/// </summary>
public void SkipWave()
{
Spawner.ClearAllMonsters();
CompleteCurrentWave();
}
}GDScript
extends Node
class_name WaveManager
## 波次管理器 —— 控制怪物出场的"导演"
## 按照配置的波次数据,按时间顺序释放怪物
# ===== 信号 =====
## 波次开始
signal wave_started(wave_number: int)
## 波次结束
signal wave_completed(wave_number: int)
## Boss 出场
signal boss_spawned()
## 波次预告
signal wave_warning(wave_number: int, countdown: float)
# ===== 导出属性 =====
## 所有波次配置
@export var waves: Array[WaveData] = []
## 怪物生成器
@export var spawner: MonsterSpawner
# ===== 运行时状态 =====
var _current_wave_index: int = -1
var _is_wave_active: bool = false
var _monsters_remaining: int = 0
var _monsters_spawned: int = 0
var _spawn_timer: Timer = null
var _current_entry_index: int = 0
var _current_entry_spawned: int = 0
## 当前波次编号
var current_wave_number: int:
get: return _current_wave_index + 1 if _current_wave_index >= 0 else 0
## 当前波次是否进行中
var is_wave_active: bool:
get: return _is_wave_active
func _ready() -> void:
# 创建生成计时器
_spawn_timer = Timer.new()
_spawn_timer.one_shot = true
_spawn_timer.autostart = false
_spawn_timer.timeout.connect(_on_spawn_timer_timeout)
add_child(_spawn_timer)
## 开始下一波
func start_next_wave() -> void:
_current_wave_index += 1
if _current_wave_index >= waves.size():
push_warning("所有波次已完成!")
return
_start_wave(waves[_current_wave_index])
## 开始指定波次
func _start_wave(data: WaveData) -> void:
_is_wave_active = true
_monsters_spawned = 0
_monsters_remaining = 0
_current_entry_index = 0
_current_entry_spawned = 0
# 计算总怪物数
for entry in data.entries:
_monsters_remaining += entry.count
if data.is_boss_wave and data.boss_data:
_monsters_remaining += 1
wave_started.emit(data.wave_number)
# 等待预告时间
_show_warning(data)
## 显示波次预告
func _show_warning(data: WaveData) -> void:
var warning_time: float = data.warning_duration
var elapsed: float = 0.0
while elapsed < warning_time:
wave_warning.emit(data.wave_number, warning_time - elapsed)
await get_tree().create_timer(0.5).timeout
elapsed += 0.5
# 预告结束,开始生成怪物
if data.entries.size() > 0:
_start_entry_spawn(data.entries[0])
if data.is_boss_wave and data.boss_data:
_start_boss_spawn(data)
## 按条目生成怪物
func _start_entry_spawn(entry: WaveEntry) -> void:
_current_entry_spawned = 0
if entry.start_delay > 0:
_spawn_timer.start(entry.start_delay)
else:
_spawn_next_monster(entry)
## 生成计时器超时
func _on_spawn_timer_timeout() -> void:
if not _is_wave_active:
return
if _current_wave_index >= waves.size():
return
var wave_data: WaveData = waves[_current_wave_index]
if _current_entry_index >= wave_data.entries.size():
return
_spawn_next_monster(wave_data.entries[_current_entry_index])
## 生成下一个怪物
func _spawn_next_monster(entry: WaveEntry) -> void:
if _current_entry_spawned >= entry.count:
_advance_to_next_entry()
return
spawner.spawn_monster(entry.monster, 0)
_current_entry_spawned += 1
_monsters_spawned += 1
if _current_entry_spawned < entry.count:
_spawn_timer.start(entry.spawn_interval)
else:
_advance_to_next_entry()
## 推进到下一个条目
func _advance_to_next_entry() -> void:
_current_entry_index += 1
if _current_wave_index >= waves.size():
return
var wave_data: WaveData = waves[_current_wave_index]
if _current_entry_index < wave_data.entries.size():
var next_entry: WaveEntry = wave_data.entries[_current_entry_index]
var delay: float = next_entry.start_delay if next_entry.start_delay > 0 else next_entry.spawn_interval
_spawn_timer.start(delay)
## Boss 延迟生成
func _start_boss_spawn(data: WaveData) -> void:
await get_tree().create_timer(data.boss_spawn_delay).timeout
if not _is_wave_active:
return
spawner.spawn_monster(data.boss_data, 0)
_monsters_spawned += 1
boss_spawned.emit()
## 通知怪物死亡
func notify_monster_killed() -> void:
_monsters_remaining -= 1
if _monsters_remaining <= 0 and _all_monsters_spawned():
_complete_current_wave()
## 通知怪物到达终点
func notify_monster_reached_end() -> void:
_monsters_remaining -= 1
if _monsters_remaining <= 0 and _all_monsters_spawned():
_complete_current_wave()
## 所有怪物是否都已生成
func _all_monsters_spawned() -> bool:
if _current_wave_index >= waves.size():
return true
var wave_data: WaveData = waves[_current_wave_index]
return _current_entry_index >= wave_data.entries.size()
## 完成当前波次
func _complete_current_wave() -> void:
_is_wave_active = false
var wave_num: int = current_wave_number
wave_completed.emit(wave_num)
GameManager.on_wave_complete()
## 跳过当前波次(调试用)
func skip_wave() -> void:
spawner.clear_all_monsters()
_complete_current_wave()波次间休息
波次间休息让玩家有时间调整策略。有两种方式:
| 方式 | 说明 | 实现 |
|---|---|---|
| 自动开始 | 间歇结束后自动开始下一波 | 用 Timer 倒计时 |
| 手动开始 | 玩家点击按钮才开始下一波 | 显示"开始下一波"按钮 |
推荐使用手动开始,给玩家更多控制权:
C
/// <summary>
/// 波次间歇控制器
/// </summary>
public partial class WaveBreakController : Node
{
[Signal]
public delegate void BreakStartedEventHandler(
int nextWave, float duration);
[Signal]
public delegate void BreakEndedEventHandler();
[Export] public float BreakDuration { get; set; } = 10.0f;
private float _breakTimer;
public bool IsOnBreak { get; private set; }
public void StartBreak(int nextWave)
{
IsOnBreak = true;
_breakTimer = BreakDuration;
EmitSignal(SignalName.BreakStarted, nextWave, BreakDuration);
}
public override void _Process(double delta)
{
if (!IsOnBreak) return;
_breakTimer -= (float)delta;
if (_breakTimer <= 0)
{
// 间歇结束,但不自动开始
// 等待玩家点击"开始"按钮
EmitSignal(SignalName.BreakEnded);
IsOnBreak = false;
}
}
}GDScript
extends Node
class_name WaveBreakController
signal break_started(next_wave: int, duration: float)
signal break_ended()
## 间歇时长
@export var break_duration: float = 10.0
var _break_timer: float = 0.0
var is_on_break: bool = false
## 开始间歇
func start_break(next_wave: int) -> void:
is_on_break = true
_break_timer = break_duration
break_started.emit(next_wave, break_duration)
func _process(delta: float) -> void:
if not is_on_break:
return
_break_timer -= delta
if _break_timer <= 0:
break_ended.emit()
is_on_break = false本节小结
| 组件 | 说明 |
|---|---|
| WaveData | 单波配置:怪物组成、数量、间隔 |
| WaveEntry | 怪物出场条目:什么怪、几只、间隔多久 |
| WaveManager | 波次导演:按剧本控制怪物出场 |
| WaveBreakController | 波次间歇:给玩家调整时间 |
| Boss 波次 | 每隔几波出一个超强 Boss |
波次系统是塔防游戏的"骨架",它决定了游戏的节奏和难度曲线。下一节我们将实现特殊机制——可清除障碍物、冰冻减速、毒伤等,让游戏玩法更加丰富。
