4. 战斗系统
2026/4/14大约 4 分钟
4. 战斗系统
4.1 伤害判定原理
横版动作游戏的伤害判定用的是碰撞区域(Area2D):
- 攻击方有一个"攻击框"(Hitbox)——代表武器/拳头能打到的范围
- 被攻击方有一个"受击框"(Hurtbox)——代表身体能被打到的范围
- 当攻击框和受击框重叠时,触发伤害
玩家挥拳 → 攻击框激活 → 检测到与敌人受击框重叠 → 造成伤害这和回合制不同——不是"我选择攻击你",而是"我的拳头真的碰到你了"。
4.2 攻击框组件
C
using Godot;
/// <summary>
/// 攻击框——检测命中并造成伤害
/// </summary>
public partial class Hitbox : Area2D
{
[Signal] public delegate void HitEventHandler(Node2D target, int damage, bool launch);
public int Damage { get; set; } = 10;
public bool Launch { get; set; } = false; // 是否击飞
public bool IsAoe { get; set; } = false; // 是否范围攻击
private bool _active = false;
public override void _Ready()
{
BodyEntered += OnBodyEntered;
Monitoring = false; // 默认关闭,攻击时才开启
}
/// <summary>
/// 激活攻击框(攻击动画开始时调用)
/// </summary>
public void Activate(int damage, bool launch = false, bool isAoe = false)
{
Damage = damage;
Launch = launch;
IsAoe = isAoe;
Monitoring = true;
_active = true;
}
/// <summary>
/// 关闭攻击框(攻击动画结束时调用)
/// </summary>
public void Deactivate()
{
Monitoring = false;
_active = false;
}
private void OnBodyEntered(Node2D body)
{
if (!_active) return;
// 检查是否是可以被攻击的目标(敌人或玩家)
if (body.HasMethod("TakeDamage"))
{
EmitSignal(SignalName.Hit, body, Damage, Launch);
body.Call("TakeDamage", Damage);
if (Launch)
ApplyKnockback(body);
}
}
/// <summary>
/// 击退效果——把敌人打飞出去
/// </summary>
private void ApplyKnockback(Node2D target)
{
if (target is CharacterBody2D body)
{
// 击退方向:从攻击者指向被攻击者
var direction = (target.GlobalPosition - GlobalPosition).Normalized();
var vel = body.Velocity;
vel.X = direction.X * 300f;
vel.Y = -200f; // 向上弹起
body.Velocity = vel;
}
}
}GDScript
extends Area2D
class_name Hitbox
## 攻击框——检测命中并造成伤害
signal hit(target: Node2D, damage: int, launch: bool)
var damage: int = 10
var launch: bool = false
var is_aoe: bool = false
var _active: bool = false
func _ready() -> void:
body_entered.connect(_on_body_entered)
monitoring = false # 默认关闭
## 激活攻击框
func activate(dmg: int, do_launch: bool = false, aoe: bool = false) -> void:
damage = dmg
launch = do_launch
is_aoe = aoe
monitoring = true
_active = true
## 关闭攻击框
func deactivate() -> void:
monitoring = false
_active = false
func _on_body_entered(body: Node2D) -> void:
if not _active:
return
if body.has_method("take_damage"):
hit.emit(body, damage, launch)
body.take_damage(damage)
if launch:
_apply_knockback(body)
## 击退效果
func _apply_knockback(target: Node2D) -> void:
if target is CharacterBody2D:
var direction: Vector2 = (target.global_position - global_position).normalized()
target.velocity.x = direction.x * 300.0
target.velocity.y = -200.04.3 难度管理器
这是动态难度系统的核心——根据玩家人数实时调整怪物属性:
C
using Godot;
/// <summary>
/// 难度管理器——根据玩家人数动态调整游戏难度
/// </summary>
public partial class DifficultyManager : Node
{
public static DifficultyManager Instance { get; private set; }
// 难度缩放表(索引0=1人,索引3=4人)
private static readonly float[] EnemyCountMultiplier = { 1.0f, 1.5f, 2.0f, 2.5f };
private static readonly float[] EnemyHpMultiplier = { 1.0f, 1.2f, 1.4f, 1.6f };
private static readonly float[] EnemyAttackMultiplier = { 1.0f, 1.1f, 1.2f, 1.3f };
public override void _EnterTree() => Instance = this;
public override void _ExitTree() { if (Instance == this) Instance = null; }
/// <summary>
/// 根据当前玩家人数,计算怪物应该生成的数量
/// </summary>
public int ScaleEnemyCount(int baseCount)
{
int playerCount = GameManager.Instance?.PlayerCount ?? 1;
int idx = Mathf.Clamp(playerCount - 1, 0, 3);
return Mathf.RoundToInt(baseCount * EnemyCountMultiplier[idx]);
}
/// <summary>
/// 根据当前玩家人数,计算怪物的实际血量
/// </summary>
public int ScaleEnemyHp(int baseHp)
{
int playerCount = GameManager.Instance?.PlayerCount ?? 1;
int idx = Mathf.Clamp(playerCount - 1, 0, 3);
return Mathf.RoundToInt(baseHp * EnemyHpMultiplier[idx]);
}
/// <summary>
/// 根据当前玩家人数,计算怪物的实际攻击力
/// </summary>
public int ScaleEnemyAttack(int baseAttack)
{
int playerCount = GameManager.Instance?.PlayerCount ?? 1;
int idx = Mathf.Clamp(playerCount - 1, 0, 3);
return Mathf.RoundToInt(baseAttack * EnemyAttackMultiplier[idx]);
}
}GDScript
extends Node
class_name DifficultyManager
## 难度管理器——根据玩家人数动态调整游戏难度
static var instance: DifficultyManager
# 难度缩放表(索引0=1人,索引3=4人)
const ENEMY_COUNT_MULT: Array[float] = [1.0, 1.5, 2.0, 2.5]
const ENEMY_HP_MULT: Array[float] = [1.0, 1.2, 1.4, 1.6]
const ENEMY_ATTACK_MULT: Array[float] = [1.0, 1.1, 1.2, 1.3]
func _enter_tree() -> void:
instance = self
func _exit_tree() -> void:
if instance == self:
instance = null
## 根据玩家人数计算怪物生成数量
func scale_enemy_count(base_count: int) -> int:
var idx: int = clampi((GameManager.instance.player_count if GameManager.instance else 1) - 1, 0, 3)
return roundi(base_count * ENEMY_COUNT_MULT[idx])
## 根据玩家人数计算怪物血量
func scale_enemy_hp(base_hp: int) -> int:
var idx: int = clampi((GameManager.instance.player_count if GameManager.instance else 1) - 1, 0, 3)
return roundi(base_hp * ENEMY_HP_MULT[idx])
## 根据玩家人数计算怪物攻击力
func scale_enemy_attack(base_attack: int) -> int:
var idx: int = clampi((GameManager.instance.player_count if GameManager.instance else 1) - 1, 0, 3)
return roundi(base_attack * ENEMY_ATTACK_MULT[idx])4.4 本章小结
| 概念 | 说明 |
|---|---|
| Hitbox/Hurtbox | 攻击框和受击框,重叠时触发伤害 |
| 击退 | 被打中时向后弹飞,增加打击感 |
| DifficultyManager | 根据玩家人数实时缩放怪物属性 |
| 动态难度 | 人数变化时立即生效,已生成怪物不受影响 |
下一章我们将设计关卡结构和区域推进系统。
