3일내내 개지랄떨면서 우회로 해결한 이슈이다...

 

현재 프로젝트에서 데이터테이블을 20개가량 사용중이고

게임인스턴스::생성자에서 FObjectFinder로 데이터테이블을 불러와서 사용중인 상황이다.

근데 패키징후 프로젝트를 키면 검은화면만 나오고 1~2초후 응답없음으로 진행이 되지 않는다.

이것은 Shipping빌드에서도 마찬가지였다...

 

로그찍어가면서 별별 지랄한 끝에 문제의 지점을 발견했는데

특정 데이터테이블을 FObjectFinder로 불러오는 순간 프로젝트가 멈춰버린다.

FObjectFinder로 불러올 때 성공,실패여부를 확인할 수 있지만 그런 문제가 아니라 그냥 멈춰버린다.

특정 데이터테이블의 정체는 영웅(캐릭터)의 데이터를 모아둔 테이블이였다.

 

영웅 데이터테이블의 변수중 하나는 TSubclassOf<>를 사용해서 영웅클래스를 지정하는 변수를 사용중인데

나는 c++영웅클래스가 아니라 c++에서 상속받은 BP영웅클래스를 데이터테이블에 넣어서 사용중이였다.

테스트를 포함해서 여러모로 영웅클래스는 블루프린트가 굉장히 편하기 때문에 애용중이였는데 혹시 특정 영웅의 블루프린트가 문제가 아닐까 싶어서 데이터테이블의 모든 값(행)의 TSubclassOf<>를 None으로 없애버리니까 게임이 시작된다!!

그래서 특정 영웅 블루프린트가 문제인가 싶어서 계속 디버깅해봤지만 뭔가이상하다 어쩔땐되고 어쩔땐 안되고 지 맘대로 된다. 점점 더 미궁으로 빠져들어가는데 몇시간 디버깅후 또 하나 알아냈다.

1. TSubclassOf<>가 문제인건 맞음

2. 10개행,20개행...그러니까 데이터테이블의 행 개수와 상관없이

TSubclassOf<>변수에 영웅블루프린트 클래스를 각기다른 4개이상의 클래스를 넣으면 문제가 생김.

간단하게 설명하기가 어렵다.. 10개행에 영웅_BP_A클래스로 꽉 채우면 문제가 없지만,

아무 행에 영웅_BP_A, B, C, D의 각기다른 블루프린트 클래스가 4개이상 행에 들어가 있다면 문제발생.

순서상관없고 행위치도 상관없다...

3. 영웅블루프린트가 아니라 영웅C++클래스는 또 문제없음.

4. 영웅클래스가 아니라 AActor를 상속받은 블루프린트도 문제없음.

너무 얼탱이가 없다. 안되면 그냥 안될것이지 왜 4개이상의 각기다른 클래스가 데이터테이블에 들어가면 문제가 생기고, 또 같은클래스를 넣으면 문제없고...

이 지점에서 그냥 영웅클래스를 C++로만 사용할까...도 생각했지만 BP를 사용하는것이 생산성,테스트등 훨~씬 편하기 때문에 BP를 포기할순 없었다.

 

아~혹시 블루프린트클래스가 포함된 데이터테이블이 초기화되는것보다 게임인스턴스생성자가 더 빨리 실행되서 초기화 되지 않는 데이터테이블을 불러오기 때문에 문제가 생긴것일까? 싶었다. 실제로 8개행밖에 없는 영웅데이터테이블을 에디터에서 로드하는데 약간의 딜레이가 생긴다. 임포트시 5~10초정도 걸린다. 영웅 데이터테이블만 생성자에서 불러오지 않고 게임인스턴스::Init() override 함수에서 LoadObject()로 런타임중 데이터테이블을 가져오는 시도를 해보았지만 되지 않았다.

 

이때, 번뜩인 생각이 혹시 영웅 데이터테이블이 패키징시 아예 누락된 것이 아닐까? 싶어서

프로젝트설정->패키징->Pak압축(Pak 파일 사용)->false로 설정해서 패키징된 에셋을 직접보니 역시 영웅데이터테이블만 누락되어 있었다. 미리 말씀드리면 아직도 왜 누락되었는지 모르겠다. 그래서 패키징할때 여러 방법으로 영웅데이터테이블을 강제로 패키징하는 방법을 찾아봤다.

1. 프로젝트설정->패키징->패키징할 추가 비에셋 디렉토리에 경로추가

-> 실패

2. 프로젝트설정->패키징->복사할 추가 비에셋 디렉토리에 경로추가

-> 실패

3. 프로젝트설정->패키징->쿠킹할 추가 에셋 디렉토리에 경로추가

 -> 성공했으나 너무 Raw한 모양의 데이터테이블에셋이 패키징경로에 추가되어있어서 상당히 찝찝했다.

그리고 경로를 추가하는방법이라 같은 폴더에 있는 모든 데이터테이블이 Pak압축되지 않고 파일명이 그대로 노출되기 때문에 영웅데이터테이블만 따로 다른폴더에서 관리해야하는데 여러모로 참 귀찮다. 결국 이 방법은 사용하지 않는다.

 

이렇게 테스트를 해보니 패키징용량이 왔다갔다하는것이 보였다.

Pak압축기준 약2기가정도 되는 패키징파일이 영웅데이터테이블을 불러오지 않으면 450메가정도로 확 줄어든다. 아마 영웅데이터테이블에 참조된 영웅클래스와 관련된 메쉬, 머터리얼등의 에셋이 빠져서 그런듯하다.

이때 과거 패키징관련 글을 읽은 기억이 났다.

 

패키징은 프로젝트에 참조된 에셋만 패키징이 되고, 프로젝트에 에셋이 있더라도 어디서도 참조형식으로 사용되지 않으면 패키징에 추가되지 않는다.

 

이 생각이 나면서 혹시 c++코드로만 참조하고 있던 데이터테이블을 블루프린트에 변수로 넣으면 더 확실하게 참조(진짜 더 확실한건진 모름)가 되고, 그러면 어떤식으로든 패키징시 데이터테이블이 패키징에 누락되지 않고 제대로 되지 않을까?라는 생각이 들면서 곧바로 시도했다.

게임인스턴스C++에

UPROPERTY(EditAnywhere)

UDataTable* _hero_datatable = nullptr;

이런식으로 변수를 하나 만들고 게임인스턴스BP에서 _hero_datatable에 영웅 데이터테이블을 넣고 패키징을 해봤다.

 

드디어... 패키징에 영웅 데이터테이블이 누락되지않고 잘 들어가있었다. 감격의 순간이었다.

사소한 단점은 이상하게 생성자에서는 여전히 불러오진 못한다...

분명 Pak압축을 하지 않은 패키징파일에서 분명! 영웅 데이터테이블에셋이 있는걸 두눈으로 봤는데 생성자에서 불러오진 못한다. 그래서 _hero_datatable변수를 직접 사용하는데 아직까지 문제는 없다.

#if WITH_EDITOR 를 이용해서 에디터에서는 생성자에서 불러오고, 에디터가 아니라면 _hero_datatable을 사용하는 방법을 채택했다. 

 

아직도 왜 패키징에서 누락되는지 모르겠다. 열심히 구글링해봤지만 동일한 문제를 겪는 글은 찾지못했고 비슷한 글을봐도 유의미한 내용은 없었다. 

 

혹시 같은 문제를 겪는다면 이런방법으로 우회로 해결하는 방법으로 도움이 되셨으면 좋겠다.

 

------------------------------------------------------------------------------------------------------------------------------------------------

TSubclassOf<>를 TSoftClassPtr<>로 변경하니까 잘된다...

결국 우회해서 해결한 방법은 없애고 TSoftClassPtr<>를 사용해서 정상?적으로 데이터테이블을 불러오고있다

 

암만 구글링해도 TSoftClassPtr<>와 패키징관련한 글이 안보여서 정확한원인은 찾지못한 상태...

	//가장 앞, 뒤의 데이터를 삭제합니다
	const bool RemoveFront()
	{
		return Remove(head->next);
	}
	const bool RemoveBack()
	{
		return Remove(tail->prev);
	}

    private:
	//데이터를 찾아서 삭제합니다
	const bool Remove(Node* nd_remove)
	{
		if (size <= 0) return false;

		nd_remove->prev->next = nd_remove->next;
		nd_remove->next->prev = nd_remove->prev;
		delete nd_remove;
		--size;
		return true;
	}

이전 포스팅에서 소개한 AddFront(),AddBack(),Add()와 동일한 구조의 함수다

Remove()는 성공과 실패를 리턴하고, 노드를 삭제하며 삭제된 노드 앞뒤의 노드끼리 연결시킨다

 


	//원하는 데이터를 찾아서 삭제합니다
	const bool RemoveData(const Data& data)
	{
		if (size <= 0) return false;

		for (Node* nd = head->next; nd != tail; nd = nd->next)
		{
			if (nd->data == data)
			{
				//삭제할 데이터를 찾았습니다. 삭제합니다
				return Remove(nd);
			}
		}

		//삭제할 데이터를 찾지 못했습니다
		return false;
	}

	//모든 데이터를 삭제합니다
	void RemoveAll()
	{
		while (RemoveFront()) {}
	}

 

 

RemoveData(): 동일한 데이터를 찾아 삭제.

RemoveAll(): 모든 데이터를 삭제.

 

RemoveData()에서 노드의 순회을 볼 수 있다.

head->next노드부터 시작해서 tail이 아닌 노드면 계속 순회한다.

 

역순회도 다르지 않다.

for (Node* nd = tail->prev; nd != head; nd = nd->prev)

논리적으로 거꾸로 돌아가게 하면된다.

 


프로그래밍 처음 공부했을때 꽤 어려웠던 연결리스트였는데 지금 다시 보니까 구현자체는 어렵지 않았다.

데이터의 삽입,삭제시 노드연결만 신경써주면된다.

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

연결리스트는 여러개의 노드로 구성되어 있기 때문에 노드 구조체를 먼저 만들어보자

	//template <typename Data>
    struct Node
	{
		Data data = NULL;
		Node* next = nullptr;
		Node* prev = nullptr;
	};

노드는 대단한건 없고, 데이터를 저장하기위한 변수와, 앞 뒤 노드를 가리키는 변수2개가 필요하다.

처음 프로그래밍을 공부할 때 이해가 어려웠던 부분이

 

Node구조체안에 왜 똑같은 노드가 2개가있지? 그리고 왜 포인터지?

노드는 노드를 가리켜야 하니까 저렇게 한건 알겠는데 논리적으로 이해를 못하니까 두루뭉술하게 이해했고,

왜 포인터로 저장하는지도 이해를 못했다.

 

노드 구조체안에 노드변수를 가져야하는것은 구현하면 알게되니 넘어가고

포인터로 저장하는 이유는 노드를 '복사'해서 가지지 않으려고 그런것이다.

call by value, call by address, call by reference라는 개념이 있는데 

여기서 이것까지 설명하면 길어지기 때문에 이 부분은 넘어가고 여기선 일단

노드구조체안의 노드변수는 앞 뒤 노드의 정보를 원본으로 가지고 있어야하기 때문이라고 이해하자


	MyLinkedList()
	{
		head = new Node;
		tail = new Node;

		head->next = tail;
		tail->prev = head;
	}
	~MyLinkedList()
	{
		//모든 노드 삭제
		RemoveAll();

		delete head;
		delete tail;
	}

	Node* head = nullptr;
	Node* tail = nullptr;
	size_t size = 0;

생성자에선 헤드와 테일을 미리 생성하고 서로 연결시켜 놓습니다.

소멸자에선 모든 데이터를 제거및해제 하고, 헤드와 테일도 해제합니다. 항상 포인터같은 할당된 변수는 꼭 해제를 해야합니다.

RemoveAll()은 다음 포스팅에서 나옵니다

 

내가 구현한 이중연결리스트는 헤드와 테일에 데이터가 없습니다


	//데이터를 추가합니다
	void AddFront(const Data& data)
	{
		Add(head->next, data);
	}
	void AddBack(const Data& data)
	{
		Add(tail, data);
	}
    
    private:
	void Add(Node* nd_next, const Data& data)
	{
		//노드를 새로생성합니다
		Node* nd = new Node;
		nd->data = data;

		//생성한 노드의 앞, 뒤 노드를 연결합니다.
		nd->next = nd_next;
		nd->prev = nd_next->prev;

		//앞, 뒤 노드도 생성한 노드에 연결합니다
		nd_next->prev->next = nd;
		nd_next->prev = nd;

		++size;
	}

AddFront(), AddBack()은 앞 혹은 뒤에 데이터를 추가하겠는 함수고 실제 로직은 Add()에 들어있다.

Add()를 보면 생성된 노드와 앞,뒤의 노드를 연결하는 부분이 헷갈릴수 있다.

하지만 하나씩 하나씩 나눠서 보면 이해가 될것이다.

그래도 이해가 안되면 직접 그려보자.

 

https://github.com/ForestBird1/MyContainer

 

GitHub - ForestBird1/MyContainer

Contribute to ForestBird1/MyContainer development by creating an account on GitHub.

github.com

 

+ Recent posts