Dev./UE 언리얼 엔진

[TIL_250122] 로그 출력, Actor's Lifecycle, Transform

raindrovvv 2025. 1. 22. 19:38

💭회고

옵시디언을 활용한 스터디 루틴을 제대로 잡았다. 아래와 같은 형식으로 앞으로 쭉 정리해 보면 머리에 좀 더 잘 남고, 맵(그래프 뷰)이 자라나는 모습이 눈에 보이기에 성취감 또한 생길 것으로 예상한다. 

Unreal Engine에서 로깅하는 법, 액터의 라이프 사이클, 그리고 트랜스폼(Transform)에 대해 공부하였다. 트랜스폼 중에서도 회전(Rotation)에 대한 부분을 어쩌다 보니 중점적으로 공부하였다. 이 과정에서 오일러 각(Euler Angles)이라는 개념을 알게 되었고, 오일러 각의 한계점에 대해 배우며 쿼터니언(Quaternion)이라는 코드 스킬도 학습하였다.

오일러 각은 회전을 표현하는 직관적인 방식으로, 각 축(X, Y, Z)에 대한 회전값을 나타낸다. 하지만 짐벌락(Gimbal Lock)이라는 문제가 발생할 수 있어 특정 상황에서는 부정확한 결과를 초래할 수 있다. 이를 해결하기 위해 쿼터니언을 사용하면 더 안정적이고 유연한 회전 표현이 가능하다는 사실을 배웠다.
특히 비행기 시뮬레이터, 드론 시뮬레이터 등 3D 공간에서의 움직임을 구현할 때 매우 유용할 것으로 보인다. 앞으로 프로젝트에 적용해 보며 실습을 통해 더 깊이 익혀보고 싶다.

🗺️마인드맵

📒학습 내용

## Actor 클래스에 로그 추가하기

개발 중 로그를 통해 디버깅하는 습관이 중요
: <특정 함수가 잘 호출되는지, 변수에 어떤 값이 들어있는지 확인>


다만, 프로젝트 최적화 단계나 출시 단계가 되면 **불필요한 로그**는 제거하거나 로그 레벨을 낮춰야 한다.
왜? ➡️ 과도한 로그는 성능 저하를 일으키고, 민감 정보 노출 위험도 있기 때문에...

 

UE에서는 `UE_LOG` 매크로를 사용해 **Output Log** 창에 메시지(로그)를 남길 수 있다.

 

### Basic.

UE_LOG(LogTemp, Warning, TEXT("LLLLLLOOOOOGGGGGG"))

  • 로그 카테고리 : LogTemp, 카테고리의 이름을 달 수 있다.
  • 로그 수준 : Warning, 로그가 노란색 글씨로 강조되어 출력된다.
    • `Display`: 일반적인 실행 흐름이나 상태 확인 메시지 (흰색)
    • `Warning`: 예상치 못한 동작이나 잠재적인 문제 (노란색)
    • `Error`: 즉시 수정이 필요한 심각한 문제 (빨간색)
// .h : 헤더 파일

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"

UCLASS()
class SPARTAPROJECT_API AItem : public AActor
{
		GENERATED_BODY()
	
public:	
		AItem();

protected:
		USceneComponent* SceneRoot;
		UStaticMeshComponent* StaticMeshComp;
		
		// BeginPlay 함수를 다시 선언
		virtual void BeginPlay() override;
};
// .cpp : 소스 파일

#include "Item.h"

AItem::AItem()
{
		// ~중략~
}

void AItem::BeginPlay()
{
	Super::BeginPlay();
	// BeginPlay 호출 시점을 로그로 확인
	UE_LOG(LogTemp, Warning, TEXT("LLLLLLOOOOOGGGGGG"));
}

 

### 고유 카테고리 정의로 로그 출력하기.

프로젝트가 커지다 보면 모든 로그를 LogTemp로 보기는 어렵다. 
따라서 `DEFINE_LOG_CATEGORY`를 사용해 고유 카테고리를 만들어 사용하자.
보통은 이러한 로그 카테고리를 따로 관리하는 헤더를 작성하여 사용한다.

 

1. DECLARE_LOG_CATEGORY_EXTERN(LogTeam, Warning, All);

: 헤더 파일에 선언 (카테고리 이름 설정, 로그 심각도 설정, 활성화)

2. DEFINE_LOG_CATEGORY(LogTeam);

: cpp 파일 상단에 구현

3. `BeginPlay()` 함수에 코드 입력

void AItem::BeginPlay()  
{  
    Super::BeginPlay();  
    UE_LOG(LogTemp, Error, TEXT("LLLLLLooooGGGGGGG"));  
    UE_LOG(LogTeam, Warning, TEXT("!!!!!!!!!!!!!!"));  
}

 

### 출력💻


## UE Actor의 라이프 사이클 이해하기

액터는 언제든지 생성(spawn) 될 수 있고, 필요가 없어지면 사라질 수 도 있다. 이를 액터의 라이프 사이클이라고 한다.

1. 생성자 (Constructor)
2. `PostInitializeComponents()`
3. `BeginPlay()`
4. `Tick(float DeltaTime)`
5. `Destroyed()`
6. `EndPlay(const EEndPlayReason::Type EndPlayReason)`

[요약]
- 초기화와 설정: Constructor → PostInitializeComponents.
- 실행과 상호작용: BeginPlay → Tick.
- 정리와 종료: Destroyed → EndPlay.
// .h (선언)

#pragma once  
  
#include "CoreMinimal.h"  
#include "GameFramework/Actor.h"  
#include "Item.generated.h"  
  
DECLARE_LOG_CATEGORY_EXTERN(LogTeam, Warning, All); // (카테고리 이름, 로그 심각도 설정, 활성화)  
  
UCLASS()  
class START_API AItem : public AActor  
{  
    GENERATED_BODY()  
    public:   
    AItem();  
  
protected:  
    USceneComponent* SceneRoot; // 멤버 변수를 포인터 형태로 선언.  
    UStaticMeshComponent* StaticMesh;  
    UAudioComponent* AudioComponent;  
  
    // 액터의 라이프 사이클 : 생성(생성자) → 초기화(PostInitializeComponents()) → 월드 배치(BeginPlay) → 실행(Tick) → 제거(EndPlay)   
	virtual void PostInitializeComponents() override;  
     virtual void BeginPlay() override;  
     virtual void Tick(float DeltaTime) override;
     virtual void Destroyed() override;
     virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;  
};
// .cpp (구현)

#include "Item.h"  
#include "Components/AudioComponent.h"  
#include "Sound/SoundBase.h"  
  
DEFINE_LOG_CATEGORY(LogTeam);  
  
AItem::AItem()  
{  
    SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot")); // 컴포넌트를 생성  
    SetRootComponent(SceneRoot); // SceneRoot를 루트 컴포넌트로 설정  
  
    StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh")); // 스태틱 메쉬 생성  
    StaticMesh->SetupAttachment(SceneRoot); // StaticMesh를 SceneRoot에 연결  
  
    AudioComponent = CreateDefaultSubobject<UAudioComponent>(TEXT("AudioComponent")); // 오디오 컴포넌트 생성  
    AudioComponent->SetupAttachment(SceneRoot); // AudioComponent를 SceneRoot에 연결  
  
    // StaticMesh 설정  
    static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/SM_Star_B.SM_Star_B"));  
    if (MeshAsset.Succeeded())  
    {       StaticMesh->SetStaticMesh(MeshAsset.Object);  
    }    // Material 설정  
    static ConstructorHelpers::FObjectFinder<UMaterialInstance> MaterialAsset(TEXT("/Game/Resources/Materials/M_Mango.M_Mango"));  
    if (MaterialAsset.Succeeded())  
    {       StaticMesh->SetMaterial(0, MaterialAsset.Object);  
    }    // Audio 설정  
    static ConstructorHelpers::FObjectFinder<USoundBase> Audio(TEXT("/Game/Resources/Audio/Crate_Break.Crate_Break"));  
    if (Audio.Succeeded())  
    {       AudioComponent->SetSound(Audio.Object);  
    }    UE_LOG(LogTeam, Warning, TEXT("%s, 생성자"), *GetName());  
}  
  
void AItem::PostInitializeComponents()  
{  
    Super::PostInitializeComponents(); // 부모 호출 반드시 해줘야 함.  
    UE_LOG(LogTeam, Warning, TEXT("%s, PostInitializeComponents"), *GetName());  
}  
  
void AItem::BeginPlay()  
{  
    Super::BeginPlay();  
    UE_LOG(LogTeam, Warning, TEXT("%s, BeginPlay"), *GetName());  
}  
  
void AItem::Tick(float DeltaTime)  
{  
    Super::Tick(DeltaTime);  
    // 매 프레임마다 동작되어 로그 넣으면 ㄷㄷ;;  
}  
  
void AItem::Destroyed()  
{  
    UE_LOG(LogTeam, Warning, TEXT("%s, Destroyed"), *GetName()) // 제거 코드는 예외적으로 로그를 앞에 표시  
    Super::Destroyed();  
}  
  
void AItem::EndPlay(const EEndPlayReason::Type EndPlayReason)  
{  
    UE_LOG(LogTeam, Warning, TEXT("%s, EndPlay"), *GetName())  
    Super::EndPlay(EndPlayReason);  
}
  • UE_LOG(LogTeam, Warning, TEXT("%s, 생성자"), *GetName());
    • *GetName : 현재 액터 이름을 반환.
  • tick() : 매 프레임 호출되므로 디버깅 시 필요할 때만 제한(조건문)하여 사용.
  • `EndPlay` vs. `Destroyed`
    • 종합적인 정리 작업: `EndPlay` 사용.
    • 특정 파괴 작업: `Destroyed` 사용.
    • 플레이 상태에서 생성된 액터를 삭제(Destroy)했을 때, Destroy 로그와 EndPlay 로그가 순서대로 출력.

액터를 생성했을 때
액터를 삭제했을 때
최종 구현


## Transform

**Transform**은 액터의 위치, 회전, 크기 정보를 나타내는 속성 집합

 

1. 위치

`FVector(X, Y, Z)`
- 예시 : `FVector Location = FVector(100.0f, 200.0f, 300.0f);`
➡️ 월드의 기준점으로부터 x축으로 100, y축으로 200, z축으로 300 떨어진 위치

 

2. 회전

`FRotator(Pitch, Yaw, Roll)`

- 예시 : `FRotator Rotation = FRotator(30.0f, 45.0f, 90.0f);`

➡️ Pitch(30도), Yaw(45도), Roll(90도)로 회전된 상태.

**Roll** (X축): 좌우로 기울어지는 회전.
**Pitch** (Y축): 앞뒤 방향으로 기울어지는 회전.
**Yaw** (Z축): 좌우 방향으로 회전.

### Pitch, Yaw, Roll의 한계점 ➡ Gimbal Lock

더보기

### **짐벌 락(Gimbal Lock)이란?**

3D 회전을 오일러 각(`Pitch`, `Yaw`, `Roll`)으로 표현할 때 발생할 수 있는 문제.

오일러 각 계산으로는 직선 방향으로 회전하지 못했다
오일러 각 계산으로는 직선 방향으로 회전하지 못했다

 

### 짐벌 락이 발생하는 이유
핵심은 **회전 축의 겹침**으로 인해 **회전의 자유도(Freedom of Rotation)가 줄어드는 상황이다.
회전을 할 때에는 이 세 축이 종속적일 수 밖에 없는데, z축을 돌리는 순간 x,y축은 함께 돌기 때문이다.

 

#### **문제 상황**
- `Pitch`가 90도 또는 -90도에 도달하면, `Yaw`(좌우 회전)과 `Roll`(좌우 기울기)의 축이 동일 선상에 놓이게 된다.
- 이 상태에서는 **Yaw와 Roll을 구분할 수 없게 되고**, 어떤 축에서 회전하든 동일한 결과가 나오게 된다.
- 3D 공간에서의 3개의 회전 자유도를 가져야 하는데, 짐벌 락이 발생하면 자유도가 3→2로 줄어든다.
- 결과적으로, 원하는 회전을 더 이상 제대로 표현할 수 없게 되는 것...

➡ 의도치 않은 방향으로 회전하거나 무시되는 경우가 발생한다. 비행기, 카메라 제어 등에서 이 문제가 발생하면 정확한 회전 구현이 어려워진다.

 

#### **해결 방법**
쿼터니언(Quaternion) 사용
- 쿼터니언은 4차원 복소수로 3D 회전을 표현.
- 장점:
    - 짐벌 락을 방지하며, 회전을 부드럽고 연속적으로 표현할 수 있다.
    - 축이 겹치지 않고, 모든 회전 방향에서 동일한 자유도 유지!
- 사용 예제 :    

FQuat MyRotation = FQuat(FRotator(90.0f, 0.0f, 0.0f));

 

1.액터_회전_설정

void AMyActor::BeginPlay()
{
    Super::BeginPlay();
    
    // Pitch 90도, Yaw 0도, Roll 0도로 회전 설정
    FRotator NewRotation = FRotator(90.0f, 0.0f, 0.0f);
    SetActorRotation(NewRotation);
    
    // 위의 동작을 쿼터니언으로 변환
    FQuat NewQuatRotation = FQuat(NewRotation);
    SetActorRotation(NewQuatRotation);
}

 

2.액터_회전_애니메이션

void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    
    // 매 프레임마다 Yaw를 1도씩 증가
    AddActorLocalRotation(FRotator(0, 1.0f, 0));
    
    // 또는 쿼터니언을 사용한 부드러운 회전
    FQuat CurrentRotation = GetActorQuat();
    
    FQuat TargetRotation = FQuat(FVector::UpVector, FMath::DegreesToRadians(1.0f));
    
    FQuat NewRotation = FQuat::Slerp(CurrentRotation, TargetRotation * CurrentRotation, DeltaTime * 0.5f);
    SetActorRotation(NewRotation);
}

 

 

3. 스케일

  • `FVector(X, Y, Z)`
    - 예시 : FVector Scale = FVector(2.0f, 1.0f, 3.0f);
    ➡️ X축으로 2배, Y축은 1배, Z축은 3배 크기.

#### 코드

void AItem::BeginPlay()  
{  
    Super::BeginPlay();  
    SetActorLocation(FVector(50, 50, 100));  
    SetActorRotation(FRotator(90, 180, 180)); // Pitch, Yaw, Roll  
}
  • 언리얼 엔진에서 `FVector`와 `FRotator`의 구성 요소가 기본적으로 float 타입으로 취급되기 때문에, 소수점이 없는 정수 값을 사용해도 문제가 없다.

🟣오늘의 옵시디언 현황