본문 바로가기

캐릭터의 상대 위치 판단 방법

@iamrain2025. 11. 28. 10:45

1. 벡터 연산

캐릭터의 상대 위치를 판단하는 가장 일반적인 방법은 벡터의 내적(Dot Product)과 외적(Cross Product)을 사용하는 것.

1.1. 기본 개념

  • 좌표계(Coordinate System): 3D 공간의 모든 오브젝트는 월드 좌표계(World Space) 상의 위치(Position)와 방향(Rotation) 값을 가집니다. 게임 엔진에 따라 왼손 좌표계(Left-handed, Unity, DirectX) 또는 오른손 좌표계(Right-handed, Unreal, OpenGL)를 사용합니다.
  • 벡터(Vector): 크기와 방향을 가진 물리량입니다. 게임에서는 위치(Position Vector), 방향(Direction Vector) 등을 나타내는 데 사용됩니다.
    • 위치 벡터: 월드 좌표계의 원점(0,0,0)에서 특정 위치까지의 벡터입니다.
    • 방향 벡터: 캐릭터가 바라보는 방향 등을 나타내는 크기가 1인 벡터(단위 벡터, Unit Vector)입니다.
  • 벡터 연산:
    • 벡터 뺄셈: 두 위치 벡터 AB가 있을 때, B - A는 A에서 B를 향하는 방향 벡터를 계산합니다.
    • 정규화(Normalization): 벡터의 크기를 1로 만드는 과정입니다. 방향은 유지하되 크기(거리)에 의한 왜곡을 없애기 위해 반드시 필요합니다.

1.2. 내적 (Dot Product): 앞/뒤 판단

내적은 두 벡터가 얼마나 "같은 방향을 향하고 있는지"를 나타내는 스칼라 값입니다.

  • 공식: A · B = |A| * |B| * cos(θ)
    • |A|, |B|: 벡터 A와 B의 크기
    • θ: 두 벡터 사이의 각도

두 벡터가 모두 단위 벡터(크기 1)라면, 내적의 결과는 cos(θ)와 같습니다.

  • cos(θ) 값의 특징:
    • θ가 -90° ~ 90° 사이 (예각) → cos(θ) > 0
    • θ가 90° 또는 -90° (직각) → cos(θ) = 0
    • θ가 90° ~ 270° 사이 (둔각) → cos(θ) < 0

[구현 방식]

  1. 플레이어의 '앞쪽' 방향 벡터(playerForward)를 구합니다. (단위 벡터여야 함)
  2. 플레이어 위치에서 상대를 향하는 방향 벡터(vectorToTarget)를 구합니다. (targetPosition - playerPosition)
  3. vectorToTarget을 정규화합니다.
  4. dotResult = playerForward · vectorToTarget (내적)을 계산합니다.
  • dotResult > 0: 상대가 플레이어의 앞에 있습니다.
  • dotResult < 0: 상대가 플레이어의 뒤에 있습니다.
  • dotResult = 0: 상대가 플레이어의 정확히 옆(90도)에 있습니다.

1.3. 외적 (Cross Product): 좌/우 판단

외적은 두 벡터에 동시에 수직인 새로운 벡터를 반환하는 연산입니다. 이 새로운 벡터의 방향을 통해 좌/우를 구분할 수 있습니다.

  • 공식: C = A x B
    • 벡터 C는 벡터 A와 B가 이루는 평면에 수직입니다.
    • C의 방향은 좌표계(왼손/오른손)와 벡터의 순서에 따라 결정됩니다. (오른손 법칙, 왼손 법칙)

[구현 방식] (Y축을 위(Up)로 사용하는 오른손 좌표계 기준)

  1. 플레이어의 '앞쪽' 방향 벡터(playerForward)와 플레이어에서 상대를 향하는 정규화된 방향 벡터(vectorToTarget)를 준비합니다.
  2. crossResult = playerForward x vectorToTarget (외적)을 계산합니다.
  3. crossResultplayerForwardvectorToTarget가 이루는 평면(보통 XZ 평면)에 수직인 벡터이므로, Y축 방향 성분을 가집니다.
  4. crossResult의 Y축 성분(crossResult.y)의 부호를 확인합니다.
  • crossResult.y > 0: 상대가 플레이어의 오른쪽에 있습니다. (오른손 법칙에 따라, playerForward에서 vectorToTarget으로 감아쥘 때 엄지손가락이 위를 향함)
  • crossResult.y < 0: 상대가 플레이어의 왼쪽에 있습니다.
  • crossResult.y = 0: 상대가 플레이어의 정면 또는 정후면에 있습니다.

2D 환경에서의 좌/우 판단:
2D(XY 평면)에서는 외적의 결과가 Z축 성분만 가지는 스칼라 값처럼 계산됩니다 (A.x * B.y - A.y * B.x). 이 값의 부호가 바로 좌/우를 결정합니다.

1.4. 로컬 좌표계 변환을 이용한 방법

더 직관적인 방법으로, 상대의 월드 좌표를 플레이어의 로컬 좌표계(Local Space)로 변환하는 방법이 있습니다.

  1. 플레이어의 월드 변환 행렬(World Transform Matrix)의 역행렬(Inverse Matrix)을 구합니다. 이 역행렬이 월드 좌표를 로컬 좌표로 변환하는 행렬이 됩니다.
  2. 상대의 월드 위치(targetWorldPosition)를 이 역행렬과 곱합니다.
  3. 결과로 나온 targetLocalPosition은 플레이어를 원점(0,0,0)으로, 플레이어의 앞쪽을 Z+ 방향(또는 엔진에 따라 다름)으로 했을 때의 상대 좌표입니다.
  • targetLocalPosition.x > 0: 상대가 오른쪽에 있습니다.
  • targetLocalPosition.x < 0: 상대가 왼쪽에 있습니다.
  • targetLocalPosition.z > 0: 상대가 앞에 있습니다.
  • targetLocalPosition.z < 0: 상대가 뒤에 있습니다.

1.5. 방법 간 비교

방법 장점 단점 사용 사례
내적 + 외적 - 계산 비용이 비교적 저렴하다.
- 직관적으로 앞/뒤, 좌/우를 분리해서 계산할 수 있다.
- 두 번의 벡터 연산이 필요하다.
- 좌표계에 따라 외적 결과의 해석이 달라질 수 있다.
간단한 위치 확인, AI의 방향 전환 결정 등 대부분의 경우에 적합하다.
로컬 좌표계 변환 - 변환 후에는 좌표의 부호만 확인하면 되므로 매우 직관적이다.
- 위치뿐만 아니라 거리, 상대 각도 등 추가 정보 계산이 용이하다.
- 역행렬 계산은 벡터 연산보다 비용이 높다.
- 이미 다른 목적으로 역행렬(View Matrix)을 계산하고 있는 경우가 아니라면 비효율적일 수 있다.
렌더링 파이프라인, 물리 엔진 등에서 이미 로컬 좌표 변환이 필요할 때 부가적으로 사용하면 효율적이다.
#include <iostream>
#include <string>
#include <cmath>
#include <vector>

// 3D 벡터를 표현하는 간단한 구조체
struct Vector3 {
    float x, y, z;

    // 벡터 뺄셈 연산자 오버로딩
    Vector3 operator-(const Vector3& other) const {
        return {x - other.x, y - other.y, z - other.z};
    }

    // 벡터의 크기(길이)를 계산
    float magnitude() const {
        return std::sqrt(x * x + y * y + z * z);
    }

    // 벡터를 정규화 (크기를 1로 만듦)
    void normalize() {
        float mag = magnitude();
        if (mag > 0) {
            x /= mag;
            y /= mag;
            z /= mag;
        }
    }
};

// 두 벡터의 내적(Dot Product)을 계산하는 함수
float dot(const Vector3& a, const Vector3& b) {
    return a.x * b.x + a.y * b.y + a.z * b.z;
}

// 두 벡터의 외적(Cross Product)을 계산하는 함수
Vector3 cross(const Vector3& a, const Vector3& b) {
    return {
        a.y * b.z - a.z * b.y,
        a.z * b.x - a.x * b.z,
        a.x * b.y - a.y * b.x
    };
}

// 게임 캐릭터를 표현하는 클래스
class Character {
public:
    std::string name;
    Vector3 position; // 캐릭터의 월드 위치
    Vector3 forward;  // 캐릭터가 바라보는 방향 (단위 벡터)

    Character(std::string name, Vector3 position, Vector3 forward)
        : name(name), position(position), forward(forward) {
        // 생성 시 forward 벡터를 정규화하여 항상 단위 벡터임을 보장
        this->forward.normalize();
    }
};

// 상대 캐릭터의 위치 관계를 분석하는 함수
void checkRelativePosition(const Character& player, const Character& target) {
    std::cout << "--- [" << player.name << "]가 [" << target.name << "]의 위치를 분석합니다. ---" << std::endl;

    // 1. 플레이어에서 타겟을 향하는 벡터 계산
    Vector3 vectorToTarget = target.position - player.position;

    // Q1: 아래 코드는 vectorToTarget을 정규화합니다. 왜 여기서 벡터를 정규화해야 할까요?
    // 만약 정규화하지 않고 내적과 외적을 계산하면 어떤 문제가 발생할 수 있을까요?
    vectorToTarget.normalize();

    // 2. 내적을 이용해 앞/뒤 판단
    // Q2: 내적(dot product)의 결과가 양수, 음수, 0일 때 각각 무엇을 의미하나요?
    // 이 값이 두 벡터 사이의 각도와 어떤 관계가 있는지 설명해보세요.
    float dotResult = dot(player.forward, vectorToTarget);

    std::string frontOrBack;
    if (dotResult > 0.0f) {
        frontOrBack = "앞";
    } else if (dotResult < 0.0f) {
        frontOrBack = "뒤";
    } else {
        frontOrBack = "정확히 옆";
    }

    // 3. 외적을 이용해 좌/우 판단
    // Q3: 외적(cross product)을 사용하여 좌우를 판단하는 원리는 무엇인가요?
    // 이 코드에서는 Y축 값을 사용했는데(crossResult.y), 그 이유는 무엇이며,
    // 만약 게임 엔진이 Z축을 'Up' 벡터로 사용한다면 이 코드는 어떻게 바뀌어야 할까요?
    Vector3 crossResult = cross(player.forward, vectorToTarget);

    std::string leftOrRight;
    // 부동소수점 오차를 고려하여 작은 임계값(epsilon)을 사용
    const float epsilon = 1e-4;
    if (crossResult.y > epsilon) {
        // 오른손 좌표계 기준: player.forward에서 vectorToTarget 방향으로 감아쥘 때
        // 오른손 엄지가 위(Y+)를 향하므로 '오른쪽'
        leftOrRight = "오른쪽";
    } else if (crossResult.y < -epsilon) {
        leftOrRight = "왼쪽";
    } else {
        // crossResult.y가 0에 가깝다는 것은 두 벡터가 거의 평행하다는 의미
        leftOrRight = "정면 또는 정후방";
    }

    // 최종 결과 출력
    std::cout << "> 결과: [" << target.name << "] (은)는 [" << player.name << "]의 ["
              << frontOrBack << "]에 있으며, [" << leftOrRight << "]에 위치합니다." << std::endl << std::endl;
}


int main() {
    // C++ 입출력 속도 향상
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    // 플레이어 캐릭터 설정 (원점에 위치, Z+ 방향을 바라봄)
    Character player("Player", {0.0f, 0.0f, 0.0f}, {0.0f, 0.0f, 1.0f});

    // 테스트할 다른 캐릭터들 설정
    std::vector<Character> others;
    others.push_back(Character("Enemy_Front_Right", {10.0f, 0.0f, 20.0f}, {0.0f, 0.0f, -1.0f})); // 앞, 오른쪽
    others.push_back(Character("Enemy_Front_Left", {-10.0f, 0.0f, 20.0f}, {0.0f, 0.0f, -1.0f})); // 앞, 왼쪽
    others.push_back(Character("Enemy_Back_Right", {10.0f, 0.0f, -20.0f}, {0.0f, 0.0f, 1.0f}));  // 뒤, 오른쪽
    others.push_back(Character("Enemy_Back_Left", {-10.0f, 0.0f, -20.0f}, {0.0f, 0.0f, 1.0f}));   // 뒤, 왼쪽
    others.push_back(Character("Enemy_Directly_Behind", {0.0f, 0.0f, -10.0f}, {0.0f, 0.0f, 1.0f})); // 정후방

    // 모든 캐릭터에 대해 위치 관계 분석 수행
    for (const auto& other : others) {
        checkRelativePosition(player, other);
    }

    return 0;
}
iamrain
@iamrain :: Annals of Unreal

iamrain 님의 블로그 입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차