1. 스택의 이해와 ADT 정의

 

∙ 스택은 ‘먼저 들어간 것이 나중에 나오는 자료구조’ 로서 초코볼이 담겨있는 통에 비유할 수 있다.

∙ 스택은 ‘LIFO(Last-in, First-out) 구조’의 자료구조이다.

 

▣ 스택의 기본 연산 

∙ 통에 초코볼을 넣는다.                                                       push 
∙ 통에서 초코볼을 꺼낸다.                                                    pop 
∙ 이번에 꺼낼 초코볼의 색이 무엇인지 통 안을 들여다 본다.         peek

※ 일반적인 자료구조의 학습에서 스택의 이해와 구현은 어렵지 않다.  오히려 활용의 측면에 서 생각할 것들이 많다!

 

ADT

 

  ADT를 대상으로 배열 기반의 스택 또는 연결 리스트 기반의 스택을 구현할 수 있다


2. 스택의 배열 기반 구현

 

 

 

 

∙ 인덱스 0의 배열 요소가 '스택의 바닥(초코볼 통의 바닥)'으로 정의되었다. 
∙ 마지막에 저장된 데이터의 위치를 기억해야 한다.


∙ push : Top을 위로 한 칸 올리고, Top이 가리키는 위치에 데이터 저장 

∙ pop  : Top이 가리키는 데이터를 반환하고, Top을 아래로 한 칸 내림

 

void Stackinit(Stack* pstack)
{
	pstack->topIndex = -1; // 비어있음
}

int IsEmpty(Stack* pstack)
{
	if (pstack->topIndex == -1)return true;
	else
		return false;
}

void SPush(Stack* pstack, Data data)
{
	pstack->topIndex += 1;
	pstack->stackArr[pstack->topIndex] = data;

}

//빼는게 아니라, topIndex를 줄여가면서 -1이 될떄까지 읽는다
Data SPop(Stack* pstack)
{
	int rIdx;
	if (IsEmpty(pstack))
	{
		cout << "Stack NULL " << endl;
	}

	rIdx = pstack->topIndex;
	pstack->topIndex -= 1;

	return pstack->stackArr[rIdx];  //
}

Data SPeek(Stack* pstack)
{
	if (IsEmpty(pstack))
	{
		cout << "Stack NULL " << endl;
	}
	return pstack->stackArr[pstack->topIndex];
}

int main()
{

	Stack stack;
	Stackinit(&stack);

	SPush(&stack,1);
	SPush(&stack, 2);
	SPush(&stack, 3);
	SPush(&stack, 4);
	SPush(&stack, 5);

	while (!IsEmpty(&stack))
		cout <<  SPop(&stack) << endl;


	return 0;
}

 

 


3. 스택의 연결 리스트 기반 구현

 

저장된 순서의 역순으로 데이터(노드)를 참조(삭제) 하는 연결 리스트가 바로 연결 기반의 스택이다





typedef struct _node
{
	Data data;
	struct _node* next;
}Node;

typedef struct _listStack
{
	Node* head;

}ListStack;

typedef ListStack Stack;


void Stackinit(Stack* pstack)
{
	pstack->head = NULL; // 비어있음
}

int IsEmpty(Stack* pstack)
{
	if (pstack->head == NULL)return true;
	else
		return false;
}

void SPush(Stack* pstack, Data data)
{
	Node* newNode = new Node();

	newNode->data = data;
	newNode->next = pstack->head;

	pstack->head = newNode;
}

Data SPop(Stack* pstack)
{
	Data rdata;
	Node* rnode;

	if (IsEmpty(pstack))
	{
		cout << NULL << endl;
	}

	rdata = pstack->head->data;
	rnode = pstack->head;

	pstack->head = pstack->head->next;
	delete rnode;

}

Data SPeek(Stack* pstack)
{
	if (IsEmpty(pstack))
	{
		cout << "Stack NULL " << endl;
	}

	return pstack->head->data;
}

int main()
{

	Stack stack;
	Stackinit(&stack);

	SPush(&stack,1);
	SPush(&stack, 2);
	SPush(&stack, 3);
	SPush(&stack, 4);
	SPush(&stack, 5);

	while (!IsEmpty(&stack))
		cout <<  SPop(&stack) << endl;


	return 0;
}

▣ C++ 클래스 장점

  • 빠른 런타임 퍼포먼스: 일반적으로 C++ 로직은 블루프린트 로직보다 훨씬 빠릅니다. 이유는 다음과 같습니다.
  • 명확한 디자인: C++ 에서 변수나 함수를 노출하면 세밀한 제어를 통해 원하는 것을 정확히 노출할 수 있으므로, 특정 함수/변수를 보호하고 클래스의 공식 "API"를 만들 수 있습니다. 따라서 지나치게 크고 따라잡기 어려운 블루프린트를 만들지 않아도 됩니다.
  • 광범위한 액세스: C++ 에서 정의(하고 제대로 노출)한 함수와 변수는 다른 모든 시스템에서 액세스할 수 있어, 여러 시스템 사이 정보 전달에 완벽합니다. 또한, C++ 에는 블루프린트보다 많은 엔진 함수 기능이 노출되어 있습니다.
  • 더 많은 데이터 제어: C++ 에서는 데이터 저장과 로드 관련해서 구체적인 함수 기능을 더 많이 사용할 수 있습니다. 버전 변경 및 시리얼라이즈 처리를 다양한 사용자 지정 방식으로 처리할 수 있습니다.
  • 네트워크 리플리케이션: 블루프린트의 리플리케이션 지원은 간단하며 작은 게임이나 고유한 일회성 액터에 사용하도록 설계되었습니다. 리플리케이션 대역폭이나 타이밍같은 것을 엄격하게 제어해야 하는 경우 C++ 를 사용해야 합니다.
  • 강력한 연산력: 블루프린트로 복잡한 수학 연산을 하는 것은 어렵고 약간 느릴 수도 있습니다. 복잡한 수학 연산은 C++ 를 고려하세요.
  • 쉬운 Diff/Merge: C++ 코드와 데이터는 (구성 및 커스텀 솔루션을 포함해서) 텍스트로 저장되므로, 여러 브랜치에서의 동시 작업이 쉽습니다.

  블루프린트 클래스 장점

  • 빠른 생성: 대부분의 경우 블루프린트 클래스를 새로 만들어 변수와 함수를 추가하는 것이 비슷한 작업을 C++ 로 하는 것보다 빠릅니다. 그래서 완전 새로운 시스템의 프로토타입을 만드는 작업은 보통 블루프린트로 하는 것이 빠릅니다.
  • 빠른 반복처리: 블루프린트 로직을 수정하고 에디터 안에서 미리보는 것이 핫 리로드 기능이 있더라도 게임을 다시 컴파일하는 것보다 훨씬 빠릅니다. 성숙한 시스템은 물론 새로운 시스템에서도 마찬가지이므로 "미세조정" 가능한 모든 값은 가급적 애셋에 저장해야 합니다.
  • 원활한 흐름: C++ 로 "게임 흐름"을 그려 보는 것은 복잡할 수 있으므로, 보통 블루프린트로 (또는 딱 그 용도로 설계된 비헤이비어 트리같은 커스텀 시스템으로) 구현하는 것이 낫습니다. 딜레이 및 비동기 노드는 C++ 델리게이트보다 흐름을 따라잡기 훨씬 쉽습니다.
  • 유연한 편집: 별도의 기술 훈련을 받지 않은 디자이너와 아티스트도 블루프린트를 생성하고 편집할 수 있으므로, 엔지니어 이외에도 수정해야 하는 애셋은 블루프린트가 이상적입니다.
  • 쉬운 데이터 사용: 블루프린트 클래스 안에 데이터를 저장하는 것은 C++ 안에 저장하는 것보다 훨씬 간단하고 안전합니다. 블루프린트는 데이터와 로직이 밀접하게 섞인 클래스에 좋습니다.

 

블루프린트에서 C++ 로 변환

 

블루프린트는 생성 및 반복처리 작업이 쉬우므로,
블루프린트에서 프로토타입을 만든 후 그 기능의 일부 또는 전부를 C++ 로 옮기는 것이 일반적입니다.

보통 시스템의 기본 기능을 입증했고 다른 사람들이 원활하게 사용할 수 있도록 "강화"하고자 하는 "리팩터링 지점"에서 이 작업을 하는 것이 좋습니다.

이 시점에서, 어떤 클래스, 함수, 변수는 C++ 로 옮기고 어떤 것은 블루프린트에 남겨둘지 결정해야 합니다.
그 결정을 내리기 전, C++ 로 리팩터링하기 위해 거쳐야 하는 프로세스를 이해하는 것이 좋습니다.

일반적으로 첫 단계는 블루프린트 클래스가 상속할 "베이스" C++ 클래스 세트를 만드는 것입니다.
게임의 베이스 네이티브 클래스를 만들었으면 프로토타입 블루프린트의 부모를 새로운 네이티브 클래스로 변경합니다. 그 작업을 완료하면 블루프린트 클래스의 변수와 함수를 네이티브 C++ 로 옮기기 시작할 수 있습니다.

네이티브 클래스의 변수 또는 함수가 블루프린트 변수와 유형 및 이름이 같다면, 블루프린트를 로드할 때 그 레퍼런스가 자동 변환됩니다. 하지만 블루프린트로의 외부 레퍼런스가 네이티브 베이스 클래스를 가리키도록 변경하고 싶을 수 있습니다. 예를 들어 ActionRPG 샘플 작업을 하는 도중, DefaultEngine.ini 파일에 다음과 같은 블록을 추가했습니다.

[CoreRedirects]
+ClassRedirects=(OldName="BP_Item_C", NewName="/Script/ActionRPG.RPGItem", OverrideClassName="/Script/CoreUObject.Class")
+ClassRedirects=(OldName="BP_Item_Potion_C", NewName="/Script/ActionRPG.RPGPotionItem", OverrideClassName="/Script/CoreUObject.Class")
+ClassRedirects=(OldName="BP_Item_Skill_C", NewName="/Script/ActionRPG.RPGSkillItem", OverrideClassName="/Script/CoreUObject.Class")
+ClassRedirects=(OldName="BP_Item_Weapon_C", NewName="/Script/ActionRPG.RPGWeaponItem", OverrideClassName="/Script/CoreUObject.Class")

 

위 블록은 코어 리디렉트 시스템을 사용하여,
Blueprint BP Item C 로의 레퍼런스를 전부 새로운 네이티브 클래스 RPGItem 으로 변환합니다. 

OverrideClassName 옵션이 필요한 이유는 이제 UBlueprintGeneratedClass 가 아닌 UClass 임을 알리기 위해서입니다. 부모변경 및 수정을 처음 한 이후에는 느려진 블루프린트 컴파일 문제를 고치고 게임의 모든 블루프린트를 다시 저장해야 할 것입니다.

목표는 블루프린트 경고 없이 리팩터링을 완료하는 것으로, 그래야 새 이슈가 생겼을 때 추적이 쉽습니다.
모든 것이 정상 작동하면 이제 수정 프로세스 도중 추가한 CoreRedirect 를 제거하고 ini 파일을 정리하면 됩니다.


퍼포먼스 고려사항

 

C++ 가 블루프린트에 비해 갖는 강점은 퍼포먼스입니다.
하지만 많은 경우 사실 블루프린트 퍼포먼스가 문제되지는 않습니다. 

대체로 C++ 코드 한 줄을 실행하는 것보다 블루프린트의 개별 노드 하나를 실행하는 속도가 느리다는 것이 큰 차이점인데, 한 노드에서 한 번 실행하는 것은 C++ 에서 호출하는 것만큼 빠릅니다.

예를 들어 값싼 최상위 노드 몇 개에 값비싼 Physics Trace 함수를 호출하는 블루프린트 클래스가 있다면,
C++ 로 변환해도 퍼포먼스 향상이 크지 않습니다. 하지만 블루프린트 클래스에 빽빽한 루프가 많거나 펼치면 노드가 수백개나 되는 중첩 매크로가 많은 경우라면, 그 코드는 C++ 로 옮길 것을 생각해야 합니다.

퍼포먼스 문제가 매우 심한 것은 Tick(틱) 함수입니다.
블루프린트 틱은 네이티브 틱보다 훨씬 느릴 수 있으므로,
인스턴스가 많은 클래스에는 절대 틱을 사용하지 말아야 합니다.


대신 타이머나 델리게이트를 사용하여 블루프린트 클래스가 필요할 때만 일을 하도록 해야 합니다.

블루프린트 클래스에 퍼포먼스 문제가 있는지 알아내는 가장 좋은 방법은 프로파일러 툴 을 사용하는 것입니다.

프로젝트의 퍼포먼스 상황을 파악하려면, 먼저 블루프린트 클래스의 퍼포먼스 스트레스가 심한 (적을 한 무더기 스폰하는 등의) 상황을 만든 뒤, 프로파일러 툴을 사용하여 프로파일을 캡처합니다.

프로파일러 툴을 사용하면 Game Thread Tick (게임 스레드 틱)을 자세히 분석해서 트리를 펼쳐가며 문제가 되는 블루프린트 클래스를 찾을 수 있습니다 (같은 클래스의 모든 인스턴스는 하나의 그룹으로 묶여 있으니, 참고하세요).

블루프린트 클래스 안에서, 시간이 걸리는 블루프린트 함수를 확인할 수 있습니다. 그 함수를 펼칩니다.
대부분의 시간이 Self 에서 발생한다면, 블루프린트 오버헤드로 인해 퍼포먼스 손실이 있는 것입니다.
하지만 대부분의 시간이 그 함수 안에 중첩된 다른 네이티브 이벤트에서 발생한다면, 블루프린트 오버헤드 문제는 아닙니다.

블루프린트 네이티브화 를 통해 이러한 문제를 줄일 수 있지만, 몇 가지 단점이 있습니다.

첫째, 쿠킹 워크플로를 변경하므로 쿠킹된 게임의 반복처리 작업이 느려질 수 있습니다. 또한,
네이티브화된 블루프린트의 런타임 로직은 보통 블루프린트와 다르므로 게임의 특성에 따라 다른 작동방식 또는 버그가 생길 수 있습니다. 대부분의 블루프린트 기능은 네이티브화에 완벽 지원되지만, 명확하지 않은 몇 가지 예외가 있을 수 있습니다.

마지막으로, C++ 를 직접 변환한 것에 비해 퍼포먼스 향상이 크지 않을 수 있습니다.
네이티브화가 퍼포먼스 이슈를 전부 해결하지는 못하지만, 해법이 될 수 있는지 조사해 볼 수는 있을 것입니다.


아키텍처 노트

 

블루프린트와 C++ 를 같이 사용하여 게임을 만들면,
게임의 규모가 커지고 복잡해짐에 따라 여러가지 문제가 생길 것입니다.
프로젝트가 커지기 시작할 때 유념해야 할 몇 가지 사항입니다.

  • 비싼 블루프린트로의 형변환 금지: 블루프린트 클래스 BP_B 에서 BP_A 로 형변환( 또는 함수나 다른 블루프린트의 변수형으로 선언)할 때마다 해당 블루프린트의 로드 종속성이 생깁니다. 그러면 BP_A 가 커다란 스태틱 메시 4 개와 사운드 20 개를 레퍼런싱하는 경우, 형변환이 실패한다 하더라도 BP_B 를 로드할 때마다 커다란 스태틱 메시 4 개와 사운드 20 개를 로드합니다. 바로 그 이유로 네이티브 베이스 클래스 또는 최소한의 블루프린트 베이스 클래스에 중요 함수와 변수만 정의하는 것이 필수입니다. 거기서 비싼 블루프린트를 자손 클래스로 만들어야 합니다.
  • 순환 블루프린트 레퍼런스 금지: 순환 레퍼런스(, 즉 한 클래스가 다른 클래스를 레퍼런스하는 데 거기서 첫 클래스를 레퍼런스하는 경우)는 C++ 의 경우 헤더 파일이 있어서 문제가 되지 않습니다. 하지만 과도한 순환 블루프린트 레퍼런스는 에디터 로드 및 컴파일 시간을 악화시킬 수 있습니다. 위와 마찬가지로 비싼 자손 블루프린트로 형변환( 또는 변수 레퍼런스를 생성)하는 대신 C++ 클래스 또는 값싼 베이스 블루프린트 클래스로 형변환하여 개선할 수 있습니다.
  • C++ 클래스에서 애셋 레퍼런스 금지: C++ 생성자에서 FObjectFinder  FClassFinder 를 사용한 애셋 레퍼런스는 가능하지만, 가급적 피해야 합니다. 이런 식의 애셋 레퍼런스는 프로젝트 시작 시 로드되므로, 레퍼런스가 실제 필요치 않은 경우 로드 시간 및 메모리 이슈가 발생합니다. 또 생성자에서 레퍼런스된 애셋은 삭제나 이름변경도 쉽지 않습니다. 일반적으로 C++ 에서 특정 스태틱 메시 레퍼런스를 만드는 것보다는 Game Data (게임 데이터) 애셋 또는 블루프린트 유형을 조금 생성한 뒤 애셋 매니저나 구성 파일을 사용하여 로드하는 것이 좋습니다.
  • 스트링으로 애셋 레퍼런스 금지: C++ 클래스에서 애셋을 로드할 때 발생하는 이슈를 피하려면, C++ 의 LoadObject 같은 함수로 디스크의 특정 애셋을 수동 로드할 수 있습니다. 하지만 이 레퍼런스는 쿠커가 전혀 추적하지 못하므로 패키지 게임에서 문제가 생길 수 있습니다. 그래서 대신 C++ 클래스에서 FSoftObjectPath 또는 TSoftObjectPtr 유형을 사용하고, ini 또는 블루프린트 클래스에서 설정한 뒤, 요청 시 로드 또는 비동기 로드를 통해 로드해야 합니다.
  • 사용자 구조체 및 열거형 주의: C++ 에서 정의한 열거형과 구조체는 C++ 에서도 블루프린트에서도 사용할 수 있지만, 사용자 구조체/열거형은 C++ 에서 사용할 수 없고 세이브 게임 섹션의 설명대로 수동 고칠 수도 없습니다. 보통 게임플레이 로직을 조금씩 더 많이 C++ 로 옮기게 되므로, 중요한 열거형과 구조체는 C++ 에서 구현하는 것이 좋습니다. 기본적으로 블루프린트 둘 이상이 사용하는 것이면 아마 네이티브 C++ 로 구현해야 할 것입니다.
  • 네트워크 아키텍처 고려: 게임의 네트워크 아키텍처 특성은 클래스 구조에 큰 영향을 줍니다. 일반적으로 프로토타입은 네트워킹을 염두에 두고 만들지 않으므로, 뭔가 "진짜로" 리팩터링을 시작할 때 어떤 액터가 어떤 데이터를 리플리케이트할지 고려하기 시작해야 합니다. 반복처리를 어렵게 만드는 부분에 대한 결정을 잘 내려야 리플리케이트되는 데이터의 원활한 흐름을 만들 수 있습니다.
  • 비동기 로드 고려: 게임의 크기가 커지면 처음에 모든 것을 로드하던 것에서 필요할 때 애셋을 로드하도록 해야 합니다. 이 시점에 도달하면, 하드 레퍼런스  소프트 레퍼런스 또는 PrimaryAssetId (프라이머리 애셋 ID)로 변환하기 시작해야 합니다. AssetManager (애셋 매니저)는 애셋 비동기 로드를 손쉽게 해주는 함수를 여럿 제공하며, 로우 레벨 함수를 제공하는 StreamableManager (스트리머블 매니저)도 노출되어 있습니다.



https://docs.unrealengine.com/4.27/ko/Resources/SampleGames/ARPG/BalancingBlueprintAndCPP/

 

블루프린트와 C++ 의 조화

블루프린트/C++ 를 간이 사용하는 게임을 만드는 법, 그 진행 과정에서 의사 결정법을 설명합니다.

docs.unrealengine.com

 

▣ 왜 언리얼 엔진을 쓰는가? 

1.  내 손으로 직접 멋진 것을 만들고 싶어서 
2.  앞으로도 계속 쓰일 엔진 
3.  좀 더 공부를 많이 하고 싶어서 
4. 게임 엔진을 더욱 깊숙히 알고 싶은 자유 




▣ 언리얼 엔진의 철학

1. 모든 직군이 프로그래밍을 몰라도 컨텐츠를 제작할 수 있도록 설계한다. 

2. 게임을 제작한 경험을 바탕으로, 최대한 완성된 프레임웍을 만들어 제공한다. 

3. 프레임웍의 설계는 (초) 대형 게임을 제작할 수 있는 스케일로 기획한

 


 Modern Game Engine 

 

▣ Modern Game Engine Architecture 

최신 게임 엔진은 세 개의 계층으로 구분할 수 있다.

 

 

▣ Modern Game Engine Architecture - Runtime

• 단단하고 빠른 게임 엔진의 심장 
• 일반적으로 개발 언어는 C++를 많이 사용 
• 사소한 오류에도 결과는 크리티컬하기 때문에 코딩에 엄격한 룰을 적용 • 컨텐츠 제작자에게 노출하지 않는다.

 

▣ Modern Game Engine Architecture - RuntimeDeveloper

• 컨텐츠 개발에 보조로 사용되는 개발 도구. 
• 방대한 엔진 기능 관리를 위해 다양한 도구의 개발이 필요. 
• 사용 프레임웍에 따라 구현. C++ 을 그대로 쓰거나 C#로 변경하거나. 
• 컨텐츠 제작자에게 노출하지 않음. 

 

▣  Modern Game Engine Architecture – Editor 

• 게임 컨텐츠 개발을 위한 에디터 UI의 제공 
• 다양한 외부 파일을 엔진 파일로 변환시켜주는 변환 기능 
• 3차원 공간을 다룰 수 있는 뷰포트 제공 
• 컨텐츠 제작자 직접 활용하고 결과물을 디스크에 저장 



▣  Modern Game Engine Architecture – Contents 제작 

•  컨텐츠를 개발하고 결과물을 디스크에 저장 
• 생산성 : 빠른 컴파일, 스크립트와 전문 개발 도구 연동 
• 안정성 : 실행 영역의 분리 : 안전한 메모리관리 


▣ Modern Game Engine Architecture – C# 스크립팅 

 

• 이미 산업적으로 널리 쓰이는 완성도 높은 언어 
• 업계 최강의 개발도구 Visual Studio 
• MS 표준 Runtime에서 안전하게 실행 
• 전문가의 눈높이를 맞출 만큼 탄탄한 설계가 가능 
• Native 라이브러리와 연동(Interop) 및 확장 가능한 설계 
• Assembly Component의 공유 ( GAC ) – 코드 재사용, 컴파일 타임을 단축 
• Native 못지 않는 빠른 성능 ( JIT / AOT 컴파일 ) 
 
 ※ 초급/고급 프로그래머 모두를 만족시키는 프로그래밍 환경 

 

▣ Modern Game Engine Architecture – 블루프린트 

○ 장점

• 에픽 게임스가 만든 시각적 프로그래밍 언어 ( Visual Programming Language ) 
• 자체 개발 도구를 제공 ( Blueprint Editor ) 
• 자체 제작한 Runtime에서 안전하게 실행 
• 빠른 컴파일 속도  
• 가독성 높은 디자인 

 ※  프로그래밍이라는 진입 장벽이 없어 모든 직군이 사용 가능 

 

○ 단점

• 전문가의 눈높이를 맞출 만큼 설계 구조가 탄탄하지 못함. 
• 블루프린트 시스템 밖으로는 확장할 수 없다.  
• 컴파일은 빠르나 실행 속도는 느리다. ( 그렇다고 게임을 못 만들 성능은 아님 ) 
• 복잡도가 증가할 수록 사용하는 리소스가 늘어나고 가독성이 떨어짐. 

 ※ 블루프린트는 만능이 아니다. 


▶ 대신 프로그래머를 위해 C++을 제공한다

 


유니티와 언리얼 비교

 

 

유니티 - 언리얼 차이점

Unity와 Unreal 비교 ▣ Unity 블랙박스 존 : 외부 개발자가 볼 수 없도록 철저하게 격리, 유니티에서 관리 배포 격리된 환경의 장점 : 프로그래머는 하나만 신경쓰면 된다. ▣ Unreal 개발자가 모든 엔진 소스를..

kyoun.tistory.com

 


소스 코드를 얻게 되면 생기는 문제점 

 

• 소스 코드 양이 너무 어마어마하다.  
• 개발 할 때 이를 함께 가지고 가야 한다. 
• 부수적으로 따르는 부작용 
• 느린 컴파일 속도 
• 느린 인텔리센스 기능 
• 엄청난 용량의 결과물 

이런 문제들은 빠른 C++ 언어의 자체적인 문제 


자동으로 완성해주는 마법의 빌드 툴 - Unreal Build Tool 


프로젝트의 Target.cs와 모듈의 Build.cs 를 통해 설정 가능 


• 멀티플랫폼 빌드 툴 
• Private 영역과 Public 영역을 구분해 빌드 목록 생성 
• 의존성을 파악하고 빌드 순서 지정 
• Modular  ( 에디터 ) / Monolithic ( 게임 ) 방식의 빌드 지원 
• 프리컴파일드헤더 컴파일 지원 
• Static Library 지원 ( 플러그인 ) 
• 유니티 빌드 방식 지원 ( 대용량 컴파일을 묶어서 빠르게 ) 

 


 관리받는 코드와 Native 코드의 혼합 

 


▣ C++ 언어의 생산성을 높이자 

• 객체의 초기 설정 값을 손쉽게 관리 
• 런타임에서 클래스와 인스턴스 정보를 검색 
• 객체의 저장과 로딩을 간편하게 
• 메모리 관리를 편하게 
• 함수를 묶어서 한번에 호출  
• 모든 플랫폼에 안정적으로 동작하는 API  

※ 언리얼은 C#과 같은 C++ 프레임웍의 제공한다

 

▣ 관리받는 코드와 Native 코드의 혼합 

 

생산성을 높이기 위해 관리받는 C++ 클래스 프레임웍을 구축 


  소스코드에 특별한 매크로가 있으면 파싱을 시도하고 규칙에 맞으면 엔진에서 관리 

 

▣ 관리받는 코드와 Native 코드의 혼합 – 모듈 초기화 

 

하지만, 편리한 기능으로 인해 고려해야 할 것이 늘어난다

 

언리얼 엔진의 실행의 처음에는 항상 모듈 단위로, 언리얼 오브젝트 초기화 과정이 들어간다.

 

▣ 관리받는 코드와 Native 코드의 혼합 – 모듈 초기화 

 

 

DLL 단위로 모듈 내 모든 언리얼 오브젝트 초기화가 완성되면 %가 올라간다. 

 

▣ 관리받는 코드와 Native 코드의 혼합 – UClass

 

하나의 언리얼 오브젝트에는 상응하는 UClass가 존재한다

 

초기화 단계에서 모듈 별로 자신이 속한 언리얼 오브젝트의 UClass DB를 구축한다. 
언리얼 엔진에서는 “/Script/모듈이름.클래스이름” 형식으로 고유 주소를 부여한다



▣ 관리받는 코드와 Native 코드의 혼합 – UClass

CDO – Class Default Object ( 클래스 기본 객체 ) 


언리얼 오브젝트는 CDO를 복제해 인스턴스를 생성하도록 설계되어 있다. 

 

▣ 관리받는 코드와 Native 코드의 혼합 – 모듈 초기화 

 

초기화 단계에서 복제하기 쉽게 미리 CDO를 만들어준다. 

초기화가 끝나면 대략 위와 같은 형태로 메모리가 완성된다. 

 

▣ 관리받는 코드와 Native 코드의 혼합 – CDO


CDO는 생성자 코드를 실행하면서 완성된다. 즉, 생성자 코드는 엔진 초기화 단계에서 실행된다. 

생성자 코드에서는 게임에 대한 내용을 전혀 알수가 없다!


각 언어 별, 난이도/확장성 분석

 

 


결론


• 빠르게 프로토타입 컨텐츠를 만들려면 (방식이) 맘에 안들어도 블루프린트가 맘 편하다. 

• 블루프린트는 확장성에 한계가 있으니, 어디까지 할 수 있는지 미리 조사하자. 

• 제대로 엔진을 다루고 싶다면 C++로 가야 한다. 

• 자유에는 (엄청난) 대가가 따른다!  ( 컴파일  타임 , 인텔리센스  ㅜㅜ ) 

• C# 개발환경보다 (많이) 불편할지라도 익숙해지려고 노력하자. 

• 하나씩 차근차근 따라오면 세계 최고 엔진이 내 것이 된다. 

 

 

 

참고// 언리얼 서밋 :: 언리얼 튜토리얼만 쌓여가는 유니티 개발자를 위한 조언 

 Unity와 Unreal 비교

 

▣   Unity  


블랙박스 존 : 외부 개발자가 볼 수 없도록 철저하게 격리, 유니티에서 관리 배포 

유니티 블랙박스 존


격리된 환경의 장점 : 프로그래머는 하나만 신경쓰면 된다. 

 

▣  Unreal


 개발자가 모든 엔진 소스를 활용해 추가 개발할 자유를 부여  

 

    무한한 자유를 얻기 위해서는 C++을 사용해야 한다


두 엔진의 Rendering Pipeline 비교


▣ Rendering Pipeline 개요

 

주어진 파이프라인을 활용하는 방법은 굉장히 다양하다. 

( 자체 엔진이 필요한 이유 ) 

 

▣ Rendering Pipeline 개요 – Unity

 

 

게임엔진은 생산성을 위해 작업자들이 일관성있게 작품을 제작하도록 일정한 규격을 제공해준다

유니티 랜더링 파이프

 

모델이 심플할수록 생산성은 올라가지만 파이프라인을 다양하게 활용하는 유연성이 떨어진다

 

▣ Rendering Pipeline 특징 – Unity 


• 유니티는 낮은 수준의 모델을 제공한다. = 엔진의 유연성이 높다
• 아티스트가 편하게 작업할 수 있도록 머티리얼 시스템이 설계되어 있다. 
• 셰이더 프로그래밍도 어느 정도 파고들 수 있다. 
• 셰이더 프로그래밍도 두 단계로 나눈다.  ( 서피스 셰이더 / 일반 ) 


 

▣ Rendering Pipeline 개요 – Unreal

 

언리얼 엔진은 유연성보다 최대한 기능과 완성도를 높인 프레임웍을 제작해 모델을 제공 

언리얼 파이프라인

 

유연함에 있어서 언리얼보다 유니티가 더 큰 장점을 가진다

 

▣  Rendering Pipeline 특징 – Unreal Engine 

• 언리얼은 높은 수준의 모델을 제공한다. = 엔진의 유연성이 떨어진다. 
• 머티리얼 시스템에서 모든 것이 끝난다.  ( 이 이상은 전문 프로그래머에게. ) 
• 옵션 체크만으로 하이엔드와 모바일 기능이 (자동으로) 처리된다

 


 두 엔진간 렌더링 시스템 철학의 비교 

 

유니티는 최소 기능에서부터  수동으로 확장하는 방식 

 

 

언리얼은 최대 기능에서부터  호환성을 유지해 자동으로 줄여주는 방식 

 

 

1. 원형 연결리스트

 

 ▣ 원형 연결 리스트의 이해

 

 

단순 연결리스트


단순 연결 리스트의 마지막 노드는 NULL을 가리킨다

원형 연결리스트

 

원형 연결 리스트의 마지막 노드는 첫 번째 노드를 가리킨다

원형 연결리스트 머리에 노드 추가

 

∙ 원형 연결리스트는 모든 노드가 원의 형태를 이루면서 연결되어 있기 때문에 원형 연결 리스트 에서는 사실상 머리와 꼬리의 구분이 없다

∙ 두 경우의 노드 연결 순서가 같으니, 원형 연결리스트는 head가 가리키는 노드가 다르다.

 

▣ 원형 연결 리스트의 장점 

 

“단순 연결 리스트처럼 머리와 꼬리를 가리키는 포인터 변수를 각각 두지 않아도,  (2개)
하나의 포인터 변수만 있어도 머리 또는 꼬리에 노드를 간단히 추가할 수 있다.”    (1개) 만으로도 가능

 

변형 중 원형 연결리스트

 

앞서 소개한 모델을 기반으로는 위의 장 점을 살리기 어렵다. 
그래서 우리는 변형 된, 그러나 보다 일반적이라고 인식이 되고 있는 변경된 원형 연결 리스트를 구현한다.

 

▣ 변형된 연결 리스트

 

변형된 연결 리스트

 

 꼬리를 가리키는 포인터 변수는? tail

∙ 머리를 가리키는 포인터 변수는? tail->next 

이렇듯 리스트의 꼬리와 머리의 주소 값을 쉽게 확인할 수 있기 때문에,
연결 리스트를 가리키는 포인터 변수는 하나만 있으면 된다.

 

▣ 변형된 연결 리스트변형된 원형 연결 리스트의 구현 범위


※ 원형 연결 리스트는 그 구조상 이 존재하지 않는다. 
따라서 LNext 함수는 계속해서 호출이 가능하고, 이로 인해서 리스트를 순환하면서 저장된 값을 반환하도록 구현한다.



▶ 구조체 정의

typedef struct _linkedList
{
	Node* tail;
	Node* cur;
	Node* before;

	int numOfData;

}LinkedList;

typedef LinkedList List;



▶ 초기화 함수

//초기화
void ListInit(List* pList)
{
	pList->tail;
	pList->cur = NULL;
	pList->before = NULL;
	pList->numOfData = 0;
}

모든 멤버를 NULL과 0으로 초기화한다.

 


▶ 노드 삽입 

void LInsertFront(List* pList, LData data)
{
	Node* newNode = new Node();
	newNode->data = data;

	if (pList->tail == NULL)
	{
		pList->tail = newNode;
		newNode->next = newNode;
	}
	else
	{

	}
	(pList->numOfData)++;
}

첫 번째 노드는 그 자체로 머리이자 꼬리이기 때문에 노드를 뒤에 추가하건 앞에 추가하건 그
결과가 동일하다

 두번쨰 이후 노드 삽입

void LInsertFront(List* pList, LData data)
{
	Node* newNode = new Node();
	newNode->data = data;

	if (pList->tail == NULL)
	{
		pList->tail = newNode;
		newNode->next = newNode;
	}
	else
	{
		newNode->next = pList->tail->next;  //1
		pList->tail->next = newNode;        //2
	}
	(pList->numOfData)++;
}

 


  원형 연결 리스트 구현: 앞과 뒤의 삽입 과정 비교

노드를 머리에 추가한 결과

 

노드를 꼬리에 추가한 결과

 

 

void LInsertTail(List* pList, LData data)
{
	Node* newNode = new Node();
	newNode->data = data;

	if (pList->tail == NULL)
	{
		//pList->tail = newNode;
		//newNode->next = newNode;

		pList->tail = newNode;
		newNode->next = newNode;

	}
	else
	{
		newNode->next = pList->tail->next;
		pList->tail->next = newNode;
		pList->tail = newNode;       //꼬리가 newNode 위치로 이동한다 (위 함수와 꼬리 위치가 차이점)
	}
	(pList->numOfData)++;
}

 


  원형 연결 리스트 구현: 조회

int LFirst(List* pList, LData* pdata)
{
	if (pList->tail == NULL)   
		return false;

	pList->before = pList->tail;     
	pList->cur = pList->tail->next;  

	*pdata = pList->cur->data;

	return true;
}

LFirst 함수 호출 결과

cur가 머리를 가리키게 한다

 

int LNext(List* pList, LData* pdata)
{
	if (pList->tail == NULL)   
		return false;

	pList->before = pList->cur;    
	pList->cur = pList->cur->next; 

	*pdata = pList->cur->data;
	return true;
}

※ 원형 연결 리스트이므로 리스트의 끝을 검사하는 코드가 없다

이어지는 LNext 호출결과

 


 

▶ 원형 연결 리스트 구현: 노드의 삭제

 

∙ 핵심연산 1. 삭제할 노드의 이전 노드가, 삭제할 노드의 다음 노드를 가리키게 한다.

∙ 핵심연산 2. 포인터 변수 cur을 한 칸 뒤로 이동시킨다.

 

//더미 연결리스트 삭제 코드
LData LRemove(List* pList)
{
	Node* rpos = pList->cur;
	LData rdata = rpos->data;


	pList->before->next = pList->cur->next;  //1
	pList->cur = pList->before;              //2


	delete rpos;
	(pList->numOfData)--;

	return rdata;
}

※ 더미 노드와 삭제하는 방법은 동일하다

 

  그림상으로는 두 연결 리스트의 삭제 과정이 비슷해 보이나
원형 연결 리스트에 는 더미 노드가 없기 때문에 삭제의 과정이 상황에 따라서 달라진다.

 

▶ 원형 연결 리스트 노드 삭제 구현

LData LRemove(List* pList)
{
	Node* rpos = pList->cur;
	LData rdata = rpos->data;

	//삭제할 노드를 tail이 가리킨다면
	if (rpos == pList->tail)
	{
		//그리고 마지막 노드라면
		if (pList->tail == pList->tail->next) //2
		{
			pList->tail == NULL;
		}
		else
		{
			pList->tail = pList->before;	   //1
		}
	}

	//단순 연결리스트와 동일한 코드
	pList->before->next = pList->cur->next;
	pList->cur = pList->before;

	delete rpos;
	(pList->numOfData)--;
	return rdata;
}

 


2. 양방향 연결리스트

 

 

 

 

 

typedef struct _node
{
	int data;  //현재 데이터
	struct _node * next;   //다음 노드를 가르킬 포인터  
	struct _node * precv;  //이전 노드를 가르킬 포인터  

} Node;

양방향 연결 리스트를 위한  노드 구조체 변경

 

 

▶ 양방향으로 노드를 연결하는 이유

 

2종류의 연결리스트

 

 ※ 단순 연결 리스트와 같이 before를 유지할 필요가 없다!

※ 오른쪽 노드로의 이동이 용이하다! 양방향으로 이동이 가능하다!

 위의 코드에서 보이듯이 양방향으로 연결한다 하여 더 복잡한 것은 아니다! 그렇게 느낀다면 선입견이다.

 

▶  구현할 양방향 연결 리스트 모델

 

 

 

  • LRemove 함수를 ADT에서 제외시킨다. 
  • 대신에 왼쪽 노드의 데이터를 참조하는 LPrevious 함수를 ADT에 추가시킨다.
  • 새 노드는 머리에 추가한다.

 

▶  연결 리스트의 구현: 리스트의 초기화

 

typedef struct _dblinkedList
{
	Node* head;
	Node* cur;
	int numOfData;

}_dblinkedList;

typedef _dblinkedList List;

ListInit 함수의 정의에 참조해야 하는 구조체의 정의

 

//초기화
void ListInit(List* pList)
{
	pList->head = NULL;
	pList->numOfData = 0;
}

멤버 cur은 조회의 과정에서 초기화 되는 멤버이니 head와 numOfData만 초기화 하면 된다

 

▶  연결 리스트의 구현: 노드 삽입

 

더미 노드를 기반으로 하지 않기 때문에 노드의 삽입 방법은 두 가지로 구분이 된다

 

▶  연결 리스트의 구현: 첫번쨰 노드 삽입

 

void LInsert(List* pList, LData data)
{
	Node* newNode = new Node();
	newNode->data = data;

	newNode->next = pList->head;
	newNode->precv = NULL;
	pList->head = newNode;
	
	(pList->numOfData)++;
}

 

첫 번째 노드의 추가 과정만을 담은 결과 

 

 

▶  연결 리스트의 구현: 두 번째 이후 노드의 삽입

void LInsert(List* pList, LData data)
{
	Node* newNode = new Node();
	newNode->data = data;

	newNode->next = pList->head;

	if (pList->head != NULL) 
	{   //(1/2)
		pList->head->precv = newNode;
	}

	//헤드 이동(2/2)
	newNode->precv = NULL;
	pList->head = newNode;
	
	(pList->numOfData)++;
}

 

첫 번째 노드의 추가 과정에 덧붙여서, if문이 포함하는 문장이 두 번째 이후의 노드 추가 과정에서는 요구가 된다. 

이미 들어있을 경우에는 헤드 이전에 추가를 하고, newNode의 이전을 NULL로 한뒤 헤드를 newNOde로 이동한다

 

 

 

▶  연결 리스트의 구현: 데이터 조회

 

int LFirst(List* pList, LData* pdata)
{
	if (pList->head == NULL)   
		return false;

	pList->cur = pList->head;  //처음

	*pdata = pList->cur->data;
	return true;
}

int LNext(List* pList, LData* pdata)
{
	if (pList->head == NULL)
		return false;

	pList->cur = pList->cur->next; //다음거

	*pdata = pList->cur->data;
	return true;
}

int LPreviout(List* pList, LData* pdata)
{
	if (pList->head == NULL)
		return false;

	pList->cur = pList->cur->precv;  //전에거

	*pdata = pList->cur->data;
	return true;
}

 

※ LFirst 함수와 LNext  함수는 사실상 단방향 연결 리스 트의 경우와 차이가 없다. 
   LPrevious 함수는 LNext 함수와 이동 방향에서만 차이가 난다

+ Recent posts