방향 (x, y) 로 각도 구하기: 총구를 마우스로 돌리는 Atan2
Esc

    방향 (x, y) 로 각도 구하기: 총구를 마우스로 돌리는 Atan2

    –Unity기초

    2D 슈팅 게임에서 총구가 마우스 쪽을 보게 하고 싶습니다. 마우스 쪽 방향 (3, 3) 은 빼기 한 번으로 나오지만, 총을 돌리려면 45° 라는 각도가 필요합니다.

    세로 ÷ 가로 를 tan 이라 부르고, 그 비율에서 각도를 거꾸로 찾는 함수가 Atan 입니다. 그런데 Atan 은 (1, 1) 과 (−1, −1) 처럼 정반대인 방향에 같은 각도를 줘서, 마우스가 왼쪽 아래에 있으면 총구가 오른쪽 위를 봅니다. x 와 y 를 따로 받는 Mathf.Atan2(y, x) 가 네 방향 모두에서 맞는 각도를 줍니다.

    방향 (x, y) 의 각도는 Mathf.Atan2(y, x) 입니다. y 가 먼저입니다. 결과는 −180° ~ 180° 를 라디안으로 준 값이라, 도로 쓰려면 Mathf.Rad2Deg 를 곱합니다. Atan(y / x) 는 오른쪽 절반의 각도만 알아서, 왼쪽을 가리키는 방향에서 정반대를 줍니다.

    무엇을 알고 무엇을 구하나

    방향은 “어느 쪽으로 얼마나” 를 가로와 세로 두 숫자로 적은 화살표입니다. 도착 위치에서 출발 위치를 빼면 출발에서 도착으로 가는 화살표가 나옵니다. 총이 (1, 2), 마우스가 (4, 5) 에 있으면 방향은 (4 − 1, 5 − 2) = (3, 3) 입니다. 오른쪽으로 3, 위로 3 입니다.

    이 화살표 끝에서 바닥으로 수직선을 내리면 직각삼각형(한 각이 90° 인 삼각형)이 생깁니다. 바닥에 누운 변이 가로 x, 내린 수직선이 세로 y 이고, 우리가 구할 것은 화살표와 바닥(오른쪽, +x) 사이의 각 θ 입니다. θ(세타)는 x 가 “어떤 수” 를 뜻하듯 “어떤 각도” 를 가리키는 글자입니다.

    아는 것 구할 것
    가로 x = 3, 세로 y = 3 바닥에서 몇 도 들렸나 (θ)

    각도에서 가로와 세로를 구할 때는 cos 와 sin 을 씁니다. 길이 1 인 화살표를 θ 만큼 들면 끝이 (cos θ, sin θ) 에 있습니다. 30° 면 (0.87, 0.5) 입니다. 여기서는 그 반대입니다. 가로와 세로를 알고 각도를 모릅니다. 그래서 가로와 세로 둘만으로 각도를 정해 주는 비율이 필요합니다.

    tan 은 세로 ÷ 가로 다

    tan θ = 세로 ÷ 가로 입니다. “탄젠트” 라고 읽고, 비탈의 기울기와 같은 말입니다. 옆으로 1 갈 때 위로 얼마 가는가입니다. cos 와 sin 처럼 이 비율도 삼각형의 크기와 상관없이 각도만으로 정해집니다. (1, 1) 도 (3, 3) 도 세로 ÷ 가로 는 1 입니다.

    각도 길이 1 일 때 (가로, 세로) tan = 세로 ÷ 가로
    0° (1, 0) 0 ÷ 1 = 0
    30° (0.87, 0.5) 0.5 ÷ 0.87 = 0.58
    45° (0.71, 0.71) 1
    60° (0.5, 0.87) 0.87 ÷ 0.5 = 1.73
    90° (0, 1) 1 ÷ 0. 나눌 수 없음

    각이 커질수록 비탈이 가팔라져 tan 이 커지고, 90° 에서는 가로가 0 이라 값이 없습니다.

    arctan 은 tan 을 거꾸로 돌린다

    tan 은 각도를 넣으면 비율을 줍니다. 우리는 비율을 알고 각도를 알고 싶으니 거꾸로 가는 함수가 필요합니다. 그것이 arctan(아크탄젠트)이고, 코드에서는 줄여서 Atan 이라고 씁니다. tan⁻¹ 라고도 적습니다.

    tan 의 방향 arctan 의 방향
    tan 45° = 1 Atan(1) = 45°
    tan 30° = 0.58 Atan(0.58) = 30°
    tan 60° = 1.73 Atan(1.73) = 60°

    방향 (4, 3) 이면 세로 ÷ 가로 = 3 ÷ 4 = 0.75 이고, Atan(0.75) ≈ 36.9° 입니다. (3, 3) 은 3 ÷ 3 = 1 이라 45° 입니다.

    이것으로 문제가 풀린 것 같지만, 마우스가 총의 왼쪽으로 가면 틀립니다.

    Atan 은 정반대 방향을 가리지 못한다

    (1, 1) 은 오른쪽 위, (−1, −1) 은 왼쪽 아래로 정반대입니다. 그런데 세로 ÷ 가로 를 구하면

    방향 가리키는 곳 세로 ÷ 가로 Atan
    (1, 1) 오른쪽 위 1 ÷ 1 = 1 45°
    (−1, −1) 왼쪽 아래 (−1) ÷ (−1) = 1 45° (틀림)
    (1, −1) 오른쪽 아래 (−1) ÷ 1 = −1 −45°
    (−1, 1) 왼쪽 위 1 ÷ (−1) = −1 −45° (틀림)

    음수끼리 나누면 양수가 되고, 한쪽만 음수면 결과가 음수일 뿐 어느 쪽이 음수였는지는 사라집니다. 비율 1 하나만 받은 Atan 은 그것이 (1, 1) 에서 왔는지 (−1, −1) 에서 왔는지 알 방법이 없습니다. 그래서 Atan 은 늘 −90° ~ 90°, 곧 원의 오른쪽 절반(x 가 양수인 쪽) 각도만 돌려줍니다. 왼쪽 절반을 가리키는 방향은 정반대로 뒤집혀 나옵니다. x 가 0 인 바로 위 (0, 1) 은 1 ÷ 0 이라 나누는 것부터 할 수 없습니다.

    Atan2 는 x 와 y 를 따로 받아 고친다

    Atan2(y, x) 는 나누기 전의 x 와 y 를 따로 받습니다. 그래서 Atan 이 잃어버린 부호를 보고 반쪽을 고칠 수 있습니다. 하는 일은 이렇습니다.

    경우 Atan2 가 하는 일 예
    x 가 양수 (오른쪽 절반) Atan(y ÷ x) 를 그대로 (1, 1) → 45°
    x 가 음수, y 가 0 이상 (왼쪽 위) Atan(y ÷ x) 에 180° 를 더함 (−1, 1) → −45° + 180° = 135°
    x 가 음수, y 가 음수 (왼쪽 아래) Atan(y ÷ x) 에서 180° 를 뺌 (−1, −1) → 45° − 180° = −135°
    x 가 0 나누지 않고 y 의 부호만 봄 (0, 1) → 90°, (0, −1) → −90°

    180° 를 옮기는 것은 화살표를 원점에 대고 정반대로 뒤집는 일입니다. Atan 이 뒤집어서 준 답을 다시 뒤집어 원래 쪽으로 돌려놓는 것입니다. 결과는 −180° ~ 180° 안에 둡니다. 오른쪽 0° 를 가운데 두고 위쪽은 +, 아래쪽은 − 로 대칭이 되게 한 것이고, 그래서 아래쪽(y 가 음수)에서는 180° 를 빼서 음수로 맞춥니다. 왼쪽 아래가 225° 가 아니라 −135° 로 나오는 이유입니다. 225° 와 −135° 는 같은 방향입니다(225 − 360 = −135).

    방향 세로 ÷ 가로 Atan Atan2(y, x)
    (1, 1) 1 45° 45°
    (−1, −1) 1 45° (틀림) −135°
    (−1, 1) −1 −45° (틀림) 135°
    (0, 1) 0 으로 나눔 계산 불가 90°

    인자 순서가 (y, x) 인 것은 tan 이 세로 ÷ 가로 라서입니다. 나눗셈의 위(y)를 먼저, 아래(x)를 나중에 적습니다.

    코드: 총구를 마우스 쪽으로

    유니티의 Mathf.Atan2 는 각도를 도가 아니라 라디안으로 줍니다. 라디안은 반지름 1 인 원에서 그 각만큼 돈 호의 길이로 각을 재는 단위이고, 180° 가 π(약 3.14) 입니다.

    라디안으로 재면 반지름 1 인 원에서 1 라디안 돌 때 원 위를 정확히 1 만큼 갑니다. 반지름이 r 이면 간 거리는 r × 각 입니다. 도로 재면 이런 식마다 × π ÷ 180 이 붙습니다. sin · cos · atan 공식도 이 변환이 없는 라디안으로 만들어져 있어서, C# 의 System.Math 와 유니티의 Mathf 도 라디안을 받고 돌려줍니다.

    Mathf.Rad2Deg(180 ÷ π ≈ 57.3)를 곱하면 도가 됩니다. (−1, −1) 이면 Atan2 가 −2.36 을 주고, −2.36 × 57.3 ≈ −135° 입니다.

    // 마우스의 화면 픽셀 좌표를 월드 좌표로 바꾼다
    // 2D 기본 카메라(Orthographic, 멀어도 작아지지 않게 찍는 방식) 기준이다
    Vector2 mouseWorldPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
    
    // 총에서 마우스로 가는 방향 (도착 − 출발)
    Vector2 toMouse = mouseWorldPosition - (Vector2)transform.position;
    
    // y 가 먼저, x 가 나중. 결과는 라디안이다
    float radians = Mathf.Atan2(toMouse.y, toMouse.x);
    
    // 라디안에 Rad2Deg(약 57.3)를 곱해 도로 바꾼다
    float angle = radians * Mathf.Rad2Deg;
    
    // Quaternion 은 유니티가 회전을 담는 자료형이다
    // Quaternion.Euler 는 도 단위 각도로 회전값을 만든다. 2D 는 z 축으로 돈다
    transform.rotation = Quaternion.Euler(0f, 0f, angle);

    Atan2 의 0° 는 오른쪽(+x)이고, 각이 커지면 반시계 방향으로 돕니다. 유니티 2D 에서 z 회전이 커질 때 도는 방향도 반시계라서 그대로 넣으면 맞습니다.

    다만 총 스프라이트가 오른쪽을 보도록 그려져 있어야 합니다. 위(+y)를 보도록 그려져 있으면 회전 0° 에서 이미 90° 쪽을 보고 있으므로, angle - 90f 를 넣어야 마우스를 봅니다.

    스프라이트가 그려진 방향 넣을 각도
    오른쪽 (+x) angle
    위 (+y) angle - 90f

    조심할 것

    • Atan2 의 인자는 y 가 먼저입니다. 오른쪽 (1, 0) 은 Atan2(0, 1) = 0° 인데, 순서를 바꿔 Atan2(1, 0) 으로 쓰면 90° 가 나와 총구가 위를 봅니다
    • Atan2 는 라디안을 줍니다. Rad2Deg 를 빼먹으면 −135° 대신 −2.36° 만큼만 돌아 거의 오른쪽을 봅니다. 반대로 Quaternion.Euler, transform.eulerAngles, 에디터의 Rotation 은 도를 씁니다
    • 방향이 (0, 0) 이면 각도가 없습니다. Atan2(0, 0) 은 0 을 돌려주니, 마우스가 총과 정확히 겹치면 총구가 갑자기 오른쪽을 봅니다. 방향의 길이가 아주 작을 때는 회전을 건너뜁니다
    • −180° 와 180° 는 같은 방향인데 숫자는 360 차이입니다. 바로 왼쪽 근처에서 마우스가 위아래로 살짝만 움직여도 각도가 179° 에서 −179° 로 튑니다. 두 각도의 차이를 그냥 빼면 358° 가 나오니, 차이는 Mathf.DeltaAngle(a, b) 로 구합니다. Mathf.DeltaAngle(179f, -179f) 는 2 입니다

    정리

    • 방향 (x, y) 에서 각도를 구하는 문제입니다. 화살표 끝에서 수직선을 내리면 가로 x, 세로 y 인 직각삼각형이 됩니다
    • tan θ = 세로 ÷ 가로 이고, 비율에서 각도를 거꾸로 찾는 것이 arctan(Atan) 입니다. Atan(1) = 45° 입니다
    • Atan 은 나눗셈 결과 하나만 받아 부호를 잃습니다. (1, 1) 과 (−1, −1) 에 같은 45° 를 주고, −90° ~ 90° 만 돌려줍니다
    • Atan2(y, x) 는 x 가 음수면 180° 를 옮겨 왼쪽 절반을 고치고, x 가 0 이어도 답을 줍니다. 결과는 −180° ~ 180° 입니다
    • 결과는 라디안이니 Mathf.Rad2Deg 를 곱하고, 스프라이트가 위를 보게 그려졌으면 90° 를 뺍니다