Unreal engine/Unreal C++

3_2_UStruct(언리얼 구조체)와 Map

devRiripong 2023. 10. 5.
반응형

강의 내용

언리얼 구조체의 특징을 이해하고, 다양한 컨테이너 라이브러리에서 구조체를 활용하기

강의 목표

언리얼 구조체의 선언과 특징 이해

언리얼 대표 컨테이너 라이브러리 TMap의 내부 구조 이해

세 컨테이너 라이브러리의 장단점을 파악하고, 알맞게 활용하는 방법의 학습

언리얼 구조체의 구조와 활용

공식 문서

https://docs.unrealengine.com/5.2/ko/using-structs-in-unreal-cpp/

 

구조체 사용하기

간단한 가이드로 구조체 사용을 시작합니다.

docs.unrealengine.com

언리얼 구조체 UStruct

- 데이터 저장/전송에 특화된 가벼운 객체

- GENERATED_BODY 매크로 선언

    - 리플렉션, 직렬화 같은 유용한 기능 지원

    - GENERATED_BODY를 선언한 구조체는 UScriptStruct클래스로 구현됨. 

    - 제한적으로 리플렉션을 지원함

        - UPROPERTY만 선언 가능하고, UFUCNTION은 선언 불가

- 언리얼 엔진 구조체 이름은 F로 시작

    - 대부분 힙 메모리 할당(포인터 연산)없이 스택 내 데이터로 사용됨

    - NewObjectAPI를 사용할 수 없음. 

 

리플렉션에 관련된 언리얼 오브젝트 계층 구조

 

언리얼 구조체 UStruct 활용 실습

지난 시간에 만든 UnrealContainer 프로젝트에 구조체를 선언해 보자.

MyGameInstance.h 상단에다가 StudentData라는 구조체를 선언해 볼거야. 별도의 헤더파일을 추가하지 않고 MyGameInstance.h에다가 한다.

// 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는 용도에 따라 넣어도 되고 안넣어도 된다. 
	// UnrealObject 포인터를 멤버변수로 갖는다면 반드시 UPROERTY()를 써줘야 한다.
	UPROPERTY()
	FString Name; 

	UPROPERTY()
	int32 Order; 
};


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

public: 

	virtual void Init() override; 
	
};

이렇게 하고 컴파일을 한다.

구조체로 300명의 학생을 만들어서 TArray로 구조체 가변배열을 생성해 보자.

랜덤하게 이름을 생성하는 함수를 MyGameInstance.cpp에 추가를 해보자.

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

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

	return RandArray.GetData(); 
}

이 함수를 사용해서 Init에서 FStudentData 구조체를 만들어서 300개의 TArray를 만들어 볼거야.

	const int32 StudentNum = 300; 
	for (int ix = 1; ix <= StudentNum; ++ix)
	{

	}

300개를 담을 수 있는 컨테이너를 헤더에서 선언하자.

private: 
	TArray<FStudentData> StudentsData;

Reflection 기능으로 뭔가 조회 안할거면 굳이 UPROPERTY() 안붙여도 된다.

다신 구현부로 가서,

	const int32 StudentNum = 300; 
	for (int ix = 1; ix <= StudentNum; ++ix)
	{
		StudentsData.Emplace(FStudentData(MakeRandomName(), ix)); 
	}

이렇게 300개를 Emplace로 넣어준다. Struct라서 Add보다 Emplace를 써서 신경써줬다.

그리고 이걸 이용해서 TArray와 TSet의 차이점에 대해 실습을 해본다.

먼저 이름들에 대해 TArray를 구성해 볼거야.

 	const int32 StudentNum = 300; 
	for (int ix = 1; ix <= StudentNum; ++ix)
	{
		StudentsData.Emplace(FStudentData(MakeRandomName(), ix)); 
	}

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

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

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

	UE_LOG(LogTemp, Log, TEXT("중복 없는 학생 이름의 수 : %d"), AllUniqueNames.Num());
}

한글이 들어가 있으니 인코딩 설정을 해주고, 컴파일 한다.

빌드가 됐으면 실행을 해서 로그 결과를 살펴보자.

디버깅 없이 런 후 플레이 해보면

모든 학생의 수가 300개인데, Set에 들어갔을 때는 중복은 없어지기 때문에 61개가 되었다는 걸 알 수 있다.

 

이번에는 언리얼 오브젝트를 하나 생성해보자.

Object를 상속받은 Student라는 클래스를 생성하자.

이걸 TArray로 관리해 볼거야. 이걸 선언 했을 때 어떤 부분을 주의해야할까.

MyGameInstance.h에서 관련된 TArray를 선언해주자.

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

public: 

	virtual void Init() override; 

private: 
	TArray<FStudentData> StudentsData; 	

	// UStudent 전방선언 한 거, 이건 TObjectPtr로 감싸서 포인터로 해준 거니 언리얼이 메모리를 관리할 수 있게 UPROPERTY() 매크로를 붙여줘야 한다.(필수)
	UPROPERTY()
	TArray<TObjectPtr<class UStudent>> Students; 
};

구조체를 TArray로 관리 할 때는 UPROPERTY를 붙일지 자유지만,

TArray에서 UStuent를 관리할 때는 반드지 컨테이너에 UPROPERTY를 부착해줘야 한다.

 

TMap의 구조와 활용

TMap의 특징

STL map 특징

STL map은 STL set과 마찬가지로 이진트리

정렬은 지원, 데이터 삭제시 재구축

모든 자료 순회엔 부적절

언리얼 TMap의 특징

키, 밸류 구성의 Tuple 데이터의 TSet 구조

해시테이블 형태라 빠른 검색 가능

독정 배열 형태로 데이터 모여있음

빠르게 순회 가능

삭제시 재구축 안일어남

비어있는 데이터 있을 수 있음

TMultiMap 이용시 중복 데이터 관리 가능

동작 원리를 STL unordered_map과 유사

키, 밸류 쌍이 필요한 자료구조에 광범위하게 사용됨

 

공식문서

https://docs.unrealengine.com/5.3/ko/map-containers-in-unreal-engine/

 

TMap

TMap, 맵은 크게 키 유형과 값 유형, 두 가지로 정의되며, 맵에 하나의 짝으로 저장됩니다.

docs.unrealengine.com

 

TMap 활용 실습

코드로 돌아가서 TMap 에 대한 자료구조를 선언하고 활용해보자.

MyGameInstance.h에서

 // 키나 벨류에 Unreal Ojbect 포인터가 들어가게 되면 UPROPERTY()를 붙여줘야 한다. 
 // 없다면 선언 안해줘도 무방하다.
	TMap<int32, FString> StudentsMap; 
};

MayGameInstance.cpp로 가서

기존에 만든 TStudents의 TArray를 TMap으로 변환해 볼텐데 Algo::Transform을 사용해 본다.

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

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

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

언리엘 에디터로 돌아가서 인코딩을 하고 플레이를 누르면

이번에는 어떤 이름을 가진 학생이 몇명 있는지 정보를 뽑아본다.

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

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

언리얼에서 빌드를 하고,

4개가 중복된 것을 볼 수 있다. 렌덤이기 때문에 실행할 때 마다 다른 숫자가 나온다.

이렇게 TMap과 TMultiMap에 대한 간단한 사용방법에 대해 알아 봤어.

이번에는 FStudent데이터를 TSet에서 사용하려면 어떻게 해야 하는지 알아 보자.

 

FStudent데이터를 TSet에서 사용해보기

TSet<FStudentData> StudentsSet;
	for (int ix = 1; ix <= StudentNum; ++ix)
	{
		StudentsSet.Emplace(FStudentData(MakeRandomName(), ix));
	}

이렇게 채워놓고 잠시 언리얼 에디터를 끄고 빌드를 진행해보자.

에러가 많이 뜨는 것을 볼 수 있다. 원인은 우리가 지정한 커스텀 구조체 FStudnetData에 대한 GetTypeHash 함수가 지정되지 않아가지고, Hash 값을 만들어 낼 수 없다는 에러 메시지라고 이해를 해주면 된다.

이걸 해결하기 위해서 FStudentData 구조체에 함수 2개를 추가해 보자.

MyGameInstance.h의 struct FStudentData로 가서

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

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

이제 빌드가 정상적으로 되는 것을 볼 수 있다.

 

자료구조의 시간 복잡도 비교

TArray: 빈틈 없는 메모리, 높은 접근성능, 높은 순회 성능

TSet: 빠른 중복 감지

TMap: 중복 불허, 키 밸류 관리

TMultiMap: 중복 허용, 키 밸류 관리

 

정리

구조체와 언리얼 컨테이너 라이브러리

1. TArray, TSet, TMap 컨테이너 라이브러리 내부 구조와 활용 방법

2. 언리얼 구조체 선언 방법

3. TSet과 TMap에서 언리얼 구조체를 사욯하기 위해 필요한 함수의 선언과 구현 방법

 

반응형

댓글