게임수학

벡터 기초

게임만드는학생 2025. 4. 1. 01:51

벡터는 크기와 방향으로 이루어졌다. 
크기는 length 또는 Magnitude 라고 한다. 

방향은 Normalized 로 표시한다. 왜냐하면 벡터에서 크기요소를 1로 고정해서 영향을 미치지 않게 하고 방향만을 나타낼수 있기 때문이다. 이를 정규화라고하며, 이때의 벡터는 단위벡터라 한다. 

 

이 벡터에서 어떠한 방향 또는 어디까지의 거리 등을 알아내기 위해서 이미 정의된 수많은 특징들이 있다. 

 

많이 쓰이는 것들에는 빼기연산, 내적 등이 있다. 

 

먼저 빼기연산 즉 B - A 는 A에서 B로 향하는 벡터이다. 

이 벡터의 단위벡터가 a에서 b로 향하는 방향벡터가 되는것이다. 

 

또 내적은 a의 크기 * b의 크기 * cos 이다.

또는 x1*x2 + y1*y2 + z1*z2 로 표현한다. 

 

이걸 이용하면 각도의 범위를 표현할 수 있다. 

 

이것들을 이용해 아주 간단한 예제를 만들어 보았다.

플레이어와 enemy 클래스인데, wasd로 움직이는 플레이어와 특정한 범위내에 들어오면 플레이어를 쫓아가는 enemy이다. 

 

 

 

 

위 예제처럼 각 오브젝트는 정면을 표시하기 위해 네모난 오브젝트를 자식오브젝트로 붙이고 있다. 

플레이어 이동부터 살펴보겠다. 

void Update()
{
    Vector3 moveDir = Vector3.zero;
    if(Input.GetKey(KeyCode.W))
    {
        moveDir += Vector3.up;
    }
    if (Input.GetKey(KeyCode.S))
    {
        moveDir += Vector3.down;
    }
    if (Input.GetKey(KeyCode.A))
    {
        moveDir += Vector3.left;
    }
    if (Input.GetKey(KeyCode.D))
    {
        moveDir += Vector3.right;
    }
    moveDir = moveDir.normalized;
    if(moveDir.normalized != Vector3.zero)
    {
        float degree = Mathf.Atan2(moveDir.y, moveDir.x) * Mathf.Rad2Deg;
        text.text = "p : " + degree.ToString();
        transform.rotation = Quaternion.Euler(0, 0, degree - 90);
        transform.position += moveDir * Time.deltaTime * _speed;
    }
}

이게 Update의 코드인데, if문을 통해 방향이 어딘지를 즉 어떤 키를 누르는지에 따라 방향에 그 값이 더해진다. 

if문 안에서 바로 하면되지 않나? 할 수 있지만 그렇게된다면 대각선 이동에서 문제가 발생한다. 

예를 들어 W와 A를 누르고있으면 왼쪽 위 방향으로 1의 속도로 움직여야하는데 더해질때는 왼쪽 1, 위쪽 1의 벡터가 더해지기 때문에 결과적으로 루트 2의 왼쪽위방향이 더해진다. 따라서 대각선으로 이동할 때 속도가 빨라지는 문제가 발생한다.

그래서 이렇게 미리 어느방향으로 이동할지를 moveDir벡터에 더해놓고 정규화를 진행해주는 것이다. 

 

그 후에는 이동할 방향으로 회전시키고 이동하는 코드이다. 

float degree = Mathf.Atan2(moveDir.y, moveDir.x) * Mathf.Rad2Deg;
text.text = "p : " + degree.ToString();
transform.rotation = Quaternion.Euler(0, 0, degree - 90);

이동할 방향을 정해야하는데 Atan2는 역탄젠트함수로 이동할 방향벡터와 지금 오브젝트의 위치벡터사이의 각을 구한다.

여기서 Atan2함수는 라디안값을 반환하기 때문에 Rad2Deg를 곱해줘서 degree로 바꾸는것이다. 

그리고 euler 함수로 값을 집어넣어 각을 바꿔준다. 2D에서는 -90을 해줘야 맞아서 -90을 해준다. 

 

다음은 enemy클래스이다. 

 

Vector3 moveDir = p.transform.position - transform.position;

먼저 플레이어와 나 사이의 벡터를 구한다.

아까 언급했던 B-A 로 A에서 B로의 방향벡터를 구하는 것이다. 

이 벡터를 이용하면 상대와의 거리와 방향을 모두 알수있는데 이를 통해 if문을 만든다.

if (moveDir.sqrMagnitude <= _chaseDist * _chaseDist)

magnitude가 벡터의 크기 즉, 거리를 뜻한다. 그것을 이용해 상대가 특정 거리보다 가깝다면, 이라는 조건을 걸 수 있게되는 것이다. 

근데 여기서 sqrMagnitude 와 _chaseDist를 제곱한 이유는 루트연산이 무겁기 때문이다. 

 

// 적에서 플레이어 방향 벡터 계산
Vector2 directionToPlayer = moveDir.normalized;  // 플레이어 방향

// 적의 정면 방향 (회전된 방향)
Vector2 enemyForward = transform.up;  // transform.up은 적의 정면 방향 벡터

// 두 벡터의 내적 계산 (cos(θ) 구하기)
float dotProduct = Vector2.Dot(enemyForward, directionToPlayer);

// 내적값이 시야각 범위 내에 있는지 확인
float angle = Mathf.Acos(dotProduct) * Mathf.Rad2Deg;  // 각도로 변환


// 두 벡터 간의 각도 계산 (Vector2.Angle() 사용)
float angle2 = Vector2.Angle(enemyForward, directionToPlayer);

그 다음은 내적을 이용해 시야각을 판단하는 코드이다. 

먼저 2D 에서는 오브젝트의 정면을 transform.up으로 알 수 있다. 

그리고 내적은 Dot이라는 함수로 구현이 되어있어 이를 사용한다. 

중요한 것은 아까 언급한 "a의 크기 * b의 크기 * cos"  이 특징을 활용한다는 것이다. 

위쪽에서 플레이어로의 방향과 내 정면 방향을 모두 단위 벡터로 저장했다. 

내적 = a의 크기 * b의 크기 * cos 일때, a와 b의 크기가 모두 1이라면?  =>  내적값이 곧 cos의 값이다. 

그래서 내적을 이용해 cos 값을 많이 구한다고 한다. 

 

이 값을 역코사인 함수를  활용해 각도를 구하고 역시나 라디안이기 때문에 degree로 바꿔주면 angle값이 나오게 된다. 

즉, 내 정면기준으로 플레이어가 몇도에 있는지를 알게 되는 것이다. 

 

근데 단순히 angle값으로만 계산하게 되면 enemy가 플레이어를 정면으로 볼 때, 동작하지 않는다.

if (angle2 <= _chaseDeg / 2f)
{
    // 적이 플레이어를 추적
    transform.position += _speed * Time.deltaTime * moveDir.normalized;

    // 플레이어 방향으로 회전 (2D에서 회전)
    float degree = Mathf.Atan2(moveDir.y, moveDir.x) * Mathf.Rad2Deg;
    transform.rotation = Quaternion.Euler(0, 0, degree - 90);

    // 디버그 텍스트
    text.text = "enemy : " + degree.ToString();
}

 

public static float Angle(Vector2 from, Vector2 to)
{
    float num = (float)Math.Sqrt(from.sqrMagnitude * to.sqrMagnitude);
    if (num < 1E-15f)
    {
        return 0f;
    }

    float num2 = Mathf.Clamp(Dot(from, to) / num, -1f, 1f);
    return (float)Math.Acos(num2) * 57.29578f;
}

왜인지 잘모르겠다. Angle을 사용하면 정면에 플레이어가 있을때도 움직이긴한다.