5. 敌人波次
2026/4/14大约 8 分钟
5. 雷霆战机——敌人波次
5.1 波次系统是什么?
想象你打一场拳击比赛:对手不是一次性全部冲上来,而是一个回合一个回合地来。第一回合来几个轻量级的,第二回合来几个中量级的,第三回合来一个重量级Boss。
波次系统(Wave System) 就是管理敌人出场的"导演"——它决定什么时候、从哪里、出什么敌人、用什么阵型。
5.2 敌人基类
所有敌人都继承自同一个基类,共享基本的行为逻辑。
C
using Godot;
/// <summary>
/// 敌人基类——所有敌人的共同行为
/// </summary>
public partial class Enemy : Area2D
{
// ===== 属性 =====
[Export] public int MaxHP { get; set; } = 1;
[Export] public int ScoreValue { get; set; } = 100;
[Export] public float MoveSpeed { get; set; } = 100f;
[Export] public float DropChance { get; set; } = 0.15f;
// ===== 状态 =====
protected int _currentHP;
protected bool _alive = true;
// ===== 引用 =====
protected Node2D _bulletContainer;
protected RaidenGameManager _gameManager;
// ===== 信号 =====
[Signal] public delegate void EnemyDestroyedEventHandler(
Vector2 position, int score, float dropChance);
public override void _Ready()
{
_currentHP = MaxHP;
_bulletContainer = GetParent()
?.GetParent()
?.GetNode<Node2D>("GameArea/EnemyBullets");
_gameManager = GetNode<RaidenGameManager>(
"/root/RaidenGameManager");
}
public override void _Process(double delta)
{
if (!_alive) return;
// 默认行为:向下移动
Position += new Vector2(0, MoveSpeed) * (float)delta;
// 超出屏幕底部
if (Position.Y > GameConfig.ScreenHeight + 50f)
{
Deactivate();
}
}
/// <summary>
/// 受到伤害
/// </summary>
public virtual void TakeDamage(int damage)
{
if (!_alive) return;
_currentHP -= damage;
// 受伤闪烁
var tween = CreateTween();
tween.TweenProperty(this, "modulate",
new Color(1, 0.5f, 0.5f), 0.05f);
tween.TweenProperty(this, "modulate",
Colors.White, 0.05f);
if (_currentHP <= 0)
{
Die();
}
}
/// <summary>
/// 死亡处理
/// </summary>
protected virtual void Die()
{
_alive = false;
EmitSignal(SignalName.EnemyDestroyed,
GlobalPosition, ScoreValue, DropChance);
// 播放爆炸特效
SpawnExplosion();
// 通知GameManager加分
_gameManager?.AddScore(ScoreValue);
// 生成道具(由PowerUpManager处理)
Deactivate();
}
/// <summary>
/// 生成爆炸特效
/// </summary>
protected void SpawnExplosion()
{
var scene = GD.Load<PackedScene>(
"res://scenes/effects/explosion.tscn");
var explosion = scene.Instantiate<Node2D>();
explosion.GlobalPosition = GlobalPosition;
GetParent()?.GetNode<Node2D>("../Effects")
?.AddChild(explosion);
}
/// <summary>
/// 发射敌人子弹
/// </summary>
protected void FireBullet(
Vector2 direction, float speed, EnemyBullet.BulletType type)
{
if (_bulletContainer == null) return;
// 获取玩家位置(用于瞄准弹)
var player = GetNodeOrNull<Node2D>(
"/root/Main/GameArea/Player");
if (player == null) return;
var bullet = GD.Load<PackedScene>(
"res://scenes/bullets/enemy_bullet.tscn")
.Instantiate<EnemyBullet>();
switch (type)
{
case EnemyBullet.BulletType.Straight:
bullet.InitStraight(GlobalPosition, direction, speed);
break;
case EnemyBullet.BulletType.Aimed:
bullet.InitAimed(
GlobalPosition, player.GlobalPosition, speed);
break;
case EnemyBullet.BulletType.Circular:
float angle = Mathf.Atan2(direction.Y, direction.X);
bullet.InitCircular(GlobalPosition, angle, speed);
break;
}
_bulletContainer.AddChild(bullet);
}
/// <summary>
/// 停用敌人
/// </summary>
protected void Deactivate()
{
_alive = false;
Visible = false;
ProcessMode = ProcessModeEnum.Disabled;
QueueFree();
}
public bool IsAlive() => _alive;
}GDScript
extends Area2D
## 敌人基类——所有敌人的共同行为
# ===== 属性 =====
@export var max_hp: int = 1
@export var score_value: int = 100
@export var move_speed: float = 100.0
@export var drop_chance: float = 0.15
# ===== 状态 =====
var _current_hp: int
var _alive: bool = true
# ===== 引用 =====
var _bullet_container: Node2D
var _game_manager: RaidenGameManager
# ===== 信号 =====
signal enemy_destroyed(position: Vector2, score: int, drop_chance: float)
func _ready() -> void:
_current_hp = max_hp
_bullet_container = get_parent().get_parent().get_node("GameArea/EnemyBullets")
_game_manager = get_node("/root/RaidenGameManager")
func _process(delta: float) -> void:
if not _alive:
return
# 默认行为:向下移动
position += Vector2(0, move_speed) * delta
# 超出屏幕底部
if position.y > GameConfig.SCREEN_HEIGHT + 50.0:
deactivate()
## 受到伤害
func take_damage(damage: int) -> void:
if not _alive:
return
_current_hp -= damage
# 受伤闪烁
var tween = create_tween()
tween.tween_property(self, "modulate", Color(1, 0.5, 0.5), 0.05)
tween.tween_property(self, "modulate", Color.WHITE, 0.05)
if _current_hp <= 0:
_die()
## 死亡处理
func _die() -> void:
_alive = false
enemy_destroyed.emit(global_position, score_value, drop_chance)
# 播放爆炸特效
_spawn_explosion()
# 通知GameManager加分
if _game_manager:
_game_manager.add_score(score_value)
deactivate()
## 生成爆炸特效
func _spawn_explosion() -> void:
var scene = load("res://scenes/effects/explosion.tscn")
var explosion = scene.instantiate()
explosion.global_position = global_position
var effects = get_parent().get_node_or_null("../Effects")
if effects:
effects.add_child(explosion)
## 发射敌人子弹
func fire_bullet(direction: Vector2, speed: float, type: int) -> void:
if _bullet_container == null:
return
var player = get_node_or_null("/root/Main/GameArea/Player")
if player == null:
return
var bullet = load("res://scenes/bullets/enemy_bullet.tscn").instantiate()
match type:
EnemyBullet.BulletType.STRAIGHT:
bullet.init_straight(global_position, direction, speed)
EnemyBullet.BulletType.AIMED:
bullet.init_aimed(global_position, player.global_position, speed)
EnemyBullet.BulletType.CIRCULAR:
var angle = atan2(direction.y, direction.x)
bullet.init_circular(global_position, angle, speed)
_bullet_container.add_child(bullet)
## 停用敌人
func deactivate() -> void:
_alive = false
visible = false
process_mode = Node.PROCESS_MODE_DISABLED
queue_free()
func is_alive() -> bool:
return _alive5.3 敌人运动模式
不同的敌人有不同的飞行方式。我们用运动模式(Movement Pattern)来定义。
| 模式 | 描述 | 代码实现 |
|---|---|---|
| 直线下落 | 从上往下直飞 | position.y += speed * delta |
| 正弦波动 | 左右摇摆下落 | x = baseX + sin(time * freq) * amplitude |
| 弧形进入 | 从侧边飞入后下落 | 贝塞尔曲线 |
| 悬停 | 到某个位置后停下来 | 到达目标位置后速度归零 |
C
/// <summary>
/// 敌人运动模式组件
/// </summary>
public partial class MovementPattern : Node
{
public enum PatternType
{
Straight, // 直线下落
SineWave, // 正弦波动
Hover, // 悬停
Zigzag // Z字形
}
private PatternType _type;
private Enemy _enemy;
private float _timer = 0f;
// 模式参数
private float _sineFrequency = 2f;
private float _sineAmplitude = 80f;
private float _zigzagInterval = 1f;
private int _zigzagDirection = 1;
private Vector2 _hoverPosition;
public void Initialize(Enemy enemy, PatternType type)
{
_enemy = enemy;
_type = type;
if (type == PatternType.Hover)
{
_hoverPosition = enemy.Position + new Vector2(0, 150f);
}
}
public Vector2 GetMovement(float delta)
{
_timer += delta;
var move = Vector2.Zero;
switch (_type)
{
case PatternType.Straight:
move.Y = _enemy.MoveSpeed;
break;
case PatternType.SineWave:
move.Y = _enemy.MoveSpeed * 0.7f;
move.X = Mathf.Sin(_timer * _sineFrequency)
* _sineAmplitude * delta * 3f;
break;
case PatternType.Hover:
var toHover = _hoverPosition - _enemy.Position;
if (toHover.Length() > 5f)
{
move = toHover.Normalized() * _enemy.MoveSpeed;
}
// 到达后不动
break;
case PatternType.Zigzag:
move.Y = _enemy.MoveSpeed * 0.5f;
if (_timer > _zigzagInterval)
{
_timer = 0f;
_zigzagDirection *= -1;
}
move.X = _zigzagDirection * _enemy.MoveSpeed * 0.8f;
break;
}
return move * delta;
}
}GDScript
extends Node
## 敌人运动模式组件
enum PatternType { STRAIGHT, SINE_WAVE, HOVER, ZIGZAG }
var _type: int
var _enemy: Enemy
var _timer: float = 0.0
# 模式参数
var _sine_frequency: float = 2.0
var _sine_amplitude: float = 80.0
var _zigzag_interval: float = 1.0
var _zigzag_direction: int = 1
var _hover_position: Vector2
func initialize(enemy: Enemy, type: int) -> void:
_enemy = enemy
_type = type
if type == PatternType.HOVER:
_hover_position = enemy.position + Vector2(0, 150.0)
func get_movement(delta: float) -> Vector2:
_timer += delta
var move = Vector2.ZERO
match _type:
PatternType.STRAIGHT:
move.y = _enemy.move_speed
PatternType.SINE_WAVE:
move.y = _enemy.move_speed * 0.7
move.x = sin(_timer * _sine_frequency) * _sine_amplitude * delta * 3.0
PatternType.HOVER:
var to_hover = _hover_position - _enemy.position
if to_hover.length() > 5.0:
move = to_hover.normalized() * _enemy.move_speed
PatternType.ZIGZAG:
move.y = _enemy.move_speed * 0.5
if _timer > _zigzag_interval:
_timer = 0.0
_zigzag_direction *= -1
move.x = _zigzag_direction * _enemy.move_speed * 0.8
return move * delta5.4 波次管理器
波次管理器是整个敌人系统的"导演"。
C
using Godot;
using System.Collections.Generic;
/// <summary>
/// 波次管理器——控制敌人的出场顺序和阵型
/// </summary>
public partial class WaveManager : Node
{
// 波次定义
private List<WaveDefinition> _waves;
private int _currentWave = 0;
private float _waveTimer = 0f;
private bool _waveActive = false;
// 当前波次的敌人待生成列表
private Queue<SpawnEntry> _spawnQueue = new Queue<SpawnEntry>();
private float _spawnTimer = 0f;
// 引用
private Node2D _enemyContainer;
[Signal] public delegate void WaveStartedEventHandler(int waveNum);
[Signal] public delegate void WaveCompletedEventHandler(int waveNum);
[Signal] public delegate void AllWavesCompletedEventHandler();
[Signal] public delegate void BossWaveStartedEventHandler();
public override void _Ready()
{
_enemyContainer = GetNode<Node2D>("../GameArea/Enemies");
LoadWaves();
StartNextWave();
}
/// <summary>
/// 加载波次定义
/// </summary>
private void LoadWaves()
{
_waves = new List<WaveDefinition>
{
// 波次1:简单的直线敌人
new WaveDefinition
{
Enemies = new List<SpawnEntry>
{
new() { Type = "basic", Delay = 0f },
new() { Type = "basic", Delay = 0.5f },
new() { Type = "basic", Delay = 0.5f },
},
PostWaveDelay = 2f
},
// 波次2:带波动的敌人
new WaveDefinition
{
Enemies = new List<SpawnEntry>
{
new() { Type = "basic", Delay = 0f },
new() { Type = "basic", Delay = 0.3f, XOffset = -50 },
new() { Type = "basic", Delay = 0.3f, XOffset = 50 },
new() { Type = "medium", Delay = 1f },
},
PostWaveDelay = 2f
},
// 波次3:编队
new WaveDefinition
{
Enemies = new List<SpawnEntry>
{
new() { Type = "basic", Delay = 0f, XOffset = -80 },
new() { Type = "basic", Delay = 0.1f, XOffset = -40 },
new() { Type = "basic", Delay = 0.1f },
new() { Type = "basic", Delay = 0.1f, XOffset = 40 },
new() { Type = "basic", Delay = 0.1f, XOffset = 80 },
},
PostWaveDelay = 2f
},
// 波次4:Boss波
new WaveDefinition
{
Enemies = new List<SpawnEntry>
{
new() { Type = "boss", Delay = 1f },
},
PostWaveDelay = 0f,
IsBossWave = true
}
};
}
public override void _Process(double delta)
{
if (!_waveActive) return;
// 处理敌人生成队列
if (_spawnQueue.Count > 0)
{
_spawnTimer -= (float)delta;
if (_spawnTimer <= 0f)
{
var entry = _spawnQueue.Dequeue();
SpawnEnemy(entry);
if (_spawnQueue.Count > 0)
{
_spawnTimer = _spawnQueue.Peek().Delay;
}
}
}
else
{
// 检查当前波次的敌人是否全部消灭
bool allDead = true;
foreach (var child in _enemyContainer.GetChildren())
{
var enemy = child as Enemy;
if (enemy != null && enemy.IsAlive())
{
allDead = false;
break;
}
}
if (allDead)
{
CompleteCurrentWave();
}
}
}
/// <summary>
/// 开始下一个波次
/// </summary>
private void StartNextWave()
{
if (_currentWave >= _waves.Count)
{
EmitSignal(SignalName.AllWavesCompleted);
return;
}
var wave = _waves[_currentWave];
_waveActive = true;
// 把波次中的敌人加入生成队列
foreach (var entry in wave.Enemies)
{
_spawnQueue.Enqueue(entry);
}
if (_spawnQueue.Count > 0)
{
_spawnTimer = _spawnQueue.Peek().Delay;
}
if (wave.IsBossWave)
{
EmitSignal(SignalName.BossWaveStarted);
}
EmitSignal(SignalName.WaveStarted, _currentWave + 1);
}
/// <summary>
/// 完成当前波次
/// </summary>
private void CompleteCurrentWave()
{
_waveActive = false;
EmitSignal(SignalName.WaveCompleted, _currentWave + 1);
var wave = _waves[_currentWave];
_currentWave++;
// 延迟后开始下一个波次
var timer = GetTree().CreateTimer(wave.PostWaveDelay);
timer.Timeout += StartNextWave;
}
/// <summary>
/// 生成敌人
/// </summary>
private void SpawnEnemy(SpawnEntry entry)
{
string scenePath = entry.Type switch
{
"basic" => "res://scenes/enemies/basic_enemy.tscn",
"medium" => "res://scenes/enemies/medium_enemy.tscn",
"heavy" => "res://scenes/enemies/heavy_enemy.tscn",
"boss" => "res://scenes/enemies/boss.tscn",
_ => "res://scenes/enemies/basic_enemy.tscn"
};
var scene = GD.Load<PackedScene>(scenePath);
var enemy = scene.Instantiate<Node2D>();
float x = GameConfig.ScreenWidth / 2f + (entry.XOffset ?? 0f);
enemy.Position = new Vector2(x, -30f);
_enemyContainer.AddChild(enemy);
}
}
/// <summary>
/// 波次定义数据
/// </summary>
public class WaveDefinition
{
public List<SpawnEntry> Enemies { get; set; }
public float PostWaveDelay { get; set; } = 2f;
public bool IsBossWave { get; set; } = false;
}
/// <summary>
/// 敌人生成条目
/// </summary>
public class SpawnEntry
{
public string Type { get; set; } = "basic";
public float Delay { get; set; } = 0.5f;
public float? XOffset { get; set; }
}GDScript
extends Node
## 波次管理器——控制敌人的出场顺序和阵型
# 波次定义
var _waves: Array = []
var _current_wave: int = 0
var _wave_timer: float = 0.0
var _wave_active: bool = false
# 当前波次的生成队列
var _spawn_queue: Array = []
var _spawn_timer: float = 0.0
# 引用
var _enemy_container: Node2D
signal wave_started(wave_num: int)
signal wave_completed(wave_num: int)
signal all_waves_completed()
signal boss_wave_started()
func _ready() -> void:
_enemy_container = get_node("../GameArea/Enemies")
_load_waves()
_start_next_wave()
## 加载波次定义
func _load_waves() -> void:
_waves = [
# 波次1
{
"enemies": [
{"type": "basic", "delay": 0.0},
{"type": "basic", "delay": 0.5},
{"type": "basic", "delay": 0.5},
],
"post_delay": 2.0,
"is_boss": false
},
# 波次2
{
"enemies": [
{"type": "basic", "delay": 0.0},
{"type": "basic", "delay": 0.3, "x_offset": -50},
{"type": "basic", "delay": 0.3, "x_offset": 50},
{"type": "medium", "delay": 1.0},
],
"post_delay": 2.0,
"is_boss": false
},
# 波次3:编队
{
"enemies": [
{"type": "basic", "delay": 0.0, "x_offset": -80},
{"type": "basic", "delay": 0.1, "x_offset": -40},
{"type": "basic", "delay": 0.1},
{"type": "basic", "delay": 0.1, "x_offset": 40},
{"type": "basic", "delay": 0.1, "x_offset": 80},
],
"post_delay": 2.0,
"is_boss": false
},
# 波次4:Boss
{
"enemies": [
{"type": "boss", "delay": 1.0},
],
"post_delay": 0.0,
"is_boss": true
}
]
func _process(delta: float) -> void:
if not _wave_active:
return
# 处理生成队列
if _spawn_queue.size() > 0:
_spawn_timer -= delta
if _spawn_timer <= 0.0:
var entry = _spawn_queue.pop_front()
_spawn_enemy(entry)
if _spawn_queue.size() > 0:
_spawn_timer = _spawn_queue[0]["delay"]
else:
# 检查敌人是否全部消灭
var all_dead = true
for child in _enemy_container.get_children():
if child.is_alive():
all_dead = false
break
if all_dead:
_complete_current_wave()
## 开始下一个波次
func _start_next_wave() -> void:
if _current_wave >= _waves.size():
all_waves_completed.emit()
return
var wave = _waves[_current_wave]
_wave_active = true
for entry in wave["enemies"]:
_spawn_queue.append(entry)
if _spawn_queue.size() > 0:
_spawn_timer = _spawn_queue[0]["delay"]
if wave.get("is_boss", false):
boss_wave_started.emit()
wave_started.emit(_current_wave + 1)
## 完成当前波次
func _complete_current_wave() -> void:
_wave_active = false
wave_completed.emit(_current_wave + 1)
var wave = _waves[_current_wave]
_current_wave += 1
var timer = get_tree().create_timer(wave.get("post_delay", 2.0))
timer.timeout.connect(_start_next_wave)
## 生成敌人
func _spawn_enemy(entry: Dictionary) -> void:
var type = entry.get("type", "basic")
var scene_path = "res://scenes/enemies/%s_enemy.tscn" % type
if type == "boss":
scene_path = "res://scenes/enemies/boss.tscn"
var scene = load(scene_path)
var enemy = scene.instantiate()
var x = GameConfig.SCREEN_WIDTH / 2.0 + entry.get("x_offset", 0)
enemy.position = Vector2(x, -30.0)
_enemy_container.add_child(enemy)5.5 本章小结
| 组件 | 说明 |
|---|---|
| 敌人基类 | 共享的HP、移动、受伤、死亡逻辑 |
| 运动模式 | 直线、正弦波、悬停、Z字形 |
| 波次管理 | 按顺序生成敌人,管理波次间隔 |
| 波次定义 | 数据驱动,容易编辑和扩展 |
关键设计点:
- 敌人用数据驱动(Dictionary),方便设计新的波次
- 波次之间有间隔,给玩家喘息时间
- Boss波有特殊标记,触发Boss战UI
- 运动模式作为独立组件,可以自由组合
下一章我们将实现道具系统。
