15. 敌人创建与AI
2026/4/14大约 6 分钟
敌人创建与AI
敌人让游戏有挑战性
没有敌人的游戏就像没有对手的棋局——少了对抗,就少了乐趣。敌人(Enemy) 是给玩家制造障碍和挑战的角色,它们的行为由程序控制,这就是所谓的 AI(人工智能)。
游戏里的 AI 不需要像真正的人工智能那么复杂。大多数游戏敌人只需要几个简单的行为规则,就能让玩家感觉"这个敌人很聪明"。
创建敌人场景
敌人场景的结构和玩家类似,但多了一个"感知范围"节点:
各节点的作用:
- CharacterBody3D:敌人的物理主体,负责移动和碰撞
- MeshInstance3D:敌人的外观模型
- CollisionShape3D:敌人的物理碰撞边界
- Area3D:检测玩家是否进入攻击/追击范围(就像敌人的"视野")
- AnimationPlayer:控制敌人的动画
- NavigationAgent3D:寻路代理,让敌人能绕过障碍物追击玩家
创建步骤
- 新建场景,根节点选择
CharacterBody3D,重命名为Enemy - 添加
MeshInstance3D,选择一个 Mesh(比如BoxMesh,用红色材质区分) - 添加
CollisionShape3D,选择BoxShape3D - 添加
Area3D子节点,再在Area3D下添加CollisionShape3D,设置一个较大的球形范围 - 保存为
Enemy.tscn
简单 AI 行为
游戏里的敌人 AI 通常由几个简单的行为组成:
巡逻(Patrol)
敌人在两个点之间来回走动,就像保安巡逻一样。
追击(Chase)
当玩家进入敌人的"视野范围"时,敌人开始追赶玩家。
攻击(Attack)
当敌人靠近玩家到一定距离时,发动攻击。
逃跑(Flee)
当敌人血量低于某个阈值时,转身逃跑(可选行为)。
AI 状态机
状态机(State Machine) 是管理 AI 行为的最常用方法。就像交通灯只有红、黄、绿三种状态,每次只处于一种状态,敌人的 AI 也是如此。
每个状态只做一件事:
| 状态 | 行为 |
|---|---|
| Idle(待机) | 站在原地,播放待机动画 |
| Patrol(巡逻) | 在两点之间来回移动 |
| Chase(追击) | 向玩家位置移动 |
| Attack(攻击) | 停下来,播放攻击动画,造成伤害 |
完整的敌人 AI 代码
C#
using Godot;
public partial class Enemy : CharacterBody3D
{
// AI 状态枚举
private enum State { Idle, Patrol, Chase, Attack }
private State _currentState = State.Patrol;
[Export] public float MoveSpeed = 3.0f;
[Export] public float ChaseSpeed = 5.0f;
[Export] public float DetectRange = 8.0f; // 发现玩家的距离
[Export] public float AttackRange = 1.5f; // 攻击距离
[Export] public float MaxHp = 100.0f;
private float _hp;
private Node3D _player;
private Vector3 _patrolPointA;
private Vector3 _patrolPointB;
private bool _goingToB = true;
public override void _Ready()
{
_hp = MaxHp;
// 设置巡逻范围(以初始位置为中心,左右各5米)
_patrolPointA = Position + new Vector3(-5, 0, 0);
_patrolPointB = Position + new Vector3(5, 0, 0);
// 查找玩家节点(假设玩家在场景里叫 "Player")
_player = GetTree().Root.FindChild("Player", true, false) as Node3D;
}
public override void _PhysicsProcess(double delta)
{
switch (_currentState)
{
case State.Idle:
ProcessIdle();
break;
case State.Patrol:
ProcessPatrol(delta);
break;
case State.Chase:
ProcessChase(delta);
break;
case State.Attack:
ProcessAttack();
break;
}
MoveAndSlide();
}
private void ProcessIdle()
{
Velocity = Vector3.Zero;
if (_player != null && Position.DistanceTo(_player.Position) < DetectRange)
_currentState = State.Chase;
}
private void ProcessPatrol(double delta)
{
// 检查是否发现玩家
if (_player != null && Position.DistanceTo(_player.Position) < DetectRange)
{
_currentState = State.Chase;
return;
}
// 向目标巡逻点移动
var target = _goingToB ? _patrolPointB : _patrolPointA;
var dir = (target - Position).Normalized();
Velocity = new Vector3(dir.X * MoveSpeed, Velocity.Y, 0);
// 到达巡逻点后切换方向
if (Position.DistanceTo(target) < 0.5f)
_goingToB = !_goingToB;
}
private void ProcessChase(double delta)
{
if (_player == null) { _currentState = State.Patrol; return; }
float dist = Position.DistanceTo(_player.Position);
if (dist > DetectRange * 1.5f)
{
_currentState = State.Patrol; // 失去玩家,返回巡逻
return;
}
if (dist < AttackRange)
{
_currentState = State.Attack; // 进入攻击范围
return;
}
var dir = (_player.Position - Position).Normalized();
Velocity = new Vector3(dir.X * ChaseSpeed, Velocity.Y, dir.Z * ChaseSpeed);
}
private void ProcessAttack()
{
Velocity = Vector3.Zero;
if (_player == null || Position.DistanceTo(_player.Position) > AttackRange)
_currentState = State.Chase;
// 实际攻击逻辑(造成伤害)在动画事件里触发
}
public void TakeDamage(float damage)
{
_hp -= damage;
if (_hp <= 0) Die();
}
private void Die()
{
QueueFree(); // 从场景中移除自身
}
}GDScript
extends CharacterBody3D
# AI 状态枚举
enum State { IDLE, PATROL, CHASE, ATTACK }
var current_state = State.PATROL
@export var move_speed: float = 3.0
@export var chase_speed: float = 5.0
@export var detect_range: float = 8.0 # 发现玩家的距离
@export var attack_range: float = 1.5 # 攻击距离
@export var max_hp: float = 100.0
var hp: float
var player: Node3D
var patrol_point_a: Vector3
var patrol_point_b: Vector3
var going_to_b: bool = true
func _ready():
hp = max_hp
# 设置巡逻范围(以初始位置为中心,左右各5米)
patrol_point_a = position + Vector3(-5, 0, 0)
patrol_point_b = position + Vector3(5, 0, 0)
# 查找玩家节点
player = get_tree().root.find_child("Player", true, false)
func _physics_process(delta):
match current_state:
State.IDLE: process_idle()
State.PATROL: process_patrol(delta)
State.CHASE: process_chase(delta)
State.ATTACK: process_attack()
move_and_slide()
func process_idle():
velocity = Vector3.ZERO
if player and position.distance_to(player.position) < detect_range:
current_state = State.CHASE
func process_patrol(delta):
if player and position.distance_to(player.position) < detect_range:
current_state = State.CHASE
return
var target = patrol_point_b if going_to_b else patrol_point_a
var dir = (target - position).normalized()
velocity = Vector3(dir.x * move_speed, velocity.y, 0)
if position.distance_to(target) < 0.5:
going_to_b = !going_to_b
func process_chase(delta):
if not player: current_state = State.PATROL; return
var dist = position.distance_to(player.position)
if dist > detect_range * 1.5:
current_state = State.PATROL
return
if dist < attack_range:
current_state = State.ATTACK
return
var dir = (player.position - position).normalized()
velocity = Vector3(dir.x * chase_speed, velocity.y, dir.z * chase_speed)
func process_attack():
velocity = Vector3.ZERO
if not player or position.distance_to(player.position) > attack_range:
current_state = State.CHASE
func take_damage(damage: float):
hp -= damage
if hp <= 0: die()
func die():
queue_free()敌人生成器(Spawner)
生成器(Spawner) 是一个专门负责在游戏运行时动态创建敌人的节点。它可以:
- 按时间间隔定期生成敌人
- 在指定位置生成
- 控制场景里同时存在的敌人数量上限
C#
using Godot;
public partial class EnemySpawner : Node3D
{
[Export] public PackedScene EnemyScene;
[Export] public float SpawnInterval = 3.0f; // 每3秒生成一个
[Export] public int MaxEnemies = 10; // 最多同时存在10个敌人
private float _timer = 0;
public override void _Process(double delta)
{
_timer += (float)delta;
if (_timer >= SpawnInterval)
{
_timer = 0;
TrySpawnEnemy();
}
}
private void TrySpawnEnemy()
{
// 检查当前敌人数量
int currentCount = GetTree().GetNodesInGroup("enemies").Count;
if (currentCount >= MaxEnemies) return;
// 在生成器位置附近随机生成
float offsetX = GD.Randf() * 4.0f - 2.0f;
var spawnPos = Position + new Vector3(offsetX, 0, 0);
var enemy = EnemyScene.Instantiate<Node3D>();
enemy.Position = spawnPos;
GetParent().AddChild(enemy);
}
}GDScript
extends Node3D
@export var enemy_scene: PackedScene
@export var spawn_interval: float = 3.0 # 每3秒生成一个
@export var max_enemies: int = 10 # 最多同时存在10个敌人
var timer: float = 0.0
func _process(delta):
timer += delta
if timer >= spawn_interval:
timer = 0.0
try_spawn_enemy()
func try_spawn_enemy():
# 检查当前敌人数量
var current_count = get_tree().get_nodes_in_group("enemies").size()
if current_count >= max_enemies:
return
# 在生成器位置附近随机生成
var offset_x = randf_range(-2.0, 2.0)
var spawn_pos = position + Vector3(offset_x, 0, 0)
var enemy = enemy_scene.instantiate()
enemy.position = spawn_pos
get_parent().add_child(enemy)敌人分组
要让生成器能统计敌人数量,需要在敌人场景里把根节点加入 enemies 组:在编辑器里选中敌人根节点 → 节点面板 → 组 → 添加 enemies。
小结
| 知识点 | 说明 |
|---|---|
| 状态机 | 用枚举管理 AI 的不同行为状态 |
| 巡逻 | 在两点之间来回移动 |
| 追击 | 检测距离,向玩家方向移动 |
| 攻击 | 靠近后停下,触发攻击动画 |
| 生成器 | 定时动态创建敌人实例 |
有了敌人,游戏就有了挑战性。最后一步,让我们来打磨游戏细节,让它从"能玩"变成"好玩"!
