방향 (x, y) 로 각도 구하기: 총구를 마우스로 돌리는 Atan2
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° 를 뺍니다