Chapter 02. Unreal Object
2.1 언리얼 C++ 클래스
2.1-1 언리얼 C++의 필요성
- 게임을 플레이하는 사람들을 위해서는 최대한 최적화되어져서 누구나 플레이 가능해야함.
게임을 만드는 사람들은 규모가 커져도 복잡한 기능을 실수없이 관리할 수 있어야함.
- 최대한 최적화 되기 위해서는 매니지드 언어인 C++ 사용이 필수적.
그러나, 매니지드 언어이기에 하드웨어에 직접 접근하며 잘못 사용하면 프로그램에 악영향.
이를 해결하기 위해서 후발 언어들에선 실수를 줄일 수 있는 기능(가비지 컬렉션)과
생산성을 높혀주는 기능(인터페이스/리플렉션/델리게이트)들이 언어에 내장되어 개발됨.
- 언리얼 C++에서는 기존 C++에 없고 후발 언어들에서 검증된 기능들을
매크로를 통해서 구현해냄. 이를 구현해낸 클래스가 언리얼 오브젝트 클래스.
2.1-2 폴더 구조
- 프로젝트 생성 폴더
StudyProject 폴더를 앞으로 "프로젝트 폴더"라고 부름.
Content Browser > Content 우클릭 > Show in Explorer 클릭 시 나오는 폴더에서
뒤로 가기 했을 때 나오는 폴더를 "프로젝트 폴더"라고 부를 예정.
- 폴더 구조 생성
앞으로 많은 클래스를 생성할 예정.
이에 대비하여 지금 폴더를 미리 생성해놓고자 함.
프로젝트 폴더 > Source > StudyProject
새 폴더 "Public", "Private"
Public > 아래 폴더들을 새로 만듦.
Game: 게임 모드와 게임 스테이트, 게임 인스턴스 클래스
Controllers: 컨트롤러 클래스
Characters: 컨트롤러가 빙의할 캐릭터 관련 클래스
Inputs: 입력 관련 클래스
Animations: 애니메이션 관련 클래스
Items: 아이템 관련 클래스
WorldStatics: 월드 상에 배치되어 캐릭터와 상호작용할 개체와 관련된 클래스. 기믹.
UI: UI와 관련된 클래스.
Examples: 테스트용 클래스. 우리는 예제 관련 클래스들도 여기에 저장.
만들어진 모든 폴더 Ctrl + C > Private > Ctrl + V
- 기존에 만들어진 StudyProjectGameModeBase 클래스 이동
Source > StudyProject > StudyProjectGameModeBase.h 파일은 Public > Game 폴더로
Source > StudyProject > StudyProjectGameModeBase.cpp 파일은 Private > Game 폴더로 이동.
프로젝트 재빌드 후 확인.
<hide/>
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Game/StudyProjectGameModeBase.h" // 변경된 경로로 지정.
- 언리얼 에디터에서 애셋 폴더 구조 생성
Content Browser > Content 우클릭 > New Folder 클릭 > "StudyProject"
앞으로 위 과정을 줄여서 "새 폴더"라고 줄임. 상당히 많이 사용하기 때문.
StudyProject > 아래 폴더들도 생성.
Animations, Characters, Controllers, Game, Inputs, Items, Examples, UI, WorldStatics
2.1-3 언리얼 오브젝트 클래스 생성
- C++ 클래스를 생성할 때, 비주얼 스튜디오에서 수동으로 생성하면 안됨.
언리얼 에디터에서 적절한 방법으로 생성하는 것이 올바른 방법.
- 언리얼 오브젝트 클래스 생성
Content Browser > C++ Classes > StudyProject > 우측 빈공간 우클릭 > New C++ Class
처음 뜨는 다이얼로그에서 Common Classes 말고 All Classes 클릭.
앞으로 위 과정을 줄여서 "새 C++ 클래스"라고 줄임.
새 C++ 클래스 > None 부모 클래스 > Public > "SUnrealObjectClass"
앞으로 항상 Public 선택할 예정.
Path > Examples (만약 해당 폴더가 없다면 만들고 지정하면 됨.)
- 클래스 생성 혹은 컴파일 시에 나오는 에디터 경고
경고창이 나오는데 아래와 같이 대응.
앞으로도 똑같이 대응해주면 됨.


- 클래스 만든 직후에 컴파일 해보기
Visual Studio 클릭 > Ctrl + Shift + F5
아래와 같이 작성 전에는 에디터에서 해당 클래스 파일을 볼 수 없음.
언리얼 오브젝트 클래스가 아니라서 관리 대상이 아니기 때문.
- SUnrealObjectClass 내용 작성.
<hide/>
// SUnrealObjectClass.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "SUnrealObjectClass.generated.h"
/**
*
*/
UCLASS()
class STUDYPROJECT_API USUnrealObjectClass : public UObject
{
GENERATED_BODY()
};
<hide/>
// SUnrealObjectClass.cpp
#include "Examples/SUnrealObjectClass.h"
- 언리얼에서 소스코드 컴파일 방법
언리얼 에디터가 이미 .dll 파일을 잡고 있기 때문에
언리얼 에티더가 켜진 상태에서는 .dll 파일을 만들 수 없음.
다만 라이브 코딩을 이용하면 이걸 가능케 해줌. 우리는 사용 안함.
에디터를 끈 상태에서 Ctrl + Shift + B를 통해서 빌드
빌드 성공 후부터는 F5로 실행
이후에는 Ctrl + Shift + F5를 통해 빌드 후 실행
- Include What You Use
UE4.15 이전에서는 #include "EngineMinimal.h" 구문을 사용했음.
개발에 필요한 대부분의 헤더 파일을 참조하는 파일.
이후 버전에서는 IWYU에 따라 #include "CoreMinimal.h" 구문을 사용함.
사용하는 헤더 파일만 참조해서 빌드 속도와 인텔리센스 과부하를 낮춤.
- #include "UObject/NoExportTypes.h"
이 자리에는 추후 부모 클래스의 헤더 파일을 작성함.
UObject 클래스를 상속 받으므로 위 헤더 파일을 상속.
- #include "SUnrealObjectClass.generated.h"
UHT을 통해 클래스 header 파일이 .generated.h 파일로 변환됨.
소스 코드 작성 시점에 이 파일은 존재하지 않지만, 컴파일 과정에서 무조건 생성됨.
따라서 모든 #include 구문 작성 후 마지막에 .generated.h 파일을 인클루드 해야함.
- UCLASS() 매크로
언리얼 클래스를 선언하기 전 작성해야 하는 매크로.
구조체 전에는 USTRUCT(), 공용체 전에는 UENUM() 매크로를 작성함.
- STUDYPROJECT_API
윈도우의 DLL 시스템은 DLL 내 클래스 정보를 외부에 공개 할지 말지 결정하는
__declspec(dllexport)라는 키워드를 제공함. 언리얼 엔진에서 이 키워드를 사용하려면
"모듈명_API" 키워드를 클래스 선언 앞에 추가해야 함.
이 키워드가 없으면 다른 모듈에서 해당 개체에 접근할 수 없음.
- 클래스 접두사 U
아래 언리얼 명명 규칙에서 설명. UObject 클래스를 상속 받은 클래스란 표시.
- UObject 클래스 상속
언리얼 오브젝트 클래스는 UObject 클래스를 상속 받아야 함.
상속 받지 않는다면 스마트 포인터(TShared_ptr)를 사용해서라도 메모리 관리해야 함.
- GENERATED_BODY() 매크로
해당 매크로를 타고 들어가면 수많은 매크로 코드를 볼 수 있음.
이를 통해 .generated.h 파일을 만들어 내고 언리얼 오브젝트의 장점들을 구현해내는 것.
- generated.h 파일의 위치
프로젝트 폴더 > Intermediate > Build > Win64 > UnrealEditor > Inc > UnrealObject > UHT
파일을 열어보면 매크로를 통해 생성된 내용들을 볼 수 있음.
- UObject 클래스를 상속 받는 UGameInstance 클래스 실습
UGameInstance 클래스는 UObject 클래스를 상속 받음.
UGameInstance 클래스를 상속 받는 우리만의 클래스를 작성해보고자 함.
새 C++ 클래스 > GameInstance 부모 클래스 > "SGameInstance"
Path > Game
<hide/>
// SGameInstance.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "SGameInstance.generated.h"
/**
*
*/
UCLASS()
class STUDYPROJECT_API USGameInstance : public UGameInstance
{
GENERATED_BODY()
};
<hide/>
// SGameInstance.cpp
#include "Game/SGameInstance.h"
- GameInstance 개체
GameInstance 클래스의 개체는 게임 실행 중에 단 하나만 존재함.
싱글톤 패턴. 따라서 특별한 설정이 필요함.
Toolbar > Maps & Modes > Game Instance Class에 SGameInstance 지정.
이렇게 SGameInstance가 곧바로 인식 되는 것도 언리얼 오브젝트 클래스이기 때문.
2.1-4 UHT와 언리얼 오브젝트
- GENERATED_BODY() 매크로 구문은 C++ 표준 문법이 아님.
언리얼 런타임 시스템에 명령을 내리고자 언리얼 엔진에서 지정한 문법들.
이 키워드들은 컴파일 진행 전, 언리얼 엔진의 언리얼 헤더 툴에 의해 분석됨.
- Unread Header Tool
언리얼 오브젝트 클래스의 헤더 파일에 작성된 언리얼 C++ 매크로를 분석하는 툴.
분석한 결과로 클래스명.generated.h 파일을 생성함.
언리얼 오브젝트의 장점들을 제공하기 위한 전처리 작업을 진행해줌.

- 특정 매크로(UCLASS(), UPROPERTY(), UFUNCTION(), ...)를 프로퍼티나 함수에 작성하면
Unreal Header Tool이 컴파일 타임에 해당 정보를 수집함.
2.1-5 언리얼 명명 표준
- 언리얼은 기본적으로 파스칼 케이싱.
특히 변수 선언시에 스테이크 케이싱을 사용하면 문제될 수 있음.
- 클래스명 접두사
템플릿 클래스는 T-
UObject 클래스를 상속 받은 클래스는 U-
AActor 클래스를 상속 받은 클래스는 A-
인터페이스 클래스는 I-
그외 구조체는 F-
- 함수명 접두사
bool 자료형을 반환하는 함수는 Is-가 붙음.
그외 자료형을 반환하면 Get-, 속성 수정하면 Set-
함수는 동사로 시작하는게 올바름.
단, 델리게이트에 바인드 되는 함수는 On- 혹은 Handle-이 붙기도 함.
- 매개변수명 접두사
해당 매개변수가 입력으로 사용되면 In-, 출력으로 사용되면 Out-
2.1-6 로그 출력
- 디버깅을 위해 출력을 다룰 수 있어야 함.
보통 Output Log 창에 로그를 보고 경고나 에러를 확인함.
Toolbar > Window > Output Log를 클릭하면 하단에 열림.
Filters > Categories > Show all의 체크 해제 후 LogTemp만 체크 해두면 실습때 편함.
- UE_LOG(로그카테고리, 로그수준, 형식문자열, 인자)
로그카테고리: 로그의 분류를 위해 카테고리 지정가능. Output Log 창에서 필터링 가능.
로그수준: Output Log 창에서 로그 수준에 따라 색상이 달라짐.
Log는 흰색, Warning은 노란색, Error는 빨간색.
형식문자열: 출력할 문자열. c언어의 printf() 함수의 인자 중 형식문자열과 유사함.
인자: 형식문자열에 출력할 값.
- UE_LOG() 실습
SGameInstance 클래스의 생성자를 통해 실습.
아래와 같이 작성 후 컴파일.
<hide/>
// SGameInstance.h
...
class STUDYPROJECT_API USGameInstance : public UGameInstance
{
...
public:
USGameInstance(); // 함수 시그니처에 커서를 두고 Ctrl + .을 누르면 정의를 간편하게 만들 수 있는 다이얼로그가 보임.
};
<hide/>
// SGameInstance.cpp
#include "Game/SGameInstance.h"
USGameInstance::USGameInstance()
{
UE_LOG(LogTemp, Log, TEXT("USGameInstance::USGameInstance() has been called."));
}
- USGameInstance 클래스 관련 실습
GameInstance에는 중요한 몇가지 함수가 있음.
대표적으로 게임이 시작될 때 호출되는 Init() 함수와 종료될 때 호출되는 ShutDown() 함수.
해당 함수를 오버라이드해서 언제 호출되는지 알아보자.
<hide/>
// SGameInstance.h
...
class STUDYPROJECT_API USGameInstance : public UGameInstance // UGameInstance를 Ctrl + 클릭 하거나, 블럭지정하고 F12를 누르면 언리얼 엔진 소스코드를 볼 수 있음.
{
GENERATED_BODY()
public:
...
virtual void Init() override;
virtual void Shutdown() override;
};
<hide/>
// SGameInstance.cpp
...
void USGameInstance::Init()
{
UE_LOG(LogTemp, Log, TEXT("USGameInstance::Init() has been called."));
Super::Init(); // 엔진 업데이트 루틴을 지키기 위해서, 언리얼 엔지니어가 작성한 코드가 먼저 실행되게끔 하기 위함.
}
void USGameInstance::Shutdown()
{
UE_LOG(LogTemp, Log, TEXT("USGameInstance::Shutdown() has been called."));
Super::Shutdown();
}
- check(조건식) / ensure(조건식) 구문
함수의 인자로 전달한 조건식이 거짓일 경우 check() 함수는 크래시를 발생시킴.
ex. check(nullptr != GameInstance)
그러나, 실행중에 크래시를 발생시켜서 재빌드 시간이 부담스럽다면 ensure() 함수를 사용함.
ex. ensure(nullptr != GameInstance)
- UE5 라이브 코딩 실습
라이브 코딩을 다시 켜고 cpp 파일만 수정 후에 에디터 우측 하단의 컴파일 버튼 누르기.
언리얼 에디터는 에디터가 점유 중인 모듈이 컴파일 되면 기존 모듈을 덮어쓰지 않고,
기존 모듈의 이름 뒤에 숫자를 붙힌 새로운 파일을 생성함.
Binaries 폴더를 확인 후 컴파일 버튼을 누르면 생성되는 걸 볼 수 있음.
에디터를 완전히 종료하고 컴파일을 수행하면 생성된 임시 모듈들을 자동으로 제거함.
확인 후에는 다시 라이브 코딩 기능을 끔.
2.2 언리얼 오브젝트의 장점
2.2-1 Class Default Object
- 클래스 기본 개체
언리얼 엔진이 초기화되면 엔진 구동에 필요한 모듈이 순차적으로 로딩됨.
해당 모듈에 작성된 클래스들도 함께 로드되면서 CDO가 클래스마다 하나씩 생성.
CDO는 생성자를 통해 설정된 초기화를 통해 생성됨.
이를 통해서 언리얼은 개체를 생성할 때 아에 새로운 개체를 생성하는 것이 아니라
기존에 생성된 CDO를 복제하는 방식으로 생성해서 메모리 효율을 높힘.
새 개체는 CDO의 기본값을 공유하기 때문.
그리고 정말 필요한 때가 아니라면 CDO로 떼움.(Lazy Object Creation)

- 그래서 엔진이 초기화되면 모든 언리얼 오브젝트 클래스 기본 개체가 메모리에 올라간 상태임.
이렇게 메모리에 올라간 클래스 기본 개체는 GetDefault() 함수를 사용해 가져올 수 있음.
클래스 기본 개체는 엔진이 종료될 때까지 상주하기 때문에 언제든지 사용해도 됨.
- CDO는 언리얼 오브젝트가 가진 기본 값을 보관하는 템플릿 개체임.
한 클래스로부터 다수의 개체가 생성될 때 일관성 있게 기본값을 조정할 수 있게끔함.
추후에는 .ini 파일을 읽어서 CDO를 초기화하고 이를 GetDefault() 함수로 활용할 예정.
언리얼 오브젝트의 클래스 기본 개체는 엔진 초기화 단계에서
생성자를 거쳐 .ini에서 설정한 값이 할당되므로
.ini 파일에서 지정한 애셋의 목록 정보를 얻어올 수도 있음.
2.2-2 Unreal Property System
- 언리얼 프로퍼티 시스템
리플렉션은 런타임 중에 자기 자신을 조사하는 기능을 뜻함. C#에도 있음.
가비지 컬렉션, 리플리케이션, 시리얼라이즈 등의 기능들의 근간을 이룸.
리플렉션은 언리얼 오브젝트만 가질 수 있음. 일반 C++ 오브젝트는 불가능.
다만, 리플렉션이라는 단어가 그래픽스에서도 쓰이기 때문에 언리얼 프로퍼티 시스템이라 부름.
- 프로퍼티 시스템의 자료형 계층구조는 아래와 같음. [보류]
UField <- UStruct <- UClass <- ...
- 프로퍼티 시스템의 동작 방법
언리얼에서는 클래스의 멤버(==멤버변수)를 속성(Property)이라고 부름.
클래스의 메서드(==멤버함수)를 함수(Function)라고 부름.
언리얼 오브젝트 클래스의 속성에 UPROPERTY(), 함수는 UFUNCTION()
매크로를 작성하고, 해당 매크로에 인자를 작성함으로써
언리얼 에디터에 메타데이터를 전달할 수 있음.
모든 언리얼 오브젝트는 자기 자신 클래스가 가진 속성과 함수 정보를
컴파일 타임(StaticClass())과 런타임(GetClass())에서 조회할 수 있음.
- 언리얼 프로퍼티 시스템 실습1
<hide/>
// SGameInstance.h
...
class STUDYPROJECT_API USGameInstance : public UGameInstance
{
...
private:
UPROPERTY()
FString Name;
};
<hide/>
// SGameInstance.cpp
...
USGameInstance::USGameInstance()
{
UE_LOG(LogTemp, Log, TEXT("USGameInstance::USGameInstance() has been called."));
Name = TEXT("USGameInstance Class Default Object");
// CDO의 Name 속성에 저장됨.
// 중단점을 걸어보면 언리얼 에디터가 실행되기 전에 호출됨을 알 수 있음.
}
void USGameInstance::Init()
{
UE_LOG(LogTemp, Log, TEXT("USGameInstance::Init() has been called."));
Super::Init();
UClass* RuntimeClassInfo = GetClass();
UClass* CompiletimeClassInfo = StaticClass();
//check(RuntimeClassInfo != CompiletimeClassInfo); 주석 풀어서 결과 확인 필요.
//ensure(RuntimeClassInfo != CompiletimeClassInfo);
//ensureMsgf(RuntimeClassInfo != CompiletimeClassInfo, TEXT("Intentional Error"));
UE_LOG(LogTemp, Log, TEXT("Class Name: %s"), *RuntimeClassInfo->GetName());
Name = TEXT("USGameInstance Object"); // CDO를 통해 생성된 개체의 Name 속성에 새롭게 대입되는 값.
UE_LOG(LogTemp, Log, TEXT("USGameInstance::Name: %s"), *(GetClass()->GetDefaultObject<USGameInstance>()->Name));
UE_LOG(LogTemp, Log, TEXT("USGameInstance::Name: %s"), *Name);
}
...
- 언리얼 프로퍼티 시스템 실습2
<hide/>
// SUnrealObjectClass.h
...
class STUDYPROJECT_API USUnrealObjectClass : public UObject
{
GENERATED_BODY()
public:
USUnrealObjectClass();
UFUNCTION()
void HelloUnreal();
const FString& GetName() const { return Name; }
public:
UPROPERTY()
FString Name;
};
<hide/>
// SUnrealObjectClass.cpp
#include "Examples/SUnrealObjectClass.h"
USUnrealObjectClass::USUnrealObjectClass()
{
Name = TEXT("USUnrealObjectClass CDO");
}
void USUnrealObjectClass::HelloUnreal()
{
UE_LOG(LogTemp, Log, TEXT("USUnrealObjectClass::HelloUnreal() has been called."));
}
<hide/>
// SGameInstance.cpp
...
void USGameInstance::Init()
{
UE_LOG(LogTemp, Log, TEXT("USGameInstance::Init() has been called."));
Super::Init();
USUnrealObjectClass* USObject1 = NewObject<USUnrealObjectClass>();
// 언리얼은 이런식으로 new 키워드를 안쓰고 NewObject<>() API를 사용해야 함.
UE_LOG(LogTemp, Log, TEXT("USObject1's Name: %s"), *USObject1->GetName()); // 우리가 정의한 Getter()
FProperty* NameProperty = USUnrealObjectClass::StaticClass()->FindPropertyByName(TEXT("Name")); // 프로퍼티 시스템을 활용한 Getter()
FString CompiletimeUSObjectName;
if (nullptr != NameProperty)
{
NameProperty->GetValue_InContainer(USObject1, &CompiletimeUSObjectName);
UE_LOG(LogTemp, Log, TEXT("CompiletimeUSObjectName: %s"), *CompiletimeUSObjectName);
}
USObject1->HelloUnreal();
UFunction* HelloUnrealFunction = USObject1->GetClass()->FindFunctionByName(TEXT("HelloUnreal"));
if (nullptr != HelloUnrealFunction)
{
USObject1->ProcessEvent(HelloUnrealFunction, nullptr);
}
}
...
2.2-3 Unreal Interface
- 인터페이스(Interface)란
자식 클래스가 반드시 구현해야 할 행동을 지정하는데 활용되는 클래스
ex. 참새, 뱁새 클래스를 만든다면 새는 날아가기 라는 행동을 반드시 하기에
새 인터페이스를 만들어서 Fly() 함수를 만들고, 참새 뱁새 클래스가
새 인터페이스를 상속 받아서 반드시 Fly() 함수를 정의하게끔 강제함.
다형성의 구현, 의존성이 분리된(Decouple) 설계에 유용하게 활용됨.
ex. 새의 주인이 있고 주인이 새를 날린다고 가정해보자.
이때 새의 주인 클래스는 새 인터페이스의 Fly() 함수를 통해서
참새 혹은 뱁새 클래스와 연결되게됨.
어떤 새든간에 정해진 시그니처의 Fly()만 있으면 됨.
그럼 그 안에서 어떤 일이 벌어져도 새의 주인 클래스는 안전함.
- 인터페이스를 생성하면 두 개의 클래스가 생성됨.
U로 시작하는 타입 클래스/I로 시작하는 인터페이스 클래스
개체를 설계할 때는 인터페이스 클래스를 사용하고
런타임에서 클래스 타입정보를 제공 할 때는 타입 클래스를 사용함.
실제로는 타입 클래스 작업을 할 일은 거의 없음.
- 언리얼 C++ 인터페이스는 인터페이스에서도 구현이 가능함.
추상 타입으로만 선언할 수 있는 Java, C#과는 다름.
- 언리얼 C++ 인터페이스 실습
새 C++ 클래스 > Ureal Interface 부모 클래스(가장 아래에 있음) > "SFlyable"
Path > Examples
새 C++ 클래스 > Object 부모 클래스 > "SPigeon"
Path > Examples
<hide/>
// SFlyable.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "SFlyable.generated.h"
UINTERFACE(MinimalAPI)
class USFlyable : public UInterface
{
GENERATED_BODY()
};
/**
*
*/
class STUDYPROJECT_API ISFlyable
{
GENERATED_BODY()
public:
virtual void Fly() = 0; // ISFlyable에서 구현해도 되고 안해도됨. 안하는게 국룰.
};
<hide/>
// SFlyable.cpp
#include "Examples/SFlyable.h"
<hide/>
// SPigeon.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "SFlyable.h" // 인클루드 구문은 항상 .generated.h 파일 위에 작성.
#include "SPigeon.generated.h"
/**
*
*/
UCLASS()
class STUDYPROJECT_API USPigeon
: public UObject
, public ISFlyable
{
GENERATED_BODY()
public:
USPigeon();
virtual void Fly() override;
private:
UPROPERTY()
FString Name;
};
<hide/>
// SPigeon.cpp
#include "Examples/SPigeon.h"
USPigeon::USPigeon()
{
Name = TEXT("Pigeon");
}
void USPigeon::Fly()
{
UE_LOG(LogTemp, Log, TEXT("%s is now flying."), *Name);
}
<hide/>
// SGameInstance.cpp
#include "Game/SGameInstance.h"
#include "Examples/SUnrealObjectClass.h"
#include "Examples/SFlyable.h"
#include "Examples/SPigeon.h"
USGameInstance::USGameInstance()
{
Name = TEXT("USGameInstance Class Default Object");
}
void USGameInstance::Init()
{
Super::Init();
USPigeon* Pigeon1 = NewObject<USPigeon>();
ISFlyable* Bird1 = Cast<ISFlyable>(Pigeon1); // 현업에서 인터페이스 개념은 대부분 이런식으로 업캐스팅 하기 위함.
if (nullptr != Bird1)
{
Bird1->Fly();
}
}
void USGameInstance::Shutdown()
{
Super::Shutdown();
}
2.2-4 Unreal Garbage Collection
- C++ 메모리 관리의 문제점
C++은 매니지드 언어임. 메모리 주소에 직접 접근하는 포인터를 사용해 오브젝트 관리.
따라서 프로그래머가 직접 할당/해지 짝맞추기를 해야 함.
이를 지키지 않으면 다양한 문제 발생.
- 가비지 컬렉션 시스템
프로그램에서 더이상 사용하지 않는 오브젝트를 자동 감지 후 메모리 회수하는 시스템.
적으로 생성된 모든 오브젝트 정보를 모아둔 저장소를 사용해서 참조되지 않는 메모리 추적.
- 대부분 Mark-Sweep 방식의 가비지 컬렉션 시스템 사용. 언리얼도 마찬가지.
1. 저장소에서 최초 검색을 시작하는 루트 오브젝트 표기.
2. 루트 오브젝트가 참조하는 개체를 찾아 Mark
3. Mark된 개체로부터 해당 개체가 참조하는 개체를 찾아서 다시 Mark. 계속 반복.
4. 이제 저장소에서 Mark된 개체와 그렇지 않은 개체로 나뉨.
5. 가비지 컬렉터는 저장소에서 Mark 되지 않는 개체(가비지)들의 메모리 회수(Sweep)
- 관리되는 모든 언리얼 오브젝트의 정보를 저장하는 전역 변수 GUObjectArray
GUObjectArray의 각 요소에는 플래그 정보가 있음.
- 가비지 컬렉터가 참고하는 주요 플래그
Garbage 플래그: 다른 언리얼 오브젝트로부터 참조가 없어서 회수 예정인 오브젝트
RootSet 플래그: 다른 언리얼 오브젝트로부터 참조가 없어도 회수되지 않는 오브젝트
- 지정된 주기(GCCycle이라고 함)마다 몰아서 없애도록 설정되어 있음.
Project Settings > Garbage Collection에서 확인 가능.
성능 저하를 막기 위해서 병렬처리, 클러스터링과 같은 기능을 탑재함.
Garbage 플래그가 Set된 오브젝트를 파악하고 메모리를 안전하게 회수함.
Garbage 플래그는 수동으로 설정하는 것이 아닌, 시스템이 알아서 설정함.
그래서 한 번 생성된 언리얼 오브젝트는 바로 삭제가 불가능함.
- 언리얼 오브젝트를 통한 포인터 문제의 해결
메모리 누수 문제
언리얼 오브젝트는 가비지 컬렉터를 통해 자동으로 해결됨.
댕글링 포인터 문제
언리얼 오브젝트는 이를 탐지하기 위한 함수를 제공함 ::IsValid()
nullptr인지 아닌지만 확인하면 댕글링 포인터 문제가 발생할 수 있음.
와일드 포인터 문제
언리얼 오브젝트에 UPROPERTY() 속성을 지정하면 자동으로 nullptr로 초기화 해줌.
UPROPERTY() 매크로로 지정되지 않은 속성의 경우에는 언리얼 가비지 컬렉터에 의해
메모리 관리가 되지 않음에 주의.
- 언리얼 스마트 포인터 라이브러리 [참고자료]
TUniquePtr: 지정된 곳에서만 메모리를 관리하는 포인터
특정 오브젝트에게 명확하게 포인터 해지 권한을 주고 싶은 경우에 사용.
delete 구문 없이 함수 실행 후 자동으로 소멸시키고 싶을때 사용함.
TSharedPtr: 더이상 사용되지 않으면 자동으로 메모리를 해지하는 포인터
여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우.
다른 함수로부터 할당된 오브젝트를 Out으로 받는 경우.
nullptr일 수도 있기에 주의.
TSharedRef: 공유 포인터와 동일하지만, 유효한 개체를 항상 보장받는 레퍼런스
여러 로직에서 할당된 오브젝트가 공유해서 사용되는 경우.
Not null을 보장받으며, 오브젝트를 편리하게 사용하고 싶은 경우.
TWeakPtr: 참조 카운팅과 관련없음. TSharedPtr을 통해 만들어짐.
TSharedPtr이 사라지면 해당 TWeakPtr도 무효화됨. 사용 전 nullptr인지 체크 필요.
2.2-5 Unreal Serialization
- 오브젝트 그래프
하나의 개체는 다른 개체를 멤버 변수로 갖고 있을 수 있음.
이런 관계를 오브젝트 그래프라고 함.
- 직렬화
오브젝트나 오브젝트 그래프을 바이트 스트림으로 변환하는 과정
복잡한 데이터를 일렬로 세우기 때문에 직렬화라고 부름.
거꾸로 복구시키는 과정도 포함해서 의미함.
시리얼라이제이션: 오브젝트 그래프에서 바이트 스트림으로
디시리얼라이제이션: 바이트 스트림에서 오브젝트 그래프로
- 직렬화가 가지는 장점
현재 프로그램의 상태를 저장하고 필요할 때 복원할 수 있음.(게임의 저장)
현재 개체의 정보를 클립보드에 복사해서 다른 프로그램에서 활할 수 있음.
네트워크를 통해 현재 프로그램의 상태를 다른 컴퓨터에 복원할 수 있음.(멀티플레이)
데이터 압축, 암호화를 통해 데이터를 효율적이고 안전하게 보관할 수 있음.

- 직렬화 구현시 고려할 점
데이터 레이아웃: 오브젝트가 소유한 다양한 데이터를 어떻게 변환할 것인가
이식성: 서로 다른 시스템에 전송해도 이식될 수 있게끔 할것인가(.ex 리틀엔디안/빅엔디안)
버전 관리: 새로운 기능이 추가될 때 이를 어떻게 확장하고 처리할 것인가(기존의 직렬화가 무효화됨)
성능: 네트워크 비용을 줄이기 위해 어떤 데이터 형식을 사용할 것인가(회전을 모두 다 보낼 필욘 없음. 양자화)
보안: 데이터를 어떻게 안전하게 보호할 것인가
에러 처리: 전송 과정에서 문제가 발생할 경우 이를 어떻게 인식하고 처리할 것인가
- 언리얼 엔진의 직렬화 시스템
직렬화 시스템을 위해 언리얼에서 제공하는 클래스 FArchive와 << 연산자
다양한 아카이브 클래스의 제공
메모리 아카이브(FMemoryReader, FMemoryWriter)
파일 아카이브(FArchiveFileReaderGeneric, FArchiveFileWriterGeneric)
기타 언리얼 오브젝트와 관련된 아카이브 클래스(FArchiveUObject)
Json 직렬화 기능도 별도의 라이브러리로 제공하고 있음.
- USTRUCT
데이터 저장/전송에 특화된 가벼운 개체
GENERATED_BODY 매크로를 내부에 작성해줌.
제한적인 프로퍼티 시스템, 직렬화와 같은 유용한 기능을 지원함.
GENERATED_BODY를 선언한 구조체는 UScriptStruct 클래스로 구현됨.
UPROPERTY()만 선언할 수 있고, UFUNCTION()은 선언할 수 없음.
스택 메모리에 저장됨. 힙 메모리 할당(포인터 연산) 없음. 저장/전송에 특화되기 때문.
NewObject API를 사용할 수 없음.
- 언리얼 리플렉션 관련 계층 구조

- 직렬화 실습
<hide/>
// SPigeon.h
...
class STUDYPROJECT_API USPigeon
...
{
...
public:
...
const FString& GetName() const { return Name; }
void SetName(const FString& InName) { Name = InName; }
int32 GetID() const { return ID; }
void SetID(int32 InID) { ID = InID; }
virtual void Serialize(FArchive& Ar) override;
private:
...
UPROPERTY()
int32 ID;
};
<hide/>
// SPigeon.cpp
...
USPigeon::USPigeon()
{
Name = TEXT("SPigeon CDO");
ID = 0;
}
...
void USPigeon::Serialize(FArchive& Ar)
{
Super::Serialize(Ar);
Ar << Name;
Ar << ID;
}
<hide/>
// SFlyable.h
...
USTRUCT()
struct FBirdData
{
GENERATED_BODY()
public:
FBirdData() {}
FBirdData(const FString& InName, int32 InID)
: Name(InName)
, ID(InID)
{
}
friend FArchive& operator<<(FArchive& Ar, FBirdData& InBirdData)
{
Ar << InBirdData.Name;
Ar << InBirdData.ID;
return Ar;
}
UPROPERTY()
FString Name = TEXT("Bird");
UPROPERTY()
int32 ID = 0;
};
..
class USFlyable : public UInterface
{
...
};
/**
*
*/
class STUDYPROJECT_API ISFlyable
{
...
};
<hide/>
// SGameInstance.h
...
class STUDYPROJECT_API USGameInstance : public UGameInstance
{
...
public:
...
private:
...
UPROPERTY()
TObjectPtr<class USPigeon> SerializedPigeon;
// 클래스의 헤더 파일을 인클루드 하지 않고, 해당 클래스 이름 앞에 class 키워드를 적는 것을 전방선언이라 함.
// 헤더 파일에서 다른 헤더 파일을 참조하면, 다른 헤더 파일이 수정되었을 때 이 헤더 파일도 함께 컴파일 됨.
// 따라서 꼭 필요한 경우가 아니라면 헤더 파일에서 다른 헤더파일을 인클루드 하지 않고 전방선언을 활용함.
};
<hide/>
// SGameInstance.cpp
...
void USGameInstance::Init()
{
Super::Init();
FBirdData SrcRawData(TEXT("Pigeon17"), 17);
UE_LOG(LogTemp, Log, TEXT("[SrcRawData] Name: %s, ID: %d"), *SrcRawData.Name, SrcRawData.ID);
const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));
UE_LOG(LogTemp, Log, TEXT("SavedDir: %s"), *SavedDir);
const FString RawDataFileName(TEXT("RawData.bin"));
FString AbsolutePathForRawData = FPaths::Combine(*SavedDir, *RawDataFileName);
UE_LOG(LogTemp, Log, TEXT("Relative path for saved file: %s"), *AbsolutePathForRawData);
FPaths::MakeStandardFilename(AbsolutePathForRawData);
UE_LOG(LogTemp, Log, TEXT("Absolute path for saved file: %s"), *AbsolutePathForRawData);
FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*AbsolutePathForRawData);
if (nullptr != RawFileWriterAr)
{
*RawFileWriterAr << SrcRawData;
RawFileWriterAr->Close();
delete RawFileWriterAr;
RawFileWriterAr = nullptr;
}
FBirdData DstRawData;
FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*AbsolutePathForRawData);
if (nullptr != RawFileReaderAr)
{
*RawFileReaderAr << DstRawData;
RawFileReaderAr->Close();
delete RawFileReaderAr;
RawFileReaderAr = nullptr;
UE_LOG(LogTemp, Log, TEXT("[DstRawData] Name: %s, ID: %d"), *DstRawData.Name, DstRawData.ID);
}
SerializedPigeon = NewObject<USPigeon>();
SerializedPigeon->SetName(TEXT("Pigeon76"));
SerializedPigeon->SetID(76);
UE_LOG(LogTemp, Log, TEXT("[SerializedPigeon] Name: %s, ID: %d"), *SerializedPigeon->GetName(), SerializedPigeon->GetID());
const FString ObjectDataFileName(TEXT("ObjectData.bin"));
FString AbsolutePathForObjectData = FPaths::Combine(*SavedDir, *ObjectDataFileName);
FPaths::MakeStandardFilename(AbsolutePathForObjectData);
TArray<uint8> BufferArray;
FMemoryWriter MemoryWriterAr(BufferArray);
SerializedPigeon->Serialize(MemoryWriterAr);
TUniquePtr<FArchive> ObjectDataFileWriterAr = TUniquePtr<FArchive>(IFileManager::Get().CreateFileWriter(*AbsolutePathForObjectData));
if (nullptr != ObjectDataFileWriterAr)
{
*ObjectDataFileWriterAr << BufferArray;
ObjectDataFileWriterAr->Close();
ObjectDataFileWriterAr = nullptr; //delete ObjectDataFileWriterAr; 와 같은 효과.
}
TArray<uint8> BufferArrayFromObjectDataFile;
TUniquePtr<FArchive> ObjectDataFileReaderAr = TUniquePtr<FArchive>(IFileManager::Get().CreateFileReader(*AbsolutePathForObjectData));
if (nullptr != ObjectDataFileReaderAr)
{
*ObjectDataFileReaderAr << BufferArrayFromObjectDataFile;
ObjectDataFileReaderAr->Close();
ObjectDataFileReaderAr = nullptr;
}
FMemoryReader MemoryReaderAr(BufferArrayFromObjectDataFile);
USPigeon* Pigeon77 = NewObject<USPigeon>();
Pigeon77->Serialize(MemoryReaderAr);
UE_LOG(LogTemp, Log, TEXT("[Pigeon77] Name: %s, ID: %d"), *Pigeon77->GetName(), Pigeon77->GetID());
}
...
- 전방선언의 이점
전방 선언은 header 파일에서 같은 모듈에 있는 다른 header 파일을 참조하지 않아도 되게끔 함.
즉, 다른 header 파일의 변경사항이 있을때 해당 header 파일은 재컴파일되지 않음.
- Json 포맷
웹 환경에서 서버와 클라이언트 사이에 데이터를 주고 받을 때 사용하는 텍스트 기반 데이터 포맷
Json 장점
텍스트임에도 데이터 크기가 가벼움.
읽기 편해서 데이터를 보고 이해할 수 있음.
사실상 웹 통신의 표준으로 널리 사용됨.
Json 단점
지원하는 타입이 몇 가지 안됨.(문자, 숫자, 불리언, 널, 배열, 오브젝트만 가능)
텍스트 형식으로만 사용할 수 있어서 극도의 효율 추구는 불가능.
- Json 데이터 유형
오브젝트 {}
오브젝트 내 데이터는 키-벨류 조합으로 구성됨. ex) { "key" : 10 }
배열 []
배열 내 데이터는 벨류로만 구성됨. ex) [ "value1", "value2", "value3" ]
이외 데이터
문자열("string"), 숫자(10, 3.141592), 불리언(true, false), 널(null)로 구성.

- 클라이언트 개발자가 Json을 왜 알아야 할까?
아무래도 현실적으로 스타트업이나 중소기업에 취직할 확률이 높음.
그럼 신입이어도 할 수 있는 일이 굉장히 많아지고, 회의중에 서버 개발자와 대화하는 일이 많아짐.
Json을 사용하게 될 때, 아에 모른다면 대화 자체가 안됨.
- Json 관련 모듈 추가 실습
언리얼은 모듈 시스템으로 필요한 기능들을 추가함.
프로젝트 폴더 > Source > StudyProject > StudyProject.Build.cs 파일을 수정해서 아래와 같이 작성.
<hide/>
// StudyProject.Build.cs
using UnrealBuildTool;
public class StudyProject : ModuleRules
{
public StudyProject(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] {
// InitialModules
"Core", "CoreUObject", "Engine", "InputCore",
// JsonModules
"Json", "JsonUtilities"
});
PrivateDependencyModuleNames.AddRange(new string[] { });
}
}
- Json 직렬화 실습
<hide/>
// SGameInstance.cpp
...
#include "JsonObjectConverter.h"
#include "UObject/SavePackage.h"
USGameInstance::USGameInstance()
{
}
void USGameInstance::Init()
{
...
const FString JsonDataFileName(TEXT("StudyJsonFile.txt"));
FString AbsolutePathForJsonData = FPaths::Combine(*SavedDir, *JsonDataFileName);
FPaths::MakeStandardFilename(AbsolutePathForJsonData);
TSharedRef<FJsonObject> SrcJsonObject = MakeShared<FJsonObject>();
FJsonObjectConverter::UStructToJsonObject(SerializedPigeon->GetClass(), SerializedPigeon, SrcJsonObject);
FString JsonOutString;
TSharedRef<TJsonWriter<TCHAR>> JsonWriterAr = TJsonWriterFactory<TCHAR>::Create(&JsonOutString);
if (true == FJsonSerializer::Serialize(SrcJsonObject, JsonWriterAr))
{
FFileHelper::SaveStringToFile(JsonOutString, *AbsolutePathForJsonData);
}
FString JsonInString;
FFileHelper::LoadFileToString(JsonInString, *AbsolutePathForJsonData);
TSharedRef<TJsonReader<TCHAR>> JsonReaderAr = TJsonReaderFactory<TCHAR>::Create(JsonInString);
TSharedPtr<FJsonObject> DstJsonObject;
if (true == FJsonSerializer::Deserialize(JsonReaderAr, DstJsonObject))
{
USPigeon* Pigeon78 = NewObject<USPigeon>();
if (true == FJsonObjectConverter::JsonObjectToUStruct(DstJsonObject.ToSharedRef(), Pigeon78->GetClass(), Pigeon78))
{
UE_LOG(LogTemp, Log, TEXT("[Pigeon78] Name: %s, ID: %d"), *Pigeon78->GetName(), Pigeon78->GetID());
}
}
}
...
2.3 언리얼 자료구조
2.3-1 언리얼의 문자열
- 기존 문자열 인코딩 방식
Single-Byte(ANSI, ASCII)
Multi-Byte(EUC-KR, CP949, ...)
Unicode(UTF-8, UTF-16, ...)
- 문자열 인코딩 방식은 플랫폼마다 다르지만 나라마다도 다름.
이또한 게임 개발의 유지보수에 큰 영향을 끼침.
- 언리얼의 문자열 인코딩 방식
언리얼은 내부적으로 UTF-16 방식을 채택함.
파일(npc 대사, ...)을 읽을 때도 UTF-16으로 읽어들임.
단, 소스코드의 경우에는 컴파일 에러가 날 수 있으므로 가급적 영문 작성 권장.
소스코드에 한글이 꼭 필요한 경우(주석, ...)에는 파일의 인코딩을 UTF-8로 변환.
- TEXT() 매크로
언리얼은 UTF-16을 위한 문자열을 생성할 때 TEXT() 매크로를 제공함.
- TCHAR와 FString 실습
<hide/>
// SGameInstance.cpp
#include "SGameInstance.h"
USGameInstance::USGameInstance()
{
}
void USGameInstance::Init()
{
Super::Init();
TCHAR LogCharArray[] = TEXT("Hello Unreal"); // UTF-16을 위한 언리얼 표준 캐릭터 타입 TCHAR. TCHAR 자료형 변수에 문자열을 저장하기 위해서는 항상 TEXT() 매크로 사용.
UE_LOG(LogTemp, Log, LogCharArray);
FString LogCharString = LogCharArray; // 문자열을 자유롭게 조작하고 싶다면, TCHAR 배열 대신 FString을 사용해야 함. FString은 TCHAR 배열을 포함하는 헬퍼 클래스.
// FString LogCharString = FString(TEXT("Hello Unreal")); 같은 코드.
UE_LOG(LogTemp, Log, TEXT("%s"), *LogCharString); // FString을 그대로 사용하면 TCHAR 배열을 반환되지 않음. 포인터 연산자를 붙혀줘야 실제 문자열이 반환됨.
// 결국 FString은 TArray<TCHAR> 멤버를 갖고 있다는 뜻.
const TCHAR* LongCharPtr = *LogCharString;
TCHAR* LogCharDataPtr = LogCharString.GetCharArray().GetData();
TCHAR LogCharArrayWithSize[100];
FCString::Strcpy(LogCharArrayWithSize, LogCharString.Len(), *LogCharString); // C 문자열 라이브러리에서 제공하는 문자열 처리 함수(strstr(), ...)를 제공하는 클래스 FCString. 다만 사용이 안전하다는건 보장받지 못함.
if (LogCharString.Contains(TEXT("unreal"), ESearchCase::IgnoreCase)) // IgnoreCase는 대소문자 구분없이 진행.
{
int32 Index = LogCharString.Find(TEXT("unreal"), ESearchCase::IgnoreCase);
FString EndString = LogCharString.Mid(Index); // "unreal" 문자열이 시작되는 곳에서부터 마지막까지 자름.
UE_LOG(LogTemp, Log, TEXT("Find Test: %s"), *EndString);
}
FString Left, Right;
if (LogCharString.Split(TEXT(" "), &Left, &Right)) // 공백을 기준으로 나눔.
{
UE_LOG(LogTemp, Log, TEXT("Split Test: %s and %s"), *Left, *Right);
// File > Save as ... > Save 버튼 우측 역삼각형 클릭 > Save with Encoding > Encoding에 UTF-8로 지정 해줘야 Output log에 한글이 제대로 출력됨.
}
int32 IntValue = 32;
float FloatValue = 3.141592;
FString FloatIntString = FString::Printf(TEXT("Int:%d Float:%f"), IntValue, FloatValue);
FString FloatString = FString::SanitizeFloat(FloatValue); // float 자료형의 표준은 상당히 복잡함. 이를 정돈해서 문자열로 바꿔줌.
FString IntString = FString::FromInt(IntValue);
UE_LOG(LogTemp, Log, TEXT("%s"), *FloatIntString);
UE_LOG(LogTemp, Log, TEXT("Int:%s Float:%s"), *IntString, *FloatString);
int32 IntValueFromString = FCString::Atoi(*IntString);
float FloatValueFromString = FCString::Atof(*FloatString);
FString FloatIntString2 = FString::Printf(TEXT("Int:%d Float:%f"), IntValueFromString, FloatValueFromString);
UE_LOG(LogTemp, Log, TEXT("%s"), *FloatIntString2);
}
- FName과 FText
FString은 FName과 FText로 변환해서 사용 가능함.
FName
애셋 관리를 위한 문자열 클래스
애셋을 빠르게 찾고 싶을 때, 문자열로 지정해주는게 사람에겐 편함.
그러나 문자열을 그대로 이용하면 연산량이 늘어남.
내부적으로는 hash value로 변환하는게 FName 클래스.
대소문자 구분이 없음. 한번 선언되면 int와 같은 정수로 변환됨. 즉 문자열 변경이 불가능.
바꾸고자 하면 다시 문자열로 만들어야 하는데,
대소문자 구분이 없어서 원본문자열로 돌아간다는 보장이 없음.
- FText
다국어 지원을 위한 문자열 클래스. UI에서 자주 사용.
일종의 키로 작용. 별도의 문자열 테이블 정보가 추가로 요구됨.
게임 빌드시 자동으로 다양한 국가별 언어로 변환됨.
- FName 구조와 활용
언리얼은 FName과 관련된 전역 Pool 자료구조를 가지고 있음.
- FName과 전역 Pool
문자열이 들어오면 해시 값을 추출 후 키를 생성해서 FName에 보관
FName 값에 저장된 키를 사용해서 전역 Pool에서 원하는 자료를 검색해 보관
문자 정보는 대소문자를 구분하기 않고 저장함.(IgnoreCase)
FName의 형성
생성자에 문자열 정보를 넣으면 전역 Pool을 조사해서 적당한 키로 변환하는 작업이 수반됨.
즉, FindOrAdd 작업이 진행됨.
결국 FName은 Key와 Value 쌍이고, 전역 Pool에는 기존의 FName들이 들어있음.
- FName 실습 예제
<hide/>
// SGameInstance.cpp
...
void USGameInstance::Init()
{
...
FName key1(TEXT("PELVIS"));
FName key2(TEXT("pelvis"));
UE_LOG(LogTemp, Log, TEXT("Compare FName: %s"), key1 == key2 ? TEXT("Same") : TEXT("Different")); // IgnoreCase
for (int i = 0; i < 10000; ++i)
{
FName SearchInNamePool = FName(TEXT("pelvis"));
const static FName StaticOnlyOnce(TEXT("pelvis")); // FName은 결국 전역 Pool을 조사해보는 작업이 수반됨. 그래서 static 키워드를 통해서 재조사가 이뤄지지 않게끔 함.
}
}
2.3-2 Array and Set
- 언리얼 엔진이 자체 제작해 제공하는 자료구조 라이브러리
줄여서 UCL이라고도 함.
언리얼 오브젝트를 안정적으로 지원하고 다수의 오브젝트 처리에 유용함.
실제 게임 제작에는 거의 TArray/TSet/TMap만 사용함.
- C++ STL Vs. UCL
STL은 범용적임. UCL은 언리얼 엔진에 특화되어 있음.
STL은 호환성이 높음. UCL은 언리얼 오브젝트에 특화됨.
STL은 많은 기능이 구현되어 있어서 컴파일 시간이 오래 걸림.
UCL은 게임 제작에 최적화되어 있음.
- TArray, TSet, TMap
TArray
STL의 Vector와 유사함. 오브젝트를 순서대로 담아 관리하는 용도.
TSet
STL의 Unordered Set과 유사함. 중복되지 않는 요소로 구성된 집합을 만드는 용도.
TMap
STL의 Unordered Map과 유사함. 중복되지 않은 키-벨류 쌍의 레코드를 관리하는 용도.
- TArray 개요
게임 제작에서는 TArray 같은 가변 배열 자료구조를 효과적으로 활용하는 것이 중요함.
데이터가 순차적으로 모여있기 때문에 메모리를 효과적으로 사용할 수 있고 캐시 효율이 높음.
컴퓨터 사양이 좋아지면서, 캐시 지역성으로 인한 성능 향상은 굉장히 중요함.
임의 데이터의 접근이 빠르고, 고속으로 요소를 순회하는 것이 가능함.
가변배열의 단점은 중간에 요소를 삽입/삭제하는 비용이 크다는 것.
데이터가 많아질수록 검색, 삽입, 삭제 작업이 느려지므로
많은 수의 데이터를 검색, 삽입, 삭제 하는 경우에는 TSet을 사용하는 것이 좋음.
프리미티브 타입의 경우엔 Add()를 사용해서 가독성을 고려하고,
개체의 경우엔 Emplace()를 사용해서 성능을 고려하는게 권장됨.
- TArray 실습
<hide/>
// SGameInstance.cpp
...
#include "Algo/Accumulate.h" // Accumulate()
...
void USGameInstance::Init()
{
Super::Init();
const int32 ArraySize = 10;
TArray<int32> IntArray;
for (int32 i = 1; i <= ArraySize; ++i)
{
IntArray.Add(i);
}
for (int32 Item : IntArray)
{
static int32 i = 0;
UE_LOG(LogTemp, Log, TEXT("[%d]: %d"), i++, Item);
}
UE_LOG(LogTemp, Log, TEXT("========"));
IntArray.RemoveAll(
[](int32 InValue)
{
return InValue % 2 == 0;
}
);
for (int32 Item : IntArray)
{
static int32 i = 0;
UE_LOG(LogTemp, Log, TEXT("[%d]: %d"), i++, Item);
}
UE_LOG(LogTemp, Log, TEXT("========"));
IntArray += {2, 4, 6, 8, 10};
for (int32 Item : IntArray)
{
static int32 i = 0;
UE_LOG(LogTemp, Log, TEXT("[%d]: %d"), i++, Item);
}
UE_LOG(LogTemp, Log, TEXT("========"));
TArray<int32> IntArrayCompare;
int32 CArray[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8 , 10 };
IntArrayCompare.AddUninitialized(ArraySize);
FMemory::Memcpy(IntArrayCompare.GetData(), CArray, sizeof(int32) * ArraySize);
UE_LOG(LogTemp, Log, TEXT("IntArray == IntArrayCompare ? %d"), IntArray == IntArrayCompare);
int32 Sum = 0;
for (const int32& Element : IntArray)
{
Sum += Element;
}
UE_LOG(LogTemp, Log, TEXT("IntArray's sum == %d"), Sum);
int32 SumByAlgo = Algo::Accumulate(IntArray, 0);
UE_LOG(LogTemp, Log, TEXT("IntArray's SumByAlgo == %d"), SumByAlgo);
}
...
- STL Set Vs. TSet Vs. TArray
| STL Set | UCL TSet |
| 이진 트리 기반. 정렬을 지원함. | 해시 테이블 기반. 빠른 검색이 가능함. |
| 메모리 구성이 효율적이지 않음. | 동적 배열의 형태로 메모리 구성이 효율적임. |
| 요소가 삭제될 때 균형을 위한 재구축이 일어날 수 있음. | 삭제해도 재구축이 일어나지 않음. |
| 모든 자료를 순회하는데 적합하지 않음. | 빠르게 순회할 수 있음. |
| 비어있는 요소가 있을 수 있음. 추후에 삽입되는 데이터가 비어있는 요소를 채움. |
| TArray (캐시 지역성, 임의접근) |
TSet (빠른 중복 감지) |
|
| 접근 | O(1) | O(1) |
| 검색 | O(N) | O(1) |
| 삽입 | O(N) | O(1) |
| 삭제 | O(N) | O(1) |
- TSet 실습
<hide/>
// SGameInstance.cpp
#include "Game/SGameInstance.h"
#include "Examples/SUnrealObjectClass.h"
#include "Examples/SFlyable.h"
#include "Examples/SPigeon.h"
#include "JsonObjectConverter.h"
#include "UObject/SavePackage.h"
#include "Algo/Accumulate.h" // Accumulate()
USGameInstance::USGameInstance()
{
Name = TEXT("USGameInstance Class Default Object");
}
void USGameInstance::Init()
{
Super::Init();
const int32 SetSize = 10;
TSet<int32> IntSet;
for (int32 ix = 1; ix <= SetSize; ++ix)
{
IntSet.Add(ix);
}
for (int32 Element : IntSet)
{
static int32 i = 0;
UE_LOG(LogTemp, Log, TEXT("[%d]: %d"), i++, Element);
}
UE_LOG(LogTemp, Log, TEXT("========"));
IntSet.Remove(2);
IntSet.Remove(4);
IntSet.Remove(6);
IntSet.Remove(8);
IntSet.Remove(10);
for (int32 Element : IntSet)
{
static int32 i = 0;
UE_LOG(LogTemp, Log, TEXT("[%d]: %d"), i++, Element);
}
UE_LOG(LogTemp, Log, TEXT("========"));
IntSet.Add(2);
IntSet.Add(4);
IntSet.Add(6);
IntSet.Add(8);
IntSet.Add(10);
for (int32 Element : IntSet)
{
static int32 i = 0;
UE_LOG(LogTemp, Log, TEXT("[%d]: %d"), i++, Element);
}
UE_LOG(LogTemp, Log, TEXT("========"));
}
...
- TMap의 특징
STL의 unordered_map과 유사함. 키-벨류 쌍 자료구조가 필요한 경우에 광범위하게 사용됨.
비어있는 요소가 있을 수 있고, TMultiMap을 사용하면 중복 데이터도 관리할 수 있음.
| STL Map | UCL TMap |
| 이진 트리 기반. | 해시 테이블 기반. 빠른 검색 가능. |
| 메모리 구성이 비효율적. | 동적 배열 형태라 메모리 구성이 효율적. |
| 데이터 삭제시 재구축이 일어날 수 있음. | 재구축이 일어나지 않음. |
| 모든 자료를 순회하는데 적합하지 않음. | 빠르게 순회할 수 있음. |

- 해시 테이블 기반의 TSet과 TMap의 주의 사항
커스텀 자료형을 만들어서 키 값으로 사용하려면
특정 연산자를 오버로딩 해줘야함. 이에 대한 내용은 [여기]에서 확인할 수 있음.
- TMap 실습
<hide/>
// SFlyable.h
...
struct FBirdData
{
...
public:
...
friend FArchive& operator<<(FArchive& Ar, FBirdData& InBirdData)
{
...
}
bool operator==(const FBirdData& InOtherBirdData) const
{
return ID == InOtherBirdData.ID;
}
friend FORCEINLINE uint32 GetTypeHash(const FBirdData& InOtherBirdData)
{
return GetTypeHash(InOtherBirdData.ID);
}
...
};
...
<hide/>
// SGameInstance.cpp
...
void USGameInstance::Init()
{
Super::Init();
TMap<int32, FString> BirdMap;
BirdMap.Add(5, TEXT("Pigeon"));
BirdMap.Add(2, TEXT("Owl"));
BirdMap.Add(7, TEXT("Albatross"));
// BirdMap == [
// { Key: 5, Value: "Pigeon" },
// { Key: 2, Value: "Owl" },
// { Key: 7, Value: "Albatross" }
// ]
BirdMap.Add(2, TEXT("Penquin"));
// BirdMap == [
// { Key: 5, Value: "Pigeon" },
// { Key: 2, Value: "Penquin" },
// { Key: 7, Value: "Albatross" }
// ]
FString* BirdIn7 = BirdMap.Find(7);
// *BirdIn7 == "Albatross"
FString* BirdIn8 = BirdMap.Find(8);
// *BirdIn8 == nullptr
}
...
- UCL 핵심 자료구조의 시간 복잡도
| TArray (캐시지역성, 임의접근) |
TSet (빠른 중복 감지) |
TMap (키-벨류 관리) |
TMultiMap (중복 허용 키-벨류) |
|
| 접근 | O(1) | O(1) | O(1) | O(1) |
| 검색 | O(N) | O(1) | O(1) | O(1) |
| 삽입 | O(N) | O(1) | O(1) | O(1) |
| 삭제 | O(N) | O(1) | O(1) | O(1) |
2.4 Delegate
2.4-1 델리게이트
- Publisher-Subscriber 패턴
발행자와 구독자, 그 사이에 브로커(Broker)가 있는 패턴.
Pub는 Bro에게 컨텐츠가 생산되었음을 알림.
Bro는 Sub에게 컨텐츠가 생산되었음을 알림.
Pub와 Sub는 서로를 몰라도, Bro를 통해 컨텐츠를 생산하고 소비할 수 있음.
- Publisher-Subscriber 패턴의 장점
Pub쪽은 Bro와의 통신만 잘 연결해주면 끝. 굳이 Sub까지 고려할 필요 없음.
Sub쪽도 Bro와의 통신만 잘 연결되면 끝. 따라서, 한쪽이 에러나도 다른 쪽에 영향이 없음.
이는 유지 보수가 쉽다는 것을 뜻함.
다만 서로 반대 쪽의 상황을 알 수가 없음. 무슨일을 하는지 자세하게 파악 불가.
- 언리얼 엔진의 Publisher-Subscriber 패턴
언리얼에서는 델리게이트 기능을 지원함.
델리게이트의 사전적 의미는 대리자. 즉 위에서 설명한 Publisher.
Broker는 Broadcast(), Subscriber는 Add() 함수로 가능함.
간단히 말하자면, 특정 이벤트 발생 시 호출되어질 함수를 등록하는 것.
- 언리얼 델리게이트 선언시 고려사항
전달방식과 프로그래밍 환경
C++ 프로그래밍에서만 사용 / C++ 프로그래밍과 블루프린트 프로그래밍 사용
일대일 전달 / 일대다 전달
전달 받을 함수 시그니처
매개변수 자료형과 그 갯수
전달 받을 함수 등록 방식
전역 C++ 함수 포인터와 연결
언리얼 오브젝트의 멤버 함수와 연결(대부분 이 방식)
- 언리얼 델리게이트 선언 매크로
DECLARE_{델리게이트유형}_DELEGATE_{함수정보}
- 전달 방식과 프로그래밍 환경
| 일대일 + C++ | DECLARE_DELEGATE |
| 일대다 + C++ | DECLARE_MULTICAST |
| 일대일 + C++ & 블루프린트 | DECLARE_DYNAMIC |
| 일대다 + C++ & 블루프린트 | DECLARE_DYNAMIC_MULTICAST |
- 전달 받을 함수 시그니처
| 인자 없음 + 반환값 없음 | 공란 (ex. DECLARE_DELEGATE) |
| 인자 한개 + 반환값 없음 | OneParam (ex. DECLARE_DELEGATE_OneParam) |
| 인자 세개 + 반환값 있음 (인자는 최대 9개까지 지원함.) |
RetVal_ThreeParams (ex. DECLARE_DELEGATE_RetVal_ThreeParams) |
- 전달 받을 함수 등록 방식
| 싱글 캐스트 델리게이트에 등록시 | Bind- |
| 멀티 캐스트 델리게이트에 등록시 | Add- |
| UObject 기반 멤버 함수를 등록시 | -UObject() |
| 전역 C++ 함수 포인터를 등록시 | -Static() |
- 언리얼 델리게이트 예제
<hide/>
// SPigeon.h
...
DECLARE_MULTICAST_DELEGATE_OneParam(FOnPigeonFly, const FString&);
...
class STUDYPROJECT_API USPigeon
...
{
...
public:
...
FOnPigeonFly OnPigeonFly;
private:
...
};
<hide/>
// SPigeon.cpp
...
void USPigeon::Fly()
{
UE_LOG(LogTemp, Log, TEXT("%s is now flying."), *Name);
OnPigeonFly.Broadcast(*Name);
}
...
<hide/>
// SGameInstance.h
...
class STUDYPROJECT_API USGameInstance : public UGameInstance
{
...
public:
virtual void Init() override;
virtual void Shutdown() override;
void HandlePigeonFly(const FString& InName);
private:
...
FDelegateHandle OnPigeonFlyDelegateHandle;
};
<hide/>
// SGameInstance.cpp
...
void USGameInstance::Init()
{
...
USPigeon* Pigeon = NewObject<USPigeon>();
OnPigeonFlyDelegateHandle = Pigeon->OnPigeonFly.AddUObject(this, &ThisClass::HandlePigeonFly);
Pigeon->Fly();
}
void USGameInstance::Shutdown()
{
if (true == OnPigeonFlyDelegateHandle.IsValid())
{
OnPigeonFlyDelegateHandle.Reset();
}
}
void USGameInstance::HandlePigeonFly(const FString& InName)
{
UE_LOG(LogTemp, Log, TEXT("[USGameInstance] %s is now flying."), *InName);
}
- 다이나믹 델리게이트(Dynamic Delegate)
블루프린트로 로직을 작성할 경우, 속성이나 함수를 런타임 중에 생성(동적 생성)이 가능해야함.
Native C++로만은 불가능함. 따라서 블루프린트와 관련된 속성들은 UPROPERTY() 매크로,
멤버 함수들은 UFUNCTION() 매크로를 달아줘야 함. 이를 통해서 동적 생성을 가능케함.
- 다만, AddDynamic() 함수는 인텔리센스에의해 검색되지 않음.
일단 무시하고 작성해도 컴파일 에러 없음.
2.4-2 델리게이트와 익명함수
- 다이내믹 델리게이트에는 C++ 람다식을 바인딩 할 수 없음.
2.5 깃과 언리얼 프로젝트
2.5-1 언리얼 프로젝트 버전 관리
- 언리얼 프로젝트 버전 관리 준비
깃을 활용한 버전 관리를 해보고자 함.
깃에 입문 하면서 동시에 개인 프로젝트를 면접관 분들께 보여주기 위함.
추후 팀 프로젝트를 한다면 깃을 통해서 여러 사람이 동시에 개발 가능.
- 깃 가입 및 레포지토리 추가
"StudyProject" 레포지토리 추가.
Private 지정.
Add .gitignore에 "UnrealEngine" 지정.
- StudyProject 클론
빈 폴더 생성 후 주소창에 "cmd" 타이핑 후 엔터.
프롬프트 창에 "git clone (레포지토리주소)" 타이핑 후 엔터.
"cd StudyProject" 타이핑 후 엔터.
- LFS 설정
언리얼의 애셋은 하나의 파일이 100MB가 넘는 경우가 있음.
이 경우엔 Git의 Large File Storage를 설정해줘야함.
열려있는 프롬프트 창에 "git lfs install" 타이핑 후 엔터.
"git lfs track "*.psd"" 타이핑 후 엔터. 그럼 StudyProject 폴더에 .gitattributes 파일이 생김.
아래 내용을 복사해서 .gitattribute 파일에 붙혀넣기.
<hide/>
# UE file types
*.uasset filter=lfs diff=lfs merge=lfs -text
*.umap filter=lfs diff=lfs merge=lfs -text
# Raw Content types
*.fbx filter=lfs diff=lfs merge=lfs -text
*.3ds filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.xcf filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
- Commit and Push
설정된 내용을 업데이트하기 위해서 아래 그림처럼 타이핑 후 엔터.

2.5-2 언리얼 프로젝트 원격에 올리기
- 프로젝트 리빌드
1. .vs 폴더, Binaries 폴더, Intermediate 폴더, Saved 폴더, .sln 파일 삭제.
2. 삭제되지 않은 모든 파일 블럭 지정 후 복사.
3. git 폴더에 붙혀넣기.
4. .uproject 파일 우클릭 > 추가 옵션 표시 > Generated Visual Studio project file 클릭.
5. .sln 파일 더블클릭 > F5
- Source tree 다운로드
설치 및 실행. 깃 설정.
프로젝트 폴더 추가.

- Commit and Push
언리얼 에디터 종료하고 Commit 후 Push.