언리얼 에디터의 로그 창에서 언리얼 에디터가 출력한 로그들을 볼 수 있다.
C++코드로도 언리얼 에디터에 로그를 출력할 수 있다.
UE_LOG(LogTemp, Warning, TEXT("My Log"));
언리얼 내의 UE_LOG함수를 통해 로그를 출력할 수 있다. 첫 번째 매개변수는 로그의 카테고리를 의미한다.
두 번재 매개변수 Warning은 이 로그 메시지를 경고 타입으로 출력해달라는 의미로 Warning를 입력하고 함수를 호출하면 언리얼 내에서 로그가 노란색으로 뜨게 된다. 마찬가지고 Display는 하얀색 Error는 빨간색으로 뜨게 된다.
세 번재 매개변수는 출력하고 싶은 텍스트를 입력한다. 스트링 값인 경우 TEXT로 감싸줘야 한다.

로그 맨 앞 쪽을 보면 로그 카테고리가 같이 출력되는 걸 볼 수 있다.

로그의 타입이 Warning이면 로그의 색깔이 노란색이다.


로그 출력 창을 우클릭하여 Clear Log를 선택하면 로그창이 깨끗하게 비워진다.
#include "Item.h"
AItem::AItem()
{
...
UE_LOG(LogTemp, Warning, TEXT("My Log"));
}
내가 만든 아이템 클래스의 생성자에 로그 출력 코드를 작성해보자.

언리얼 에디터에 내가 만든 클래스의 인스턴스를 뷰포트에 추가해보면

로그가 출력된 모습을 확인할 수 있다.

로그의 카테고리를 선택하여 출력할 로그의 카테고리를 따로 선정할 수 있고 필터를 선택하고 나면 선택한 로그 카테고리만 보이게 된다.
C++ 코드 내에서 로그 카테고리를 따로 만들어 줄 수 있다.
// *.h
...
DECLARE_LOG_CATEGORY_EXTERN(LogSparta, Warning, All);
...
헤더 파일에서 위의 코드를 입력하면 코드 카테고리가 생성된다.
첫 번째 매개변수 LogSparta는 로그 카테고리의 이름이다.
두 번째 매개변수 Warning은 로그의 심각도는 Warning이상의 로그만 출력하겠다는 의미를 가진다.
세 번째 매개변수 All은 필요하면 모든 로그를 활성화할 수 있다고 가능성을 열어두는 것이다.
// *.cpp
DEFINE_LOG_CATEGORY(LogSparta);
...
헤더파일에 카테고리를 선언해줬으면 cpp파일에도 해당 카테고리를 사용할 것이라는 표시를 해주어야 하므로 cpp파일에도 위와 같은 코드를 작성해주어야 한다.
보통 로그 카테고리를 클래스의 헤더파일에 작성하지 않고 카테고리 관련 파일을 따로 만들어서 관리하는 것이 일반적이다.
액터는 언제든지 스폰(맵에 배치)될 수 있고 필요가 없어지면 언제든지 삭제될 수 있다.
액터가 스폰되고 다시 삭제되는 주기를 라이프 사이클이라고 부른다.
액터에 같이 붙여놓은 리소스나 컴포넌트들이 액터가 삭제될 때 동시에 삭제되어야 하는데 이런걸 관리하기 위해서 라이프 사이클을 이해하는게 중요하다.
라이프 사이클 함수
생성자 : 오브젝트가 메모리에 생기면서 딱 한 번 호출
PostInitializeComponents() : 액터에 붙은 모든 컴포넌트들이 호출과 초기화가 된 직후에 호출이 되는 함수다. 컴포넌트끼리 데이터를 주고 받거나, 상호작용을 해야하는 상황일 때 이 함수에서 처리해주면 좋다.
BeginPlay() : 배치(Spawn)직후에 호출되는 함수
Tick(float DeltaTime) : 매 프레임마다 호출되는 함수로 매 프레임마다 호출이 되기 때문에 무거운 로직이 담기면 프로그램의 성능을 떨어뜨리기도 한다.
Destroyed : 객체가 사라지기 직전에 호출되는 함수.데이터들을 강제로 파기한다. 리소스들을 정리해주면 좋다.
EndPlay() : 파괴(Destroyed), 게임 종료, 레벨 전환 시에 호출되는 함수, 메모리나 파일들을 정리한다. 만약 Destroyed가 호출되면 그 다음 EndPlay가 호출되고 게임을 끄거나 레벨이 전환되면 Destroyed가 호출되지 않고 EndPlay가 바로 호출되게 된다.
이제 이 라이프 사이클 함수에 로그 출력문을 작성하여 어떤식으로 작동하는지 살펴보자.
// Item.h
class SPARTAPROJECT_API AItem : public AActor
{
GENERATED_BODY()
public:
AItem();
protected:
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;
};
먼저 부모 클래스에서 이미 선언되어 있는 함수들을 오버라이딩하여 멤버함수로 선언해준다.
// Item.cpp
AItem::AItem()
{
UE_LOG(LogSparta, Warning, TEXT("%s Constructor"), *GetName());
}
void AItem::PostInitializeComponents()
{
Super::PostInitializeComponents();
UE_LOG(LogSparta, Warning, TEXT("%s PostInitializeComponents"), *GetName());
}
void AItem::BeginPlay()
{
Super::BeginPlay();
UE_LOG(LogSparta, Warning, TEXT("%s BeginPlay"), *GetName());
}
void AItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AItem::Destroyed()
{
UE_LOG(LogSparta, Warning, TEXT("%s Destroyed"), *GetName());
Super::Destroyed();
}
void AItem::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
UE_LOG(LogSparta, Warning, TEXT("%s EndPlay"), *GetName());
Super::EndPlay(EndPlayReason);
}
*GetName() 현재 아이템 클래스의 이름을 가져온다.
이렇게 각 라이프 사이클 함수마다 로그를 출력하게 만든 다음 빌드하여 언리얼 에디터를 실행하여 각 라이프 사이클이 어떤 식으로 작동하는지 살펴보자.
*GetName() : 현재 아이템 클래스의 이름을 가져온다.
오버라이딩된 함수들의 대부분은 Super를 통해 부모 클래스의 함수를 호출해주어야 정상적으로 작동한다.
Destroyed함수와 EndPlay함수에서 로그 출력문이 부모 클래스의 함수 호출보다 선행되어 실행되는 이유는 로그 출력문이 후순으로 오게되면 로그를 출력하기 전에 객체 삭제가 먼저 이루어질 수 있기 때문이다.


맵 내에 인스턴스를 생성하자 생성자가 호출되고


프로그램을 실행해보니 PostInitializeComponent와 BeginPlay가 실행되는 모습을 볼 수 있다.


프로그램을 종료하자 EndPlay가 실행된 모습


이제 Item의 인스턴스를 삭제하자 Destroyed가 실행된 모습을 볼 수 있다.
트랜스폼이란 액터의 크기, 회전, 좌표의 기능을 의미한다.
액터의 크기나 현재의 액터의 각도, 액터의 위치를 바꾸고 싶을 땐 액터의 트랜스폼 값을 바꾸면 된다.
액터의 위치를 바꿀 땐 액터의 루트 컴포넌트로 설정한 컴포넌트의 트랜스폼 값을 바꿔주면 그 자손 컴포넌트들의 트랜스폼 값이 전부 바뀌게 된다.
class SPARTAPROJECT_API AItem : public AActor
{
GENERATED_BODY()
public:
AItem();
protected:
USceneComponent* SceneRoot; // 씬 루트 컴포넌트
UStaticMeshComponent* StaticMeshComp; // 스태틱 메쉬 컴포넌트
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
};
우선 Item클래스에서 BeginPlay와 Tick함수만을 사용할 것이기 때문에 이외의 함수들은 전부 삭제해준다.
void AItem::BeginPlay()
{
Super::BeginPlay();
SetActorLocation(FVector(300.0f, 200.0f, 100.0f));
}
먼저 SetActorLocation은 액터의 월드 좌표를 바꿔주는 기능을 한다. 좌표를 적을 땐 FVector라는 구조체를 사용하여 좌표를 설정해주어야 한다. 순서대로 x, y, z좌표를 의미한다.
SetActorLocation(FVector(300.0f, 200.0f, 100.0f));는 해당 액터의 월드 좌표를 300, 200, 100으로 이동시킨다는 의미이다.
void AItem::BeginPlay()
{
Super::BeginPlay();
SetActorRotation(FRotator(0.0f, 90.0f, 0.0f));
}
SetActorRotation는 해당 액터를 회전시킨다는 의미이다. 액터를 회전시킬 때 회전량을 결정할 때에는 FRotator구조체를 사용한다. FRotator의 입력 값은 순서대로 Pitch(y), Yaw(z), Roll(x)를 의미한다. x, y, z순서가 아니기 때문에 이 점을 유념해야 한다.
SetActorRotation(FRotator(0.0f, 90.0f, 0.0f));는 해당 액터를 z축으로 90도 회전시킨다는 의미이다.
void AItem::BeginPlay()
{
Super::BeginPlay();
SetActorScale3D(FVector(2.0f, 1.0f, 1.0f));
}
SetActorScale3D는 해당 액터의 크기를 변환시키는 기능을 한다. FVector구조체를 사용한다.
SetActorScale3D(FVector(2.0f, 1.0f, 1.0f));는 액터의 크기를 x축으로 2배 키운다는 의미를 가진다.
액터를 x,y,z축의 방향으로 키우고 싶을 때 즉 액터 전체의 크기를 키우고 싶을 때는
SetActorScale3D(FVector(2.0f)) FVector구조체에 수를 하나만 넣으면 넣은 수의 배만큼 크기를 키워준다.
void AItem::BeginPlay()
{
Super::BeginPlay();
SetActorLocation(FVector(300.0f, 200.0f, 100.0f));
SetActorRotation(FRotator(0.0f, 90.0f, 0.0f));
SetActorScale3D(FVector(2.0f, 1.0f, 1.0f));
}
이제 이 코드를 작성하고 빌드한 다음 언리얼 에디터를 켜서 확인해보자.


프로그램을 실행하면 실행하자마자 액터의 크기와 각도, 위치가 바뀐 것을 볼 수 있다.

언리얼 에디터에서 디테일 바의 트랜스폼 항목을 확인해보면 코드에서 수정한 값대로 트랜스폼의 값이 변경된 것을 확인할 수 있다.
void AItem::BeginPlay()
{
Super::BeginPlay();
SetActorScale3D(FVector(2.0f));
FVector NewLocation(300.0f, 200.0f, 100.0f);
FRotator NewRotation(0.0f, 90.0f, 0.0f);
FVector NewScale(2.0f, 1.0f, 1.0f);
FTransform NewTransform(NewRotation, NewLocation, NewScale);
SetActorTransform(NewTransform);
}
SetActorTransform이라는 함수로 Transform자체를 바꿔버릴 수도 있는데, 회전값이 들어있는 FRotator값, 새로운 좌표가 들어있는 FVecotr값, 새로운 크기 값이 들어있는 FVector값을 차례대로 넣어서 함수를 실행시키면 트랜스폼 자체의 값을 한 번에 바꿔준다.
class SPARTAPROJECT_API AItem : public AActor
{
GENERATED_BODY()
public:
AItem();
protected:
USceneComponent* SceneRoot; // 씬 루트 컴포넌트
UStaticMeshComponent* StaticMeshComp; // 스태틱 메쉬 컴포넌트
float RotationSpeed;
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
};
일단 일정한 회전량을 설정하기 위해서 Item헤더파일에서 멤버 변수 RotationSpeed를 선언한다.
AItem::AItem()
{
...
RotationSpeed = 90.0f;
}
그런 다음 Item의 생성자에서 RotationSpeed의 값을 설정한다.
이제 Tick함수에서 매 프레임마다 액터를 회전시켜주면 된다.
void AItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
AddActorLocalRotation(FRotator(0.0f, RotationSpeed, 0.0f));
}
}
AddActorLocalRotation은 안에 FRotator구조체를 넣었을 때 넣은 구조체 만큼 해당 액터의 Rotation값에 더해준다.
언뜻 봤을 때 이 Tick함수를 작동시켜보면 액터가 잘 회전될 것이라고 생각할 수 있지만 Tick함수가 프레임마다 한 번 씩 작동한다는 것을 생각해야 한다.
문제는 모든 사용자의 컴퓨터마다 사양이 다 다를 것이고 프로그램을 켰을 때 나오는 프레임수가 각자 다르다.
그런데 Tick함수에서 프레임마다 회전시켜버리면 유저의 컴퓨터 사양별로 회전하는 속도가 천차만별로 나올 수 있는 것이다.
이 때 사용하는 것이 Tick함수의 DeltaTime이다. DeltaTime은 프레임 당 시간을 의미한다.
만약 1초에 120프레임이 나오는 컴퓨터에선 DeltaTime = 1초 / 120이 되고
1초에 30프레임이 나오는 컴퓨터에선 DeltaTime = 1초 / 30이 된다.
액터를 1초에 90도씩 사양에 따라 다르지 않고 동일하게 회전을 시키려면 RotationSpeed에 1초당 프레임의 비율을 곱해줘서 프레임당 회전하는 비율을 설정해주어야 하는데 RotationSpeed * DeltaTime을 해주면 된다.
void AItem::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
AddActorLocalRotation(FRotator(0.0f, RotationSpeed * DeltaTime, 0.0f));
}
}
그런 다음 생성자에서 Tick함수를 사용하겠다는 표시를 해주어야 한다.
AItem::AItem()
{
...
PrimaryActorTick.bCanEverTick = true;
RotationSpeed = 90.0f;
}
PrimaryActorTick.bCanEverTick을 true로 해놓으면 Tick 함수를 사용하겠다는 의미가 되고 false로 해놓으면 Tick함수를 사용하지 않겠다는 의미가 된다.
Tick함수의 사용 유무를 따로 설정해놓는 이유는 Tick함수의 실행 유무에 따라서 Tick함수에 담긴 로직에 따라서 프로그램의 실제 실행 속도에 영향을 주기 때문이다.

코드를 빌드하고 언리얼 에디터를 실행시켜 액터가 돌아가는지 한 번 확인해보자.
블루프린트로 액터를 회전시켰을 땐 생각보다 좀 복잡하다고 느꼈던 부분이 있었는데 코드를 사용해서 회전시키니까 오히려 더 직관적이라는 느낌을 받았다.
블루프린트를 사용하면 모든 로직이 눈에 보이긴 하지만 익숙하지 않아서인지 봐도 제대로 이해가 되지 않았었는데 코드로 보니까 수식이 눈에 보이고 어떤 함수가 어떤 값을 뱉는지 더 확실하게 보이는 느낌을 받았다.