
오늘은 뭔가 많은 걸 한 날이네요. 사실 어제와 그저께는 새로운 모니터 설정하고 벨로그 개편하고 이것 저것 할 게 많아서 많이 하지 못했습니다. 그래서 오늘부터 치타처럼 달리기로 했습니다! 늙고 팔팔하지 못한 치타 제가 보여드리겠습니다. 아무튼 오늘은 로그 출력, 엑터의 생명주기, Tick 함수로 엑터의 Transform 조정, 언리얼 C++ 구조에 관하여 배웠습니다. 또한, 어쩌다보니 팀 스터디의 진행 요원(?)이 되어서 언리얼 C++ 구조에 대하여 깊게 살펴 보는 시간을 가졌습니다. 먼저 로그 출력부터 살펴보겠습니다.
먼저 로그 출력부터 보겠습니다.

UE_LOG 함수를 사용해서 기본 제공 로그 카테고리인 LogTemp를 사용하고, 로그 심각도를 Warning으로 설정한 뒤 "My Item Appears!"라는 메시지를 출력합니다.

그 이후 헤더파일에서LogSparta라는 로그 카테고리를 선언(Declare니까)하고, 로그의 기본 심각도(verbosity)를 Warning, 허용할 로그 범위를 All로 설정합니다.

그리고 아까 선언한 로그 카테고리를 이제 소스파일에서 로그 카테고리를 정의(Define이니까)했습니다.

그 이후 카테고리가 로그 스파르타이고 심각도가 오류인 my log 로그를 만들었습니다.

window에서 output log를 선택하면 로그 출력되는 곳이 열립니다.

이런 식으로 뜹니다. 근데 이렇게 보면 너무 많아서 보기 싫죠? 그럴 댄 필터를 씁시다.

필터에서 나머지를 다 안 보이게 설정한 후 logtemp와 logsparta을 선택합니다.

이렇게 깔끔하게 잘 나옵니다! 야호~~
먼저 액터의 생명주기가 뭔지 한 번 알아보겠습니다.
액터도 태어나서(?) 게임에 들어오고, 게임이 진행되는 동안 계속 활동하다가 언젠가는 사라집니다. 이 과정에서 언리얼이 정해진 순서에 따라 여러 함수를 호출해주는데, 이것이 바로 액터의 생명주기입니다.
쉽게 말하면 액터가 생성되고 → 초기화되고 → 게임에 들어오고 → 게임이 진행되는 동안 계속 동작하다가 → 제거되는 과정이라고 보면 됩니다.
그럼 하나씩 살펴보겠습니다.
1. Constructor
가장 먼저 생성자입니다.
액터가 생성될 때 호출되는 녀석으로, 여기서는 CreateDefaultSubobject()를 사용해서 컴포넌트를 만들거나 기본값을 설정합니다.
즉, 액터가 태어날 때 기본적인 세팅을 해주는 단계라고 생각하면 됩니다.
2. PostInitializeComponents()
그다음은 PostInitializeComponents()입니다.
이름 그대로 모든 컴포넌트가 초기화된 이후에 호출됩니다.
그래서 컴포넌트끼리 연결하거나 서로 참조해야 하는 작업을 여기서 해주기 좋습니다.
3. BeginPlay()
그리고 게임이 시작되면 BeginPlay()가 호출됩니다.
여기부터는 본격적으로 게임에 들어왔다고 보면 됩니다.
그래서 실제 게임 로직을 시작하거나 타이머, Delegate 등을 초기화하는 데 사용합니다.
4. Tick()
그리고 오늘의 주인공(?) Tick()입니다.
Tick()은 매 프레임마다 호출됩니다.
그래서 액터의 위치나 회전처럼 게임이 진행되는 동안 계속 변해야 하는 값을 처리할 때 사용할 수 있습니다.
다만 매 프레임마다 실행되는 만큼 아무 생각 없이 이것저것 넣어버리면 성능에 영향을 줄 수 있으니 꼭 필요한 경우에 사용하는 것이 좋습니다.
저의 생명주기가 아마 짧은 것으로 예상되는 이유는 바로 이것입니다.
Tick()이 너무 열심히 일하고 있거든요. 💀💀💀💀💀💀
5. Destroyed()
이제 액터가 사라질 때가 됐습니다.
Destroy()를 통해 액터를 직접 제거하면 Destroyed()가 호출됩니다.
액터가 제거되기 직전에 필요한 정리 작업을 해줄 수 있습니다.
6. EndPlay()
마지막은 EndPlay()입니다.
액터가 더 이상 월드에서 활동하지 않게 될 때 호출됩니다.
액터가 제거되는 경우뿐만 아니라 레벨 전환이나 게임 종료 등으로 액터의 플레이가 끝나는 경우에도 호출될 수 있습니다.
그래서 타이머나 이벤트 같은 것들을 마지막으로 정리하는 작업을 해주기에 좋습니다.
정리하면 액터의 생명주기는 대략
생성 → 컴포넌트 초기화 → 게임 시작 → 매 프레임 동작 → 제거/종료
이런 흐름으로 진행된다고 볼 수 있습니다.
액터도 태어나서 열심히 Tick()하다가 결국 EndPlay()를 맞이하는군요.
자 그럼 이걸 이제 코딩으로 구현해봅시다~!

먼저 헤더파일에서 선언해줍니다.

선언을 해줬으니 소스파일에서 이 함수들이 뭔지 정의해줍시다.
먼저 PostInitializeComponents()을 정의해줍시다. 일단 제대로 되는지 확인하기 위해서는 방금 배운 로그 출력을 넣었습니다.

그 다음 없는 Destroyed()와 EndPlay()도 정의해줍시다. 확인을 위해 로그 출력 넣었습니다. 아 그리고 언리얼 C++ 코드를 보다 보면 함수 앞에 Super::가 붙어있는 경우가 있습니다.
Super::는 쉽게 말해서 “부모 클래스의 해당 함수도 실행해라!”라는 뜻입니다.
예를 들어
Super::BeginPlay();
라고 하면 부모 클래스인 AActor의 BeginPlay()도 실행해주는 것입니다.
그리고 로그를 출력할 때 자주 보이는 %s도 있습니다.
%s는 문자열을 넣어서 출력하겠다는 형식 지정자이고, 뒤에 있는 값이 %s 자리에 들어갑니다.
UE_LOG(LogTemp, Warning, TEXT("Actor Name : %s"), *GetName());
여기서 GetName()은 현재 액터의 이름을 가져오는 함수입니다.
그런데 GetName()의 반환값은 FString이고, %s가 기대하는 형태와 맞춰주기 위해 앞에 *를 붙여서 넘겨줍니다.
즉,
%s → 문자열이 들어갈 자리
GetName() → 현재 액터 이름 가져오기
*GetName() → %s가 사용할 수 있는 형태로 넘기기
이렇게 생각하면 됩니다.
덕분에 액터 이름을 직접 적지 않아도 언리얼이 붙여준 현재 액터의 이름이 자동으로 로그에 출력됩니다.
아무튼 여기서 또 왜 Tick은 로그 출력 안 넣냐라는 의문점이 생길 수 있는데 넣으면 Tick마다 로그가 출력이 되기에 로그 폭탄이 되지 않기 위해 이 친구는 안 넣었습니다. 필요하면 디버깅용 코드나 별도 조건으로 하는 것을 추천드립니당~!

그럼 언리얼에서 한 번 볼까요? 게임을 시작하면 이렇게 로그를 찍어보니 액터가 생성되고 초기화된 후 BeginPlay()가 호출되고, 이후 액터의 생명주기가 끝나면서 EndPlay()와 Destroyed()가 호출되는 것을 직접 확인할 수 있었습니다. 역시 액터도 태어나서 언젠가는 떠나는군용~
그럼 이제 Tick으로 Transform을 조정하는 법을 배우겠습니다. 이미 Tick이 뭐고 Transform이 뭔지는 이전 TIL에서 자세히 설명해놨으니 어떻게 조정하는지에 집중해서 살펴보겠습니다.
먼저 Transform 조정부터 살펴보겠습니다.
Transform을 묶어서 관리하는 FTransform 구조체가 있고, 액터의 Transform을 조정할 때는 SetActorLocation(), SetActorRotation(), SetActorScale3D(), SetActorTransform() 등의 함수를 사용할 수 있습니다.
Transform은 크게 Location, Rotation, Scale로 구성되어 있습니다.
FVector(X, Y, Z)FRotator(Pitch, Yaw, Roll)FVector(X, Y, Z)먼저 위치부터 설정해보겠습니다.
SetActorLocation(FVector(0.0f, 220.0f, 10.0f));
이렇게 하면 액터의 위치를 X=0, Y=220, Z=10으로 설정할 수 있습니다.
여기서 0.0f, 220.0f처럼 숫자 뒤에 붙어있는 f는 해당 숫자가 float 타입이라는 것을 나타내는 접미사입니다.
즉,
220.0f
는 float 타입의 숫자라고 보면 됩니다.
그다음은 회전입니다.
SetActorRotation(FRotator(-24.404497f, 57.267592f, -32.732407f));
FRotator는 액터의 회전을 나타내는데, 순서는
Pitch → Yaw → Roll
입니다.

그래서 위 코드에서는
Pitch = -24.404497
Yaw = 57.267592
Roll = -32.732407
로 설정한 것입니다.
마지막으로 Scale도 설정할 수 있습니다.
SetActorScale3D(FVector(1.0f));
FVector(1.0f)는 X, Y, Z 모두 1.0f로 설정하는 것이기 때문에 액터의 크기를 각 축 1배로 설정한다는 의미입니다.
반대로
SetActorScale3D(FVector(2.0f));
라고 하면 X, Y, Z 모두 2배가 되겠죠.
이렇게 각각의 함수를 사용해서 위치, 회전, 크기를 따로 조정할 수도 있고, FTransform을 이용해서 Transform 전체를 한 번에 다룰 수도 있습니다.
그리고 여기서 하나 알아두면 좋은 것이 월드 좌표계와 로컬 좌표계입니다.
월드 좌표계는 말 그대로 게임 월드 전체를 기준으로 하는 좌표이고, 로컬 좌표계는 자신의 부모를 기준으로 하는 상대적인 좌표입니다. 전에 블루프린트로 문 애니메이션을 만들 때 문제가 생겼던 그 친구가 맞습니다. 이렇게 보니 반갑(?)군용. 아무튼 이 부모-자식 관계에서는 이 차이가 중요합니다.
예를 들어 어떤 컴포넌트가 다른 컴포넌트의 자식이라면 부모가 움직일 때 자식도 함께 움직이게 됩니다. 자식의 위치를 부모를 기준으로 설정하기 때문입니다.
그래서 상대적인 Transform을 다룰 때는 Relative가 붙은 함수들을 사용합니다.
SetRelativeLocation()
SetRelativeRotation()
SetRelativeScale3D()
이런 함수들은 월드 전체가 아니라 부모를 기준으로 위치, 회전, 크기를 설정하는 함수입니다.
즉 간단하게 정리하면,
World → 게임 전체를 기준으로 함
Relative → 부모를 기준으로 함
이라고 생각하면 됩니다.
그리고 이러한 액터가 실제로 레벨에 배치되어 있어야 해당 액터의 BeginPlay()가 호출되고, 우리가 작성한 코드도 실행됩니다. 생성자에서 CreateDefaultSubobject()로 컴포넌트를 생성했다고 해서 액터가 자동으로 레벨에 배치되는 것은 아닙니다. 생성자는 액터의 기본적인 구성과 컴포넌트를 설정하는 단계이고, 실제로 레벨에 존재하는 액터 인스턴스(실제로 만들어진 하나의 객체)가 있어야 BeginPlay()와 이후의 동작을 확인할 수 있습니다.
그런데 여기서 의문점이 하나 생겼습니다. 과연 액터가 두 개 이상이라면 어떻게 될까요?

일단 하나가 잘 작동되는지 보겠습니다. 먼저 코드에서 설정한 위치로 제대로 이동하는지 확인하기 위해 액터를 저 하늘에 배치해봤습니다.

의도한 위치로 움직였습니다.

자 이제 그럼 두 개를 넣어봅시다.

실행하면 이렇게 의도한 위치에서 두 개가 동시에 공존(?)하고 있습니다. 같은 AItem 클래스에서 만들어진 액터라고 해서 각각이 하나의 액터로 합쳐지는 것은 아니고, 레벨에 배치된 각각의 액터는 서로 다른 인스턴스이기 때문에 각각 독립적으로 존재합니다.
오호... 그러면 굳이 액터마다 새로운 헤더랑 소스파일을 만들 필요는 없겠군요. 같은 설계도에서 만들어진 녀석들이지만 실제로 레벨에 배치된 액터는 각각 별개의 객체였습니다. 신기하군용~
그럼 이제 Transform을 Tick과 함께 사용하면 어떻게 될까요?
BeginPlay()에서 Transform을 설정하면 게임이 시작될 때 한 번 설정되지만, Tick()에서 Transform을 변경하면 매 프레임마다 Transform을 변경할 수 있습니다.
예를 들어 액터의 회전값을 계속 변경하면 액터가 빙글빙글 회전하게 만들 수 있습니다.
이렇게 Tick() 안에서 회전값을 계속 변경해주면 액터가 계속 회전하게 됩니다.
그런데 여기서 또 하나의 문제가 생깁니다.
"매 프레임마다 값을 변경하면 컴퓨터 성능에 따라 속도가 달라지는 거 아닌가?"
맞습니다.
그래서 저번에 배웠던 DeltaTime을 사용합니다.
DeltaTime은 이전 프레임부터 현재 프레임까지 걸린 시간입니다.
그렇다면 실제로 DeltaTime을 어떻게 사용하는지 살펴보겠습니다.
그럼 이제 이걸 구현해봅시다.

먼저 헤더 파일에서 회전 속도 변수인 float RotationSpeed를 선언해줍니다.
이 변수는 액터가 얼마나 빠르게 회전할지를 정하는 변수입니다.
예를 들어 RotationSpeed가 360.0f라면 1초에 360도, 즉 한 바퀴를 회전하게 만들 수 있습니다. 180.0f라면 1초에 180도만큼 회전하기 때문에 2초에 한 바퀴를 회전하게 됩니다.

그럼 다음으로 생성자에서 Tick 함수를 사용할 수 있도록 설정해줍시다.
PrimaryActorTick.bCanEverTick = true;
bCanEverTick은 이 액터가 Tick을 사용할 수 있도록 허용할지를 설정하는 값입니다.
true로 설정하면 해당 액터가 Tick()을 사용할 수 있게 됩니다.
반대로 Tick이 필요하지 않은 액터라면 false로 설정해서 불필요한 Tick 호출을 막을 수 있습니다. 매 프레임마다 실행되는 함수인 만큼 사용하지 않는 액터까지 Tick을 돌릴 필요는 없겠죠? 이런 식으로 불필요한 Tick을 줄이는 것도 성능 최적화에 도움이 됩니다.
또한, Tick(float DeltaTime)에서 DeltaTime은 직전 프레임부터 현재 프레임까지 걸린 시간(초)입니다.
예를 들어 대략적으로
60 FPS → DeltaTime ≈ 0.0167초
120 FPS → DeltaTime ≈ 0.0083초
정도가 됩니다.
프레임 레이트가 높아질수록 한 프레임에 걸리는 시간이 짧아지기 때문에 DeltaTime도 작아지고, 반대로 FPS가 낮아지면 DeltaTime이 커집니다.
그런데 만약 DeltaTime을 사용하지 않고 매 프레임마다 X 좌표를 1씩 증가시키거나 회전값을 일정하게 증가시키면 어떻게 될까요?
60FPS에서는 1초에 60번 실행되고, 120FPS에서는 1초에 120번 실행되기 때문에 FPS가 높은 컴퓨터에서 액터가 더 빠르게 움직이는 문제가 발생할 수 있습니다.
그래서 이런 문제를 해결하기 위해 DeltaTime을 사용합니다.
예를 들어 초당 100만큼 이동하고 싶다고 해봅시다.
이때 매 프레임 이동할 양을 X라고 한다면, 1초 동안 이동하는 양이 100이 되도록 X를 정해야 합니다.
DeltaTime은 한 프레임에 걸리는 시간이므로,
X = 100.0f × DeltaTime
으로 계산할 수 있습니다.
따라서
100.0f * DeltaTime
을 매 프레임의 이동량으로 사용하면 됩니다.
60FPS에서는 DeltaTime이 약 0.0167이므로,
100 × 0.0167 ≈ 1.67
이 되어 한 프레임마다 약 1.67씩 이동합니다.
60번 반복하면 약 100이 됩니다.
120FPS에서는 DeltaTime이 약 0.0083이므로,
100 × 0.0083 ≈ 0.83
이 되어 한 프레임마다 약 0.83씩 이동합니다.
120번 반복하면 역시 약 100이 됩니다.
즉, 프레임마다 움직이는 양은 달라지지만 1초 동안 움직이는 실제 거리는 거의 동일하게 유지됩니다.
회전도 똑같습니다.
RotationSpeed에 DeltaTime을 곱해주면 초당 일정한 각도로 회전하게 만들 수 있습니다.
예를 들어 RotationSpeed가 360.0f라면,
360.0f * DeltaTime
을 매 프레임 회전량으로 사용하여 1초에 360도, 즉 한 바퀴 회전하게 만들 수 있습니다.
이제 Tick에서 실제로 회전을 시켜봅시다.

AddActorLocalRotation()을 사용해봅시다.
AddActorLocalRotation(FRotator(RotationSpeed * DeltaTime, RotationSpeed * DeltaTime, 0.0f));
AddActorLocalRotation()은 액터의 현재 로컬 좌표계를 기준으로 회전을 추가하는 함수입니다.
여기서 FRotator의 순서는 Pitch → Yaw → Roll이므로,
FRotator(RotationSpeed * DeltaTime, RotationSpeed * DeltaTime, 0.0f)
는
Pitch → RotationSpeed * DeltaTime만큼 회전
Yaw → RotationSpeed * DeltaTime만큼 회전
Roll → 0.0f으로 회전하지 않음
이라는 의미입니다.
즉, 현재 코드에서는 Pitch와 Yaw 방향으로 동시에 회전하게 됩니다.
여기서 RotationSpeed가 360.0f라면 DeltaTime을 곱해 1초에 360도씩 Pitch와 Yaw 방향으로 회전하게 됩니다.
예를 들어 60FPS라면 한 프레임마다
360.0f * 0.0167f
정도인 약 6도씩 회전하게 됩니다.
그리고 AddActorLocalRotation()은 이름 그대로 로컬 좌표계를 기준으로 회전을 추가합니다.
만약 월드 좌표를 기준으로 회전시키고 싶다면
AddActorWorldRotation()
을 사용할 수도 있습니다.
즉,
AddActorLocalRotation() → 로컬 기준으로 회전 추가
AddActorWorldRotation() → 월드 기준으로 회전 추가
라고 생각하면 됩니다.
그리고 여기서 하나 더 알아두면 좋은 함수가 있습니다.
바로 FMath::IsNearlyZero()입니다.
컴퓨터에서 사용하는 float 같은 부동소수점 숫자는 우리가 생각하는 것처럼 항상 정확하게 떨어지는 것은 아닙니다.
그래서 어떤 값이 정확하게 0인지 비교하기보다는 0에 거의 가까운 값인지 확인하고 싶을 때 FMath::IsNearlyZero()를 사용할 수 있습니다.
예를 들어 회전 속도가 거의 0인지 확인하고 싶다면,
FMath::IsNearlyZero(RotationSpeed)
처럼 사용할 수 있습니다.
이 함수는 RotationSpeed가 0에 매우 가까우면 true, 그렇지 않으면 false를 반환합니다.
이런 식으로 언리얼에서는 부동소수점 값을 비교할 때 단순하게 == 0으로 비교하기보다 FMath::IsNearlyZero()를 사용하는 경우가 있습니다.
결국 이번에 구현한 회전을 정리하면,
RotationSpeed → 얼마나 빠르게 회전할지
DeltaTime → 프레임 차이를 보정
AddActorLocalRotation() → 로컬 기준으로 회전 추가
이렇게 서로 연결되어 있습니다.
그리고 PrimaryActorTick.bCanEverTick = true;를 통해 이 액터가 Tick을 사용할 수 있도록 활성화해준 것입니다.
이제 실제로 실행해보면 액터가 계속 회전하는 것을 확인할 수 있습니다.

오호... 이제 진짜로 액터가 매 프레임마다 열심히 빙글빙글~~ 돌아가고 있습니다.
처음에는 그냥 매 프레임 회전시키면 되는 줄 알았는데, FPS에 따라 속도가 달라지는 문제를 DeltaTime으로 해결할 수 있다는 것까지 알게 되었습니다.
결국 Tick은 매 프레임 실행되지만, 실제 움직임은 시간 기준으로 계산해야 프레임에 관계없이 일정한 속도를 만들 수 있다는 것이 핵심이었습니다.
자 이제 마지막입니다! 오늘 마지막으로 배운 것은 언리얼 C++ 구조입니다. 사실 수업을 듣고 제가 메모한 내용을 위주로 아래처럼 화면 공유하여 다른 동기들과 같이 이야기하면서, 서로 의문을 갖고 있던 부분이나 궁금했던 내용들을 하나씩 해결하며 공부했습니다.

제가 진행요원이 되어 제가 메모한 내용을 위주로 쭉 내려가면서, 제가 이해한 방식대로 설명해보고 다른 분들이 의문을 갖고 있던 내용이나 추가로 나온 이야기들을 같이 정리했습니다. 그러다 보니 제가 서기(?) 겸 진행요원(?)이 되어 제 방식대로 다시 정리하면서 공부하게 되었습니다.
이렇게 다른 사람에게 제가 이해한 내용을 설명하다 보니 저도 애매하게 알고 있던 부분을 다시 확인하게 되고, 다른 분들의 질문을 통해 미처 생각하지 못했던 부분까지 알아갈 수 있어서 꽤 유익한 시간이었습니다. 그런 부분들은 제 메모장에 실시간으로 반영하면서 진행했습니다.
아무튼 이 부분은 메모장 내용 그대로 올리겠습니다.
왜냐고요?
지금은 몇 시인지는 비밀이지만 매우 늦은 시간이고 저는 팔팔하지 않으니까요! 🫠
팀 스터디 메모장
MADE BY YOONJAE JUNG
---손튜터님 강의, 2026-08-13 part 1---
언리얼 엔진은 어떻게 보면 보수적이고 정해진 틀이 있다
WHY? FPS 게임을 만들기 위한 엔진이다
언리얼 5 C++컴파일이랑
일반 C++ 프로젝트랑 다르다
(거의 동일하긴 하다)
언리얼 UBT(UnrealBuildTool) 여기서 빌드 관리
UHT(Unreal Header Tool)-매크로 생성해서 사용,
UCLASS, USTRUCT, UENUM, UPROPERTY, UFUNCTION
같은 매크로가 들어간 헤더를 파생해 리플렉션용 코드를 생성한다
(리플렉션용 코드가 정확히 어떻게 적용이 되는지 궁금)
일반 C++ 언리얼이 알 수 없다
(스터디 정리 확인 필요!)
언리얼이 알 수 있게 해주는 기능=리플렉션
리플렉션용 함수는 UHT한테 알려줄 수 있도록 하는 필요한 코드를 만듦
GENERATED_BODY()->전체 것들을 활성화 해주는 것
리플렉션 함수가 정보를 생성해서 UHT한테 전달하고
UHT에서 이 정보를 받고 실행되는 것
리플렉션(정보)
VOD에서 새 클래스를 만들면 .h/.cpp 파일이 생긴다
근데 이거면 생성하는 게 아니라 게임 모듈과 프로젝트 파일 반영까지 함께 따라온다.
그래서 실무에서는 구현만 바뀌면 Live Coding, 헤더나 구조가 바뀌면 에디터 종료 후 IDE 빌드를 한다.
헤더 파일이 바뀌었거나 클래스 구조가 바뀌었다면 에디터를 끄고 컴파일
여기서 말하는 헤더나 구조 변경은 변수 추가 삭제, 함수 선언 변경, 상속 구조 변경, UPROPERTY/UFUNCTION 추가, 클래스 설계 자체 변경
이런 변경은 단순 구현 수정이 아니라 UHT 다시 해석해야하는 영역까지 건드릴 수 있어서 영향 범위가 커진다.
LiveCoding은 실행 중인 바이너리를 패치하는 방식이라 빠르지만, 공식 문서 기준으로는 특히 구현 CPP 변경에 잘 맞는 도구임.
바이너리는 컴파일·빌드된 결과물로, 프로그램이 실제로 실행할 때 사용하는 컴퓨터용 코드임 그래서 이걸 패치하는 거 ㄱㅊ은 거임
그래서 헤더나 구조가 바뀌면 에디터를 닫고 전체 빌드를 하는게 안전!
소스 파일 구현만 바뀌었다면 Live Coding 컴파일
예시로는) 함수 내부 로직 수정(구현부), 조건문 변경, 알고리즘 수정, 로그 추가
이런 거 할 때는 LiveCoding 빠르다.
Live Coding은 에디터가 실행 중이어도 C++ 코드를 다시 빌드하고 실행 중인 바이너리에 패치할 수 있게 해주는 기능
반복 테스트가 많은 게임 플레이 로직일 때 효율적
(내부 구현 리팩토링 or 크게 바꾸지 않고
간단히 로직을 변경했을 경우 LIVE CODING됨)
주의할 점!!!
공식 문서 기준으로 변수 기본값을 .cpp 생성자에서 바꾼 경우에는 이미지 존재하는 오브젝트 인스턴스에 그 값이 바로 반영 안 됨
무슨 말이냐~~
초기화 하는 거에서 값을 넣어주는데 이미 돌고 있는 상태면 반영이 안 된다
(스터디 정리 확인 필요!
인스턴스화 되면서 실행 되고 객체가 생성될 때 처음에 호출되는 생성자
객체는 생성되어 있는데 배치할 때 실행되는게 BEGIN PLAY다
)
인스턴스라는 것은 예시로 언리얼 에디터 맵에 특정 엑터를 만들었으면 리플렉스 정보에서 변수를 본 다음에 언리얼 에디터에서는 0.0이지만 C++에서는 10으로 바꾸어도 안 바뀜
어떻게 보면 선언부를 바꾼 것이기 때문에 에디터를 끄고 다시 빌드를 진행해야한다.
일반 C++ 수동으로 헤더랑 소스파일을 바꾸고 클래스를 추가했으면
이걸 하면 안 되는 이유(절대 안 된다!!는 아님, 가능은 함, 비쥬얼 스튜디오의 일반적인 Add New Item/Add Class 방식은 피하는 것이 좋다)
-1.일반 C++로 만들게 되면 새 클래스의 필요한 헤더, 소스, 게임 모듈을 맞게 업데이트 할 수 없습니다.
언리얼에서 클래스를 만들면 필요한 것들이 알아서 딸려옴(UHT, 모듈)
수동으로 클래스를 넣을 수 있겠지만 프로젝트 파일이 바로 갱신되지 않음, 모듈 반영이 꼬임, 에디터와 IDE 인식 상태 어긋남
이런 경우에는 .uproject 우클릭해서 Generate Project Files 다시 하거나 수동으로 하나 하나 필요한 모듈을 적용해줘야 함
요약
구현 수정은 Live Coding
구조 변경은 에디터 종료 후 빌드
언리얼 C++ 일반 C++과 다르게 UBT, UHT, 모듈, 리플렉션 시스템이 함께 엮여 있다. 그래서 빌드를 해야한다.
컴파일(Compile): 내가 작성한 소스 코드 → 컴퓨터가 이해할 수 있는 코드로 변환하는 과정
빌드(Build): 컴파일 + 필요한 파일들을 묶고 연결해서 실행 가능한 프로그램을 만드는 전체 과정
---2026-08-13 손튜터님 part 2----
컴파일한다 로드한다
의미가 비슷한 거 같지만 다르다
컴파일-C++ 코드나 블루프린트 그래프를 실행 가능한 형태로 반환
쿠킹-텍스처, 메시, 사운드. 블루프린트, 레벨 같은 컨텐츠를 대상 플랫폼에서 사용할 수 있는 형식으로 변환
패키징
컴파일된 코드와 쿠킹된 콘텐츠를 배포 가능한 파일 형태로 묶는 것
(배포하는 파일, 플랫폼에 맞게 변경해주세용~)
로드
게임 실행 중 필요한 에셋이나 레벨을 저장 장치에서 메모리로 가져오는 것
(동기화와 로드를 해 그러면 이제 SSD나 저장된 곳에서 가져오는 거겠죵?)
그렇기에 모든 것을 로드하면 느려지니까 비동기 동기가 있음
모든 것을 갖고 와서 보여주면 비효율적이겠죵?
언리얼 패키징 과정은 일반적으로 Buil, Cook, Stage, Package, Deploy, Run 단계로 구분
언리얼 C++ 코드는 UBT 빌드 과정을 관리
UCLASS, USTRUCT, UPROPERTY< UFUNCTION 같은 매크로가 들어간 코드는 바로 UHT가 먼저 분석
공식 문서에서는 언리얼 C++ 컴파일 다음 두 단계로 구분
1.UBT가 UHT를 실행
UHT가 언리얼 관련 헤더를 분석하고 UObject 시스템에 필요한 코드를 생성
(전처리 같은 느낌스~)
(왜 이렇게 복잡하게 해? 모든 코드를 포함시키면 성능 이슈 발생)
2.UBT가 C++ 컴파일을 실행(UBT-Unreal Build Tool임)
프로젝트 코드와 생성된 코드를 컴파일
즉 일반 C++ 컴파일러만 호출되는 게 아니라 언리얼 리플렉션 시스템을 위한 코드 생성 과정이 먼저 실행
UCLASS, UFunction 같은 것들은 리플렉션 이 필요하니까 그것을 찾아보고 없으면 아싸리 리플렉션을 빼먹음
블루프린트도 게임에서 사용되기 전에 컴파일 과정이 필요함(소화를 두번 해야함)
이러면 컴파일을 두 번해야함
블루프린트 에디터에서 컴파일 버튼을 누르면 노드 연결과 변수, 함수, 이벤트 그래프를 검사하고 실행 가능한 형태로 생성
ㅈㄴ 큰 성능 이슈가 생김
그래서 C++보다 느려
(블루프린트 자체가 코딩와 조합했을 때 성능이 좋다고 들었는데 이유를 PLEASE TELL ME WHY~)
블루프린트가 수정도 쉽고 직관적이라 편하지만 복잡한 연산에서는 속도가 너무 느려서 복잡한 연산은 C++로 하고 간단한건 블루프린트로 한다 들었습니다
간단한 건 블루프린트 계산은 C++
블프가 있으면 연계가 편해진다
배틀그라운드가 블루프린트로 만들어서 성능 이슈->C++ 바꿨다. 10배의 성능 향상되었다다는 추문이
C++은 렌더링으로 바로 바로 안 보여서 쬐끔 아쉽다~
딸각의 시대가 도래했다
또 컴파일하는 거
Material, Shader 컴파일 과정도 별도임
유저들은 겉 모습을 본다 그렇기에 저런 보이는 것도 중요하겠죵
언리얼 엔진은 캐시된 세이더 맵이 없는 머티리얼이 로되면 세이더 컴파일 요청을 큐(대기열 신호)에 넣을 수 있음
세이더 컴파일 작업은 Shader Compile Worker 프로세서를 통해 병렬로 처리 될 수 있음
C++ 컴파일은 다름
이는 메시나 텍스처를 로드하는 작업이 아니며, GPU에서 사용할 세이더 코드를 준비하는 작업임
AI 활용: 단어, 용어, 필요한 코드, 함수검색
리플렉션와 관련 된 오류가 많이 나는데 뭔지 알아보자
리플렉션이란?
프로그램이 자기 자신에 대한 정보를 알 수 있게 해주는 기능
일반 C++에서는 클래스 이름, 변수 목록, 함수 목록, 클래스 등등 정보를 런타임(실행 상태)에 자동으로 자세히 알기 어렵다
class PlayerData
{
public:
int HP;
float MoveSpeed;
};
사람은 이 코드를 보면 HP와 MoveSpeed를 알고 있다.
하지만 엔진이나 에디터가 이 클래스의 변수 목록을 자동으로 읽기, 디테일 패널, 블루프린트 접근하려면 별도의 정보가 필요함
이 역할을 누가 하냐?
그 역할을 언리얼에서는 리플렉션 시스템이 한다.
언리얼 공식 문서에서는 리플렉션 시스템이 엔진 및 에디터 기능을 제공하는 다양한 매크로로 클래스를 캡슐화한다고 설명함
그리고 UCLASS 매크로를 사용해서 UObject 파생 클래스를 UObject 처리 시스템이 인식하게 할 수 있음
리플렉션이 있으면 무엇을 할 수 있을까?
언리얼 엔진이 클래스, 변수, 함수 정보를 알 수 있음
C++ 함수를 에디터 디테일 패널에 표시ㅏ
C++ 함수를 블루프린트에서 호출
C++ 클래스를 블루프린트 부모 클래스로 사용
변수 값을 저장하고 불러오기
네트워크 복제 대상 변수 관리
런타임에 클래스 타입 확인
에디터에서 속성 검색과 수정
가비지 컬렉션이란??
더 이상 사용하지 않는 오브젝트를 엔진이 자동으로 정리하는 메모리 관리 시스템
쓰레기 봉지, 처리반
#pragma once
얘는 메크로가 아님, 전처리 지시문임!!!
매크로처럼 전처리 단계와 관련은 있음
역할은 헤더 파일이 한 번만 포함되도록 하는 거임
#include generated.h
언리얼 C++헤더는 include 순서도 중요함!!!!!!!!
#include "CoreMinimal.h"-언리얼 엔진이 돌아가야하는 최소한의 필수 파일들
#include "GameFramWork/Actor.h"
#include "TestActor.generated.h"
#include "TestActor.generated.h" 헤더 파일은 항상 마지막에 두어야함
공식 문서에도 generated.h 파일은 해당 헤더의 마지막 include 지시문이어야함
만약에 generated.h 아래에 다른 거 넣으면 안 됨
그 이유가 여러가지 있는데
GENERATED_BODY()와 관련이 있는데
일반적인 C++ 컴파일이 아닌 엔진 내부의 컴파일 과정에 의해서 제한됨
코드 변환 순서라던가
UCLASS()란?
얘도 매크로임
C++ 클래스를 언리얼 오브젝트 시스템에 등록하겠다라는 표시임
리플렉션 매크로와 관련이 되어있음
빛 쏜 걸 그대로 반사해서 언리얼 엔진에서 알아들을 수 있게 함
class FrontLineFps_API ATestActor:: public AActor
AActor 부모 ATestActor는 AActor 상속 받음
AActor 언리얼의 대표적인 UObject 계열 클래스
클래스가 언리얼의 에디터, 블루프린트, 리플렉션, 가비지 컬렉션 시스템과 연결이 되려면 엔진이 이 클래스를 알아야 알 수 있음
그 표시가 UCLASS()임!
얘는 UHT에 있음
공식 문서에도 언리얼 오브젝트의 베이스 클래스는 UOBject이고,
UObject에서 파생된 클래스에 UCLASS 매크로를 사용해서 UObject 처리 시스템이 클래스를 인식하게 할 수 있다고 말함
UCLASS() 안에는 옵션을 넣을 수 있음
클래스가 에디터나 블루프린트에서 어떻게 동작할지 정하는 옵션이 있음
옵션들을 클래스 지정자라고 부름
공식 문서에서도 클래스 지정자는 UCLASS를 선언할 때 클래스가 엔진과 에디터의 여러 부분에서 어떻게 동작할지 제어하는 키워드
이게 리플렉션 시스템
예를 들어 블루프린트에서 상속 가능한 클래스로 만들고 싶으면
중요!! Blueprintable
UCLASS(Blueprintable) 이렇게 해야한다!!!!!!!!!!
BlueprintType 블루프린트 변수 타입으로 사용할 수 있게 만들 수 있다.
NotBLuePrintable or NotBlueprintType 가능
청기 백기 가능
API이란? Application Program Interface
매크로입니다
(API = 서로 다른 프로그램이나 모듈이 기능·데이터를 주고받을 수 있게 해주는 연결 규칙)
프로젝트 이름이 Myproject라면 MYPROJEFCT_API 형태로 자동으로 생성
모듈 API 지정자입니다.
쉽게 말하면 이 클래스를 다른 모듈에서도 사용할 수 있도록 내보낼지 정하는 표시임
롤 API 정보를 어느정도 오픈했다~~
Public과 비슷한 느낌
다른 모듈에서 접근하여 사용할 수 있게 하는 거임
언리얼 엔진 프로젝트는 여러 모듈로 나뉘어 빌드함
예를 들어서
Core, CoreUObject Engine, UMG GameplayAbliities 같은 모듈이 있고 내 게임 프로젝트도 하나의 게임 모듈을 가지고 있음(UMG=Unreal Motion Graphic)
공식 문서에서도 모듈 API 지정자를 클래스와 함수를 다른 모듈에 노출시키는 방법임 또한 함수, 클래스, 데이터를 자기 모듈 DLL 파일에 public 으로 태그하는데 사용됨
자기 모듈 DLL파일 public 태그란?
(의문점)
접근 제한자 하는 것처럼 API가 다른 모듈에서 쓰게 해주는 표시처럼 거기 안에 있는 기능들을 public처럼 하는 거냐?
API 안 하면 언리얼 엔진 이 놈은 죽어도 모름
접근 지정자라고 생각하면 편함
DLL
DLL(Dynamic Link Library) = 프로그램이 실행될 때 필요한 코드나 기능을 따로 저장해 둔 라이브러리 파일
여러 프로그램이 가져다 쓸 수 있는 기능 모음 파일
언리얼에게 주는 파일
빌드를 통해 만들어주는 결과물
비유 모음집!!
대상 => 클래스 정보들
렌즈 => 리플렉션
필름 => 엔진에서 사용가능한 형태
사진 => 에디터 상에서 사용 가능
냉장고에서 재료 꺼내는거 -> 생성자 호출
재료 꺼내는 것에 순서가 있나? 아니오
고로 생성자 단계에서는 순서를 확정 할 수 없음
BeginPlay
냉장고에서 꺼내진 재료들이 준비되고 손질되기 시작(Begin)
DLL=>주먹밥!
오늘은 정말 이것저것 많이 배운 날이었습니다.
처음에는 단순히 로그를 출력하는 방법부터 시작했는데, 공부하다 보니 UE_LOG() 같은 기본적인 기능부터 액터의 생명주기, Tick()을 이용한 Transform 조정, DeltaTime을 활용한 프레임 독립적인 움직임, 그리고 마지막에는 언리얼 C++의 컴파일 구조와 리플렉션, UHT, UBT, 모듈, API까지 꽤 깊게 들어가게 되었습니다.
특히 오늘 공부하면서 느낀 것은 언리얼 C++은 그냥 일반적인 C++ 코드를 작성하는 것에서 끝나는 것이 아니라, 언리얼 엔진이 코드를 이해하고 활용할 수 있도록 만들어진 여러 시스템과 함께 동작한다는 것이었습니다.
UCLASS(), UPROPERTY(), UFUNCTION(), GENERATED_BODY() 같은 매크로가 처음에는 그냥 외워야 하는 문법처럼 보였는데, 오늘 리플렉션과 UHT에 대해 살펴보면서 "아, 언리얼이 C++ 코드를 엔진과 에디터에서 활용하기 위해 이런 장치를 사용하는구나"라는 느낌을 조금은 받을 수 있었습니다.
물론 아직 완벽하게 이해했다고 하기는 어렵습니다. 특히 리플렉션이 실제로 어떤 과정을 거쳐 생성되고 적용되는지, UHT가 생성한 코드가 실제 빌드 과정에서 어떻게 연결되는지, 모듈과 DLL이 정확히 어떤 관계를 가지고 있는지 같은 부분은 앞으로 더 공부해야 할 것 같습니다.
그래도 처음에는 그냥
"왜 언리얼 C++은 일반 C++이랑 뭔가 다르지?"
라는 생각이었다면, 이제는
"아, 언리얼만의 빌드 시스템과 리플렉션 시스템이 추가로 존재하기 때문이구나."
정도까지는 이해하게 된 것 같습니다.
그리고 개인적으로 오늘 가장 좋았던 부분은 다른 분들에게 제가 이해한 내용을 직접 설명해본 것이었습니다. 설명하려고 하니 제가 알고 있다고 생각했던 내용에서도 빈틈이 보이고, 다른 분들의 질문을 듣다 보니 혼자 공부할 때는 그냥 넘어갔을 부분도 다시 생각해보게 되었습니다.
결국 오늘 하루를 정리하면,
로그 출력 → 액터 생명주기 → Tick → Transform → DeltaTime → 언리얼 C++ 구조 → 리플렉션 → UHT/UBT → 빌드
까지 하나의 흐름으로 연결해서 공부한 날이라고 할 수 있겠습니다.
어제와 그저께 조금 쉬어간 만큼 오늘부터 다시 달려보려고 했는데, 일단 오늘은 꽤 열심히 달린 것 같습니다. 🐆
다만 문제는...
치타도 지금은 자야 합니다.
늙고 팔팔하지 못한 치타는 여기서 쓰러지도록 하겠습니다. 🫠💀
내일의 저는 오늘보다 조금 더 나아져 있기를 바라며,
오늘 공부 끝!
MADE BY YOONJAE JUNG