내적: 적이 앞에 있나 뒤에 있나
뒤에서 때리면 두 배 피해를 주는 백어택을 만들려면 “적이 공격자를 등지고 있나” 를 알아야 합니다. 적 AI 에 시야를 주려면 “플레이어가 내 앞 120도 안에 있나” 를 알아야 합니다. 둘 다 각도를 구하지 않고 풉니다.
두 방향 벡터의 내적, 곧 같은 자리끼리 곱해서 더한 숫자 하나면 됩니다. 길이가 1 이면 이 숫자가 사이각의 cos 입니다. cos 는 길이 1 인 막대를 θ 만큼 기울였을 때 바닥에 누운 가로 길이입니다. 0° 면 1, 90° 면 0(똑바로 섬), 180° 면 −1(반대로 누움)이라, 부호가 앞과 뒤를 가릅니다.
Vector3.Dot(a, b)는a.x × b.x + a.y × b.y + a.z × b.z입니다. a 와 b 가 길이 1 이면 결과는 1(같은 방향)에서 −1(정반대) 사이이고, 사이각이 클수록 작아집니다. 앞뒤 판정은 부호만, 시야각 판정은cos(시야각의 절반)과 비교합니다.
내적은 같은 자리끼리 곱해서 더한 숫자다
벡터는 방향과 길이를 가진 화살표이고, 유니티에서는 Vector3 의 x, y, z 세 숫자로 씁니다.
두 벡터의 내적은 x 끼리, y 끼리, z 끼리 곱한 다음 전부 더한 값입니다.
결과는 벡터가 아니라 숫자 하나입니다.
위에서 내려다본 바닥이라 y 를 빼고 (x, z) 두 개만 씁니다. 나는 앞 (0, 1) 을 보고 있습니다.
| 적이 있는 방향 | 계산 | 내적 | 뜻 |
|---|---|---|---|
| 바로 앞 (0, 1) | 0 × 0 + 1 × 1 | 1 | 같은 방향 |
| 오른쪽 앞 (0.71, 0.71) | 0 × 0.71 + 1 × 0.71 | 0.71 | 앞쪽 |
| 바로 오른쪽 (1, 0) | 0 × 1 + 1 × 0 | 0 | 옆 |
| 오른쪽 뒤 (0.71, −0.71) | 0 × 0.71 + 1 × (−0.71) | −0.71 | 뒤쪽 |
| 바로 뒤 (0, −1) | 0 × 0 + 1 × (−1) | −1 | 정반대 |
적의 방향은 모두 길이 1 로 맞췄습니다. (0.71, 0.71) 은 대각선 (1, 1) 을 길이 1 로 줄인 값입니다.
부호가 앞뒤를 가릅니다. 0 보다 크면 앞쪽 반, 작으면 뒤쪽 반입니다. 백어택은 “적의 앞 방향” 과 “적에서 공격자로 가는 방향” 의 내적이 0 보다 작은지로 판정합니다.
// 적에서 공격자로 가는 방향 (도착 − 출발)
Vector3 toAttacker = attacker.position - enemy.position;
// 0 보다 작으면 공격자가 적의 뒤쪽 반에 있다
bool isBackAttack = Vector3.Dot(enemy.forward, toAttacker) < 0f;
왜 부호가 그렇게 나오나: 그림자 길이
내적에는 계산하는 법이 두 가지 있습니다. 하나는 위에서 한 같은 자리끼리 곱해서 더하기이고, 다른 하나는 길이 × 길이 × cos(사이각) 입니다. 둘은 늘 같은 값이 나옵니다. 열쇠는 그림자입니다. 한 화살표의 끝에서 다른 화살표가 놓인 직선으로 수직선을 내리면, 그 직선 위에 그림자가 생깁니다.
먼저 쉬운 경우입니다. a = (4, 0) 은 바닥(오른쪽)에 누워 있고, b = (3, 4) 는 비스듬히 서 있습니다.
| 단계 | 계산 | 결과 |
|---|---|---|
| 같은 자리끼리 곱해 더하기 | 4 × 3 + 0 × 4 | 12 |
| a 의 길이 | 오른쪽으로 4 | 4 |
| b 의 길이 (빗변) | √(3² + 4²) = √25 | 5 |
| b 가 바닥에 드리운 그림자 (가로) | b 의 끝 (3, 4) 에서 수직선을 내리면 | 3 |
| 사이각의 cos | 가로 ÷ 빗변 = 3 ÷ 5 | 0.6 |
| 길이 × 길이 × cos | 4 × 5 × 0.6 | 12 |
두 길로 같은 12 가 나왔습니다. 이유도 보입니다. a 의 y 가 0 이라 곱해서 더하기가 4 × 3 하나만 남는데, 4 는 a 의 길이이고 3 은 b 의 그림자입니다.
그리고 그림자(가로)는 cos 의 뜻 그대로 빗변 × cos 입니다. 그래서 a 의 길이 × 그림자 = a 의 길이 × b 의 길이 × cos 가 됩니다.
a 가 비스듬해도 같습니다. 두 화살표를 함께 돌리면 길이와 사이각은 그대로이고, 곱해서 더한 값도 그대로입니다. 위의 둘을 함께 90° 돌리면 a = (0, 4), b = (−4, 3) 이 되는데, 0 × (−4) + 4 × 3 = 12 로 같습니다.
그래서 내적의 정식 모양은 이렇습니다.
a · b = (a 의 길이) × (b 의 길이) × cos(사이각)
= a.x × b.x + a.y × b.y + a.z × b.z
한쪽이 길이 1 이면 내적은 다른 쪽이 그 방향에 드리운 그림자의 길이입니다. 둘 다 길이 1 이면 앞의 두 곱이 1 이 되어 내적 = cos(사이각) 만 남습니다. 그림자가 같은 쪽에 생기면 양수, 수직이면 0, 반대쪽에 생기면 음수라서 부호가 앞뒤를 가릅니다.
| 사이각 | cos (가로 ÷ 빗변) | 빗변이 1 일 때 그림자 |
|---|---|---|
| 0° | 1 | 1. 빗변이 그대로 눕는다 |
| 60° | 0.5 | 0.5 |
| 90° | 0 | 0. 수직이라 그림자가 없다 |
| 120° | −0.5 | 반대쪽으로 0.5 |
| 180° | −1 | 반대쪽으로 1 |
처음 표에서 본 1, 0.71, 0, −0.71, −1 이 cos 0°, 45°, 90°, 135°, 180° 입니다.
시야각은 cos 와 비교한다
적의 시야가 앞쪽 120 도라면, 좌우로 60 도씩입니다. 사이각이 60 도 이하면 보입니다. 각이 작을수록 cos 가 크니까, 사이각 ≤ 60° 는 내적 ≥ cos 60° = 0.5 와 같은 말입니다.
| 시야 (전체) | 좌우로 | 기준 cos |
|---|---|---|
| 180° | 90° | 0 (앞쪽 반 전부) |
| 120° | 60° | 0.5 |
| 90° | 45° | 0.71 |
| 60° | 30° | 0.87 |
float viewAngle = 120f;
// 좌우로 절반씩. Deg2Rad 는 도(°)를 Mathf.Cos 가 받는 단위(라디안)로 바꾸는 값
float minDot = Mathf.Cos(viewAngle * 0.5f * Mathf.Deg2Rad); // 0.5
// .normalized 는 방향은 그대로 두고 길이만 1 로 맞춘 벡터 (정규화)
Vector3 toPlayer = (player.position - transform.position).normalized;
bool canSee = Vector3.Dot(transform.forward, toPlayer) >= minDot;
minDot 은 시야각이 안 바뀌면 한 번만 구해 두면 됩니다. 매 프레임 적마다 하는 일은 곱셈 세 번과 덧셈 두 번입니다.
시야각을 볼 때는 둘 다 길이 1 이어야 한다
toPlayer 에 .normalized 를 붙인 이유가 있습니다.
내적은 길이 × 길이 × cos 라서, 길이가 1 이 아니면 값이 cos 가 아닙니다.
앞이 (0, 1) 일 때, 플레이어 방향을 정규화했는지에 따라 판정이 갈립니다.
| 플레이어까지 | 사이각 | 정규화 안 한 내적 | 정규화한 내적 | 120° 시야 |
|---|---|---|---|---|
| (6, 8) | 37° | 8 | 0.8 | 안 |
| (10, 1) | 84° | 1 | 0.1 | 밖 |
(10, 1) 은 거의 옆인데, 정규화하지 않으면 내적이 1 이라 기준 0.5 를 넘어 “보인다” 가 됩니다. 멀리 있을수록 값이 커져서 생기는 일입니다.
반대로 부호만 볼 때는 정규화가 필요 없습니다. 길이는 늘 양수라서 곱해도 부호가 바뀌지 않습니다. 백어택 코드에 .normalized 가 없던 이유입니다.
| 하려는 것 | 비교 | 정규화 |
|---|---|---|
| 앞인가 뒤인가 | Dot > 0 |
필요 없음 |
| 시야각 안인가 | Dot >= cos(절반) |
둘 다 필요 |
| 얼마나 정면인가 (0 ~ 1 점수) | Dot 값 그대로 |
둘 다 필요 |
각도가 꼭 필요하면 Vector3.Angle
사이각을 도(°)로 보고 싶으면 Vector3.Angle(a, b) 가 있습니다. 안에서 내적을 두 길이로 나눠 cos 를 구한 뒤, acos 로 각도로 바꿉니다. acos 는 cos 를 거꾸로 돌리는 함수로, 0.5 를 넣으면 60° 를 돌려줍니다.
Vector3.Dot |
Vector3.Angle |
|
|---|---|---|
| 돌려주는 것 | 숫자 (길이 1 이면 cos) | 0° ~ 180° |
| 하는 일 | 곱셈과 덧셈 | 정규화 + 내적 + acos |
| 쓰는 곳 | 판정, 비교 | 화면에 각도를 보여 줄 때, 디버그 |
둘 다 왼쪽과 오른쪽을 가리지 못합니다. 왼쪽 45 도와 오른쪽 45 도의 내적이 똑같이 0.71 이었습니다.
어느 쪽으로 돌아야 하는지까지 알려면 두 벡터 모두에 수직인 벡터를 만드는 외적(Vector3.Cross)을 함께 씁니다. 그 벡터가 위를 향하는지 아래를 향하는지가 왼쪽과 오른쪽을 가릅니다.
내적은 판정 말고도 곳곳에 나옵니다. 빛이 면을 정면으로 비추면 밝고 비스듬히 비추면 어두운 것도, 면의 방향과 빛의 방향의 내적으로 밝기를 정하기 때문입니다.
정리
- 내적은 x 끼리, y 끼리, z 끼리 곱해서 더한 숫자 하나입니다
- 내적은
길이 × 길이 × cos(사이각)과 같습니다. (4, 0) · (3, 4) 는 4 × 3 + 0 × 4 = 12 이고, 4 × 5 × 0.6 = 12 입니다 - 길이 1 인 방향 f 와 v 의 내적은 v 를 f 위에 내린 그림자의 길이이고, 둘 다 길이 1 이면 cos 만 남습니다
- 부호가 앞뒤를 가릅니다. 0 보다 크면 앞, 0 이면 수직, 작으면 뒤입니다. 부호만 볼 때는 정규화가 필요 없습니다
- 시야각은
Dot >= cos(시야각 / 2)로 판정하고, 이때는 두 벡터 모두 길이 1 이어야 합니다 - 내적은 왼쪽과 오른쪽을 가리지 못합니다