You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 02:20:27