본문 바로가기

언리얼C++게임개발/03.움직이는 액터의 제작

[이득우 언리얼 C++] 로깅 환경의 설정

로깅 환경의 설정

출력 로그 윈도우

언리얼 엔진은 로그(Log) 환경을 위해 UE_LOG 라는 매크로를 제공하고 있다. 표준 카테고리를 사용하면 아무런 선언없이 사용할 수 있다. 그냥 표준을 쓰는걸 권장드린다. 괜히 커스텀 카테고리 만들라고 이런걸로 시간 없애지 말자.

UE_LOG(카테고리, 로깅수준, 형식 문자열, 인자... )

Fountain.cpp BeginPlay()에 다음과 같이 로그를 넣으면 플레이시 콘솔에서 로그를 볼 수 있다.

#include "Fountain.h"
//DEFINE_LOG_CATEGORY(ArenaBattle);

// Called when the game starts or when spawned
void AFountain::BeginPlay()
{
    Super::BeginPlay();
    UE_LOG(LogTem, Warning, TEXT("Actor Name : %s, ID : %d, Location X : %.3f"), *GetName(), ID, GetActorLocation().X);
}

해당 매크로를 사용하여 생성된 로그는 Saved\Logs 폴더의 Log 파일과 출력로그 윈도우에서 확인할 수 있다.

로그 카테고리

로그에는 카테고리를 지정할 수 있다. 로그를 확인해보면 로그가 "카테고리:" 으로 시작하는것을 확인할 수 있다.

로깅 수준

로깅 수준은 크게 메시지(Log), 경고(Warning), 에러(Error) 세가지로 나뉜다. 각각 흰색, 노란색, 붉은색으로 출력 로그 윈도우에 표시된다. 색상은 편집 > 에디터 개인설정 > 외형 메뉴에서 변경할 수 있다.

로그 필터

출력 로그 윈도우에는 필터 기능이 있어 원하는 로깅 수준과 카테고리에 해당하는 로그만 출력하도록 설정할 수 있다.

형식 문자열

로그 매크로는 printf 와 같은 형식 문자열 기능을 지원한다. 문자열을 정의할 때는 TEXT 매크로를 사용하고, FString 변수로 사용할 경우 * 연산자를 앞에 지정해줘야 한다.

로깅을 위한 공용 매크로 설정

실습을 위한 ArenaBattle이라는 로그 카테고리를 새롭게 정의 해보자. ArenaBattle.h 에 로그 매크로를 선언하고 모듈의 모든 헤더들이 해당 헤더를 참조하도록 구성하자는데 언리얼 프로젝트 생성시 자동으로 생성되는 ArenaBattle.h를 Include하면 에러가 난다. 아직 이유는 모르겠다 그래서 그냥 언리얼에디터에서 부모를 None으로 ABLog.h를 만들었다. 앞으로 ABLog.h나 ArenaBattle.h는 동일하다고 생각해주시면 좋겠다.

먼저 ArenaBattle.h  로그 카테고리를 새롭게 선언한다.

// 이건 다른 모듈이 ArenaBattle.h 를 상속할 때, EngineMinimal.h 가 포함되도록 만들기 위해서임
#define "EngineMinimal.h"
DECLARE_LOG_CATEGORY_EXTERN(ArenaBattle, Log, All);

#define ABLOG_CALLINFO (FString(__FUNCTION__) + TEXT("(") + FString::FromInt(__LINE__) + TEXT(")"))
#define ABLOG_S(Verbosity) UE_LOG(ArenaBattle, Verbosity, TEXT("%s"), *ABLOG_CALLINFO)
#define ABLOG(Verbosity, Format, ...) UE_LOG(ArenaBattle, Verbosity, TEXT("%s %s"), *ABLOG_CALLINFO, *FString::Printf(Format, ##__VA_ARGS__))

전 ArenaBattle.h를 인크루드가 잘안되 ABLog.h를 다음과 같이 선언하였다. CATEGORY_EXTERN도 지정하지 않고 LogTemp를 사용하였다.

#pragma once

#include "EngineMinimal.h"

//DECLARE_LOG_CATEGORY_EXTERN(ArenaBattle, Log, All);
#define ABLOG_CALLINFO (FString(__FUNCTION__) + TEXT("(") + FString::FromInt(__LINE__) + TEXT(")"))
#define ABLOG_S(Verbosity) UE_LOG(LogTemp, Verbosity, TEXT("%s"), *ABLOG_CALLINFO)
#define ABLOG(Verbosity, Format, ...) UE_LOG(LogTemp, Verbosity, TEXT("%s %s"), *ABLOG_CALLINFO, *FString::Printf(Format, ##__VA_ARGS__))
//DEFINE_LOG_CATEGORY(ArenaBattle);  프로젝트명.cpp에 선언한다.

 

두 종류의 로그 매크로를 추가로 선언했는데, 각 기능은 다음과 같다.

  • ABLOG_S : 코드가 들어 있는 파일 이름과 함수, 그리고 라인 정보를 추가한다.
  • ABLOG : ABLOG_S 정보에 형식 문자열로 추가 정보를 지정해 로그를 남긴다.

 

그 다음, ArenaBattle.cpp 에 해당 카테고리를 정의하라는데. 전 하지 않았습니다.

DEFINE_LOG_CATEGORY(ArenaBattle);

 

그 후, Fountain.h 의 #define "EngineMinimal.h" 를 "ArenaBattle.h" 로 변경한다.

마지막으로, Fountain.cpp 의 BeginPlay 부분에서 로그 매크로를 실행시킨다.

// Called when the game starts or when spawned
void AFountain::BeginPlay()
{
	Super::BeginPlay();

	ABLOG_S(Warning);
	ABLOG(Warning, TEXT("Actor Name : %s, ID : %d, Location X : %.3f"), *GetName(), ID, GetActorLocation().X);
}

컴파일 후 플레이 버튼을 누르면, Fountain 액터가 생성될 때 적절한 로그가 출력되게 된다.

 

이후 실행시 배치된 분수대 만큼 로그가 출력됨을 확인하였다.

로그 출력 결과는 다음과 같다

 

어설션

솔직히 이부분은 책에 있지만 그냥 없어도 사용하는데 문제없는듯 하다 머리 아프신 분들은 나중에 보시길

프로그래밍에서 어설션(Assertion)은 반드시 확인하고 넘어가야하는 점검 코드를 의미한다. 원활한 디버깅을 위해 디버깅 기호를 추가로 설치한다. 런처를 열고 엔진 버전의 옵션을 선택하여 디버깅을 위한 편집기 기호를 선택하여 설치하였다.

Assert

assert는 코드의 검증 도구이다. 언리얼 엔진에서는 검증을 위한 매크로 시리즈가 제공된다. 메크로를 확인하려면 아래의 파일에서 찾아볼 수 있다

/UE4/Engine/Source/Runtime/Core/Public/Misc/AssertionMacros.h

런타임 어서트 매크로는 다음과 같은 3가지 종류가 있다.

  • 실행 중지 (DO_CHECK)
  • 디버그 빌드에서 실행 중지 (DO_GUARD_SLOW)
  • 실행 중지하지 않고 오류 보고 (DO_CHECK)

check(표현식)

표현식의 결과가 false인 경우 실행을 중지한다. 가장 간단한 형태의 매크로

check(Mesh != nullptr)
check(bInitialized);

checkf(표현식, ...)

check와 동일하지만 표현식의 결과가 false인 경우 추가정보를 출력할 수 있다.

checkf(WasDestroyed, TEXT( "Failed to destroy Actor %s (%s)"), *Actor->GetClass()->GetName(), *Actor->GetActorLabel());

verify(표현식)

DO_CHECK가 켜져있으면 check와 동일하고, DO_CHECK가 꺼져있어도 실행된다. 검증하는데 사용 된다.

verify((Mesh = GetRenderMesh()) != nullptr);

verifyf(표현식, ...)

checkf처럼 실행을 중지하면서 디버그 메시지를 출력한다.

verifyf(Module_libeay32, TEXT("Failed to load DLL %s"), *DLLToLoad);

checkCode(표현식)

중괄호 범위안의 표현식을 실행할수 있다. (checkf의 확장버전같은 느낌)
DO_CHECK가 꺼져 있으면 컴파일에서 제외된다.

checkCode( if( Object->HasAnyFlags( RF_PendingKill ) ) { UE_LOG(LogUObjectGlobals, Fatal, TEXT("Object %s is part of root set though has been marked RF_PendingKill!"), *Object->GetFullName() ); } );

checkNoEntry()

절때 실행될 일 없는 코드 경로에 표시

switch (MyEnum)
{
    case MyEnumValue:
        break;
    default:
        checkNoEntry();
        break;
}

checkNoReentry(), checkNoRecursion()

함수의 재진입을 방지하기 위한 용도

void NoReentry()
{
    checkNoReentry();
}
int32 Recurse(int32 A, int32 B)
{
    checkNoRecursion();
    return Recurse(A - 1, B - 1);
}

unimplemented()

함수내 구현이 없어서 특정 클래스에서 호출이 안되거나 오버라이딩 해야하는 함수를 표시하는데 사용

class FNoImpl
{
    virtual void DoStuff()
    {
        // You must override this
        unimplemented();
    }
};

ensure(표현식)

표현식 검증에 실패하면 콜스택을 생성

if (ensure( InObject != NULL ))
{
    InObject->Modify();
}

ensureMsg(표현식, 메시지)

ensure에 메시지를 추가한 버전

ensureMsg(Node != nullptr, TEXT("Node is invalid"));

ensureMsgf(표현식, 메시지, ...)

checkf나 verifyf처럼 컨텍스트 정보를 포함해서 메시지를 출력할 수 있도록 함

if (ensureMsgf(!bModal, TEXT("Could not create dialog because modal is set to (%d)"), int32(bModal)))
{
    ...
}

참고

https://docs.unrealengine.com/4.27/ko/ProgrammingAndScripting/ProgrammingWithCPP/Assertions/

액터의 주요 이벤트 함수

PostInitializeComponents 함수는 액터에 속한 모든 컴포넌트의 세팅이 완료되었을때 호출된다. 액터가 게임에 참여할 때는 BeginPlay 함수가 호출되고, 매 프레임마다 액터의 Tick 함수를 호출한다. 액터가 게임에서 퇴장되면 EndPlay를 호출하고 이후 엔진의 가비지콜렉션에 의해 메모리에서 소멸된다. 액터에 PostInitializeComponents, EndPlay 함수를 추가로 선언하여 호출 순서가 어떻게 되는지 확인해보자.

움직이는 액터의 설계

액터의 Tick 함수를 이용하여 회전하는 분수대를 실습해보자. z축 기준으로 회전시키며 회전 속도 정보를 에디터에서 편집할 수 있도록 UPROPERTY 매크로에 EditAnywhere 키워드를 넣었다. OOP에서 캡슐화(Encapsulation)를 위해 속도 변수를 private로 선언했다면 에디터에서도 편집할 수 있도록 AllowPrivateAccess 키워드를 추가해야한다. 그리고 Tick함수에서 액터의 FRotator 자료형으로 회전 속도만큼 회전하도록 하였다.

FRotator는 회전값을 지정하는 데이터이다.

  • Pitch : 좌우를 기준으로 돌아가는 회전값이다. 언리얼 엔진의 Y축 회전에 해당
  • Yaw : 상하를 기준으로 돌아가는 회전값이다. 언리얼 엔진의 Z축 회전에 해당
  • Roll : 정면을 기준으로 돌아가는 회전값이다. 언리얼 엔진의 X축 회전에 해당

무브먼트 컴포넌트의 활용

위처럼 액터를 움직이기 위해 Tick과 DeltaSeconds를 활용하는 것은 게임 제작에서 일반적인 방법이다. 언리얼 엔진에서는 움직임이라는 요소를 분리하여 관리하도록 별도의 프레임워크를 구성하여 무브먼트 컴포넌트 통해 기능을 제공하고 있다. 다음과 같은 무브먼트 컴포넌트 종류가 있다.

  • FloatingPawnMovement : 중력의 영향을 받지않는 움직임
  • RotatingMovement : 회전 운동
  • InterpMovement : 지정한 위치로 보간하여 이동
  • ProjectileMovement : 발사체 움직임

RotatingMovement 컴포넌트를 사용하여 이전과 동일한 움직임을 구현해보자. Tick기능을 끄고, MovementComponent 가 틱마다 액터를 회전 시키도록 수정하였다.

MoveComponent를 추가하면 기존 컴포넌트와 달리 컴포넌트 목록에서 분리되어 표시된다. 컴포넌트 목록은 다음과 같이 두가지 종류로 분리되어 표시된다.

  • SceneComponent : 트랜스폼 정보가 필수인 컴포넌트
  • ActorComponent : 액터에 기능만 제공하는 컴포넌트