내적으로 시야각, 외적으로 좌우 판정하기
수식보다 '어떤 질문에 답하는 도구인가'
적이 나를 봤는가, 내가 적의 왼쪽에 있는가 — 두 판정은 각각 내적과 외적 하나로 끝난다. 각도를 방향 벡터로 바꾸는 삼각함수부터 RPG 시야각 판정 적용까지.
적이 나를 봤는지, 내가 적의 왼쪽에 있는지 — 게임에서 자주 나오는 이 두 판정은 각각 내적과 외적 하나로 끝난다. 문제는 두 연산을 수식으로만 외우면 어느 상황에 뭘 꺼내야 할지 헷갈린다는 것이다. 이 글에서는 둘을 “얼마나 같은 방향인가”와 “어느 쪽에 있는가”라는 질문으로 나눠 정리하고, 각도를 방향 벡터로 바꾸는 삼각함수부터 시작해 RPG 시야각 판정 문제에 직접 적용한 과정을 이야기하려 한다.
삼각함수 — 각도를 방향 벡터로
캐릭터를 각도 θ 방향으로 움직이려면 그 각도를 방향 벡터로 바꿔야 한다. 공식은 하나다.
1
방향 벡터 = (cos θ, sin θ)
단위원 위의 점이 곧 단위 방향 벡터라서, 어떤 각도든 이 식에 넣으면 크기 1짜리 방향 벡터가 나온다.
| 각도 | cos θ | sin θ | 벡터 |
|---|---|---|---|
| 0° | 1 | 0 | (1, 0) → 오른쪽 |
| 90° | 0 | 1 | (0, 1) → 위 |
| 180° | -1 | 0 | (-1, 0) → 왼쪽 |
| 270° | 0 | -1 | (0, -1) → 아래 |
1
2
// UE5: 각도로 이동 방향 구하기
FVector dir = FVector(FMath::Cos(AngleRad), FMath::Sin(AngleRad), 0);
내적 — 얼마나 같은 방향?
내적이 답하는 질문은 하나다 — 두 벡터가 얼마나 같은 방향을 가리키는가.
1
a · b = |a||b| cos θ
단위벡터끼리라면 크기가 1이므로 내적값이 곧 cos θ다.
1
â · b̂ = cos θ
| 결과값 | 의미 |
|---|---|
| 1.0 | 완전히 같은 방향 (0°) |
| 0.5 | 60° 차이 |
| 0.0 | 수직 (90°) |
| -0.5 | 120° 차이 |
| -1.0 | 완전히 반대 방향 (180°) |
내적값에서 각도를 역산할 수도 있다.
1
cos θ = 0.5 → θ = arccos(0.5) = 60°
계산 방법
실제 계산은 성분곱으로 한다.
1
2
3
a = (ax, ay), b = (bx, by)
a · b = ax×bx + ay×by
예시: a = (3, 4), b = (5, 0)
1
a · b = (3×5) + (4×0) = 15 + 0 = 15
게임 활용: 시야각 판정
전방 벡터와 적 방향 벡터를 내적하면 cos 값이 나오고, 그걸 시야각의 cos 임계값과 비교하면 시야 안인지 판정할 수 있다.
1
2
3
4
5
6
// 시야각 90° (±45°) 내에 적이 있는지 판정
float dot = FVector::DotProduct(ForwardVec, ToEnemyVec); // 단위벡터 가정
if (dot > FMath::Cos(FMath::DegreesToRadians(45.f))) // cos45° ≈ 0.707
{
// 적이 시야 안에 있음
}
시야각이 좁을수록 cos 임계값이 1에 가까워진다는 것만 기억하면 된다.
| 시야각 | cos 임계값 | 조건 |
|---|---|---|
| ±30° | cos30° ≈ 0.866 | dot > 0.866 |
| ±45° | cos45° ≈ 0.707 | dot > 0.707 |
| ±60° | cos60° = 0.5 | dot > 0.5 |
외적 — 어느 쪽?
외적이 답하는 질문은 방향의 유사도가 아니라 위치다 — b는 a의 왼쪽에 있는가, 오른쪽에 있는가.
1
|a × b| = |a||b| sin θ (크기 = 평행사변형 넓이)
2D에서는 스칼라 부호로 방향을 판단한다 (y-up 기준).
| 결과 | 의미 |
|---|---|
| 양수 | b가 a의 왼쪽 (반시계) |
| 음수 | b가 a의 오른쪽 (시계) |
| 절댓값 | 평행사변형 넓이 |
2D 외적 계산
1
2
3
a = (ax, ay), b = (bx, by)
a × b = ax×by − ay×bx
예시: a = (3, 4), b = (5, 0)
1
2
3
a × b = (3×0) − (4×5) = 0 − 20 = -20
→ 음수 → b가 a의 오른쪽
→ |−20| = 20 → 평행사변형 넓이
이 공식이 어디서 왔는지는 3D 외적을 보면 된다 — z성분만 꺼낸 것이다.
1
2
3
| i j k |
| ax ay 0 | → z성분 = ax×by − ay×bx
| bx by 0 |
게임 활용: 좌우 판정
1
2
3
4
5
float cross = FVector::CrossProduct(ForwardVec, ToEnemyVec).Z;
if (cross > 0)
TurnLeft(); // 적이 왼쪽
else
TurnRight(); // 적이 오른쪽
벡터 정규화 (단위벡터)
방향만 남기고 크기를 1로 만드는 게 정규화다.
1
d̂ = 벡터 / |벡터| = (x/|v|, y/|v|)
예시: A = (4, 2)
1
2
3
|A| = √(4²+2²) = √20 = √(4×5) = 2√5 ≈ 4.47
d̂_A = (4/2√5, 2/2√5) = (2/√5, 1/√5) ≈ (0.894, 0.447)
루트 간소화는 루트 안에서 완전제곱수(4, 9, 16…)를 꺼내면 된다.
1
2
3
√20 = √(4×5) = 2√5
√18 = √(9×2) = 3√2
√45 = √(9×5) = 3√5
자주 쓰는 루트 근사값: √2 ≈ 1.414, √3 ≈ 1.732, √5 ≈ 2.236
문제 풀이 — RPG 시야각 판정
조건: 캐릭터 (0,0), 전방 f̂ = (1,0), 시야각 90° (±45°)
적: A=(4,2), B=(3,4), C=(-2,3), D=(5,-1)
Step 1: 정규화
| 적 | 크기 | d̂ (근사) |
|---|---|---|
| A | 2√5 ≈ 4.47 | (0.894, 0.447) |
| B | 5 | (0.6, 0.8) |
| C | √13 ≈ 3.61 | (-0.555, 0.832) |
| D | √26 ≈ 5.10 | (0.981, -0.196) |
Step 2: 내적 f̂·d̂
f̂ = (1,0)이라 x성분만 남는다 (y항은 0이 곱해져서 사라짐).
1
f̂·d̂ = (1×dx) + (0×dy) = dx
| 적 | f̂·d̂ | > 0.707? |
|---|---|---|
| A | 0.894 | O |
| B | 0.600 | X |
| C | -0.555 | X |
| D | 0.981 | O |
시야 내 적: A, D
Step 3: 좌우 판정 (f × d̂의 z성분)
f̂ = (1,0)이라 f×d̂ = dy (x항이 사라짐).
1
2
적 A: f×d̂_A = 0.447 > 0 → 왼쪽
적 D: f×d̂_D = -0.196 < 0 → 오른쪽
시야 내 + 왼쪽: A
정리 — 내적과 외적을 고르는 질문
- 삼각함수 = 각도 → 방향 변환 도구.
(cos θ, sin θ)으로 어떤 각도든 단위벡터로 변환할 수 있다. - 내적 = 앞/뒤 판단. 단위벡터 내적값이 cos θ와 같으므로, 시야각 임계값을 cos으로 바꿔서 비교하면 된다.
- 외적 = 좌/우 판단. 2D 외적
ax×by − ay×bx의 부호만으로 어느 쪽인지 바로 알 수 있다. - f̂ = (1,0)일 때 계산 단순화. 내적은 x성분, 외적은 y성분만 남는다 — 전방이 축과 정렬되면 계산이 극단적으로 간단해진다.
- 수식 암기보다 “어떤 도구인가” 먼저. 내적·외적을 구분하는 질문: “방향이 얼마나 같냐(내적)” vs “어느 쪽에 있냐(외적)”.
핵심 요약 — 내적은 “얼마나 같은 방향인가”(시야각 판정), 외적은 “어느 쪽에 있는가”(좌우 판정)에 답하는 도구다. 전방 벡터가 (1,0)처럼 축과 정렬되면 내적은 x성분, 외적은 y성분만 남아서 계산이 크게 단순해진다.