Unreal5 구조체, Map

이승한·2024년 4월 4일
0

Unreal5

목록 보기
9/11

구조체

구현하기

  1. 구조체를 정의하려는 헤더(.h)파일 열기
  2. C++구조체를 정의하고 앞에 USTRUCT 매크로 추가, 구조체에 필요한 모든 UStruct 지정자를 포함
  3. 구조체 상단에 GENERATED_BODY 매크로 추가
  4. 구조체의 멤버 변수를 UPROPERTY로 태그하여 언리얼 엔진의 리플렉션 시스템과 블루프린트 스크립팅에 표시할 수 있다.

구조체는 단순한 데이터 타입에 적합

프로젝트 내부에서 보다 복잡한 인터랙션을 하기위해서는, UObject 또는 AActor 서브클래스를 만드는것이 좋다.

대부분 GENERATE_BODY 매크로 선언

  • 리플렉션, 직렬화와 같은 유용한 기능을 지원
  • GENERATE_BODY를 선언한 구조체는 UScriptStruct 클래스로 구현
  • 이 경우 제한적으로 리플렉션을 지원 ( 속성 UPROPERTY만 선언할 수 있고 함수 UFUNCTION은 선언할 수 없음)

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

  • 대부분 힙 메모리 할당(포인터 연산)없이 스택 내 데이터로 사용
  • NewObjectAPI를 사용할 수 없음

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;
	}
	//구조체 인자를 가진 생성자 자유롭게 사용가능
	//구조체는 언리얼 오브젝트가 아니기에 New API를 통해 생성될 일이 없기 때문에
	FStudentData(FString InName, int32 InOrder) : Name(InName), Order(InOrder) {}

	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;
    
    //Object를 상속받은 Student클래스
	//언리얼 헤더에서 언리얼 오브젝트 포인터를 선언할때는 TObjectPtr로 감싸줘야한다 -> 잊지말자
	//전방선언으로 헤더 의존성 최소화 
	//언리얼 오브젝트를 TArray로 다룰때 UPROPERTY() 필수 -> 언리얼이 메모리를 관리할수 있도록
	UPROPERTY()
	TArray<TObjectPtr<class UStudent>> Students;
};

구조체로 선언한 StudentsData와 클래스로 선언한 Students의 차이 주석참조

MyGameInstance.cpp

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


#include "MyGameInstance.h"
#include "Algo/Accumulate.h"


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

	//TArray는 TCHAR배열을 포함하고 있는 컨테이너
	//이 포인터 값을 넘겨주면 반환 값을 FString으로 지정했기에 자동으로 FString이 만들어져서 반환하게 된다.
	TArray<TCHAR> RandArray;
	//빈공간 3개 확보
	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();
}
void UMyGameInstance::Init()
{
	Super::Init();

	const int32 StudentNum = 300;
	for (int32 ix = 1; ix <= StudentNum; ++ix)
	{
		StudentsData.Emplace(FStudentData(MakeRandomName(), ix));
	}
	//이름정보만 모아둠, StudentsData들을 옮겨보자
	TArray<FString> AllStudentsNames;
	Algo::Transform(StudentsData, AllStudentsNames,
		[](const FStudentData& Val)
		{
			return Val.Name;
		}
	);

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


	//중복을 허용 x
	TSet<FString> AllUniqueNames;
	Algo::Transform(StudentsData, AllUniqueNames,
		[](const FStudentData& Val)
		{
			return Val.Name;
		}
	);

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

구조체로 선언한것을 랜덤이름을 생성하는 함수로 300개의 이름을 생성하고 각각 TArray와 TSet에 Name정보를 옮겼을 때
어떻게 다른지 확인

출력 값 :
모든 학생 이름의 수 : 300
중복 없는 학생의 수 : 64


TMap의 구조와 활용

STL map과 TMap의 비교

STL map의 특징

  • STL map은 STL set과 동일하게 이진 트리로 구성
  • 정렬은 지원하지만, 메모리 구성이 효율적이지 않으며, 데이터 삭제시 재구축이 일어날 수 있음.
  • 모든 자료를 순회하는데 적합하진 않음

언리얼 TMap의 특징

  • 키,밸류 구성의 튜플(Tuple) 데이터의 TSet 구조로 구현되어 있음
  • 해시테이블 형태로 구축되어 있어 빠른 검색이 가능함
  • 동적 배열의 형태로 데이터가 모여있음
  • 데이터는 빠르게 순회할 수 있음
  • 데이터는 삭제해도 재구축이 일어나지 않음
  • 비어있는 데이터가 있을 수 있음 , 중복이 허용되지않는데 중복이 필요할땐 밑에
  • TMultiMap을 사용하면 중복 데이터를 관리할 수 있음

동작 원리는 STL unordered_map과 유사함

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

한마디로 TSet인데 키,밸류쌍으로 이루어져있다.


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;
	}
	//구조체 인자를 가진 생성자 자유롭게 사용가능
	//구조체는 언리얼 오브젝트가 아니기에 New API를 통해 생성될 일이 없기 때문에
	FStudentData(FString InName, int32 InOrder) : Name(InName), Order(InOrder) {}

	UPROPERTY()
	FString Name;

	UPROPERTY()
	int32 Order;
};


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

	GENERATED_BODY()
	
public:
	virtual void Init() override;

private:
	
	//만약 TMap안에 언리얼 오브젝트 포인터가 들어간다면 UPROPERTY를 선언해줘야한다.
	TMap<int32, FString> StudentsMap;
};

MyGameInstance.cpp

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


#include "MyGameInstance.h"
#include "Algo/Accumulate.h"


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

	//TArray는 TCHAR배열을 포함하고 있는 컨테이너
	//이 포인터 값을 넘겨주면 반환 값을 FString으로 지정했기에 자동으로 FString이 만들어져서 반환하게 된다.
	TArray<TCHAR> RandArray;
	//빈공간 3개 확보
	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();
}
void UMyGameInstance::Init()
{
	Super::Init();

	const int32 StudentNum = 300;
	for (int32 ix = 1; ix <= StudentNum; ++ix)
	{
		StudentsData.Emplace(FStudentData(MakeRandomName(), ix));
	}
	//이름정보만 모아둠, StudentsData들을 옮겨보자
	TArray<FString> AllStudentsNames;
	Algo::Transform(StudentsData, AllStudentsNames,
		[](const FStudentData& Val)
		{
			return Val.Name;
		}
	);

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


	//중복을 허용 x
	TSet<FString> AllUniqueNames;
	Algo::Transform(StudentsData, AllUniqueNames,
		[](const FStudentData& Val)
		{
			return Val.Name;
		}
	);

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

	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> StudentsMapByUnique;

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

	TMultiMap<FString, int32> StudentMapByName;
	Algo::Transform(StudentsData, StudentMapByName,
		[](const FStudentData& Val)
		{
			return TPair<FString, int32>(Val.Name, Val.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());
}

출력 값:
LogTemp: 순번에 따른 학생 맵의 레코드 수 : 300
LogTemp: 이름에 따른 학생 맵의 레코드 수 : 62
LogTemp: 이름에 따른 학생 멀티맵의 레코드 수 : 300
LogTemp: 이름이 이혜은인 학생 수 : 4

TSet에 구조체 FStudentData를 사용할때 주의점

TSet<FStudentData> StudentsSet;
for (int32 ix = 1; ix <= 300; ++ix)
{
	StudentsSet.Emplace(FStudentData(MakeRandomName(), ix));
}
  • 수많은 오류가 뜬다. 이는 커스텀 구조체를 사용했기에 operator== 과 GetTypeHash함수를 선언해줘야한다.
USTRUCT()
struct FStudentData
{
	GENERATED_BODY()
	
	//생성자
	FStudentData()
	{
		Name = TEXT("홍길동");
		Order = 1;
	}
	//구조체 인자를 가진 생성자 자유롭게 사용가능
	//구조체는 언리얼 오브젝트가 아니기에 New API를 통해 생성될 일이 없기 때문에
	FStudentData(FString InName, int32 InOrder) : Name(InName), Order(InOrder) {}

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

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

	UPROPERTY()
	FString Name;

	UPROPERTY()
	int32 Order;
};

각 자료구조의 시간복잡도

0개의 댓글