안녕하세요! 여러분들과 함께 게임 개발을 공부하는 베르입니다!

디버그에 대해서 이런 저런 이야기를 해본 스트리밍의 편집본입니다!

 

타임라인

0:00 인트로

1:25 디버그란?

2:23 버그의 유래

5:02 프로그래머의 숙명

5:56 버그를 줄이고 버그를 빨리 찾기 위해서는?

7:42 버그가 발생할 확률을 줄이는 법 1

13:54 버그가 발생할 확률을 줄이는 법 2

16:49 버그가 발생할 확률을 줄이는 법 3

20:17 초보자때 할 수 있는 실수

22:17 버그 리포트

24:38 불필요한 경고문 없애기

26:18 빌드 테스트

28:55 유니티에서 문제 해결하는 법

31:23 베르의 게임 개발 유튜브 멤버십 개발단!

32:14 아웃트로

반응형

 

Debug 클래스를 사용하려고 할때 System.Diagnostics 네임스페이스가 계속해서 자동으로 using 선언되는 문제와 Visual Studio에서 유니티 관련 코드가 자동완성이 되지 않는 문제에 대한 해결법을 알아봅시다!

 

타임라인

0:00 개요

0:16 using System.Diagnostics;가 계속 생겨나는 문제

3:11 Visual Studio에서 유니티 관련 자동완성이 되지 않는 문제

반응형

로그 출력하기

 

작성버전 :: 4.20.3

 

로그는 개발중에 여러가지 피드백을 얻기 위해서 중요한 도구다. 그렇기 때문에 항상 새로운 언어, 새로운 엔진 등을 배울 때는 거기서 어떤 방식으로 로그를 출력하는지 알아두는 것이 좋다. 이번에는 언리얼 엔진 4에서 로그를 출력하는 방법을 알아보자.

 

 

로그가 출력되는 출력 로그 탭 열기

 

 

 

우선은 로그를 출력하는 법을 배운 이후에 로그가 출력될 로그 탭을 여는 방법에 대해서 배워보자.

 

- 창 -> 개발자 툴 -> 출력 로그 (Window -> Developer Tools -> Output Log) 항목을 선택한다.

 

 

 

지시대로 출력 로그(Output Log) 항목을 선택하면 출력 로그 탭이 열리는 것을 확인할 수 있다. 에디터에서 우리가 출력하도록 명령한 로그들은 저 출력 로그 탭에서 출력될 것이다.

 

 

간단한 사용법

 

UE_LOG(LogTemp, Log, TEXT("Log Message"));

 

위의 코드를 사용하는 것으로 간단하게 로그를 출력할 수 있다. 언리얼에서 로그 기능은 매크로 함수로 정의 되어있으며, 매개변수는 앞에서부터 순서대로 로그 카테고리(Log Category), 로그 상세 수준(Log Verbosity Level), 로그 내용이다.

 

 

로그 상세 수준(Log Verbosity Level)

 

로그 상세 수준의 종류는 다음과 같다.

 

  • Fatal
Fatal 수준 로그는 항상 콘솔 및 로그 파일에 출력되며 로그가 비활성화된 경우에도 작동이 중단된다.
  • Error
Error 수준 로그는 콘솔 및 로그 파일에 출력되며, 이 로그는 기본적으로 빨간색으로 표시된다.
  • Warning
Warning 수준 로그는 콘솔 및 로그 파일에 출력되며, 이 로그는 기본적으로 노란색으로 표시된다.
  • Display
Display 수준 로그는 콘솔및 로그 파일에 출력된다.
  • Log
Log 수준 로그는 로그 파일에는 출력되지만, 게임 내의 콘솔에서는 출력되지 않지만, 에디터의 출력 로그 탭을 통해서는 계속 출력된다.
  • Verbose
Verbose 수준의 로그는 로그 파일에는 출력되지만, 게임 내의 콘솔에는 출력되지 않는다. 일반적으로 자세한 로깅 및 디버깅에 사용된다.
  • VeryVerbose
VeryVerbose 수준의 로그는 로그 파일에는 출력되지만, 게임 내의 콘솔에는 출력되지 않는다. 이 수준의 로그는 일반적으로 대량의 로그를 출력하는 상세한 로깅에 사용된다.

 

 

로그 상세 수준 중에 자주 사용될 에러, 경고, 로그 수준을 사용해서 로그를 출력하면 위의 이미지와 같이 로그가 출력된다.

 

Fatal 수준의 로그는, 출력되거나 컴파일 혹은 빌드할 때 코드가 실행되는 상황이라면 충돌 리포트를 띄우지만 이것은 의도된 충돌이기 때문에 로그 파일이나 충돌 호출 스택(Crash call stack)을 확인하면 된다.

 

 

 

 

 

로그 카테고리(Log Category)

 

로그 카테고리는 출력된 로그가 어떤 시스템에서 발생한 로그인지 알려주는 역할을 한다. 위의 간단한 사용법 파트에서는 이 로그 카테고리에 LogTemp를 넣어서 사용했는데, 이것은 특정한 카테고리에 속하지 않고 임시로 띄우는 로그라는 의미다. 언리얼 엔진에서는 이러한 카테고리를 90개 이상을 기본적으로 제공한다.

 

 

 

어떤 카테고리에서 로그가 발생했는지 알려줄 수 있기 때문에 로그 출력 코드를 작성할 때, 제대로 된 카테고리를 분류해서 넣어주기만 한다면 로그가 제공하는 정보가 좀 더 상세해질 수 있다.

 

 

커스텀 로그 카테고리(Custom Log Category)

 

언리얼 엔진에서 제공하는 로그 카테고리 이외에 개발자가 필요한 카테고리를 직접 만들어서 사용할 수 있다.

 

만약, 당신의 프로젝트 이름이 MyGame이라면 비주얼 스튜디오의 솔루션 탐색기에서 MyGame.h와 MyGame.cpp를 찾아서 다음과 같이 추가하면 된다.

 

MyGame.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"

DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, All);

 

MyGame.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "MyGame.h"
#include "Modules/ModuleManager.h"

IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, MyGame, "MyGame" );

DEFINE_LOG_CATEGORY(LogMyGame);

 

그리고 추가한 로그 카테고리를 사용할 때는 아래의 예시 코드와 같이 사용할 소스 파일에 MyGame.h를 포함한뒤 사용하면 된다.

 

MyActor.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "MyActor.h"
#include "Engine.h"
#include "MyGame.h"

// Sets default values
AMyActor::AMyActor()
{
     // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;

    UE_LOG(LogMyGame, Log, TEXT("My Log"));
}

 

 

 

 

 

로그 포맷(Log Formatting)

 

로그를 작성할 때, 로그의 내용이 고정되어 있는 경우보다, 상황이나 데이터의 내용에 따라 유동적으로 바뀌는 경우가 많다. 그렇기 때문에 간단한 사용법 파트의 예시처럼 고정된 문자열 방식이 아니라 포맷팅을 통해서 원하는 데이터의 내용 등을 고정된 로그의 내용과 함께 출력되도록 해야한다. 언리얼 엔진에서 로그 포맷팅을 하는 방법은 CPP에서 문자열 포맷팅하는 방법과 같다.

 

일반 메시지

UE_LOG(LogTemp, Log, TEXT("Log Message"));

 

FString

 

언리얼 엔진에서 기본적으로 사용되는 문자열 클래스는 FString이다. 로그에서 %s는 TCHAR* 타입을 받는다. 이것은 *FString에 대응한다.

UE_LOG(LogTemp, Log, TEXT("Character Name :: %s"), *MyCharacter->GetName());

 

bool

UE_LOG(LogTemp, Log, TEXT("Character is Die :: %s"), MyCharacter->IsDie ? TEXT("true") : TEXT("false"));

 

int

UE_LOG(LogTemp, Log, TEXT("Character HP :: &d"), MyCharacter->Hp);

 

float

UE_LOG(LogTemp, Log, TEXT("Character Stamina :: %f"), MyCharacter->Stamina);

 

FVector

 

FVector는 언리얼 엔진에서 위치를 표현하는 클래스이다.

UE_LOG(LogTemp, Log, TEXT("Character Location :: %s"), MyCharacter->GetActorLocation().ToString());

 

FName

UE_LOG(LogTemp, Log, TEXT("Character FName :: &d"), MyCharacter->GetFName().ToString());

 

여러 자료형 한번에 로그로 출력하기

UE_LOG(LogTemp, Log, TEXT("CharacterName :: &s / HP :: &d / Stamina :: %f"), *MyCharacter->GetName(), MyCharacter->Hp, MyCharacter->Stamina);

 

추가로...

 

bool 같은 논리 변수는 로그로 출력하는 과정이 조금 번거롭기 때문에, 로그를 자주 사용한다면 이러한 번거로운 과정을 간소화하기 위해서 로그를 위한 클래스로 래핑해서 사용하는 것도 추천해볼만하다.

 


 

참고

 

Unreal Engine 4 Wiki - Logs, Printing Messages To Yourself During Runtime (https://wiki.unrealengine.com/index.php?title=Logs,_Printing_Messages_To_Yourself_During_Runtime&action=edit)

반응형
  1. 익명 2021.05.07 15:19

    비밀댓글입니다

    • 익명 2021.05.12 09:46

      비밀댓글입니다

    • 익명 2021.05.16 15:50

      비밀댓글입니다

로그(Log) 출력시 스택 트레이스(Stack trace) 관리하기


로그(Log)는 개발자들에게 없어서는 안될 중요한 동반자다. 개발자는 로그를 통해서 코드가 제대로 동작하는지, 데이터 값들이 정상인지 등을 확인할 수 있다. 만약 로그가 없다면, 개발자는 버그를 찾아내는데 더 많은 고생을 하게 될 것이다.



유니티에서는 이러한 로그를 출력할 때, 위의 이미지처럼 개발자가 출력하고자하는 로그의 내용과 함께 로그가 출력된 코드의 위치를 알려주는 스택 트레이스 역시 함께 보여준다. 로그를 출력하도록 설정해놓았다면 빌드한 어플이케이션에서도 로그가 찍힐 때 스택 트레이스 역시 함께 출력되도록 되어있다.


위의 이미지는 간단한 테스트 코드이기 때문에 스택 트레이스가 3줄 밖에 안되지만 본격적으로 개발에 들어간 이후에는 스택 트레이스가 기본적으로 4-5줄에서 많은 10여줄을 넘는 경우가 자주 발생한다.


에디터에서라면 스택 트레이스와 로그가 분리되어서 출력되기 때문에 로그를 읽는데는 큰 문제가 없지만 윈도우 빌드에서 나오는 로그 파일이나, 안드로이드로 빌드된 어플리케이션으로 로그캣에서 로그를 볼때는 로그 바로 아랫줄에 스택 트레이스가 바로 출력되기 때문에 로그를 제대로 읽기가 매우 어려워진다.


물론 코드의 어느 지점에서 에러 로그가 발생했는지 확인해야하는 로그라면 스택 트레이스가 출력되는게 좋지만, 로그가 출력된 위치보다는 출력되는 내용이 더 중요한 로그라면 스택 트레이스는 출력되지 않는 편이 로그의 가독성을 더 높혀줄 것이다.


Application.SetStackTraceLogType(LogType logType, StackTraceLogType stackTraceLogType);


Application 클래스의 SetStackTraceLogType() 함수를 통해서 스택 트레이스의 출력 수준을 결정할 수 있다.


LogType 열거형은 스택 트레이스 수준을 설정할 로그의 종류를 의미한다. 로그의 종류는 다음과 같다.


LogType.Error
LogType.Assert
LogType.Warning
LogType.Log
LogType.Exception


StackTraceLogType은 스택 트레이스의 출력 수준을 의미한다. 스택 트레이스 출력 수준의 종류는 다음과 같다.


StackTraceLogType.None;
StackTraceLogType.ScriptOnly;
StackTraceLogType.Full;


스택 트레이스 타입을 수정하지 않았을 때, 유니티의 기본 수준은 ScriptOnly이다. None으로 설정하면 스택 트레이스가 전혀 출력되지 않고 Full로 설정하면 기존의 스택 트레이스보다 더 자세한 정보를 제공하는 스택 트레이스가 출력된다.





응용


스택 트레이스 타입을 변경하는 순간부터 모든 로그에 스택 트레이스 타입이 변경되기 때문에, 제대로 다루지 않으면 스택 트레이스가 필요한 로그에서 스택 트레이스가 출력되지 않거나 혹은 그 반대의 경우가 발생할 수 있다.


필요한 상황에서만 스택 트레이스를 켜고 끄기 위한 방법으로는 Debug 클래스를 래핑해서 사용하는 방법이 있다.


using UnityEngine;

public static class Debug
{
    public static void Log(object message, StackTraceLogType stackTraceLogType = StackTraceLogType.ScriptOnly)
    {
        Application.SetStackTraceLogType(LogType.Log, stackTraceLogType);
        UnityEngine.Debug.Log(message);
    }
}


반응형

유니티 에디터에서 플레이 중에 씬 전환시 다음 씬에서 라이트가 어두워지는 버그(5.6)


유니티 5에는 고질적인 버그가 하나 있다. 유니티 에디터에서 게임 테스트를 위해서 플레이 버튼을 눌러서 플레이할 때 다른 씬으로 이동하면 라이트가 어둡게 보이는 문제가 바로 그것이다.



의도한 씬의 밝기는 첫 번째 그림과 같지만 유니티 에디터에서 플레이 버튼을 눌러서 게임을 실행한 뒤에 플레이 도중에 씬을 넘어가면 라이트의 밝기가 두 번째 그림처럼 어두워 진다. 이러한 현상은 게임을 빌드해서 실행했을 경우에는 발생하지 않지만, 라이팅 테스트 하나만을 위해 매번 게임을 새로 빌드해서 실행하는 것은 매우 번거로운 일이다.


이 문제를 해결하는 방법은 다음과 같다.



상단 메뉴에서 Window > Lighting > Setting 을 선택하면 라이팅 세팅을 할 수 있는 창이 열린다.




열린 Lighting Setting 창의 가장 아래쪽에서 Auto Generate가 체크되어 있는 것을 볼 수 있는데 이 체크를 해제하면 옆의 Generate Lighting 버튼이 활성화된다. 이 버튼을 클릭하면 유니티 에디터에서 테스트를 진행할 때에도 빛이 제대로 들어오는 것을 확인할 수 있다.


반응형

static_assert

프로그래밍을 하는 과정에서 버그의 발생과 디버그는 필연적이다. 아무리 설계가 완벽하다고 해도, 코드의 작성자가 인간인 이상 실수로 인하여 버그는 발생하기 때문에 코딩 이후에는 반드시 테스트와 디버그가 이루어져야 한다.


게임 프로그래밍의 경우에는 예외처리(Exception Handling)을 성능 상의 문제로 잘 사용하지 않고 개발 도중에 버그를 잡기 위해서 assert를 사용하는 경우가 많다. assert는 <assert.h> 헤더를 포함시키면 사용할 수 있으며 어떤 식이 참인지 거짓인지 판별해주고 그 식이 거짓이라면 에러 메시지 박스를 띄우고 어느 cpp의 몇 번째 줄에서 중단되었는지 알려주고 프로그램이 종료된다. 이 기능은 디버그 빌드에서만 작동하고 릴리즈 빌드에서는 작동하지 않는다.


기존의 assert 사용 예시

#include <assert.h>


class Player

{

// 플레이어에 대해 정의된 클래스

// ...

}


class GameManager

{

// 게임을 관리하는 매니저

// ...


static Player* GetPlayer(/*특정조건*/)

{

// 특정 조건에 맞는 플레이어를 반환한다.

// 만약 조건에 맞는 플레이어가 없다면 nullptr을 반환

}

}


int main()

{

Player * player = GetPlayer();

// 만약 player가 nullptr이라면 프로그램은 정지되고 에러 메시지 박스를 출력될 것이다.

assert(player != nullptr);

}


C++ 11에 들어서 새로 도입된 static_assert라는 것이 있는데 이것은 별도의 헤더를 포함시키지 않고도 사용할 수 있다. 이 static_assert가 기존의 assert와 다른 점은 기존의 assert는 런타임 도중에만 작동해서 해당 코드가 실행되기 전에는 에러가 발생하는지 알기 어려운 반면에 static_assert는 컴파일 타임에 발생하기 때문에 문제가 발생할 부분이라면 해당 코드가 작동하지 않을 확률이 높다고 하더라도 반드시 에러를 잡아낼 수 있다는 것이다. 다만 컴파일 타임에만 작동하는 static_assert의 특성 상 컴파일 타임에 결정되지 않았고 런타임이 되지 않으면 알 수 없는 부분에는 사용할 수 없다. 예를 들자면 위의 assert 예시 코드에서처럼 player 객체가 nullptr인지는 런타임 동안 GetPlayer()함수를 지나봐야만 결정되기 때문에 컴파일 타임에는 알 수 없어서 저런 곳에는 static_assert를 사용할 수 없다.



static_assert 사용 예시

/* 기존에 사용되던 구조체 a

struct a

{

int i

}

//*/


//* 수정된 구조체 a

struct a

{

int i;

float f;

}

//*/


int main()

{

static_assert(sizeof(a) == 8, "Old struct a used.");

}


위의 예시처럼 구조체 a가 수정된 이후에 실수로 이전 구조체를 사용하고 있는지 컴파일 타임에 확인해서 발생할 버그를 미리 막을 수 있게 된다. 다음은 각 상황에서 static_assert의 반응이다 :


실수로 이전의 구조체를 사용한 경우 컴파일 에러를 발생시킨다.


구조체를 제대로 사용하면 컴파일 에러를 발생시키지 않는다.


static_assert를 사용할 때, 주의할 점은 유니코드와 한글을 지원하지 않기 때문에 에러 메시지를 작성할 때, 멀티바이트 영어로 작성하는게 좋다.


반응형

'C++' 카테고리의 다른 글

[C++11] enum class  (1) 2017.07.17
[C++ 11] static_assert  (0) 2017.05.23
[C++ 11] Auto Vectorization  (1) 2016.11.01
[C++ 11] Range-Based For  (0) 2016.11.01
[C++ 11] Scoped Lock  (0) 2016.11.01

효과적으로 Debug를 하기 위해서는 Debug 빌드를 유지해야한다.


하지만 사람들은 Debug 빌드를 잘 사용하지 않는데 그것은 Debug 빌드의 속도가 Release 빌드에 비해서 컴파일 시간도 실행 시간도 느리기 때문이다.


Debug 빌드는 기본적으로 최적화 기능이 꺼져있기도 하고, 프로그래머가 에러를 찾기위해 온갖 assert를 넣고 하다보면 프로그램이 더욱 느려지기 마련이다.


하지만 Release 빌드는 속도가 빠른 반면에 에러를 잡기위한 디버깅이 힘들어진다. 최적화 단계에 들어선 이후에는 성능이 매우 중요하겠지만 그 이전 단계에서는 프로그램이 얼마나 올바르게 동작하는 가가 제일 중요하다. 그렇기 때문에 버그를 빠르게 찾아내고 수정하기 위해서는 반드시 Debug 빌드를 언제나 컴파일되고 실행되게 유지해야한다.


만약 Debug 빌드가 컴파일되지 않게 망가뜨린 프로그래머가 있다면 그는 반드시 다른 프로그래머가 언제든지 Debug 빌드를 실행시킬 수 있도록 복구시켜 두어야 한다.


Debug 빌드를 사람들이 잘 쓰지 않는 이유는 속도가 느리기 때문인데 속도를 빠르게 하기 위해 무조건 Release 빌드는 쓰는 것이 아니라 어떻게 해서든 Debug 빌드를 빠르게 만들어야 한다.


Debug 기능을 쓸 수 있지만 어느 정도 최적화 기능을 켠 Fast Debug 빌드를 만든다던가. 각 부서별로 자기 부서에 필요한 부분만 Debug로 돌리고 나머지는 Release로 돌릴 수 있게 자동화를 한다던가. 여기저기 있는 assert를 효과적으로 제거한다던가하는 방식으로 말이다.


참고 ::



반응형

Direct3D는 어떻게 보면 두 개의 레이어가 따로 있다. C++ 쪽에서 Debug를 돌려도 렌더링 쪽에서는 느려질 수 있는 것은 그렇게 많지 않다.


Direct3D에서는 실제 GPU로 들어가는 부분에 Direct3D Runtime이 있다. 여기에도 Debug 빌드와 Release 빌드가 있는데 이것은 C++ 코드와 전혀 상관없이 DirectX Control Panel에 들어가서 수정할 수 있는 것이다. 그것을 Release로 만들면 최적화가 적용되어서 빨라지고 Debug로 만들면 최적화 기능이 꺼지고 Debug 기능들로 인해서 느려진다.


일반적으로 대부분의 프로그래머는 빠르고 에러도 덜 뱉는다는 이유로 Direct3D를 Release로 켜놓고 사용하는데 어느 순간엔가는 이것을 Debug 빌드로 돌려서 Debug를 해야하는 순간이 온다. Memory leak 문제가 발생할 수도 있고, 참조 카운터 문제가 있을 수도 있다. 그래서 이것을 Debug로 바꾸는 순간 엄청난 Warning과 Error를 발생시키는 경우가 있다. 매 프레임마다 같은 혹은 비슷한 장면은 계속해서 그려야하는 렌더링의 특징 상 break를 걸기도 어렵고 Debugging도 어려워진다.


그렇기 때문에 그래픽 프로그래머는 반드시 Direct3D Runtime을 Debug 빌드로 두고 작업을 해야한다. Release 빌드에선 에러나 경고를 뱉지 않던 코드가 Debug 빌드에서 쏟아져 나올 수 있다. 그런 것을 무시하고 Release 빌드로만 작업하고 그런 에러와 경고가 쌓인 상태로 나중에 다른 프로그래머가 Debug를 하려고 하면 수많은 경고와 에러로 인해서 도저히 Debug 작업을 할 수 없게 된다. 결국 작업을 하기 위해서는 이 수많은 에러와 경고들을 처리할 수 밖에 없는데 이렇게 수 개월치 쌓인 문제를 한꺼번에 처리하는 것보다는 에러 하나, 경고 하나가 발생했을때 바로 처리하는 것이 훨씬 깔끔하고 빠른 문제 처리 방법이다.


렌더링 쪽에서는 크게 Debug 빌드가 느린 것 같지는 않은데 게임 쪽에서는 프레임 속도가 제대로 안나오면 문제가 있을 수도 있다.


참고 :: https://www.youtube.com/watch?v=eOF6IZU4nxQ


반응형

+ Recent posts