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

游戏中敌人朝向玩家的3D向量角度计算异常问题排查

敌人朝向玩家旋转的问题及修复方案

问题概述

需求是让游戏中的敌人绕Y轴旋转朝向玩家,但现有实现存在两个问题:

  • 使用acos计算向量夹角时,旋转出现未定义行为
  • 使用atan2计算时,敌人旋转逻辑异常(部分敌人仅在玩家特定移动方向下才旋转)

核心代码

实体定义与旋转逻辑

struct Entity {
    glm::vec3 Position;
    glm::vec3 Scale;
    glm::vec3 Rotation;
};

void turnEnemies() {
// 所有舰船生成时的默认朝向
    glm::vec3 defaultShipForward = glm::vec3( 0.0f, 0.0f, 1.0f );

    std::vector<Entity*> ships = ...;

    for (int i = 0; i < ships.size(); i++) {
        Entity* curShip = ships[i];
        if (curShip == m_player) continue;

// 仅绕Y轴旋转舰船,创建旋转矩阵将默认前向量转换为当前实际前向量
        glm::mat4 rotationMat = glm::rotate( glm::mat4( 1.0f ), curShip->Rotation.y, 
        glm::vec3( 0.0f, 1.0f, 0.0f ) );
        glm::vec3 curShipForward =  glm::normalize(rotationMat * glm::vec4(defaultShipForward, 1.0f));

// 获取当前舰船指向玩家的向量
        glm::vec3 vecToPlayer = glm::normalize( m_player->Position - ships[i]->Position );

        float turnAngle = getAngleBetweenVectorsDeg( curShipForward, vecToPlayer );

        ships[i]->Rotation.y += turnAngle;
    }
}

两种角度计算实现

acos方案
float getAngleBetweenVectorsDeg( glm::vec3 _vec1, glm::vec3 _vec2 ) {
    return acos( glm::dot( _vec1, _vec2 ) );
}
atan2方案
float getAngleBetweenVectorsDeg( glm::vec3 _vec1, glm::vec3 _vec2 ) {
    return  atan2( glm::length( glm::cross( _vec1, _vec2 ) ), glm::dot( _vec1, _vec2 ) );
}

问题根源

  1. acos方案的问题

    • 浮点数精度误差会导致点积结果超出[-1, 1]范围,触发acos返回NaN,引发未定义旋转行为
    • acos仅返回[0, π]的无符号角度,无法区分顺时针/逆时针旋转方向,导致敌人可能绕远路或反向旋转
  2. atan2方案的问题

    • 当前实现计算的是无符号夹角,没有判断旋转方向,直接将角度加到Rotation.y会导致方向错误
    • 忽略了坐标系的旋转方向逻辑,导致敌人在玩家不同位置时旋转行为异常

修复方案

核心思路:计算带符号的旋转角度

我们需要计算敌人绕Y轴旋转时的带符号角度(正/负对应不同旋转方向),确保敌人朝正确的方向转向玩家。

修改后的代码

带符号角度计算函数

// 计算绕Y轴旋转的带符号角度(单位:弧度,正为逆时针,负为顺时针)
float getSignedYawAngleBetweenVectors(glm::vec3 forward, glm::vec3 target) {
    // 投影到XZ平面,忽略Y分量(因为仅绕Y轴旋转)
    glm::vec2 forwardXZ(forward.x, forward.z);
    glm::vec2 targetXZ(target.x, target.z);
    
    // 确保向量归一化(输入已归一化可跳过)
    forwardXZ = glm::normalize(forwardXZ);
    targetXZ = glm::normalize(targetXZ);
    
    // 计算两个向量在XZ平面的方位角
    float forwardAngle = atan2(forwardXZ.y, forwardXZ.x);
    float targetAngle = atan2(targetXZ.y, targetXZ.x);
    
    // 计算角度差,并限制在[-π, π]范围内,避免过度旋转
    float angleDiff = targetAngle - forwardAngle;
    angleDiff = glm::mod(angleDiff + glm::pi<float>(), 2 * glm::pi<float>()) - glm::pi<float>();
    
    return angleDiff;
}

优化后的旋转逻辑

void turnEnemies() {
    glm::vec3 defaultShipForward = glm::vec3(0.0f, 0.0f, 1.0f);
    std::vector<Entity*> ships = ...;

    for (Entity* curShip : ships) {
        if (curShip == m_player) continue;

        // 计算当前舰船的前向向量(使用0.w表示方向向量,避免平移影响)
        glm::vec3 curShipForward = glm::rotate(glm::mat4(1.0f), curShip->Rotation.y, glm::vec3(0.0f, 1.0f, 0.0f)) * glm::vec4(defaultShipForward, 0.0f);
        curShipForward = glm::normalize(curShipForward);

        glm::vec3 vecToPlayer = glm::normalize(m_player->Position - curShip->Position);

        // 获取带符号的旋转角度
        float turnAngle = getSignedYawAngleBetweenVectors(curShipForward, vecToPlayer);

        // 可选:添加旋转速度限制,避免瞬间转向(需传入deltaTime)
        // float turnSpeed = 1.5f * deltaTime;
        // turnAngle = glm::clamp(turnAngle, -turnSpeed, turnSpeed);

        curShip->Rotation.y += turnAngle;
    }
}

额外优化建议

  • 添加旋转速度限制:直接应用角度差会导致敌人瞬间转向,不符合游戏体验,可根据帧时间(deltaTime)限制每次旋转的最大角度
  • 缓存前向向量:如果舰船旋转不频繁,可以缓存前向向量,避免每次通过旋转矩阵计算,提升性能
  • acos方案的临时修复:如果必须使用acos,可通过glm::clamp限制点积范围:float dot = glm::clamp(glm::dot(_vec1, _vec2), -1.0f, 1.0f); return acos(dot);,但仍无法解决方向问题,不推荐

内容的提问来源于stack exchange,提问作者justanewb

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 23:15:40