8. Boss战设计
2026/4/13大约 9 分钟
Boss战设计
Boss战概述
什么是Boss战?
Boss战就是每个关卡最后的大决战。Boss通常是一个体型巨大、血量很高、攻击方式多样的强敌。打败Boss就意味着通关。Boss战是整个关卡的高潮——之前所有的战斗都是在为这一刻做准备。
Boss设计原则
| 原则 | 说明 | 好的例子 | 不好的例子 |
|---|---|---|---|
| 大体积 | Boss要比普通敌人大很多 | 占据屏幕1/3的巨型机器人 | 和普通敌人一样大 |
| 多攻击模式 | 至少2-3种不同的攻击方式 | 扫射+弹幕+冲撞 | 只会一种攻击 |
| 弱点暴露 | 有明显的弱点,让玩家知道打哪 | 发光的红色核心 | 全身都可以打,没有策略 |
| 阶段变化 | 血量低时变形态或变快 | 血量低于50%时暴走 | 从头到尾一个样 |
| 公平 | 攻击模式有规律,可以被学习 | 每次攻击前有预兆动作 | 随机攻击,无法预判 |
Boss状态机
Boss的状态切换
Boss有几种不同的"状态"(也叫"阶段"),在不同状态下做不同的事。就像一个人:平时在休息(待机),看到你了开始追你(追踪),靠近了就打你(攻击),打不过就跑(逃跑)。Boss也是一样——它在不同状态下有不同的行为模式。
状态转换图
┌──────────┐
┌──────→│ 待机 │←──────┐
│ └────┬─────┘ │
│ │ 玩家进入 │
│ │ 攻击范围 │
│ ┌────▼─────┐ │
│ │ 追踪 │ │
│ └────┬─────┘ │
│ │ 到达 │
│ │ 攻击距离 │
│ ┌────▼─────┐ │
│ ┌───→│ 攻击 │ │
│ │ └────┬─────┘ │
│ │ │ 攻击结束 │
│ │ ┌────▼─────┐ │
│ └────│ 冷却中 │ │
│ └────┬─────┘ │
│ │ │
│ │ 血量=0 │
│ ┌────▼─────┐ │
└───────│ 死亡 │───────┘
└──────────┘Boss控制器脚本
C#
using Godot;
using System.Collections.Generic;
/// <summary>
/// Boss控制器 - 管理Boss的所有行为
/// </summary>
public partial class BossController : CharacterBody3D
{
// ===== Boss参数 =====
[ExportGroup("基础属性")]
[Export] public int MaxHealth = 100; // 最大血量
[Export] public float MoveSpeed = 2.0f; // 移动速度
[Export] public float Phase2HealthPercent = 0.5f; // 第二阶段血量阈值
[ExportGroup("攻击参数")]
[Export] public float ShootCooldown = 0.8f; // 射击间隔
[Export] public float ChargeSpeed = 10.0f; // 冲撞速度
[Export] public float ChargeDuration = 1.5f; // 冲撞持续时间
[ExportGroup("弹幕参数")]
[Export] public int BulletCount = 8; // 弹幕子弹数
[Export] public float BulletSpeed = 8.0f; // 弹幕子弹速度
[Export] public float BulletDamage = 1.0f; // 弹幕伤害
// ===== 状态 =====
public enum BossState
{
Idle, // 待机
Chase, // 追踪玩家
Attack1, // 攻击模式1:扫射
Attack2, // 攻击模式2:弹幕
Charge, // 冲撞攻击
Stunned, // 眩晕(被击中弱点后)
Dying, // 死亡动画
Dead // 死亡
}
private BossState _currentState = BossState.Idle;
private int _currentHealth;
private int _currentPhase = 1; // 当前阶段(1或2)
private float _stateTimer = 0.0f;
private float _attackTimer = 0.0f;
private Player _player;
private bool _isInvincible = false;
// ===== 信号 =====
[Signal] public delegate void BossPhaseChangedEventHandler(int phase);
[Signal] public delegate void BossDefeatedEventHandler();
public override void _Ready()
{
_currentHealth = MaxHealth;
_player = GetTree().GetFirstNodeInGroup("player") as Player;
}
public override void _PhysicsProcess(double delta)
{
if (_currentState == BossState.Dead) return;
float dt = (float)delta;
_stateTimer += dt;
// 状态机更新
switch (_currentState)
{
case BossState.Idle:
UpdateIdle(dt);
break;
case BossState.Chase:
UpdateChase(dt);
break;
case BossState.Attack1:
UpdateAttack1(dt);
break;
case BossState.Attack2:
UpdateAttack2(dt);
break;
case BossState.Charge:
UpdateCharge(dt);
break;
case BossState.Stunned:
UpdateStunned(dt);
break;
case BossState.Dying:
UpdateDying(dt);
break;
}
// 应用移动
Velocity = new Vector3(Velocity.X, Velocity.Y - 15f * dt, Velocity.Z);
MoveAndSlide();
}
/// <summary>
/// 受到伤害
/// </summary>
public void TakeDamage(float damage)
{
if (_isInvincible || _currentState == BossState.Dying)
return;
_currentHealth -= (int)damage;
GD.Print($"Boss 受到 {damage} 伤害,剩余血量: {_currentHealth}/{MaxHealth}");
// 检查阶段变化
float healthPercent = (float)_currentHealth / MaxHealth;
if (healthPercent <= Phase2HealthPercent && _currentPhase == 1)
{
EnterPhase2();
}
// 检查死亡
if (_currentHealth <= 0)
{
_currentState = BossState.Dying;
}
}
// ===== 各状态更新 =====
private void UpdateIdle(float dt)
{
// 待机1秒后开始追踪
if (_stateTimer > 1.0f)
{
ChangeState(BossState.Chase);
}
}
private void UpdateChase(float dt)
{
if (_player == null) return;
// 追踪玩家
Vector3 dir = (_player.GlobalPosition - GlobalPosition).Normalized();
Velocity = new Vector3(dir.X * MoveSpeed, Velocity.Y, 0);
// 到达攻击距离,随机选择攻击模式
float dist = GlobalPosition.DistanceTo(_player.GlobalPosition);
if (dist < 12.0f && _stateTimer > 2.0f)
{
// 随机选择攻击
float roll = GD.Randf();
if (roll < 0.4f)
ChangeState(BossState.Attack1);
else if (roll < 0.7f)
ChangeState(BossState.Attack2);
else
ChangeState(BossState.Charge);
}
}
private void UpdateAttack1(float dt)
{
// 扫射攻击:快速发射多颗子弹
Velocity = new Vector3(0, Velocity.Y, 0);
_attackTimer -= dt;
if (_attackTimer <= 0)
{
ShootAtPlayer();
_attackTimer = ShootCooldown;
}
// 扫射持续3秒后冷却
if (_stateTimer > 3.0f)
{
ChangeState(BossState.Chase);
}
}
private void UpdateAttack2(float dt)
{
// 弹幕攻击:发射一圈子弹
Velocity = new Vector3(0, Velocity.Y, 0);
// 在状态开始时发射弹幕
if (_stateTimer < 0.1f)
{
FireBulletPattern();
}
// 弹幕后冷却
if (_stateTimer > 2.0f)
{
ChangeState(BossState.Chase);
}
}
private void UpdateCharge(float dt)
{
// 冲撞攻击:快速冲向玩家
if (_player == null) return;
Vector3 dir = (_player.GlobalPosition - GlobalPosition).Normalized();
Velocity = new Vector3(dir.X * ChargeSpeed, Velocity.Y, 0);
// 冲撞结束
if (_stateTimer > ChargeDuration)
{
ChangeState(BossState.Stunned);
}
}
private void UpdateStunned(float dt)
{
// 眩晕2秒(暴露弱点)
Velocity = new Vector3(0, Velocity.Y, 0);
if (_stateTimer > 2.0f)
{
ChangeState(BossState.Chase);
}
}
private void UpdateDying(float dt)
{
// 死亡动画
Velocity = new Vector3(0, Velocity.Y, 0);
_isInvincible = true;
// 闪烁效果
if ((int)(_stateTimer * 10) % 2 == 0)
Visible = true;
else
Visible = false;
// 3秒后完全死亡
if (_stateTimer > 3.0f)
{
_currentState = BossState.Dead;
EmitSignal(SignalName.BossDefeated);
GD.Print("Boss 被击败了!");
// 大爆炸特效
CreateExplosion();
QueueFree();
}
}
// ===== 攻击方法 =====
private void ShootAtPlayer()
{
if (_player == null) return;
Vector3 dir = (_player.GlobalPosition - GlobalPosition).Normalized();
GD.Print("Boss 扫射!");
// 子弹生成通过射击管理器
}
private void FireBulletPattern()
{
GD.Print("Boss 弹幕攻击!");
// 向四周发射一圈子弹
for (int i = 0; i < BulletCount; i++)
{
float angle = (Mathf.Tau / BulletCount) * i;
Vector3 dir = new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0);
GD.Print($" 弹幕方向 {i}: {dir}");
}
}
// ===== 阶段管理 =====
private void EnterPhase2()
{
_currentPhase = 2;
GD.Print("=== Boss 进入第二阶段!暴走! ===");
EmitSignal(SignalName.BossPhaseChanged, 2);
// 第二阶段参数强化
MoveSpeed *= 1.5f;
ShootCooldown *= 0.6f; // 射击更快
BulletCount += 4; // 弹幕更密
}
private void ChangeState(BossState newState)
{
_currentState = newState;
_stateTimer = 0.0f;
_attackTimer = 0.0f;
}
private void CreateExplosion()
{
GD.Print("Boss 大爆炸!");
// 爆炸特效在音效特效章节实现
}
}GDScript
extends CharacterBody3D
## Boss控制器 - 管理Boss的所有行为
# ===== Boss参数 =====
@export_group("基础属性")
@export var max_health: int = 100 # 最大血量
@export var move_speed: float = 2.0 # 移动速度
@export var phase2_health_percent: float = 0.5 # 第二阶段血量阈值
@export_group("攻击参数")
@export var shoot_cooldown: float = 0.8 # 射击间隔
@export var charge_speed: float = 10.0 # 冲撞速度
@export var charge_duration: float = 1.5 # 冲撞持续时间
@export_group("弹幕参数")
@export var bullet_count: int = 8 # 弹幕子弹数
@export var bullet_speed: float = 8.0 # 弹幕子弹速度
@export var bullet_damage: float = 1.0 # 弹幕伤害
# ===== 状态 =====
enum BossState {
IDLE, # 待机
CHASE, # 追踪玩家
ATTACK1, # 攻击模式1:扫射
ATTACK2, # 攻击模式2:弹幕
CHARGE, # 冲撞攻击
STUNNED, # 眩晕
DYING, # 死亡动画
DEAD # 死亡
}
var _current_state: BossState = BossState.IDLE
var _current_health: int
var _current_phase: int = 1
var _state_timer: float = 0.0
var _attack_timer: float = 0.0
var _player: Node
var _is_invincible: bool = false
# ===== 信号 =====
signal boss_phase_changed(phase: int)
signal boss_defeated()
func _ready():
_current_health = max_health
_player = get_tree().get_first_node_in_group("player")
func _physics_process(delta):
if _current_state == BossState.DEAD:
return
_state_timer += delta
# 状态机更新
match _current_state:
BossState.IDLE:
_update_idle(delta)
BossState.CHASE:
_update_chase(delta)
BossState.ATTACK1:
_update_attack1(delta)
BossState.ATTACK2:
_update_attack2(delta)
BossState.CHARGE:
_update_charge(delta)
BossState.STUNNED:
_update_stunned(delta)
BossState.DYING:
_update_dying(delta)
# 应用移动
velocity = Vector3(velocity.x, velocity.y - 15.0 * delta, velocity.z)
move_and_slide()
## 受到伤害
func take_damage(damage: float):
if _is_invincible or _current_state == BossState.DYING:
return
_current_health -= int(damage)
print("Boss 受到 %d 伤害,剩余血量: %d/%d" % [int(damage), _current_health, max_health])
# 检查阶段变化
var health_percent = float(_current_health) / max_health
if health_percent <= phase2_health_percent and _current_phase == 1:
_enter_phase2()
# 检查死亡
if _current_health <= 0:
_current_state = BossState.DYING
## 各状态更新
func _update_idle(_dt: float):
if _state_timer > 1.0:
_change_state(BossState.CHASE)
func _update_chase(_dt: float):
if not _player:
return
var dir = (_player.global_position - global_position).normalized()
velocity = Vector3(dir.x * move_speed, velocity.y, 0)
var dist = global_position.distance_to(_player.global_position)
if dist < 12.0 and _state_timer > 2.0:
var roll = randf()
if roll < 0.4:
_change_state(BossState.ATTACK1)
elif roll < 0.7:
_change_state(BossState.ATTACK2)
else:
_change_state(BossState.CHARGE)
func _update_attack1(dt: float):
velocity = Vector3(0, velocity.y, 0)
_attack_timer -= dt
if _attack_timer <= 0:
_shoot_at_player()
_attack_timer = shoot_cooldown
if _state_timer > 3.0:
_change_state(BossState.CHASE)
func _update_attack2(_dt: float):
velocity = Vector3(0, velocity.y, 0)
if _state_timer < 0.1:
_fire_bullet_pattern()
if _state_timer > 2.0:
_change_state(BossState.CHASE)
func _update_charge(_dt: float):
if not _player:
return
var dir = (_player.global_position - global_position).normalized()
velocity = Vector3(dir.x * charge_speed, velocity.y, 0)
if _state_timer > charge_duration:
_change_state(BossState.STUNNED)
func _update_stunned(_dt: float):
velocity = Vector3(0, velocity.y, 0)
if _state_timer > 2.0:
_change_state(BossState.CHASE)
func _update_dying(dt: float):
velocity = Vector3(0, velocity.y, 0)
_is_invincible = true
if int(_state_timer * 10) % 2 == 0:
visible = true
else:
visible = false
if _state_timer > 3.0:
_current_state = BossState.DEAD
boss_defeated.emit()
print("Boss 被击败了!")
_create_explosion()
queue_free()
## 攻击方法
func _shoot_at_player():
if not _player:
return
var dir = (_player.global_position - global_position).normalized()
print("Boss 扫射!")
func _fire_bullet_pattern():
print("Boss 弹幕攻击!")
for i in range(bullet_count):
var angle = (TAU / bullet_count) * i
var dir = Vector3(cos(angle), sin(angle), 0)
## 阶段管理
func _enter_phase2():
_current_phase = 2
print("=== Boss 进入第二阶段!暴走! ===")
boss_phase_changed.emit(2)
move_speed *= 1.5
shoot_cooldown *= 0.6
bullet_count += 4
func _change_state(new_state: BossState):
_current_state = new_state
_state_timer = 0.0
_attack_timer = 0.0
func _create_explosion():
print("Boss 大爆炸!")Boss弹幕设计
弹幕类型
| 弹幕类型 | 描述 | 视觉效果 |
|---|---|---|
| 扇形弹幕 | 从Boss位置向前方扇形散射 | 像一把打开的扇子 |
| 圆形弹幕 | 向四面八方同时发射 | 像放烟花 |
| 螺旋弹幕 | 子弹沿螺旋轨迹飞出 | 像龙卷风 |
| 追踪弹 | 子弹会追踪玩家 | 带尾焰的导弹 |
| 激光束 | 一道持续的光束 | 粗大的红色光柱 |
扇形弹幕代码示例
C#
/// <summary>
/// 发射扇形弹幕
/// </summary>
private void FireFanPattern()
{
int count = 5; // 子弹数
float spread = 60f; // 散射角度(度)
float startAngle = -spread / 2;
float step = count > 1 ? spread / (count - 1) : 0;
for (int i = 0; i < count; i++)
{
float angle = Mathf.DegToRad(startAngle + step * i);
// 基础方向朝左(Boss面朝左)
Vector3 dir = new Vector3(
-Mathf.Cos(angle),
Mathf.Sin(angle),
0
).Normalized();
GD.Print($"扇形弹幕 {i}: 角度={angle:F2}, 方向={dir}");
}
}GDScript
## 发射扇形弹幕
func _fire_fan_pattern():
var count: int = 5 # 子弹数
var spread: float = 60.0 # 散射角度(度)
var start_angle = -spread / 2
var step = spread / (count - 1) if count > 1 else 0
for i in range(count):
var angle = deg_to_rad(start_angle + step * i)
# 基础方向朝左(Boss面朝左)
var dir = Vector3(
-cos(angle),
sin(angle),
0
).normalized()
print("扇形弹幕 %d: 角度=%.2f, 方向=%s" % [i, angle, str(dir)])Boss血条UI
Boss战需要一个醒目的血条,让玩家知道Boss还剩多少血。
C#
using Godot;
/// <summary>
/// Boss血条UI - 显示Boss的血量
/// </summary>
public partial class BossHealthBar : Control
{
[Export] public ProgressBar HealthBar;
[Export] public Label BossName;
[Export] public Label PhaseLabel;
private BossController _boss;
public override void _Ready()
{
// 默认隐藏
Visible = false;
// 查找Boss
_boss = GetTree().GetFirstNodeInGroup("boss") as BossController;
if (_boss != null)
{
_boss.Connect("BossPhaseChanged",
Callable.From((int phase) => OnPhaseChanged(phase)));
_boss.Connect("BossDefeated",
Callable.From(OnBossDefeated));
}
}
public void ShowHealthBar(string bossName, int maxHealth)
{
Visible = true;
BossName.Text = bossName;
HealthBar.MaxValue = maxHealth;
HealthBar.Value = maxHealth;
PhaseLabel.Text = "阶段 1";
}
public override void _Process(double delta)
{
if (_boss == null || !Visible) return;
// 更新血条
// 这里需要从Boss获取当前血量
}
private void OnPhaseChanged(int phase)
{
PhaseLabel.Text = $"阶段 {phase}";
// 血条变红表示Boss暴走
HealthBar.Modulate = new Color(1, 0.3f, 0.3f);
}
private void OnBossDefeated()
{
// 播放胜利动画
var tween = CreateTween();
tween.TweenProperty(this, "modulate:a", 0, 2.0);
tween.TweenCallback(Callable.From(() => Visible = false));
}
}GDScript
extends Control
## Boss血条UI - 显示Boss的血量
@export var health_bar: ProgressBar
@export var boss_name: Label
@export var phase_label: Label
var _boss: Node
func _ready():
# 默认隐藏
visible = false
# 查找Boss
_boss = get_tree().get_first_node_in_group("boss")
if _boss:
_boss.boss_phase_changed.connect(_on_phase_changed)
_boss.boss_defeated.connect(_on_boss_defeated)
func show_health_bar(name: String, max_health: int):
visible = true
boss_name.text = name
health_bar.max_value = max_health
health_bar.value = max_health
phase_label.text = "阶段 1"
func _on_phase_changed(phase: int):
phase_label.text = "阶段 %d" % phase
# 血条变红表示Boss暴走
health_bar.modulate = Color(1, 0.3, 0.3)
func _on_boss_defeated():
# 播放胜利动画
var tween = create_tween()
tween.tween_property(self, "modulate:a", 0, 2.0)
tween.tween_callback(func(): visible = false)Boss战设计清单
| 设计要点 | 是否实现 | 说明 |
|---|---|---|
| Boss血条 | 是 | 醒目显示在屏幕上方 |
| 多攻击模式 | 是 | 扫射、弹幕、冲撞 |
| 弱点暴露 | 是 | 冲撞后眩晕,暴露弱点 |
| 阶段变化 | 是 | 血量低于50%进入暴走模式 |
| 死亡动画 | 是 | 闪烁+大爆炸 |
| 胜利奖励 | 是 | 击败后掉落好道具 |
本章小结
本章我们实现了完整的Boss战系统:
- Boss状态机——待机/追踪/攻击/冲撞/眩晕/死亡
- 多攻击模式——扫射、弹幕、冲撞三种攻击
- 阶段变化——血量低于50%时Boss暴走,攻击更强
- 弹幕系统——扇形、圆形等多种弹幕模式
- Boss血条UI——显示血量和阶段信息
- 死亡动画——闪烁+爆炸+奖励掉落
Boss战设计心得
- 第一阶段让玩家学习Boss的攻击模式
- 第二阶段加快节奏,增加压力
- 眩晕机制给玩家反击的机会
- 每次攻击前给0.5秒的预兆动作
- 打败Boss后要有强烈的成就感反馈
