11. 敌人AI与行为模式
2026/4/14大约 5 分钟
敌人AI与行为模式
上一章讲了游戏 UI 界面,本章专门讲敌人怎么"思考"和行动——从简单的巡逻到复杂的追踪、攻击行为。
敌人行为的三种模式
| 模式 | 比喻 | 适用场景 |
|---|---|---|
| 固定路线巡逻 | 保安在走廊里来回走 | 平台跳跃、动作游戏 |
| 追踪玩家 | 狗看到人就跟过来 | 动作冒险、RPG |
| 触发式行为 | 陷阱有人靠近就激活 | 所有类型 |
固定路线巡逻
敌人沿预设路径来回移动,这是最简单的敌人行为。
使用 Path2D 巡逻
C#
// 敌人沿路径巡逻
using Godot;
public partial class EnemyFollower : PathFollow2D
{
private float _speed = 50.0f;
private int _direction = 1; // 1=正向, -1=反向
public override void _PhysicsProcess(double delta)
{
Progress += _speed * _direction * (float)delta;
// 到达路径终点后反向
if (ProgressRatio >= 1.0)
_direction = -1;
else if (ProgressRatio <= 0.0)
_direction = 1;
}
}GDScript
# 敌人沿路径巡逻
extends PathFollow2D
var speed = 50.0
var direction = 1 # 1=正向, -1=反向
func _physics_process(delta):
progress += speed * direction * delta
# 到达路径终点后反向
if progress_ratio >= 1.0:
direction = -1
elif progress_ratio <= 0.0:
direction = 1场景结构:
EnemyPath (Path2D) ← 定义巡逻路径
└── EnemyFollower (PathFollow2D) ← 沿路径移动
├── Sprite2D
└── CollisionShape2D简单来回巡逻(不用 Path2D)
如果敌人只是在两点之间来回走,不需要 Path2D:
C#
// 敌人在两点之间巡逻
using Godot;
public partial class PatrolEnemy : CharacterBody2D
{
[Export] private float _speed = 60.0f;
[Export] private float _patrolDistance = 200.0f;
private Vector2 _startPosition;
private int _direction = 1;
public override void _Ready()
{
_startPosition = Position;
}
public override void _PhysicsProcess(double delta)
{
Velocity = new Vector2(_direction * _speed, Velocity.Y);
MoveAndSlide();
// 走到巡逻范围尽头就掉头
if (Position.X > _startPosition.X + _patrolDistance)
_direction = -1;
else if (Position.X < _startPosition.X - _patrolDistance)
_direction = 1;
// 碰到墙壁也掉头
if (IsOnWall())
_direction *= -1;
}
}GDScript
# 敌人在两点之间巡逻
extends CharacterBody2D
@export var speed = 60.0
@export var patrol_distance = 200.0
var start_position: Vector2
var direction = 1
func _ready():
start_position = position
func _physics_process(delta):
velocity = Vector2(direction * speed, velocity.y)
move_and_slide()
# 走到巡逻范围尽头就掉头
if position.x > start_position.x + patrol_distance:
direction = -1
elif position.x < start_position.x - patrol_distance:
direction = 1
# 碰到墙壁也掉头
if is_on_wall():
direction *= -1追踪玩家
敌人检测到玩家后,朝玩家方向移动。这是最常用的敌人行为。
简单追踪
C#
// 敌人追踪玩家
using Godot;
public partial class ChaseEnemy : CharacterBody2D
{
private const float Speed = 80.0f;
private Node2D _player;
public override void _Ready()
{
// 找到玩家节点(通过分组)
_player = GetTree().GetFirstNodeInGroup("players") as Node2D;
}
public override void _PhysicsProcess(double delta)
{
if (_player != null)
{
Vector2 direction = (_player.Position - Position).Normalized();
Velocity = direction * Speed;
MoveAndSlide();
}
}
}GDScript
# 敌人追踪玩家
extends CharacterBody2D
const SPEED = 80.0
var player: Node2D
func _ready():
# 找到玩家节点(通过分组)
player = get_tree().get_first_node_in_group("players")
func _physics_process(_delta):
if player:
var direction = (player.position - position).normalized()
velocity = direction * SPEED
move_and_slide()带检测范围的追踪
不是一开始就追,而是玩家靠近了才追。用 RayCast2D 或者距离检测:
C#
// 带检测范围的追踪敌人
using Godot;
public partial class SmartEnemy : CharacterBody2D
{
[Export] private float _chaseSpeed = 100.0f;
[Export] private float _patrolSpeed = 40.0f;
[Export] private float _detectRange = 200.0f;
[Export] private float _attackRange = 30.0f;
private Node2D _player;
private bool _isChasing = false;
public override void _Ready()
{
_player = GetTree().GetFirstNodeInGroup("players") as Node2D;
}
public override void _PhysicsProcess(double delta)
{
if (_player == null) return;
float distanceToPlayer = Position.DistanceTo(_player.Position);
if (distanceToPlayer < _detectRange)
{
_isChasing = true;
}
else if (distanceToPlayer > _detectRange * 1.5f)
{
// 超出追踪范围的 1.5 倍后放弃追踪
_isChasing = false;
}
if (_isChasing)
{
Vector2 direction = (_player.Position - Position).Normalized();
Velocity = new Vector2(direction.X * _chaseSpeed, Velocity.Y);
}
else
{
// 原地不动或缓慢巡逻
Velocity = new Vector2(0, Velocity.Y);
}
MoveAndSlide();
}
}GDScript
# 带检测范围的追踪敌人
extends CharacterBody2D
@export var chase_speed = 100.0
@export var patrol_speed = 40.0
@export var detect_range = 200.0
@export var attack_range = 30.0
var player: Node2D
var is_chasing = false
func _ready():
player = get_tree().get_first_node_in_group("players")
func _physics_process(_delta):
if not player:
return
var distance_to_player = position.distance_to(player.position)
if distance_to_player < detect_range:
is_chasing = true
elif distance_to_player > detect_range * 1.5:
# 超出追踪范围的 1.5 倍后放弃追踪
is_chasing = false
if is_chasing:
var direction = (player.position - position).normalized()
velocity = Vector2(direction.x * chase_speed, velocity.y)
else:
# 原地不动或缓慢巡逻
velocity = Vector2(0, velocity.y)
move_and_slide()触发式行为
有些敌人不需要移动,而是在玩家靠近时触发事件(如突然出现、发射子弹)。
使用 Area2D 的信号:
Trap (Area2D)
├── CollisionShape2D ← 检测区域
└── Sprite2D ← 陷阱外观C#
// 陷阱触发
using Godot;
public partial class Trap : Area2D
{
[Export] private int _damage = 20;
public override void _Ready()
{
BodyEntered += OnBodyEntered;
}
private void OnBodyEntered(Node2D body)
{
if (body.IsInGroup("players"))
{
body.Call("take_damage", _damage);
// 可以播放动画、音效
}
}
}GDScript
# 陷阱触发
extends Area2D
@export var damage = 20
func _ready():
body_entered.connect(_on_body_entered)
func _on_body_entered(body):
if body.is_in_group("players"):
body.take_damage(damage)
# 可以播放动画、音效简单状态机
当敌人有多种行为(巡逻、追踪、攻击、死亡)时,用状态机管理最清晰。
什么是状态机
打个比方:红绿灯就是状态机——它只有三个状态(红、黄、绿),任何时刻只能处于其中一个状态,并且按固定规则切换(红→绿→黄→红)。
敌人的行为也是一样:同一时刻只能做一件事(巡逻、追踪、攻击、死亡),并且按规则切换状态。
C#
// 简单的敌人状态机
using Godot;
public enum EnemyState { Patrol, Chase, Attack, Dead }
public partial class StateMachineEnemy : CharacterBody2D
{
[Export] private float _patrolSpeed = 40.0f;
[Export] private float _chaseSpeed = 100.0f;
[Export] private float _detectRange = 200.0f;
[Export] private float _attackRange = 30.0f;
private EnemyState _state = EnemyState.Patrol;
private Node2D _player;
private int _patrolDirection = 1;
private float _hp = 100.0f;
public override void _Ready()
{
_player = GetTree().GetFirstNodeInGroup("players") as Node2D;
}
public override void _PhysicsProcess(double delta)
{
switch (_state)
{
case EnemyState.Patrol:
PatrolBehavior(delta);
break;
case EnemyState.Chase:
ChaseBehavior(delta);
break;
case EnemyState.Attack:
AttackBehavior(delta);
break;
case EnemyState.Dead:
return; // 什么都不做
}
MoveAndSlide();
}
private void PatrolBehavior(double delta)
{
Velocity = new Vector2(_patrolDirection * _patrolSpeed, Velocity.Y);
if (IsOnWall())
_patrolDirection *= -1;
// 检测到玩家就切换到追踪
if (_player != null && Position.DistanceTo(_player.Position) < _detectRange)
_state = EnemyState.Chase;
}
private void ChaseBehavior(double delta)
{
if (_player == null) { _state = EnemyState.Patrol; return; }
float dist = Position.DistanceTo(_player.Position);
// 太远了就放弃
if (dist > _detectRange * 2.0f)
{
_state = EnemyState.Patrol;
return;
}
// 够近了就攻击
if (dist < _attackRange)
{
_state = EnemyState.Attack;
return;
}
Vector2 dir = (_player.Position - Position).Normalized();
Velocity = new Vector2(dir.X * _chaseSpeed, Velocity.Y);
}
private void AttackBehavior(double delta)
{
Velocity = Vector2.Zero;
// 播放攻击动画、造成伤害
// 攻击结束后回到追踪
_state = EnemyState.Chase;
}
public void TakeDamage(float amount)
{
_hp -= amount;
if (_hp <= 0)
{
_state = EnemyState.Dead;
QueueFree();
}
}
}GDScript
# 简单的敌人状态机
extends CharacterBody2D
enum EnemyState { PATROL, CHASE, ATTACK, DEAD }
@export var patrol_speed = 40.0
@export var chase_speed = 100.0
@export var detect_range = 200.0
@export var attack_range = 30.0
var state = EnemyState.PATROL
var player: Node2D
var patrol_direction = 1
var hp = 100.0
func _ready():
player = get_tree().get_first_node_in_group("players")
func _physics_process(delta):
match state:
EnemyState.PATROL:
patrol_behavior(delta)
EnemyState.CHASE:
chase_behavior(delta)
EnemyState.ATTACK:
attack_behavior(delta)
EnemyState.DEAD:
return # 什么都不做
move_and_slide()
func patrol_behavior(_delta):
velocity = Vector2(patrol_direction * patrol_speed, velocity.y)
if is_on_wall():
patrol_direction *= -1
# 检测到玩家就切换到追踪
if player and position.distance_to(player.position) < detect_range:
state = EnemyState.CHASE
func chase_behavior(_delta):
if not player:
state = EnemyState.PATROL
return
var dist = position.distance_to(player.position)
# 太远了就放弃
if dist > detect_range * 2.0:
state = EnemyState.PATROL
return
# 够近了就攻击
if dist < attack_range:
state = EnemyState.ATTACK
return
var dir = (player.position - position).normalized()
velocity = Vector2(dir.x * chase_speed, velocity.y)
func attack_behavior(_delta):
velocity = Vector2.ZERO
# 播放攻击动画、造成伤害
# 攻击结束后回到追踪
state = EnemyState.CHASE
func take_damage(amount):
hp -= amount
if hp <= 0:
state = EnemyState.DEAD
queue_free()下一章
敌人行为搞定了,接下来学习精灵动画,让角色和敌人"动起来"。
