6. 敌人AI
2026/4/14大约 4 分钟
6. 敌人AI
6.1 横版动作游戏的敌人行为
横版游戏的敌人AI不需要很复杂,核心是三个状态:
[巡逻] → 发现玩家 → [追击] → 进入攻击范围 → [攻击]
↑ │
└──────────── 玩家跑远了 ──────────────────────┘| 状态 | 敌人在做什么 |
|---|---|
| 巡逻 | 在固定区域左右徘徊 |
| 追击 | 朝最近的玩家方向移动 |
| 攻击 | 停下来,挥出攻击动作 |
6.2 敌人基类
C
using Godot;
/// <summary>
/// 敌人基类——包含AI状态机和基础行为
/// </summary>
public partial class EnemyBase : CharacterBody2D
{
public enum AiState { Patrol, Chase, Attack, Stun, Dead }
[Export] public float MoveSpeed { get; set; } = 80f;
[Export] public float DetectRange { get; set; } = 300f; // 发现玩家的距离
[Export] public float AttackRange { get; set; } = 60f; // 进入攻击的距离
[Export] public int BaseHp { get; set; } = 50;
[Export] public int BaseAttack { get; set; } = 10;
protected int _hp;
protected int _attack;
protected AiState _state = AiState.Patrol;
protected Node2D _target; // 当前追击的玩家
protected float _attackCooldown = 0f;
private const float Gravity = 980f;
private float _patrolTimer = 0f;
private float _patrolDirection = 1f;
public override void _Ready()
{
// 根据玩家人数缩放属性
_hp = DifficultyManager.Instance?.ScaleEnemyHp(BaseHp) ?? BaseHp;
_attack = DifficultyManager.Instance?.ScaleEnemyAttack(BaseAttack) ?? BaseAttack;
}
public override void _PhysicsProcess(double delta)
{
if (_state == AiState.Dead) return;
// 应用重力
if (!IsOnFloor())
{
var vel = Velocity;
vel.Y += Gravity * (float)delta;
Velocity = vel;
}
// 更新攻击冷却
if (_attackCooldown > 0f)
_attackCooldown -= (float)delta;
// 根据状态执行行为
switch (_state)
{
case AiState.Patrol: DoPatrol((float)delta); break;
case AiState.Chase: DoChase(); break;
case AiState.Attack: DoAttack(); break;
}
// 检测玩家,决定是否切换状态
UpdateAiState();
MoveAndSlide();
}
/// <summary>
/// 巡逻:左右徘徊
/// </summary>
private void DoPatrol(float delta)
{
_patrolTimer -= delta;
if (_patrolTimer <= 0f)
{
_patrolDirection *= -1f; // 换方向
_patrolTimer = GD.RandRange(1.5f, 3f);
}
var vel = Velocity;
vel.X = _patrolDirection * MoveSpeed * 0.5f;
Velocity = vel;
}
/// <summary>
/// 追击:朝玩家方向移动
/// </summary>
private void DoChase()
{
if (_target == null) return;
float dir = Mathf.Sign(_target.GlobalPosition.X - GlobalPosition.X);
var vel = Velocity;
vel.X = dir * MoveSpeed;
Velocity = vel;
// 翻转朝向
Scale = new Vector2(dir >= 0 ? 1 : -1, 1);
}
/// <summary>
/// 攻击:停下来打
/// </summary>
protected virtual void DoAttack()
{
var vel = Velocity;
vel.X = 0;
Velocity = vel;
if (_attackCooldown <= 0f)
{
PerformAttack();
_attackCooldown = 1.5f; // 1.5秒攻击间隔
}
}
protected virtual void PerformAttack()
{
// 子类重写:不同敌人有不同的攻击方式
GD.Print($"{Name} 攻击!伤害:{_attack}");
}
/// <summary>
/// 更新AI状态——找最近的玩家
/// </summary>
private void UpdateAiState()
{
_target = FindNearestPlayer();
if (_target == null)
{
_state = AiState.Patrol;
return;
}
float dist = GlobalPosition.DistanceTo(_target.GlobalPosition);
if (dist <= AttackRange)
_state = AiState.Attack;
else if (dist <= DetectRange)
_state = AiState.Chase;
else
_state = AiState.Patrol;
}
/// <summary>
/// 找到距离最近的存活玩家
/// </summary>
private Node2D FindNearestPlayer()
{
Node2D nearest = null;
float minDist = float.MaxValue;
var players = GetTree().GetNodesInGroup("players");
foreach (var p in players)
{
if (p is not Node2D player) continue;
// 检查玩家是否存活
if (player.HasMethod("IsAlive") && !(bool)player.Call("IsAlive")) continue;
float d = GlobalPosition.DistanceTo(player.GlobalPosition);
if (d < minDist)
{
minDist = d;
nearest = player;
}
}
return nearest;
}
/// <summary>
/// 受到伤害
/// </summary>
public void TakeDamage(int damage)
{
_hp -= damage;
if (_hp <= 0)
Die();
}
private void Die()
{
_state = AiState.Dead;
// 通知BattleZone
GetParent().Call("OnEnemyDied", this);
QueueFree();
}
}GDScript
extends CharacterBody2D
class_name EnemyBase
## 敌人基类——包含AI状态机和基础行为
enum AiState { PATROL, CHASE, ATTACK, STUN, DEAD }
@export var move_speed: float = 80.0
@export var detect_range: float = 300.0
@export var attack_range: float = 60.0
@export var base_hp: int = 50
@export var base_attack: int = 10
var _hp: int
var _attack: int
var _state: AiState = AiState.PATROL
var _target: Node2D = null
var _attack_cooldown: float = 0.0
var _patrol_timer: float = 0.0
var _patrol_direction: float = 1.0
const GRAVITY: float = 980.0
func _ready() -> void:
if DifficultyManager.instance:
_hp = DifficultyManager.instance.scale_enemy_hp(base_hp)
_attack = DifficultyManager.instance.scale_enemy_attack(base_attack)
else:
_hp = base_hp
_attack = base_attack
func _physics_process(delta: float) -> void:
if _state == AiState.DEAD:
return
if not is_on_floor():
velocity.y += GRAVITY * delta
if _attack_cooldown > 0.0:
_attack_cooldown -= delta
match _state:
AiState.PATROL: _do_patrol(delta)
AiState.CHASE: _do_chase()
AiState.ATTACK: _do_attack()
_update_ai_state()
move_and_slide()
## 巡逻:左右徘徊
func _do_patrol(delta: float) -> void:
_patrol_timer -= delta
if _patrol_timer <= 0.0:
_patrol_direction *= -1.0
_patrol_timer = randf_range(1.5, 3.0)
velocity.x = _patrol_direction * move_speed * 0.5
## 追击:朝玩家方向移动
func _do_chase() -> void:
if not _target:
return
var dir: float = signf(_target.global_position.x - global_position.x)
velocity.x = dir * move_speed
scale.x = 1.0 if dir >= 0 else -1.0
## 攻击
func _do_attack() -> void:
velocity.x = 0
if _attack_cooldown <= 0.0:
_perform_attack()
_attack_cooldown = 1.5
func _perform_attack() -> void:
print("%s 攻击!伤害:%d" % [name, _attack])
## 更新AI状态
func _update_ai_state() -> void:
_target = _find_nearest_player()
if not _target:
_state = AiState.PATROL
return
var dist: float = global_position.distance_to(_target.global_position)
if dist <= attack_range:
_state = AiState.ATTACK
elif dist <= detect_range:
_state = AiState.CHASE
else:
_state = AiState.PATROL
## 找到最近的存活玩家
func _find_nearest_player() -> Node2D:
var nearest: Node2D = null
var min_dist: float = INF
for p in get_tree().get_nodes_in_group("players"):
if not p is Node2D:
continue
if p.has_method("is_alive") and not p.is_alive:
continue
var d: float = global_position.distance_to(p.global_position)
if d < min_dist:
min_dist = d
nearest = p
return nearest
## 受到伤害
func take_damage(damage: int) -> void:
_hp -= damage
if _hp <= 0:
_die()
func _die() -> void:
_state = AiState.DEAD
get_parent().call("on_enemy_died", self)
queue_free()6.3 本章小结
| 概念 | 说明 |
|---|---|
| 状态机 | 巡逻→追击→攻击,根据距离自动切换 |
| 发现范围 | 距离玩家300像素内就追击 |
| 攻击范围 | 距离玩家60像素内就攻击 |
| 难度缩放 | 创建时根据玩家人数调整血量和攻击力 |
| 寻找最近玩家 | 从 players 组中找到距离最近的存活玩家 |
下一章我们将实现4人多人系统,包括玩家加入/离开和难度动态调整。
