16. 敌人创建与战斗AI
敌人创建与战斗AI
敌人角色的场景结构
创建一个有智能行为的敌人,就像给游戏角色装上一个"大脑"。敌人需要知道:自己在哪、玩家在哪、什么时候追、什么时候打。
各节点的作用说明:
| 节点 | 作用 | 比喻 |
|---|---|---|
| CharacterBody3D | 敌人物理主体 | 敌人的身体 |
| CollisionShape3D | 敌人的碰撞形状 | 敌人的"边界" |
| MeshInstance3D | 敌人的外观模型 | 敌人的"长相" |
| NavigationAgent3D | AI 寻路组件 | 敌人的"地图导航" |
| Area3D(检测范围) | 感知玩家进入范围 | 敌人的"眼睛和耳朵" |
| Area3D(攻击范围) | 判断能否攻击 | 敌人的"手臂长度" |
| RayCast3D | 检查视线是否被遮挡 | 敌人的"凝视" |
| Node(AI控制器) | 控制行为状态切换 | 敌人的"大脑" |
为什么需要 NavigationAgent3D?
如果敌人只是朝玩家"直线"跑过去,遇到墙壁就会被卡住。NavigationAgent3D 就像手机里的导航软件——它能自动规划一条绕过障碍物的路线,让敌人聪明地追击玩家。
巡逻行为(PatrolState)
巡逻是敌人最基本的行为——在没有发现玩家时,在几个固定点之间来回走动,就像保安在巡逻。
// 巡逻行为
using Godot;
using System.Collections.Generic;
public partial class PatrolState : Node
{
[Export] public float PatrolSpeed { get; set; } = 2.0f;
[Export] public float WaitTimeAtPoint { get; set; } = 2.0f;
[Export] public float ArrivalThreshold { get; set; } = 0.5f;
private CharacterBody3D _enemy;
private NavigationAgent3D _navAgent;
private List<Vector3> _patrolPoints = new();
private int _currentPointIndex;
private double _waitTimer;
private bool _isWaiting;
public override void _Ready()
{
_enemy = GetParent() as CharacterBody3D;
_navAgent = _enemy.GetNode<NavigationAgent3D>("NavigationAgent3D");
}
/// <summary>设置巡逻点</summary>
public void SetPatrolPoints(Vector3[] points)
{
_patrolPoints.Clear();
_patrolPoints.AddRange(points);
_currentPointIndex = 0;
MoveToNextPoint();
}
public override void _PhysicsProcess(double delta)
{
if (_patrolPoints.Count == 0) return;
if (_isWaiting)
{
_waitTimer -= delta;
if (_waitTimer <= 0)
{
_isWaiting = false;
_currentPointIndex = (_currentPointIndex + 1) % _patrolPoints.Count;
MoveToNextPoint();
}
return;
}
// 检查是否到达巡逻点
Vector3 targetPos = _navAgent.GetNextPathPosition();
Vector3 direction = (targetPos - _enemy.GlobalPosition);
direction.Y = 0; // 忽略Y轴
float distance = direction.Length();
if (distance <= ArrivalThreshold)
{
// 到达巡逻点,等待一会儿
_isWaiting = true;
_waitTimer = WaitTimeAtPoint;
_enemy.Velocity = Vector3.Zero;
}
else
{
// 朝下一个路径点移动
direction = direction.Normalized();
Vector3 velocity = _enemy.Velocity;
velocity.X = direction.X * PatrolSpeed;
velocity.Z = direction.Z * PatrolSpeed;
_enemy.Velocity = velocity;
// 让敌人面向移动方向
if (direction != Vector3.Zero)
{
_enemy.LookAt(
new Vector3(targetPos.X, _enemy.GlobalPosition.Y, targetPos.Z),
Vector3.Up
);
}
}
_enemy.MoveAndSlide();
}
private void MoveToNextPoint()
{
if (_patrolPoints.Count > 0)
{
_navAgent.TargetPosition = _patrolPoints[_currentPointIndex];
}
}
}# 巡逻行为
extends Node
@export var patrol_speed: float = 2.0
@export var wait_time_at_point: float = 2.0
@export var arrival_threshold: float = 0.5
var _enemy: CharacterBody3D
var _nav_agent: NavigationAgent3D
var _patrol_points: Array[Vector3] = []
var _current_point_index: int = 0
var _wait_timer: float = 0.0
var _is_waiting: bool = false
func _ready():
_enemy = get_parent() as CharacterBody3D
_nav_agent = _enemy.get_node("NavigationAgent3D")
## 设置巡逻点
func set_patrol_points(points: Array[Vector3]):
_patrol_points.clear()
_patrol_points.assign(points)
_current_point_index = 0
move_to_next_point()
func _physics_process(delta: float):
if _patrol_points.size() == 0:
return
if _is_waiting:
_wait_timer -= delta
if _wait_timer <= 0:
_is_waiting = false
_current_point_index = (_current_point_index + 1) % _patrol_points.size()
move_to_next_point()
return
# 检查是否到达巡逻点
var target_pos = _nav_agent.get_next_path_position()
var direction = target_pos - _enemy.global_position
direction.y = 0 # 忽略Y轴
var distance = direction.length()
if distance <= arrival_threshold:
# 到达巡逻点,等待一会儿
_is_waiting = true
_wait_timer = wait_time_at_point
_enemy.velocity = Vector3.ZERO
else:
# 朝下一个路径点移动
direction = direction.normalized()
var velocity = _enemy.velocity
velocity.x = direction.x * patrol_speed
velocity.z = direction.z * patrol_speed
_enemy.velocity = velocity
# 让敌人面向移动方向
if direction != Vector3.ZERO:
_enemy.look_at(
Vector3(target_pos.x, _enemy.global_position.y, target_pos.z),
Vector3.UP
)
_enemy.move_and_slide()
func move_to_next_point():
if _patrol_points.size() > 0:
_nav_agent.target_position = _patrol_points[_current_point_index]追击行为(ChaseState)
当敌人发现玩家后,会切换到追击状态——全力追向玩家。
// 追击行为
using Godot;
public partial class ChaseState : Node
{
[Export] public float ChaseSpeed { get; set; } = 4.5f;
[Export] public float LosePlayerDistance { get; set; } = 15.0f;
private CharacterBody3D _enemy;
private NavigationAgent3D _navAgent;
private Node3D _player;
public override void _Ready()
{
_enemy = GetParent() as CharacterBody3D;
_navAgent = _enemy.GetNode<NavigationAgent3D>("NavigationAgent3D");
}
/// <summary>设置玩家引用</summary>
public void SetPlayer(Node3D player)
{
_player = player;
}
public override void _PhysicsProcess(double delta)
{
if (_player == null) return;
// 更新寻路目标为玩家位置
_navAgent.TargetPosition = _player.GlobalPosition;
// 获取下一个路径点
Vector3 nextPos = _navAgent.GetNextPathPosition();
Vector3 direction = (nextPos - _enemy.GlobalPosition);
direction.Y = 0;
float distance = direction.Length();
// 如果距离玩家太远,失去目标
float distToPlayer = _enemy.GlobalPosition.DistanceTo(_player.GlobalPosition);
if (distToPlayer > LosePlayerDistance)
{
GD.Print("失去玩家目标,返回巡逻");
// 通知 AI 控制器切换回巡逻状态
GetParent().Call("SwitchToPatrol");
return;
}
// 朝玩家方向移动
if (direction != Vector3.Zero)
{
direction = direction.Normalized();
Vector3 velocity = _enemy.Velocity;
velocity.X = direction.X * ChaseSpeed;
velocity.Z = direction.Z * ChaseSpeed;
_enemy.Velocity = velocity;
// 面向玩家
_enemy.LookAt(
new Vector3(_player.GlobalPosition.X, _enemy.GlobalPosition.Y, _player.GlobalPosition.Z),
Vector3.Up
);
}
_enemy.MoveAndSlide();
}
}# 追击行为
extends Node
@export var chase_speed: float = 4.5
@export var lose_player_distance: float = 15.0
var _enemy: CharacterBody3D
var _nav_agent: NavigationAgent3D
var _player: Node3D
func _ready():
_enemy = get_parent() as CharacterBody3D
_nav_agent = _enemy.get_node("NavigationAgent3D")
## 设置玩家引用
func set_player(player: Node3D):
_player = player
func _physics_process(delta: float):
if _player == null:
return
# 更新寻路目标为玩家位置
_nav_agent.target_position = _player.global_position
# 获取下一个路径点
var next_pos = _nav_agent.get_next_path_position()
var direction = next_pos - _enemy.global_position
direction.y = 0
var distance = direction.length()
# 如果距离玩家太远,失去目标
var dist_to_player = _enemy.global_position.distance_to(_player.global_position)
if dist_to_player > lose_player_distance:
print("失去玩家目标,返回巡逻")
get_parent().switch_to_patrol()
return
# 朝玩家方向移动
if direction != Vector3.ZERO:
direction = direction.normalized()
var velocity = _enemy.velocity
velocity.x = direction.x * chase_speed
velocity.z = direction.z * chase_speed
_enemy.velocity = velocity
# 面向玩家
_enemy.look_at(
Vector3(_player.global_position.x, _enemy.global_position.y, _player.global_position.z),
Vector3.UP
)
_enemy.move_and_slide()攻击行为(AttackState)
当敌人足够接近玩家时,开始攻击。
// 攻击行为
using Godot;
public partial class AttackState : Node
{
[Export] public int AttackDamage { get; set; } = 10;
[Export] public float AttackRange { get; set; } = 2.0f;
[Export] public float AttackCooldown { get; set; } = 1.0f;
[Export] public float AttackDuration { get; set; } = 0.5f;
private CharacterBody3D _enemy;
private Node3D _player;
private double _cooldownTimer;
private double _attackTimer;
private bool _isAttacking;
private bool _hasHitThisAttack;
public override void _Ready()
{
_enemy = GetParent() as CharacterBody3D;
}
public void SetPlayer(Node3D player)
{
_player = player;
}
public override void _PhysicsProcess(double delta)
{
if (_player == null) return;
float distToPlayer = _enemy.GlobalPosition.DistanceTo(_player.GlobalPosition);
// 如果玩家跑出了攻击范围,切换回追击
if (distToPlayer > AttackRange * 1.5f)
{
GD.Print("玩家跑远了,继续追击");
GetParent().Call("SwitchToChase");
return;
}
// 面向玩家
_enemy.LookAt(
new Vector3(_player.GlobalPosition.X, _enemy.GlobalPosition.Y, _player.GlobalPosition.Z),
Vector3.Up
);
// 攻击中
if (_isAttacking)
{
_attackTimer -= delta;
if (_attackTimer <= 0)
{
_isAttacking = false;
}
return;
}
// 冷却中
if (_cooldownTimer > 0)
{
_cooldownTimer -= delta;
return;
}
// 执行攻击
if (distToPlayer <= AttackRange)
{
PerformAttack();
}
}
private void PerformAttack()
{
_isAttacking = true;
_hasHitThisAttack = false;
_attackTimer = AttackDuration;
_cooldownTimer = AttackCooldown;
// 对玩家造成伤害
if (_player.HasMethod("TakeDamage"))
{
_player.Call("TakeDamage", AttackDamage);
}
GD.Print($"敌人攻击玩家,造成 {AttackDamage} 点伤害!");
}
}# 攻击行为
extends Node
@export var attack_damage: int = 10
@export var attack_range: float = 2.0
@export var attack_cooldown: float = 1.0
@export var attack_duration: float = 0.5
var _enemy: CharacterBody3D
var _player: Node3D
var _cooldown_timer: float = 0.0
var _attack_timer: float = 0.0
var _is_attacking: bool = false
func _ready():
_enemy = get_parent() as CharacterBody3D
func set_player(player: Node3D):
_player = player
func _physics_process(delta: float):
if _player == null:
return
var dist_to_player = _enemy.global_position.distance_to(_player.global_position)
# 如果玩家跑出了攻击范围,切换回追击
if dist_to_player > attack_range * 1.5:
print("玩家跑远了,继续追击")
get_parent().switch_to_chase()
return
# 面向玩家
_enemy.look_at(
Vector3(_player.global_position.x, _enemy.global_position.y, _player.global_position.z),
Vector3.UP
)
# 攻击中
if _is_attacking:
_attack_timer -= delta
if _attack_timer <= 0:
_is_attacking = false
return
# 冷却中
if _cooldown_timer > 0:
_cooldown_timer -= delta
return
# 执行攻击
if dist_to_player <= attack_range:
perform_attack()
func perform_attack():
_is_attacking = true
_attack_timer = attack_duration
_cooldown_timer = attack_cooldown
# 对玩家造成伤害
if _player.has_method("take_damage"):
_player.take_damage(attack_damage)
print("敌人攻击玩家,造成 %d 点伤害!" % attack_damage)AI 寻路完整实现
使用 NavigationRegion3D + NavigationAgent3D 实现 AI 寻路,是让敌人"聪明"的关键。
首先,在场景中添加 NavigationRegion3D 节点并烘焙导航网格。然后在敌人上使用 NavigationAgent3D:
// 完整的敌人 AI 控制器
using Godot;
public enum EnemyState
{
Patrol,
Chase,
Attack,
Flee,
Dead
}
public partial class EnemyAIController : Node
{
[Export] public float DetectionRange { get; set; } = 10.0f;
[Export] public float FieldOfView { get; set; } = 120.0f;
[Export] public float AttackRange { get; set; } = 2.0f;
[Export] public float LoseSightRange { get; set; } = 15.0f;
[Export] public float FleeHealthPercent { get; set; } = 0.2f;
private EnemyState _currentState = EnemyState.Patrol;
private CharacterBody3D _enemy;
private NavigationAgent3D _navAgent;
private RayCast3D _lineOfSight;
private Area3D _detectionArea;
private Node3D _player;
private PatrolState _patrolState;
private ChaseState _chaseState;
private AttackState _attackState;
// 仇恨系统
private int _aggroScore;
public override void _Ready()
{
_enemy = GetParent() as CharacterBody3D;
_navAgent = _enemy.GetNode<NavigationAgent3D>("NavigationAgent3D");
_lineOfSight = _enemy.GetNode<RayCast3D>("LineOfSight");
_detectionArea = _enemy.GetNode<Area3D>("DetectionArea");
// 获取状态组件
_patrolState = GetNode<PatrolState>("PatrolState");
_chaseState = GetNode<ChaseState>("ChaseState");
_attackState = GetNode<AttackState>("AttackState");
// 初始化巡逻点
_patrolState.SetPatrolPoints(new Vector3[]
{
new(5, 0, 5),
new(-5, 0, 5),
new(-5, 0, -5),
new(5, 0, -5)
});
// 检测区域信号
_detectionArea.BodyEntered += OnBodyEntered;
_detectionArea.BodyExited += OnBodyExited;
GD.Print("敌人 AI 初始化完成");
}
public override void _PhysicsProcess(double delta)
{
if (_currentState == EnemyState.Dead) return;
// 更新状态
UpdateState();
// 仇恨系统衰减
if (_aggroScore > 0)
{
_aggroScore = Mathf.Max(0, _aggroScore - 1);
}
}
private void UpdateState()
{
if (_player == null)
{
if (_currentState != EnemyState.Patrol)
SwitchState(EnemyState.Patrol);
return;
}
float distToPlayer = _enemy.GlobalPosition.DistanceTo(_player.GlobalPosition);
switch (_currentState)
{
case EnemyState.Patrol:
if (CanSeePlayer())
{
SwitchState(EnemyState.Chase);
}
break;
case EnemyState.Chase:
if (distToPlayer <= AttackRange)
{
SwitchState(EnemyState.Attack);
}
else if (distToPlayer > LoseSightRange || !CanSeePlayer())
{
SwitchState(EnemyState.Patrol);
}
break;
case EnemyState.Attack:
if (distToPlayer > AttackRange * 1.5f)
{
SwitchState(EnemyState.Chase);
}
break;
case EnemyState.Flee:
if (distToPlayer > LoseSightRange)
{
SwitchState(EnemyState.Patrol);
}
break;
}
}
/// <summary>检查是否能"看到"玩家</summary>
private bool CanSeePlayer()
{
if (_player == null) return false;
Vector3 toPlayer = _player.GlobalPosition - _enemy.GlobalPosition;
float distance = toPlayer.Length();
// 距离检测
if (distance > DetectionRange) return false;
// 角度检测(视野锥)
Vector3 forward = -_enemy.GlobalTransform.Basis.Z;
float angle = Mathf.RadToDeg(forward.AngleTo(toPlayer.Normalized()));
if (angle > FieldOfView / 2.0f) return false;
// 射线检测(是否有遮挡物)
_lineOfSight.LookAt(_player.GlobalPosition, Vector3.Up);
_lineOfSight.ForceRaycastUpdate();
if (_lineOfSight.IsColliding())
{
var hit = _lineOfSight.GetCollider();
return hit == _player; // 只有直接看到玩家才算
}
return false;
}
private void SwitchState(EnemyState newState)
{
if (_currentState == newState) return;
GD.Print($"敌人状态:{_currentState} → {newState}");
_currentState = newState;
// 禁用所有状态组件
SetStateActive(_patrolState, false);
SetStateActive(_chaseState, false);
SetStateActive(_attackState, false);
// 启用当前状态
switch (newState)
{
case EnemyState.Patrol:
SetStateActive(_patrolState, true);
break;
case EnemyState.Chase:
_chaseState.SetPlayer(_player);
SetStateActive(_chaseState, true);
break;
case EnemyState.Attack:
_attackState.SetPlayer(_player);
SetStateActive(_attackState, true);
break;
case EnemyState.Dead:
_enemy.SetPhysicsProcess(false);
break;
}
}
private void SetStateActive(Node state, bool active)
{
state.SetProcess(active);
state.SetPhysicsProcess(active);
}
private void OnBodyEntered(Node3D body)
{
if (body.IsInGroup("player"))
{
_player = body;
GD.Print("发现玩家!");
}
}
private void OnBodyExited(Node3D body)
{
if (body == _player)
{
_player = null;
GD.Print("玩家离开检测范围");
}
}
// 公开方法,供状态组件调用
public void SwitchToPatrol() => SwitchState(EnemyState.Patrol);
public void SwitchToChase() => SwitchState(EnemyState.Chase);
/// <summary>被攻击时增加仇恨值</summary>
public void AddAggro(int amount)
{
_aggroScore += amount;
GD.Print($"仇恨值增加:{_aggroScore}");
// 仇恨足够高时立即进入追击状态
if (_aggroScore > 5 && _player != null)
{
SwitchState(EnemyState.Chase);
}
}
}# 完整的敌人 AI 控制器
extends Node
enum EnemyState {
PATROL,
CHASE,
ATTACK,
FLEE,
DEAD
}
@export var detection_range: float = 10.0
@export var field_of_view: float = 120.0
@export var attack_range: float = 2.0
@export var lose_sight_range: float = 15.0
@export var flee_health_percent: float = 0.2
var _current_state: EnemyState = EnemyState.PATROL
var _enemy: CharacterBody3D
var _nav_agent: NavigationAgent3D
var _line_of_sight: RayCast3D
var _detection_area: Area3D
var _player: Node3D
var _patrol_state: PatrolState
var _chase_state: ChaseState
var _attack_state: AttackState
# 仇恨系统
var _aggro_score: int = 0
func _ready():
_enemy = get_parent() as CharacterBody3D
_nav_agent = _enemy.get_node("NavigationAgent3D")
_line_of_sight = _enemy.get_node("LineOfSight")
_detection_area = _enemy.get_node("DetectionArea")
# 获取状态组件
_patrol_state = get_node("PatrolState")
_chase_state = get_node("ChaseState")
_attack_state = get_node("AttackState")
# 初始化巡逻点
_patrol_state.set_patrol_points([
Vector3(5, 0, 5),
Vector3(-5, 0, 5),
Vector3(-5, 0, -5),
Vector3(5, 0, -5)
])
# 检测区域信号
_detection_area.body_entered.connect(_on_body_entered)
_detection_area.body_exited.connect(_on_body_exited)
print("敌人 AI 初始化完成")
func _physics_process(delta: float):
if _current_state == EnemyState.DEAD:
return
# 更新状态
update_state()
# 仇恨系统衰减
if _aggro_score > 0:
_aggro_score = maxi(_aggro_score - 1, 0)
func update_state():
if _player == null:
if _current_state != EnemyState.PATROL:
switch_state(EnemyState.PATROL)
return
var dist_to_player = _enemy.global_position.distance_to(_player.global_position)
match _current_state:
EnemyState.PATROL:
if can_see_player():
switch_state(EnemyState.CHASE)
EnemyState.CHASE:
if dist_to_player <= attack_range:
switch_state(EnemyState.ATTACK)
elif dist_to_player > lose_sight_range or not can_see_player():
switch_state(EnemyState.PATROL)
EnemyState.ATTACK:
if dist_to_player > attack_range * 1.5:
switch_state(EnemyState.CHASE)
EnemyState.FLEE:
if dist_to_player > lose_sight_range:
switch_state(EnemyState.PATROL)
## 检查是否能"看到"玩家
func can_see_player() -> bool:
if _player == null:
return false
var to_player = _player.global_position - _enemy.global_position
var distance = to_player.length()
# 距离检测
if distance > detection_range:
return false
# 角度检测(视野锥)
var forward = -_enemy.global_transform.basis.z
var angle = rad_to_deg(forward.angle_to(to_player.normalized()))
if angle > field_of_view / 2.0:
return false
# 射线检测(是否有遮挡物)
_line_of_sight.look_at(_player.global_position, Vector3.UP)
_line_of_sight.force_raycast_update()
if _line_of_sight.is_colliding():
var hit = _line_of_sight.get_collider()
return hit == _player # 只有直接看到玩家才算
return false
func switch_state(new_state: EnemyState):
if _current_state == new_state:
return
print("敌人状态:%s → %s" % [EnemyState.keys()[_current_state], EnemyState.keys()[new_state]])
_current_state = new_state
# 禁用所有状态组件
set_state_active(_patrol_state, false)
set_state_active(_chase_state, false)
set_state_active(_attack_state, false)
# 启用当前状态
match new_state:
EnemyState.PATROL:
set_state_active(_patrol_state, true)
EnemyState.CHASE:
_chase_state.set_player(_player)
set_state_active(_chase_state, true)
EnemyState.ATTACK:
_attack_state.set_player(_player)
set_state_active(_attack_state, true)
EnemyState.DEAD:
_enemy.set_physics_process(false)
func set_state_active(state: Node, active: bool):
state.set_process(active)
state.set_physics_process(active)
func _on_body_entered(body: Node3D):
if body.is_in_group("player"):
_player = body
print("发现玩家!")
func _on_body_exited(body: Node3D):
if body == _player:
_player = null
print("玩家离开检测范围")
# 公开方法,供状态组件调用
func switch_to_patrol():
switch_state(EnemyState.PATROL)
func switch_to_chase():
switch_state(EnemyState.CHASE)
## 被攻击时增加仇恨值
func add_aggro(amount: int):
_aggro_score += amount
print("仇恨值增加:%d" % _aggro_score)
# 仇恨足够高时立即进入追击状态
if _aggro_score > 5 and _player != null:
switch_state(EnemyState.CHASE)视野检测详解
视野检测是判断敌人能否"看到"玩家的核心逻辑,需要同时满足三个条件:
- 距离判断:玩家是否在检测范围内
- 角度判断:玩家是否在敌人的视野锥内
- 遮挡判断:敌人到玩家之间是否有墙壁等障碍物
// 独立的视野检测工具类
using Godot;
public static class VisionUtils
{
/// <summary>
/// 检查敌人是否能看到目标
/// </summary>
/// <param name="observer">观察者(敌人)</param>
/// <param name="target">目标(玩家)</param>
/// <param name="maxRange">最大视野距离</param>
/// <param name="fovDegrees">视野角度(度)</param>
/// <param name="raycast">用于遮挡检测的射线</param>
public static bool CanSeeTarget(
Node3D observer,
Node3D target,
float maxRange,
float fovDegrees,
RayCast3D raycast
)
{
Vector3 toTarget = target.GlobalPosition - observer.GlobalPosition;
float distance = toTarget.Length();
// 第1步:距离检测
if (distance > maxRange) return false;
// 第2步:角度检测
Vector3 forward = -observer.GlobalTransform.Basis.Z;
float angle = Mathf.RadToDeg(forward.AngleTo(toTarget.Normalized()));
if (angle > fovDegrees / 2.0f) return false;
// 第3步:遮挡检测(射线)
raycast.GlobalPosition = observer.GlobalPosition + Vector3.Up * 1.0f;
raycast.TargetPosition = raycast.ToLocal(
target.GlobalPosition + Vector3.Up * 1.0f
);
raycast.ForceRaycastUpdate();
if (raycast.IsColliding())
{
var hit = raycast.GetCollider();
// 如果射线碰到的不是目标,说明被遮挡
if (hit != target) return false;
}
return true;
}
}# 独立的视野检测工具类
class_name VisionUtils
## 检查敌人是否能看到目标
## observer: 观察者(敌人)
## target: 目标(玩家)
## max_range: 最大视野距离
## fov_degrees: 视野角度(度)
## raycast: 用于遮挡检测的射线
static func can_see_target(
observer: Node3D,
target: Node3D,
max_range: float,
fov_degrees: float,
raycast: RayCast3D
) -> bool:
var to_target = target.global_position - observer.global_position
var distance = to_target.length()
# 第1步:距离检测
if distance > max_range:
return false
# 第2步:角度检测
var forward = -observer.global_transform.basis.z
var angle = rad_to_deg(forward.angle_to(to_target.normalized()))
if angle > fov_degrees / 2.0:
return false
# 第3步:遮挡检测(射线)
raycast.global_position = observer.global_position + Vector3.UP * 1.0
raycast.target_position = raycast.to_local(
target.global_position + Vector3.UP * 1.0
)
raycast.force_raycast_update()
if raycast.is_colliding():
var hit = raycast.get_collider()
# 如果射线碰到的不是目标,说明被遮挡
if hit != target:
return false
return true仇恨系统
仇恨系统决定了敌人"优先攻击谁"。当多个玩家攻击同一个敌人时,对敌人造成伤害最多的玩家会获得最高的仇恨值,敌人会优先追击高仇恨的玩家。
// 仇恨系统
using Godot;
using System.Collections.Generic;
public partial class AggroSystem : Node
{
private readonly Dictionary<Node3D, int> _aggroTable = new();
/// <summary>增加对某个目标的仇恨值</summary>
public void AddAggro(Node3D target, int amount)
{
if (!_aggroTable.ContainsKey(target))
{
_aggroTable[target] = 0;
}
_aggroTable[target] += amount;
}
/// <summary>获取仇恨值最高的目标</summary>
public Node3D GetHighestAggroTarget()
{
Node3D highest = null;
int maxAggro = 0;
foreach (var pair in _aggroTable)
{
if (pair.Value > maxAggro && IsInstanceValid(pair.Key))
{
maxAggro = pair.Value;
highest = pair.Key;
}
}
return highest;
}
/// <summary>清除某个目标的仇恨</summary>
public void RemoveTarget(Node3D target)
{
_aggroTable.Remove(target);
}
/// <summary>清除所有仇恨</summary>
public void ClearAll()
{
_aggroTable.Clear();
}
}# 仇恨系统
extends Node
var _aggro_table: Dictionary = {}
## 增加对某个目标的仇恨值
func add_aggro(target: Node3D, amount: int):
if not _aggro_table.has(target):
_aggro_table[target] = 0
_aggro_table[target] += amount
## 获取仇恨值最高的目标
func get_highest_aggro_target() -> Node3D:
var highest: Node3D = null
var max_aggro: int = 0
for target in _aggro_table.keys():
if _aggro_table[target] > max_aggro and is_instance_valid(target):
max_aggro = _aggro_table[target]
highest = target
return highest
## 清除某个目标的仇恨
func remove_target(target: Node3D):
_aggro_table.erase(target)
## 清除所有仇恨
func clear_all():
_aggro_table.clear()三种敌人类型设计
| 属性 | 近战型(战士) | 远程型(射手) | Boss 型 |
|---|---|---|---|
| 生命值 | 80 | 50 | 500 |
| 移动速度 | 3.5 | 2.5 | 3.0 |
| 攻击伤害 | 15 | 10 | 30 |
| 攻击范围 | 2.0 | 15.0 | 4.0 |
| 攻击冷却 | 1.0s | 2.0s | 0.8s |
| 检测范围 | 8.0 | 12.0 | 15.0 |
| 视野角度 | 120度 | 180度 | 360度 |
| 特殊能力 | 无 | 无 | 范围攻击、召唤小兵 |
| 碰撞形状 | CapsuleShape3D | CapsuleShape3D | 更大的 CapsuleShape3D |
| 掉落奖励 | 少量金币 | 中量金币 | 大量金币 + 稀有装备 |
// 敌人类型配置
using Godot;
public enum EnemyType
{
Melee,
Ranged,
Boss
}
[GlobalClass]
public partial class EnemyConfig : Resource
{
[Export] public EnemyType Type { get; set; }
[Export] public int MaxHealth { get; set; }
[Export] public float MoveSpeed { get; set; }
[Export] public int AttackDamage { get; set; }
[Export] public float AttackRange { get; set; }
[Export] public float AttackCooldown { get; set; }
[Export] public float DetectionRange { get; set; }
[Export] public float FieldOfView { get; set; }
/// <summary>创建近战型敌人配置</summary>
public static EnemyConfig CreateMelee()
{
return new EnemyConfig
{
Type = EnemyType.Melee,
MaxHealth = 80,
MoveSpeed = 3.5f,
AttackDamage = 15,
AttackRange = 2.0f,
AttackCooldown = 1.0f,
DetectionRange = 8.0f,
FieldOfView = 120.0f
};
}
/// <summary>创建远程型敌人配置</summary>
public static EnemyConfig CreateRanged()
{
return new EnemyConfig
{
Type = EnemyType.Ranged,
MaxHealth = 50,
MoveSpeed = 2.5f,
AttackDamage = 10,
AttackRange = 15.0f,
AttackCooldown = 2.0f,
DetectionRange = 12.0f,
FieldOfView = 180.0f
};
}
/// <summary>创建 Boss 型敌人配置</summary>
public static EnemyConfig CreateBoss()
{
return new EnemyConfig
{
Type = EnemyType.Boss,
MaxHealth = 500,
MoveSpeed = 3.0f,
AttackDamage = 30,
AttackRange = 4.0f,
AttackCooldown = 0.8f,
DetectionRange = 15.0f,
FieldOfView = 360.0f
};
}
}# 敌人类型配置
class_name EnemyConfig
extends Resource
enum EnemyType {
MELEE,
RANGED,
BOSS
}
@export var type: EnemyType
@export var max_health: int
@export var move_speed: float
@export var attack_damage: int
@export var attack_range: float
@export var attack_cooldown: float
@export var detection_range: float
@export var field_of_view: float
## 创建近战型敌人配置
static func create_melee() -> EnemyConfig:
var config = EnemyConfig.new()
config.type = EnemyType.MELEE
config.max_health = 80
config.move_speed = 3.5
config.attack_damage = 15
config.attack_range = 2.0
config.attack_cooldown = 1.0
config.detection_range = 8.0
config.field_of_view = 120.0
return config
## 创建远程型敌人配置
static func create_ranged() -> EnemyConfig:
var config = EnemyConfig.new()
config.type = EnemyType.RANGED
config.max_health = 50
config.move_speed = 2.5
config.attack_damage = 10
config.attack_range = 15.0
config.attack_cooldown = 2.0
config.detection_range = 12.0
config.field_of_view = 180.0
return config
## 创建 Boss 型敌人配置
static func create_boss() -> EnemyConfig:
var config = EnemyConfig.new()
config.type = EnemyType.BOSS
config.max_health = 500
config.move_speed = 3.0
config.attack_damage = 30
config.attack_range = 4.0
config.attack_cooldown = 0.8
config.detection_range = 15.0
config.field_of_view = 360.0
return config常见问题
NavigationAgent3D 不寻路
问题:敌人不移动或直线穿墙。
解决方案:
- 确保场景中有
NavigationRegion3D节点 - 在 NavigationRegion3D 上烘焙导航网格(点击 Bake 按钮)
- 确保 NavigationAgent3D 的
Avoidance Enabled可选开启 - 确认敌人的位置在导航网格范围内
视野检测不准确
问题:敌人背对玩家也能"看到"。
解决方案:
- 检查
FieldOfView角度设置是否合理(建议 90-120 度) - 确认 forward 方向计算正确(
-GlobalTransform.Basis.Z) - 确保角度比较时使用的是正确的度数/弧度单位
敌人卡在墙角
问题:敌人在拐角处卡住不动。
解决方案:
- 重新烘焙导航网格,确保拐角处有足够空间
- 增加导航网格的边距(Margin)
- 降低敌人移动速度
调试 AI 行为的方法
在 Godot 编辑器中,运行游戏后点击 Debug → Visible Navigation 可以看到导航网格的蓝色覆盖层。同时,可以在 NavigationAgent3D 上开启 Debug Enabled 属性,这样在运行时会用线条显示敌人的寻路路径。
本章小结
本章我们学习了完整的敌人 AI 系统:
| 模块 | 功能 | 关键技术 |
|---|---|---|
| 巡逻行为 | 在固定点之间来回移动 | NavigationAgent3D + 巡逻点数组 |
| 追击行为 | 发现玩家后追击 | NavigationAgent3D 实时更新目标 |
| 攻击行为 | 到达范围后攻击 | 距离判断 + 冷却计时 |
| 视野检测 | 三重判断(距离+角度+遮挡) | 射线 + 向量运算 |
| 仇恨系统 | 被攻击时触发追击 | 仇恨值累加 + 阈值触发 |
| 状态机 | 控制行为切换 | 枚举 + switch/match |
掌握这些技术后,你就能创建各种有趣的敌人——从简单的巡逻兵到复杂的 Boss 都不在话下。
