지난 포스팅에서 디자인패턴의 정의 및 특징을 비롯한 디자인패턴 종류들에 대해 알아보았습니다.

디자인패턴 정의 및 종류

 

이번 포스팅에서는 게임 클라이언트 개발 시 자주 사용하는 디자인 패턴중 하나인 커맨드 패턴에 대해서 알아보겠습니다.

 

커맨드 패턴

  • 요청 자체를 캡슐화 하는패턴으로, 서로 다른 사용자 (Ivoker-Receiver)를 매개변수로 만들고, 요청을 대기시키거나 로깅하며 되돌릴 수 있는 연산을 지원한다.
  • 어떤 행동을 나타내는 객체를 변수에 할당(캡슐화)하여 유동적으로 교체 할 수 있게 한다.
  • 행동 패턴(Behavioral Patterns)중 하나.
    • 객체나 클래스 사이의 알고리즘 혹은 책임 분배에 관련된 패턴이다.
    • 객체 사이의 결합도를 최소화 하는것에 중점을 가지고 있다.

 

장점 및 단점

  • 호출시 Invoker 객체에서 실행만 하는 구조로 설계되어 클래스간의 책임이 분명해진다. - SRP
  • 수정 시 기존의 코드를 손상하지 않고 새 커맨드들을 도입할 수 있다. - OCP
  • 유연성과 확장성을 높이고 게임 내 다양한 명령을 객체화하여 관리할 수 있다.
  • 커맨드 패턴은 각 명령마다 별도의 클래스가 필요하므로 복잡성이 증가한다.
  • 불필요한 객체화 시 코드 효율성이 떨어진다.

 

사용 예시

  • 입력 키 변경시
    • 사용자(Client)들의 입력하는 키들의 값을 바꿀 수 있게 해준다.
    • 게임 UI버튼과 상호작용 시.
  • Undo/Redo
    • 게임에서 발생된 이벤트를 되돌리거나 다시 실행 시킬 때
    • 싱글 턴제 게임에서 행동 되돌리기 등에 사용한다.

 

예시코드

 

1. 캡슐화 하기위한 인터페이스 Command 클래스.

// 구현 클래스들을 캡슐화 하기위한 추상 클래스
// Invoker와 Receiver가 직접 연결되어있지 않은 낮은 결합도 유지
class Command {
public:
	virtual ~Command() = default;
	virtual void Execute() = 0; 
};

 

2.  실제 동작을 담당하는 Receiver 구현.

// Receiver - 구현 클래스. 실제 동작
// Command를 상속받은 구현클래스. 특정 행동에 대한 실행 로직들을 캡슐화 한다.
class Skill1 : public Command {
public:
	void Execute() override {
		// Skill 1 관련 로직
	}
};

class Skill2 : public Command {
public:
	void Execute() override {
		// Skill 2 관련 로직
	}
};

 

3.  Client의 입력을 받으며, 입력받는 Key와 해당하는 스킬들을 매핑하는 Invoker

// Invoker - 사용자의 입력을 받고, Command의 실제 동작을 담당한다.
class InputHandler {
public :
	// std::unique_ptr == UE5 - TUniquePtr
	void BindKey(char Key, std::unique_ptr<Command> command) {
		keyBindings_[Key] = std::move(command);
	}

	void HandleInput(char key) {
		auto it = keyBindings_.find(key);
		if (it != keyBindings_.end() && it->second) {
			it->second->Execute();	// 해당 Key에 매핑된 행동 구현 클래스 실행
		}
	}

private:
	// 입력 Key와 행동 Skill 클래스를 매핑하기위한 UnorderedMap
	std::unordered_map<char, std::unique_ptr<Command>> keyBindings_;
};

 

4. Client는 매핑된 Key를 입력하면 그에 맞는 Receiver를 전달받으며, 수정시에도 BindKey를 사용한 할당만 하면 된다.

-> 느슨한 결합을 유지하고 있으며, 유지보수성이 높다.

	InputHandler inputHandler;

	// Client - 각 Key에맞는 Command를 바인딩한다.
	inputHandler.BindKey('Q', std::make_unique<Skill1>());
	inputHandler.BindKey('W', std::make_unique<Skill2>());

	if (IE_Pressed == 'Q')
	{
		inputHandler.HandleInput('Q');
	}
	else if (IE_Pressed == 'W')
	{
		inputHandler.HandleInput('W');
	}
	

 

 

정리

  • 디자인패턴 Behavioral Pattern중 하나.
  • 사용자의 입력을 받는 Invoker, 실제 동작 Receiver을 가지고있으며 인터페이스를 통해 캡슐화를 유지한다.
  • 수정 시 다른 코드에 영향을 주지 않는다. - OCP
  • 호출 시 Invoker는 그에 맞는 Receiver를 호출, Receiver는 실행만 담당한다. - SRP
  • 각 커맨드마다 클래스가 추가되므로 코드 내 복잡성이 증가할 수 있다.

지난 포스팅에서 언리얼 컨테이너 라이브러리의 정의 및 특징 그리고 STL과의 차이점에 대해 알아보았습니다.

이번시간에는 지난 포스팅에 작성하지 못한 TMap, TSet을 이어서 작성해보도록 하겠습니다.

언리얼 컨테이너 라이브러리 + TArray

TSet

  • std::set과 유사한 기능을 제공한다.
  • 중복된 값을 허용하지 않는다.
  • 동적 가변 배열의 형태로 데이터가 이루어져있다.
  • 해시 테이블 형태로 해시 기반의 Key데이터가 구축되어있어 빠른 조회가 가능하다.
    • 시간복잡도 O(1)를 보장한다.
  • 동적 배열 사이에 데이터가 비어있을 수 있다. 순서가 보장되지 않는다.

 

TSet의 주요 특징은 해당 사항이 있었습니다.

또한 추가 / 제거 / 삽입하는 함수들의 대부분이 TArray와 동일하기 때문에, 그 외 다른 주요 함수들에 대해 알아보겠습니다.

 

 

슬랙 (Slack)

  • 할당된 메모리에 엘리먼트가 없는것
  • Reset() 또는 Empty를 사용하여 메모리 할당 해제 없이 모든 엘리먼트를 제거하여 슬랙을 만들 수 있다.
  • TSet을 비우고 엘리먼트 수가 같거나 적은 TSet들을 Append할 시 효율적이다.

정렬 (Sorting)

  • 정렬 이후 배열이 수정된다면, 해당 순서를 보장할 수 없다
  • Key를 사용해 검색을 해서 시간복잡도가 O(1)이기 때문에 특별한 이유가 없으면 정렬을 하지 않아도 된다.

 

TMap

  • std::unordered_map과 유사한 동작을 가지고 있다.
  • TSet과 같이 해시 기반의 Key를 가지고 있어 검색 및 삽입속도가 빠르다.
  • Key와 Value 구조로 되어있으며, Key는 중복될 수 없지만, Value는 중복될 수 있다.
    • TMultiMap은 중복된 키를 저장할 수 있다.
    • TMap은 동일 Key를 사용하면 기존 것에서 대체되며, TMultiMap은 새로 저장한다.
  • iterator를 사용하여 모든 키-값을 조회할 수 있다. 
  • 데이터를 삭제해도 재구축이 일어나지 않는다
    • 비어있는 데이터가 있을 수 있다.

KeyFuncs

한 유형에 operator== 와 멤버가 아닌 GetTypeHash 오버로드가 있는 한, 그 유형은 변경 없이 TMap 의 키 유형으로 사용해도 됩니다. 하지만 그 함수 오버로드 없이 유형을 키로 사용하고 싶은 경우가 있습니다. 이러한 경우, 별도의 커스텀 KeyFuncs 를 제공해 주면 됩니다. 키 유형에 대해 KeyFunc 를 만들려면, 다음과 같이 두 개의 typedef 및 세 개의 static 함수 정의가 필요합니다:

  • KeyInitType - 키 전달에 사용됩니다.
  • ElementInitType - 엘리먼트 전달에 사용됩니다.
  • KeyInitType GetSetKey(ElementInitType Element) - 엘리먼트의 키를 반환합니다.
  • bool Matches(KeyInitType A, KeyInitType B) - A 와 B 가 동일하면 true, 아니면 false 를 반환합니다.
  • uint32 GetKeyHash(KeyInitType Key) - 키의 해시 값을 반환합니다. 보통 외부 GetTypeHash 함수를 호출합니다.

언리얼 공식문서 참조 - https://dev.epicgames.com/documentation/ko-kr/unreal-engine/map-containers-in-unreal-engine

 

 

이상으로 언리얼 컨테이너 라이브러리 중 자주 사용하는 TMap, TSet들에 대해서 알아보았습니다.

그 외에도 다양한 자료구조들이 존재하며, 각 자료구조들에 존재하는 함수들에 대해서도 공식문서를 참조하여 알아볼 수 있습니다.

 

이번학습으로 언리얼 엔진을 사용한 게임 개발시에도 어떤 라이브러리를 사용하는게 효율적인지 알 수 있었습니다.

또한, 언리얼 내부구조에서도 이진 트리형태의 TArray혹은, TMap를 사용하여 이루어져있다는것을 알 수 있었습니다.

 

WinAPI환경에서 2D게임 개발을 할 시에는 자료구조가 필요할 시 STL에 있는 자료구조들을 자주 사용했습니다.

언리얼 엔진을 접하면서 STL의 자료구조들과 같이 언리얼 내부에 있는 자료구조들이 있는지에 대해 알아보았는데, 언리얼 컨테이너 라이브러리(Unreal Container Library)에 포함되어있는 TArray, TMap, TSet의 자료구조 외에도 다양한 클래스 및 구조체들이 있었습니다.

효율적인 리소스 사용을 위해서 어떤것들이 더 효율적인지 비교하며 학습한 내용들을 이번 포스팅을 통해 알아보겠습니다.

 

언리얼 컨테이너 라이브러리(UCL)

  • 언리얼에서 제공하는 대표 컨테이너 라이브러리
  • 언리얼 오브젝트를 안정적으로 지원하며, 다수의 오브젝트 처리에 효율적이다.
    • UCL의 라이브러리는 가비지컬렉션을 통해 메모리 관리가 가능하다.
  • 대표적인 라이브러리는 TMap, TSet, TArray가 존재한다.
  • 접두사 T는 Template Library를 의미한다.

 

UCL과 C++의 STL의 차이점

  • C++ STL은 게임외에도 사용할 수 있도록 범용적으로 설계되어 있다.
  • STL에는 많은 기능들이 포함되어 있어 컴파일 시 UCL보다 많은 시간이 걸린다.
  • STL은 런타임 오버헤드가 최소화 되어있으며, UCL은 STL보다 런타임 환경에서 오버헤드가 더 발생할 수 있다.
  • UCL은 언리얼 오브젝트에 특화되어있다. 즉, 게임 개발이라는 한정된 범위가 존재한다.
  • UCL은 게임 개발에 적합한 API와 기능들을 가지고 있어, 게임 제작에 최적화된 구조로 설계되어있다.
  • UCL은 디버거 및 로거와 연동할 수 있어 문제가 발생했을 때 원인을 쉽게 파악할 수 있다.
  • UCL은 UPROPERTY()를 사용하여 블루프린터와 연동하여 사용할 수 있다.

즉, 언리얼 엔진 환경에서의 게임 개발시에는 UCL을 사용하는것이 더 적합하다고 할 수 있습니다.

 

 

TArray

  • 동적 배열(Dynamic Array)클래스이다.
  • STL의 vector와 동작 원리가 유사하다.
  • 기존 Array의 특징과 같이, 데이터가 순차적으로 모여있기 때문에 메모리를 효과적으로 사용할 수 있다.
  • 임의 데이터를 접근하는데에 있어 빠른 속도를 보장한다. 시간복잡도 O(1)
  • 탐색, 삽입, 삭제시에는 시간복잡도 O(N)이다.
  • 중복된 값을 허용한다.

배열 만들고 채우기

	// 정의
	TArray<int32> IntArray;

	// Init : n의 값을 n번 채운다.
	IntArray.Init(10, 5);
	// IntArray == {10, 10, 10, 10, 10}

	
	// Add, Emplace : 배열의 가장 마지막 위치에 추가한다. (Dynamic Array이므로 최대 사이즈 고려 X)
	IntArray.Add(10);			// 값 타입의 경우 해당 자료형의 임시 값을 추가해 복사한다.)
	IntArray.Emplace(10);		// 임시 변수를 생성하지 않는다.
	

	FString Arr[] = { TEXT("TEXT1"), (TEXT("TEXT2")) };
	TArray<FString> StrArray;

	// Append : 해당 배열의 크기에 다수의 엘리먼트를 한꺼번에 추가한다.
	StrArray.Append(Arr, sizeof(Arr));
	// StrArray == {TEXT1, TEXT2};


	//Insert : 단일 엘리먼트 또는 엘리먼트 배열 사본을 주어진 인덱스에 추가한다.
	StrArray.Insert(TEXT("TEXT3"), 1);
	// StrArray == {TEXT1, TEXT3, TEXT2};

 

정렬 (Sort)

	TArray<FString> FStrArray = {TEXT("!"), TEXT("Hello"), TEXT("Apple"), TEXT("Boat"), TEXT("World")};
	FStrArray.Sort();		// 안정적이지 않다. FString의 경우 대소문자 구분없이 사전식 비교후 정렬을 실행한다.
	// FStrArray == {!, Apple, Boat, Hello, World};

	FStrArray.HeapSort();	// 안정적이지 않다. FString의 경우 FString의 길이별로 정렬하지만, 같은 크기의 경우 순서를 보장하지 않는다.
	// FStrArray == {!, Boat, Hello, Apple, World};

	FStrArray.StableSort();	// Merge Sort형태로 구현되어있다. 순서를 보장한다.(A->Z의 순서 정렬)
	// FStrArray == {!, Boat, Apple, Hello, World};

 

 

쿼리

	TArray<FString> StrArray = { TEXT("!"), TEXT("Hello"), TEXT("Apple"), TEXT("Boat"), TEXT("World") };

	// Num : 배열의 Element가 몇개인지 확인하기
	int32 Count = StrArray.Num();
	// Count == 5

	// GetData : 배열 내 Element에 대한 포인터를 반환받는다.
	FString* StrPtr = StrArray.GetData();
	// Index 별로 개별 접근이 가능하다.
	// StrPtr[0] == "!"
	// StrPtr[1] == "Hello"
	// ...
	// StrPtr[5] - undefined behavior

	// Element 값 받아오기
	FString Elem1 = StrArray[1];
	// Elem1 == "Hello"

	// IsValidIndex(n) : 특정 인덱스가 유효한지에 대한 검사
	bool isValid1 = StrArray.IsValidIndex(1);
	bool isValid6 = StrArray.IsValidIndex(6);
	// isValid1 == true
	// isValid6 == false

	// Contains : 배열에 특정 엘리먼트가 들어가있는지 조회하기
	bool bContain = StrArray.Contains(TEXT("Hello"));
	bool bContain2 = StrArray.Contains(TEXT("NotHello"));
	// bContain == true
	// bContain == false

 

 

제거

	TArray<int32> ValArr;
	int32 Temp[] = { 10, 20, 30, 5, 10, 15, 20, 25, 30 };
	ValArr.Append(Temp, UE_ARRAY_COUNT(Temp));

	// Remove : 배열에서 일치하는 엘리먼트를 모두 지운다.
	ValArr.Remove(20);
	// ValArr == {10,30,5,10,15,25,30}

	// RemoveSingle : 배열에서 처음으로 일치하는 엘리먼트를 하나 지운다.
	ValArr.RemoveSingle(10);
	// ValArr == {10,5,10,15,25,30}

	// RemoveAt : 인덱스N의 엘리먼트를 지운다.
	ValArr.RemoveAt(2);
	// ValArr == {10,5,15,25,30}


	// RemoveAll : 일치하는 엘리먼트를 모두 지운다.
	ValArr.RemoveAll([](int32 Val) {
		return Val % 3 == 0;
		});
	// ValArr == {10,5,25}

	// Empty : 배열에 있는 모든 것을 제거한다.
	ValArr.Empty();
	// ValArr == []

 

힙(Heap)

  • TArray는 이진 힙 데이터 구조체를 지원하는 함수가 존재하며, 이진 트리 유형의 구조로 되어있다.
  • Heapify 함수룰 사용하여 기존 배열을 Heap으로 변환할 수 있다.
	TArray<int32> HeapArr;
	for (int32 Val = 10; Val != 0; --Val)
		HeapArr.Add(Val);
	// HeapArr == [10,9,8,7,6,5,4,3,2,1]
	

	// Heapify : 이진 힙으로 변경시킨다.
	HeapArr.Heapify();
	// HeapArr == [1,2,4,3,6,5,8,10,7,9]

 

Heapify() 결과 - 이진 트리 형태의 Heap으로 변경되었다.

 

 

 

	// HeapPust : 힙에 새로운 엘리먼트를 추가하며, 내부적으로 노드 순서가 변경된다.
	HeapArr.HeapPush(4);
	// HeapArr == [1,2,4,3,4,5,8,10,7,9,6]

HeapPush를 통해 Element 4를 추가한 경우

 

 

슬랙

  • 배열이 추가될 때마다 매번 재할당을 피하기 위해 얼로케이터는 요청보다 더 넉넉한 메모리를 제공하여 Add 호출시 재할당에 드는 퍼포먼스 비용을 물지 않도록 한다.
  • 엘리먼트를 삭제한다고해도 메모리가 해제되지는 않으며, 배열에 slack(여유분)을 가지고 있는다.
  • 초기 슬랙의 값은 0이며, GetSlack함수를 사용해 해당 배열의 슬랙 크기가 얼마나 되는지 알 수 있다.
  • Max 함수를 사용해 재할당이 일어나기 전까지 배열에 저장할 수 있는 최대 엘리먼트를 알 수 있다.
	TArray<int32> SlackArray;
	// SlackArray.GetSlack() == 0
	// SlackArray.Num()      == 0
	// SlackArray.Max()      == 0

	SlackArray.Add(1);
	// SlackArray.GetSlack() == 3
	// SlackArray.Num()      == 1
	// SlackArray.Max()      == 4

	SlackArray.Add(2);
	SlackArray.Add(3);
	SlackArray.Add(4);
	SlackArray.Add(5);
	// SlackArray.GetSlack() == 17
	// SlackArray.Num()      == 5
	// SlackArray.Max()      == 22

 

원시메모리

  • TArray는 할당된 메모리를 둘러싼 포장 단위
  • AddUninitialized, InsertUninitialized : 배열에 초기화 되지 않은 공간에 Element를 추가한다.
    • 해당 엘리먼트 타입의 생성자는 호출하지 않는다.
    • Memcpy 호출로 전체 구조체를 덮어 쓰려는 경우 생성자 호출을 피할 떄 효율적이다.

 

 

 

이상으로 언리얼 컨테이너 라이브러리가 어떤것인지, 어떤 특징을 가지고 있는지

그리고 STL과 어떤 차이점이 있는지 비교하며 알아보았습니다.

 

또한 언리얼 C++의 주요 컨테이너 라이브러중 하나인 TArray와 해당 라이브러리의 주요 함수들이 어떤것들이 있는지에 대해 알아보았습니다.

 

다음 포스팅에서는 이어서 TSet, TMap이 어떤 특징을 가지고 있는지, 그리고 STL의 어떤 자료구조와 유사한지에 대해 알아보겠습니다.

 


https://dev.epicgames.com/documentation/ko-kr/unreal-engine/array-containers-in-unreal-engine

https://designerd.tistory.com/entry/UE-%EC%96%B8%EB%A6%AC%EC%96%BC-C%EC%9D%98-TArray-TSet-TMap-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EC%99%80-%ED%99%9C%EC%9A%A9%EB%B0%A9%EB%B2%95

+ Recent posts