FPS游戏远程敌人AI异常求助:高度变化或过近时失效
FPS远程敌人AI异常问题排查与优化方案
问题概述
开发FPS游戏时,远程敌人AI存在以下异常:
- 玩家处于不同高度位置时,AI会停止工作
- 玩家距离AI过近时,AI会停止工作
- 近战敌人不受上述问题影响
以下是针对视野角度(sight angle)和视线检测(LoS)逻辑的排查与优化方案。
核心问题分析
从提供的代码中,可定位到3个关键问题:
1. 视野角度检测未忽略高度差
player_within_angle函数计算玩家方向时未剔除Y轴分量,导致玩家与AI的高度差会大幅影响角度判定。例如玩家在AI上方时,两者的方向向量夹角会被放大,超出设定的sight_angle阈值,导致AI判定“看不到玩家”。
2. 距离判断函数存在参数错误
within_dis_of_player函数中硬编码使用attack_range而非传入的dis参数,导致所有距离判断逻辑都复用攻击范围值,无法正确区分“追逐距离”“最小攻击距离”等不同阈值。
3. 远程AI状态切换逻辑混淆近战逻辑
当前process_state_chase中进入攻击状态的条件为within_dis_of_player(attack_range) or has_los_player(),这会导致只要能看到玩家就进入攻击状态。但远程AI需要保持安全距离:距离过近时应后退而非停止移动,距离过远时应追逐,只有在合适的攻击区间内才进入攻击状态。
代码修复方案
1. 修正视野角度检测逻辑
修改player_within_angle函数,仅计算水平面上的方向夹角,忽略高度差:
func player_within_angle(angle: float): # 仅保留水平方向分量,剔除高度影响 var dir_to_player = (player.global_transform.origin - global_transform.origin).horizontalized().normalized() var forwards = global_transform.basis.z.horizontalized().normalized() return rad2deg(forwards.angle_to(dir_to_player)) <= angle
2. 修复距离判断函数的参数错误
func within_dis_of_player(dis: float): # 使用传入的dis参数,而非硬编码的attack_range return global_transform.origin.distance_to(player.global_transform.origin) < dis
3. 优化远程AI状态切换逻辑
为远程AI添加最小攻击距离参数,并调整状态切换条件:
# 添加最小攻击距离参数 export var min_attack_range = 5.0 func process_state_chase(delta): var player_dist = global_transform.origin.distance_to(player.global_transform.origin) # 仅当玩家在攻击区间内(大于最小攻击距离,小于最大攻击距离)且有视线时,进入攻击状态 if player_dist > min_attack_range && player_dist < attack_range && has_los_player(): set_state_attack() # 距离过近时,后退远离玩家 elif player_dist < min_attack_range: var dir = global_transform.origin - player.global_transform.origin dir.y = 0 character_mover.set_move_vec(dir) face_dir(dir, delta) # 距离过远时,追逐玩家 else: var player_pos = player.global_transform.origin var our_pos = global_transform.origin path = nav.get_simple_path(our_pos, player_pos) var goal_pos = player_pos if path.size() > 1: goal_pos = path[1] var dir = goal_pos - our_pos dir.y = 0 character_mover.set_move_vec(dir) face_dir(dir, delta)
4. 优化视线检测逻辑
调整射线起点为AI眼睛高度,并仅在命中非玩家对象时判定为无视线:
func has_los_player(): # 模拟AI眼睛高度作为射线起点 var our_pos = global_transform.origin + Vector3.UP * 1.7 var player_pos = player.global_transform.origin + Vector3.UP * 1.5 # 指向玩家头部 var space_state = get_world().get_direct_space_state() # 排除自身碰撞体,避免误判 var exclude = [self] var result = space_state.intersect_ray(our_pos, player_pos, exclude, 1) # 仅当未命中任何物体,或命中的是玩家时,判定为有视线 if not result or result.collider == player or player.is_parent_of(result.collider): return true return false
完整优化后代码
extends Character onready var character_mover = $CharacterMover onready var anim_player = $Graphics/AnimationPlayer onready var health_mananger = $HealthManager onready var aimer = $AimAtObject var start_pos : Vector3 var start_facing_dir : Vector3 var nav enum STATES {IDLE, CHASE, ATTACK, DEAD} var cur_state = STATES.IDLE var player = null var cur_target : Character var path = [] export var sight_angle = 45.0 export var turn_speed = 360.0 export var attack_angle = 5.0 export var attack_range = 10.0 # 远程AI增大攻击范围 export var min_attack_range = 5.0 # 新增:远程AI最小攻击距离 export var attack_rate = 0.5 export var attack_anim_speed_mod = 0.5 var attack_timer : Timer var can_attack = true signal attack signal chase signal death var alive = true func _ready(): var navigation = get_tree().get_nodes_in_group("Navigation")[0] player = get_tree().get_nodes_in_group("player")[0] nav = navigation attack_timer = Timer.new() attack_timer.wait_time = attack_rate attack_timer.connect("timeout", self, "finish_attack") attack_timer.one_shot = true add_child(attack_timer) start_pos = global_transform.origin start_facing_dir = -global_transform.basis.z var bone_attachments = $Graphics/Armature/Skeleton.get_children() for bone_attachment in bone_attachments: for child in bone_attachment.get_children(): if child is HitBox: child.connect("hurt", self, "hurt") health_mananger.connect("dead", self, "set_state_dead") health_mananger.connect("gibbed", $Graphics, "hide") character_mover.init(self) set_state_idle() func _process(delta): match cur_state: STATES.IDLE: process_state_idle(delta) STATES.CHASE: process_state_chase(delta) STATES.ATTACK: process_state_attack(delta) STATES.DEAD: process_state_dead(delta) func set_state_idle(): cur_state = STATES.IDLE anim_player.play("idle_loop") func set_state_chase(): cur_state = STATES.CHASE anim_player.play("walk_loop", 0.2) emit_signal("chase") func set_state_attack(): cur_state = STATES.ATTACK func set_state_dead(): cur_state = STATES.DEAD anim_player.play("death") func process_state_idle(delta): if can_see_player(): set_state_chase() func process_state_chase(delta): var player_dist = global_transform.origin.distance_to(player.global_transform.origin) # 仅在攻击区间内且有视线时进入攻击状态 if player_dist > min_attack_range && player_dist < attack_range && has_los_player(): set_state_attack() # 距离过近时后退 elif player_dist < min_attack_range: var dir = global_transform.origin - player.global_transform.origin dir.y = 0 character_mover.set_move_vec(dir) face_dir(dir, delta) # 距离过远时追逐 else: var player_pos = player.global_transform.origin var our_pos = global_transform.origin path = nav.get_simple_path(our_pos, player_pos) var goal_pos = player_pos if path.size() > 1: goal_pos = path[1] var dir = goal_pos - our_pos dir.y = 0 character_mover.set_move_vec(dir) face_dir(dir, delta) func process_state_attack(delta): character_mover.set_move_vec(Vector3.ZERO) if can_attack: var player_dist = global_transform.origin.distance_to(player.global_transform.origin) # 超出攻击区间或无视线时回到追逐状态 if player_dist <= min_attack_range || player_dist >= attack_range || !can_see_player(): set_state_chase() elif !player_within_angle(attack_angle): face_dir(global_transform.origin.direction_to(player.global_transform.origin).horizontalized(), delta) else: start_attack(delta) func process_state_dead(delta): if character_mover.body_to_move.is_on_floor(): $CollisionShape.disabled = true alive = false emit_death_signal() character_mover.freeze() func hurt(damage: int, dir: Vector3): if cur_state == STATES.IDLE: set_state_chase() health_mananger.hurt(damage, dir) func start_attack(delta): can_attack = false anim_player.play("attack", -1, attack_anim_speed_mod) attack_timer.start() aimer.face_point(player.global_transform.origin + Vector3.UP, delta) func emit_attack_signal(): emit_signal("attack") func emit_death_signal(): return true func finish_attack(): can_attack = true func can_see_player(): return player_within_angle(sight_angle) and has_los_player() func player_within_angle(angle: float): # 仅计算水平方向夹角,忽略高度差 var dir_to_player = (player.global_transform.origin - global_transform.origin).horizontalized().normalized() var forwards = global_transform.basis.z.horizontalized().normalized() return rad2deg(forwards.angle_to(dir_to_player)) <= angle func has_los_player(): # 使用眼睛高度作为射线起点 var our_pos = global_transform.origin + Vector3.UP * 1.7 var player_pos = player.global_transform.origin + Vector3.UP * 1.5 var space_state = get_world().get_direct_space_state() var exclude = [self] var result = space_state.intersect_ray(our_pos, player_pos, exclude, 1) # 未命中物体或命中玩家时判定为有视线 if not result or result.collider == player or player.is_parent_of(result.collider): return true return false func face_dir(dir: Vector3, delta): dir = dir.horizontalized().normalized() var angle_diff = global_transform.basis.z.horizontalized().angle_to(dir) var turn_right = sign(global_transform.basis.x.dot(dir)) if abs(angle_diff) < deg2rad(turn_speed) * delta: rotation.y = atan2(dir.x, dir.z) else: rotation.y += deg2rad(turn_speed) * delta * turn_right func alert(check_los=true): if cur_state != STATES.IDLE: return if check_los and !has_los_player(): return set_state_chase() func within_dis_of_player(dis: float): return global_transform.origin.distance_to(player.global_transform.origin) < dis
内容的提问来源于stack exchange,提问作者Mr Bri
相关产品推荐
相关产品推荐

