5. 敌人波次
2026/4/14大约 6 分钟
塔防——敌人波次
想象你在玩"打地鼠"——第一轮只有一个洞里冒出地鼠,第二轮两个洞同时冒,第三轮冒的速度越来越快。塔防游戏里的"波次"也是这个道理:每一波敌人比上一波更多、更强、更难对付。
波次系统就是游戏的"进度条"——它决定了难度曲线,让玩家始终感受到挑战,又不会突然被压垮。
什么是波次系统
波次系统控制着敌人"什么时候来、来多少、来什么种类"。简单来说:
- 波次 1:5 个普通小怪,间隔 2 秒出一个
- 波次 5:10 个普通怪 + 3 个快速怪,间隔 1.5 秒
- 波次 10:15 个怪 + 5 个快速怪 + 1 个 Boss
波次间休息
每波结束后有一段休息时间,让玩家放新塔、升级现有塔。这段"喘息期"是策略决策的关键时刻。
波次数据结构
首先,定义每波敌人的数据。用 Godot 的 Resource 类型来存储。
C
using Godot;
/// <summary>
/// 单个敌人生成信息——描述"在什么时间、生成什么敌人"。
/// </summary>
[GlobalClass]
public partial class EnemySpawnData : Resource
{
[Export] public PackedScene EnemyScene { get; set; } // 敌人场景
[Export] public float SpawnDelay { get; set; } = 1.0f; // 生成间隔(秒)
}
/// <summary>
/// 波次数据——描述一整波敌人。
/// 就像一份菜单,列出了这一波要上哪些"菜"(敌人)。
/// </summary>
[GlobalClass]
public partial class WaveData : Resource
{
[Export] public string WaveName { get; set; } = "第 1 波"; // 波次名称
[Export] public float StartDelay { get; set; } = 5.0f; // 波次开始前的等待时间
[Export] public int GoldReward { get; set; } = 50; // 通关奖励金币
[Export] public Godot.Collections.Array<EnemySpawnData> Enemies { get; set; } = new();
}GDScript
class_name EnemySpawnData
extends Resource
## 单个敌人生成信息——描述"在什么时间、生成什么敌人"。
@export var enemy_scene: PackedScene # 敌人场景
@export var spawn_delay: float = 1.0 # 生成间隔(秒)
class_name WaveData
extends Resource
## 波次数据——描述一整波敌人。
## 就像一份菜单,列出了这一波要上哪些"菜"(敌人)。
@export var wave_name: String = "第 1 波" # 波次名称
@export var start_delay: float = 5.0 # 波次开始前的等待时间
@export var gold_reward: int = 50 # 通关奖励金币
@export var enemies: Array[EnemySpawnData] = []波次管理器
波次管理器是"发牌人"——它按照预定的计划,一张一张地发敌人出来。
C
using Godot;
/// <summary>
/// 波次管理器——按计划生成敌人。
/// 它就像一个闹钟,到了时间就叫敌人出来。
/// </summary>
public partial class WaveManager : Node
{
[Export] public Godot.Collections.Array<WaveData> Waves { get; set; } = new();
private Marker2D _spawnPoint; // 敌人出生点
private int _currentWaveIndex = 0;
private int _currentEnemyIndex = 0;
private Timer _spawnTimer;
private Timer _waveDelayTimer;
private bool _waveActive = false;
private int _enemiesAlive = 0; // 当前存活的敌人数
[Signal]
public delegate void WaveStartedEventHandler(int waveIndex);
[Signal]
public delegate void WaveCompletedEventHandler(int waveIndex);
[Signal]
public delegate void AllWavesCompletedEventHandler();
public override void _Ready()
{
_spawnPoint = GetNode<Marker2D>("/root/Game/Map/PathStart");
// 生成计时器:控制敌人出场的间隔
_spawnTimer = new Timer { OneShot = true };
_spawnTimer.Timeout += OnSpawnTimerTimeout;
AddChild(_spawnTimer);
// 波次间歇计时器:波次之间的等待
_waveDelayTimer = new Timer { OneShot = true };
_waveDelayTimer.Timeout += OnWaveDelayTimeout;
AddChild(_waveDelayTimer);
}
/// <summary>开始下一波</summary>
public void StartNextWave()
{
if (_currentWaveIndex >= Waves.Count) return;
if (_waveActive) return;
_waveActive = true;
_currentEnemyIndex = 0;
EmitSignal(SignalName.WaveStarted, _currentWaveIndex);
WaveData wave = Waves[_currentWaveIndex];
// 先等待 start_delay 秒,然后开始出怪
_waveDelayTimer.Start(wave.StartDelay);
}
/// <summary>波次延迟结束后开始出怪</summary>
private void OnWaveDelayTimeout()
{
SpawnNextEnemy();
}
/// <summary>生成下一个敌人</summary>
private void SpawnNextEnemy()
{
WaveData wave = Waves[_currentWaveIndex];
if (_currentEnemyIndex >= wave.Enemies.Count)
{
// 这波所有敌人都已安排出场
// 等它们全部死完才算波次结束
return;
}
EnemySpawnData spawnData = wave.Enemies[_currentEnemyIndex];
// 生成敌人实例
var enemy = spawnData.EnemyScene.Instantiate<Node2D>();
enemy.GlobalPosition = _spawnPoint.GlobalPosition;
// 监听敌人死亡
if (enemy is EnemyBase eb)
{
eb.TreeExited += () => OnEnemyDied();
}
var container = GetNode<Node2D>("/root/Game/EnemyContainer");
container.AddChild(enemy);
_enemiesAlive++;
// 安排下一个敌人
_currentEnemyIndex++;
if (_currentEnemyIndex < wave.Enemies.Count)
{
_spawnTimer.Start(spawnData.SpawnDelay);
}
}
/// <summary>生成计时器到期</summary>
private void OnSpawnTimerTimeout()
{
SpawnNextEnemy();
}
/// <summary>敌人死亡回调</summary>
private void OnEnemyDied()
{
_enemiesAlive--;
// 检查这波是否全部结束
WaveData wave = Waves[_currentWaveIndex];
if (_enemiesAlive <= 0 && _currentEnemyIndex >= wave.Enemies.Count)
{
OnWaveCompleted();
}
}
/// <summary>当前波次完成</summary>
private void OnWaveCompleted()
{
_waveActive = false;
WaveData wave = Waves[_currentWaveIndex];
// 发放通关奖励
GameManager.Instance.Gold += wave.GoldReward;
EmitSignal(SignalName.WaveCompleted, _currentWaveIndex);
_currentWaveIndex++;
// 检查是否所有波次都通过
if (_currentWaveIndex >= Waves.Count)
{
EmitSignal(SignalName.AllWavesCompleted);
}
}
}GDScript
extends Node
## 波次管理器——按计划生成敌人。
## 它就像一个闹钟,到了时间就叫敌人出来。
@export var waves: Array[WaveData] = []
@onready var spawn_point: Marker2D = get_node("/root/Game/Map/PathStart")
var current_wave_index: int = 0
var current_enemy_index: int = 0
var spawn_timer: Timer
var wave_delay_timer: Timer
var wave_active: bool = false
var enemies_alive: int = 0 # 当前存活的敌人数
signal wave_started(wave_index: int)
signal wave_completed(wave_index: int)
signal all_waves_completed()
func _ready():
# 生成计时器:控制敌人出场的间隔
spawn_timer = Timer.new()
spawn_timer.one_shot = true
spawn_timer.timeout.connect(_on_spawn_timer_timeout)
add_child(spawn_timer)
# 波次间歇计时器:波次之间的等待
wave_delay_timer = Timer.new()
wave_delay_timer.one_shot = true
wave_delay_timer.timeout.connect(_on_wave_delay_timeout)
add_child(wave_delay_timer)
## 开始下一波
func start_next_wave():
if current_wave_index >= waves.size():
return
if wave_active:
return
wave_active = true
current_enemy_index = 0
wave_started.emit(current_wave_index)
var wave = waves[current_wave_index]
# 先等待 start_delay 秒,然后开始出怪
wave_delay_timer.start(wave.start_delay)
## 波次延迟结束后开始出怪
func _on_wave_delay_timeout():
_spawn_next_enemy()
## 生成下一个敌人
func _spawn_next_enemy():
var wave = waves[current_wave_index]
if current_enemy_index >= wave.enemies.size():
# 这波所有敌人都已安排出场
return
var spawn_data = wave.enemies[current_enemy_index]
# 生成敌人实例
var enemy = spawn_data.enemy_scene.instantiate()
enemy.global_position = spawn_point.global_position
# 监听敌人死亡
if enemy is EnemyBase:
enemy.tree_exited.connect(_on_enemy_died)
var container = get_node("/root/Game/EnemyContainer")
container.add_child(enemy)
enemies_alive += 1
# 安排下一个敌人
current_enemy_index += 1
if current_enemy_index < wave.enemies.size():
spawn_timer.start(spawn_data.spawn_delay)
## 生成计时器到期
func _on_spawn_timer_timeout():
_spawn_next_enemy()
## 敌人死亡回调
func _on_enemy_died():
enemies_alive -= 1
# 检查这波是否全部结束
var wave = waves[current_wave_index]
if enemies_alive <= 0 and current_enemy_index >= wave.enemies.size():
_on_wave_completed()
## 当前波次完成
func _on_wave_completed():
wave_active = false
var wave = waves[current_wave_index]
# 发放通关奖励
GameManager.instance.gold += wave.gold_reward
wave_completed.emit(current_wave_index)
current_wave_index += 1
# 检查是否所有波次都通过
if current_wave_index >= waves.size():
all_waves_completed.emit()敌人种类设计
好的塔防游戏需要多种敌人,让玩家需要思考"该用什么塔来对付"。
| 敌人类型 | 特点 | 应对策略 |
|---|---|---|
| 普通怪 | 血量中等、速度中等 | 箭塔即可 |
| 快速怪 | 血量低、速度快 | 冰塔减速 + 箭塔输出 |
| 重甲怪 | 血量高、速度慢 | 炮塔范围伤害 |
| 飞行怪 | 不走路径,直线飞向终点 | 需要防空塔 |
| Boss | 血量极高、有特殊能力 | 集中火力 + 升级塔 |
不同敌人类型的实现
在基类 EnemyBase 的基础上,可以通过调整属性来创建不同类型的敌人:
C
using Godot;
/// <summary>快速敌人——跑得快但血少</summary>
public partial class FastEnemy : EnemyBase
{
public FastEnemy()
{
Speed = 200.0f; // 速度是普通怪的 2 倍
MaxHealth = 50; // 血量是普通怪的一半
}
}
/// <summary>重甲敌人——走得慢但血厚</summary>
public partial class TankEnemy : EnemyBase
{
public TankEnemy()
{
Speed = 50.0f; // 速度是普通怪的一半
MaxHealth = 500; // 血量是普通怪的 5 倍
}
}
/// <summary>Boss敌人——超强</summary>
public partial class BossEnemy : EnemyBase
{
public BossEnemy()
{
Speed = 60.0f;
MaxHealth = 2000;
}
}GDScript
## 快速敌人——跑得快但血少
class_name FastEnemy
extends EnemyBase
func _init():
speed = 200.0 # 速度是普通怪的 2 倍
max_health = 50 # 血量是普通怪的一半
## 重甲敌人——走得慢但血厚
class_name TankEnemy
extends EnemyBase
func _init():
speed = 50.0 # 速度是普通怪的一半
max_health = 500 # 血量是普通怪的 5 倍
## Boss敌人——超强
class_name BossEnemy
extends EnemyBase
func _init():
speed = 60.0
max_health = 2000难度曲线设计
波次的难度应该是一条"逐渐上升但有起伏"的曲线,而不是一条直线。
好的难度曲线
- 每 5 波有一个"小高潮"(出精英怪)
- 每 10 波有一个"大高潮"(出 Boss)
- 两次高潮之间有"喘息波"(敌人较少较弱),让玩家恢复
难度
^
| /\ /\
| /\ / \ /\ / \ /\ /\
| / \/ \/ \/ \/ \ / \/
|/ \ /
+--------------------------\/--------> 波次
1 2 3 4 5 6 7 8 9 10 11 12 13
精英 Boss下一章
敌人一波一波地来了,接下来实现塔的升级系统。
