futurelabunseen / B-JeonganLee

UNSEEN 2nd Term Learning and Project Repo.
5 stars 0 forks source link

11강: 언리얼 컨테이너 라이브러리 II - 구조체와 Map #44

Closed fkdl0048 closed 4 months ago

fkdl0048 commented 4 months ago

11강: 언리얼 컨테이너 라이브러리 II - 구조체와 Map

언리얼 구조체

구조체 는 멤버 프로퍼티를 체계화 및 조작할 수 있는 데이터 구조체입니다. 언리얼 엔진의 리플렉션 시스템은 구조체를 UStruct 로 인식하지만, 구조체는 UObject 생태계의 일부가 아니며, UClass 내부에서 사용됩니다.

데이터에 특화된 자료 형태로 데이터를 한 곳으로 모을 수 있다.

리플리케이션용으로 간주되지 않는 이유는 접두사 자체가 F로 지정되어 있기 때문에 엔진에서 일반 C++객체로 취급한다는 것이다.

언리얼 구조체 UStruct

언리얼 리플렉션 관련 계층 구조

image

가장 한 눈에 들어오는 구조로 UObject아래로 UField가 있고 (UObject를 상속 받은), 그 아래로 UStructUEnum이 있다. UStruct는 내부 컴포지션으로 UFild를 가지고 있는 구조로 활용되며 이를 상속받은 UClass가 있다. 이 외에도 UScriptStructUFuntion이 있다. (둘다 UStruct를 상속받은 구조체)

UClass는 특이하게 내부 컴포지션으로 UFunction을 가지고 있어서 위에서 UStruct에서 사용할 수 없는 UFunction을 사용할 수 있다.

언리얼 구조체 실습

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

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"

// 원래는 따로 헤더 파일을 만들어서 사용해야 하지만, 편의상 여기에 작성
USTRUCT()
struct FStudentData
{
 GENERATED_BODY()

 FStudentData()
 {
  Name = TEXT("홍길동");
  Order = -1;
 }

 FStudentData (FString InName, int32 InOrder) : Name(InName), Order(InOrder) {}

 // UPROPERTY()를 사용한다는 것은 리플렉션 기능이나
 // 블루프린트와 호환한다는 것으로 명확한 사용 목적이 있어야 함
 // But 언리얼 오브젝트 포인터를 멤버 변수로 가진다면 반드시 사용해야 함
 UPROPERTY() 
 FString Name;

 UPROPERTY()
 int32 Order;
};

UCLASS()
class UNREALCONTAINER_API UMyGameInstance : public UGameInstance
{
 GENERATED_BODY()

public:
 virtual void Init() override;

private:
 // 값 타입이기 때문에 UPROPERTY()를 사용하지 않아도 된다.
 TArray<FStudentData> StudentsData;
};
#include "MyGameInstance.h"
#include "Algo/Accumulate.h"

FString MakeRandomName()
{
 TCHAR FirstChar[] = TEXT("김이박최");
 TCHAR MiddleChar[] = TEXT("상혜지성");
 TCHAR LastChar[] = TEXT("수은원연");

 TArray<TCHAR> RandArray;
 RandArray.SetNum(3);
 RandArray[0] = FirstChar[FMath::RandRange(0, 2)];
 RandArray[1] = MiddleChar[FMath::RandRange(0, 3)];
 RandArray[2] = LastChar[FMath::RandRange(0, 3)];

 // TArray는 TCHAR을 담고있는 배열이기에 해당 포인터를 반환하면 자동으로
 // FString으로 변환된다.
 return RandArray.GetData(); 
}

void UMyGameInstance::Init()
{
 // Struct
 const int32 StudentNum = 300;
 for (int32 i = 1; i <= StudentNum; ++i)
 {
  StudentsData.Emplace(FStudentData(MakeRandomName(), i));
 }

 TArray<FString> AllStudentsNames;
 Algo::Transform(StudentsData, AllStudentsNames, [](const FStudentData& StudentData) { return StudentData.Name; });

 UE_LOG(LogTemp, Log, TEXT("모든 학생 이름의 수: %d"), AllStudentsNames.Num());

 TSet<FString> AllUniqueNames;
 Algo::Transform(StudentsData, AllUniqueNames, [](const FStudentData& StudentData) { return StudentData.Name; });

 UE_LOG(LogTemp, Log, TEXT("모든 학생 이름의 수: %d"), AllUniqueNames.Num());
}

객체의 동적 배열 관리를 위한 예제

UCLASS()
class UNREALCONTAINER_API UStudent : public UObject
{
 GENERATED_BODY()

 // 헤더에서 언리얼 오브젝트 포인터를 선언할 때는 반드시 TObjectPtr를 사용해야 한다.
 // 전방선언을 사용하여 UStudent 클래스를 가변 배열로 가진다.
 // 이렇게 TArray에서 포인터를 관리하게 된다면 언리얼에서 자동으로 메모리를 관리할 수 있도록
 // UPROPERTY()를 사용해야 한다. (필수적인)
 UPROPERTY()
 TArray<TObjectPtr<class UStudent>> Students;
};

구조체를 TArray로 관리하게 되는 경우에는 별도로 UPROPERTY()를 붙이거나 붙이지 않거나 이것은 자유롭게 선택할 수 있는 반면에 TArray에서 UStudent 언리얼 오브젝트를 관리할 때는 반드시 UPROPERTY()를 붙여야 한다.

TMap의 구조와 활용

STL map과 TMap의 비교

동작원리 자체는 STL unordered_map과 유사하다. 키, 밸류 쌍이 필요한 자료구조에 광범위하게 사용된다.

TMap의 내부 구조

언리얼 엔진 에서 TArray (배열) 다음으로 가장 자주 사용되는 컨테이너는 TMap (맵)입니다. TMap 이 TSet (세트)와 비슷한 점은 그 구조하 키 해시 기반이라는 점입니다. 그러나 TSet 와 달리 이 컨테이너는 데이터를 키-값 짝으로 (TPair<KeyType, ValueType>) 저장하며, 저장 및 불러올 때는 키만 사용합니다.

TSet과 동일하나 TPair라는 구조체로 키, 밸류를 관리한다.

TMap의 활용

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"

// 원래는 따로 헤더 파일을 만들어서 사용해야 하지만, 편의상 여기에 작성
USTRUCT()
struct FStudentData
{
 GENERATED_BODY()

 FStudentData()
 {
  Name = TEXT("홍길동");
  Order = -1;
 }

 FStudentData (FString InName, int32 InOrder) : Name(InName), Order(InOrder) {}

 bool operator==(const FStudentData& Other) const
 {
  return Order == Other.Order;
 }

 friend FORCEINLINE uint32 GetTypeHash(const FStudentData& StudentData)
 {
  return GetTypeHash(StudentData.Order);
 }

 // UPROPERTY()를 사용한다는 것은 리플렉션 기능이나
 // 블루프린트와 호환한다는 것으로 명확한 사용 목적이 있어야 함
 // But 언리얼 오브젝트 포인터를 멤버 변수로 가진다면 반드시 사용해야 함
 UPROPERTY() 
 FString Name;

 UPROPERTY()
 int32 Order;
};

UCLASS()
class UNREALCONTAINER_API UMyGameInstance : public UGameInstance
{
 GENERATED_BODY()

public:
 virtual void Init() override;

private:
 // 값 타입이기 때문에 UPROPERTY()를 사용하지 않아도 된다.
 TArray<FStudentData> StudentsData;

 // 키 타입이 int32이고 값 타입이 FString인 TMap
 // 내부에 값 타입만 있기 때문에 UPROPERTY()를 사용하지 않아도 된다.
 // 만약 언리얼 오브젝트 포인터를 값으로 가진다면 UPROPERTY()를 사용해야 한다.
 TMap<int32, FString> StudentMap;
};
#include "MyGameInstance.h"
#include "Algo/Accumulate.h"

FString MakeRandomName()
{
 TCHAR FirstChar[] = TEXT("김이박최");
 TCHAR MiddleChar[] = TEXT("상혜지성");
 TCHAR LastChar[] = TEXT("수은원연");

 TArray<TCHAR> RandArray;
 RandArray.SetNum(3);
 RandArray[0] = FirstChar[FMath::RandRange(0, 2)];
 RandArray[1] = MiddleChar[FMath::RandRange(0, 3)];
 RandArray[2] = LastChar[FMath::RandRange(0, 3)];

 // TArray는 TCHAR을 담고있는 배열이기에 해당 포인터를 반환하면 자동으로
 // FString으로 변환된다.
 return RandArray.GetData(); 
}

void UMyGameInstance::Init()
{
 Super::Init();

 Algo::Transform(StudentsData, StudentMap, [](const FStudentData& StudentData) { return TPair<int32, FString>(StudentData.Order, StudentData.Name);});
 UE_LOG(LogTemp, Log, TEXT("순번에 따른 학생 맵의 레코드 수: %d"), StudentMap.Num());

 TMap<FString, int32> StudentsMapByUniqueName;
 Algo::Transform(StudentsData, StudentsMapByUniqueName, [](const FStudentData& StudentData) { return TPair<FString, int32>(StudentData.Name, StudentData.Order); });
 UE_LOG(LogTemp, Log, TEXT("이름에 따른 학생 맵의 레코드 수: %d"), StudentsMapByUniqueName.Num());

 TMultiMap<FString, int32> StudentMapByName;
 Algo::Transform(StudentsData, StudentMapByName, [](const FStudentData& StudentData) { return TPair<FString, int32>(StudentData.Name, StudentData.Order); });
 UE_LOG(LogTemp, Log, TEXT("이름에 따른 학생 멀티맵의 레코드 수: %d"), StudentMapByName.Num());

 const FString TargetName(TEXT("이혜은"));
 TArray<int32> AllOrders;
 StudentMapByName.MultiFind(TargetName, AllOrders);

 UE_LOG(LogTemp, Log, TEXT("이름이 %s인 학생 수: %d"), *TargetName, AllOrders.Num());

 TSet<FStudentData> StudentSet;
 for (int32 i = 1; i <= StudentNum; ++i)
 {
  StudentSet.Emplace(FStudentData(MakeRandomName(), i));
 }
}

정리