2026-04-09 TIL (33일차)
Unreal
• SetupPlayerInputComponent
플레이어의 물리적 입력을 캐릭터의 C++ 행동(함수)과 연결해 주는 핵심 함수입니다.
핵심 특징
- 타이밍: 게임이 시작되고 플레이어가 이 캐릭터에 ‘빙의(Possess)’하는 순간 엔진이 자동으로 한 번 호출하여 배선 작업을 세팅합니다.
- 플레이어 전용: NPC나 몬스터처럼 AI가 조종하는 폰(Pawn)에게는 이 함수가 호출되지 않습니다. 오직 Local Player이 조종할 때만 작동합니다.
- 상속:
APawn(또는ACharacter) 클래스에 기본적으로 제공되는 가상(Virtual) 함수이므로, 우리가Override해서 사용합니다.
• CustomTick 계산공식
언리얼 엔진의 기본 Tick(DeltaTime)은 컴퓨터의 성능에 따라 프레임 호출되는 시간이 다릅니다.
하지만 물리 연산은 시간이 변하면 결과가 달라지거나 물리 엔진이 오류가 발생하는 문제가 생깁니다.
비유: ‘물통과 종이컵’
DeltaTime(현실 시간): 수도꼭지에서 불규칙하게 떨어지는 물방울Accumulator(누적기): 물방울을 모아두는 물통TimeStep(고정 시간): 정확히 1/60초의 양만 퍼낼 수 있는 정량 종이컵
1
2
3
4
5
6
7
8
9
10
11
12
13
14
float Accumulator = 0.0 f;
const float TimeStep = 1.0 f / 60.0 f;
void AWeek3Drone::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
Accumulator += DeltaTime;
while (Accumulator >= TimeStep)
{
CustomTick(TimeStep);
Accumulator -= TimeStep;
}
}
원리
| 코드 | 원리 |
|---|---|
Accumulator += DeltaTime; | [시간 누적] 흐른 시간(DeltaTime)을 누적기(Accumulator)에 계속 더해줍니다. |
while (Accumulator >= TimeStep) | [실행 조건] Accumulator가 설정한 프레임보다 많거나 같아질때만 실행하게 합니다. |
CustomTick(TimeStep); | [고정 연산] DeltaTime 대신, 고정된 TimeStep으로 Tick을 반복함 |
Accumulator -= TimeStep; | [차감 및 이월] 연산을 했으니 Accumulator에서 TimeStep만큼 빼주고 남은 시간은 다음 프레임으로 이월됩니다. |
이게 수직동기화인가?
• Input Mapping Context에 값이 들어오는 원리
Input Action(IA)을 만들 때, 이 액션이 어떤 형태의 값을 받을지 값 타입을 정해줍니다.
🔴 Digital (bool)
- 개념:
true아니면false만 존재합니다. - 사용처: Jump, 총 쏘기, 상호작용
- 할당 원리: 키보드를 누르면 그냥
true가 전달됩니다.
🟢 Axis 1D (float)
- 개념: 1개의 차원(X축)만 가지는 값입니다. 보통
-1.0에서1.0사이의 값을 가집니다. - 사용처: 게임패드의 트리거 버튼(살짝 누르기/세게 누르기), 마우스 휠 굴리기.
- 할당 원리: 키보드(W)를 누르면
1.0이 그대로 들어옵니다. 만약 반대로 작동하게 하고 싶다면Negate만 추가해 주면 됩니다.
🔵 Axis 2D & 3D
- 키보드의 키는 1차원(1.0) 신호만 쏘는데, IA는 2칸(X,Y) 또는 3칸(X,Y,Z)을 열어두고 기다립니다.
- 원칙: 모든 1차원 입력은 무조건 첫 번째 칸으로 들어간다
예시
키보드 스페이스바(Spacebar)를 눌렀을 때 IA(Vector3D)에 값이 꽂히는 과정을 봅시다.
- 기본 상태: 스페이스바를 누르면 무조건 X축으로 꽂힙니다. ->
(1.0, 0.0, 0.0) - 드론을 만들 때, 스페이스바는 보통 Z축(상승)으로 써야 합니다. 이때 Swizzle이 사용
Modifier
아무 설정도 하지 않으면, 언리얼은 들어온 값을 X축(앞뒤) 에 때려 넣습니다. 그래서 우리는 Modifier를 사용해 “값을 X가 아니라 Y로 보내”라고 지시해야 합니다.
- Negate (반전): 들어온 값에
-1을 곱합니다. - Swizzle Input Axis Values (축 방향 틀기): 들어온 값의 위치(X, Y, Z)를 강제로 다른 축으로 보냅니다. (예:
YXZ를 선택하면, 기본 X로 들어오던 값을 Y로 보냅니다.)
• 언리얼 이동 공식
이동할 거리 = 방향 × 속도 × 시간
언리얼 매칭표
| 기본 공식 | 언리얼 엔진 (C++) 개념 | 설명 |
|---|---|---|
| 방향 (입력) | MoveInput (FVector) | 향상된 입력에서 받아온 1, 0, -1 등의 방향 값 |
| 속도 | MoveSpeed (float) | 1초에 얼만큼 이동할지 정해둔 변수 (예: 500.0f) |
| 시간 | DeltaTime (float) | 프레임과 프레임 사이의 시간 (또는 FixedDeltaTime) |
| = 거리 (결과) | DeltaLocation (FVector) | 이번 프레임에서 최종적으로 더해질 이동 결괏값 |
• 언리얼 엔진의 PITCH와 현실 Pitch
마우스 좌표 입력
‘마우스’의 특징은 모니터 화면은 왼쪽 맨 위가 (0, 0)이기 때문에 다음과 같이 작동합니다.
- 마우스를 위로 올릴 때: Y값은 감소 (-) 합니다.
- 마우스를 아래로 내릴 때: Y값은 증가 (+) 합니다.
현실 pitch의 행동
수학적인 3D 각도(Rotation)의 기준
- 고개를 위로 들 때 (하늘 보기): Pitch 각도가 증가 (+) 합니다.
- 고개를 아래로 숙일 때 (땅 보기): Pitch 각도가 감소 (-) 합니다.
언리얼에서의 pitch의 행동
언리얼 엔진에서 카메라를 제어하는 ‘컨트롤 로테이션(Control Rotation)’ 시스템에 값을 전달할 때는 결과가 정반대로 나타납니다.
- 엔진 시스템에 값을 전달하면:
-값이 들어갈 때 위를 보고,+값이 들어갈 때 아래를 봅니다.
그러면 왜 캐릭터 Movement를 이용할 때 Negate를 사용하는가?
제공되는 캐릭터나 PlayerController를 쓸 때, 입력 세팅(IMC)에서 마우스 Y축에 Negate(반전) 모디파이어를 넣는 이유가 바로 여기에 있습니다.
그 이유는 언리얼 내부 엔진 코드인 PlayerController::AddPitchInput(float Val)에 숨어있습니다. 이 함수 내부를 까보면, 우리가 넘겨준 값에 InputPitchScale (기본값이 음수, 주로 -1.0 또는 -2.5) 이라는 변수를 강제로 곱해버립니다.
- 즉, 엔진이 내부적으로 들어온 값에 마이너스(-)를 한 번 더 곱해서 부호를 뒤집어 버리기 때문에, 우리가 원하는 정상적인 조작(마우스 올리면 위 보기)을 맞추기 위해 입력 단계에서 미리
Negate를 걸어 ‘마이너스의 마이너스’로 상쇄시키는 작업이 필요한 것입니다.
커스텀 구현 시: 그대로 값을 받아도 되는 이유
AddPitchInput 같은 엔진 기본 함수를 쓰지 않고, 직접 회전(Rotation) 공식을 코딩해서 구현한다면? 상황이 Negate가 필요 없어집니다.
- 마우스를 위로 올린다 -> 순수한
-값이 들어온다. - 엔진의 기본 함수를 안 거치므로 중간에 쓸데없이
-가 곱해지지 않는다. - 마우스에서 넘어온
-값을 우리가 짠 로직에 그대로 적용하면 언리얼의 Pitch 특성에 맞물려 위를 보게 구현할 수 있다.
• AddActorLocalOffset과 AddActorLocalRotation 매개변수
첫 번째 매개변수: DeltaLocation / NewRotation (변화량)
의미: “현재 상태에서 얼마나 더 움직이거나 회전할 것인가?”입니다.
예를 들어 현재 위치가 (100, 0, 0)일 때 DeltaLocation에 (5, 0, 0)을 넣으면 (105, 0, 0)으로 갑니다.
Local의 특징: 드론이 90도 꺾여 있다면, 월드의 X축이 아니라 드론이 바라보는 앞방향으로 5만큼 전진합니다.
두 번째 매개변수: bSweep (true / false)
이것이 질문하신 핵심 스위치입니다. 한 문장으로 정의하면 “가는 길에 장애물이 있는지 검사(Sweep)할 것인가?”입니다.
true (충돌 감지 모드):
캐릭터가 벽을 향해 이동할 때, 벽에 닿으면 더 이상 가지 못하고 멈춥니다.
CustomPhysicsTick에서 구현한 물리적 일관성을 유지하려면 반드시 true여야 합니다.
false (유령/텔레포트 모드):
벽이 있든 말든 상관하지 않고 계산된 좌표로 위치를 고정해버립니다.
결과적으로 벽을 뚫고 들어가는 현상이 발생합니다.
• SetCollisionProfileName
충돌 컴포넌트를 생성하고 충돌 프로필을 지정하지 않으면, 엔진은 기본값인 OverlapAllDynamic 상태로 만듭니다.
이름에서 알 수 있듯이 이 상태는 모든 것과 겹침을 허용하겠다는 뜻이라서 통과가 됩니다.
1
SphereComp->SetCollisionProfileName(TEXT("Pawn"));
바닥을 뚫고 떨어지거나 벽을 통과하지 않고 정상적으로 부딪히게(충돌하게) 만들어 준 핵심 코드입니다.
코드 해석
| 코드 부분 | 역할 및 의미 |
|---|---|
SphereComp-> | 이 드론의 가장 바깥쪽 껍질(루트 컴포넌트) 역할을 하는 구체(Sphere)에게 명령합니다. |
SetCollisionProfileName | “너의 충돌 세팅(Collision Settings)을 지금 내가 적어주는 ‘이름표’의 규칙대로 세팅해라!” |
TEXT("Pawn") | 언리얼 엔진 프로젝트 세팅에 미리 만들어져 있는 “Pawn”이라는 충돌 규칙 세트(Preset)의 이름입니다. |
• FHitResult
언리얼 엔진에서 Line Trace을 쏘거나 물체끼리 부딪혔을 때, 누가, 어디서, 어떻게 맞았는지에 대한 모든 정보가 담겨있는 결과 보고서(구조체)입니다.
자주 사용하는 정보 (변수 & 함수)
코드 (HitResult.XXX) | 의미 |
|---|---|
GetActor() | 정확히 누구랑 부딪혔어? |
GetComponent() | 적의 어느 부위(컴포넌트)에 맞았어? |
ImpactPoint | 정확한 3D 우주상의 타격 좌표가 어디야? |
ImpactNormal | 맞은 표면이 어느 방향을 보고 있어? |
Distance | 총구에서부터 맞은 곳까지 거리가 얼마야? |
BoneName | (캐릭터일 경우) 어느 뼈에 맞았어? |
• FCollisionQueryParams
언리얼 엔진에서 Line Trace를 쏘거나 충돌 검사를 할 때, 누구는 무시하고, 얼마나 정밀하게 검사할 것인지 엔진에게 세팅해 주는 옵션/필터 구조체입니다.
자주 사용하는 함수
코드 (TraceParams.XXX) | 의미 |
|---|---|
AddIgnoredActor(AActor*) | “얘는 쏴도 무시하고 통과시켜라!” |
bTraceComplex | “대충 박스로 치지 말고, 실제 모델링(폴리곤) 모양대로 아주 정밀하게 검사해라!” |
bReturnPhysicalMaterial | “맞은 곳의 재질(나무, 철, 살점) 정보도 같이 조사해 와라!” |