본문 바로가기
프로그래밍/Unreal 부트캠프

TIL 2025.02.10 기록

by Rozentea 2025. 2. 10.

0. 개요


오늘은 주말 여행을 다녀와서.. 조금 더 시간이 빠듯해졌다.. ㅠ

그래서 최대한 강의를 다 듣고, 빠르게 정리하는 것을 목표로 공부했다.

하지만, 아무리 빠르게 들어도.. 아직 2강의를 못들어서 내일부터 과제를 진행할 수 있을 것 같다.

 

때문에 TIL 정리는 강의 2~3개 정리 + 챌린지반 정리로 진행할 것이다.

 

또, 오후에는 커리어 데이를 진행해 여러 취업 관련 이야기들을 들을 수 있어서 매우 유익했다.

 

내일 부터는 밀린 강의 정리 2~3개 + 과제로 진행할 예정이다.

 

과제도 도전하고 싶은 것은 많았는데, 아쉽게도 많이는 못해볼 것 같다.

;ㅅ;... 시간이... 아니야.. 아니야!!

그래도 할래!!

내일부터 3일간 밤을 새서라도 하고 싶은거 다 한다...!

 

1. 챌린지 수업 정리


map과 unordered_map, set, unordered_set은 코딩테스트 뿐 아니라 실제로 로직을 짤 때에도 많이 유용하게 사용되니 공부해두면 정말 정말 좋다고 하셨다.

나도 그 동안 조금씩이나마 사용해본 결과 어떤 말씀이신지 조금은 알것같았다.

하지만 여전히 처음보는 함수들도 많았고, 몰랐던 기능들을 더 알아갈 수 있어서 유의미한 수업이었다.

 

1. 우선 순위 큐

queue라는 것은 선입선출 자료구조였다.

우선 순위 큐는 중요도에 따라서 우선 순위가 높은 데이터들이 앞쪽에 위치되어 먼저 나가는 구조이다.

내부적으로는 힙자료구조를 사용한다.

(이때 힙은 힙메모리와는 다른 단어이다.)

(이때 힙은 캠프 초반에 힙정렬을 구현하면서 공부했던 해당 힙!)

가장 큰/작원 원소를 순식간에 추출해야하는 문제에 최적화되어 있다.

 

기본적으로 최대 힙 구조이기 때문에 가장 큰 원소가 먼저 나온다.

즉, 운선순위가 높다는 것은 값이 크다는 것을 의미한다.

최소 힙도 사용이 가능한데, greater<> 템플릿을 인자로 전달하면, 정렬을 바꿀 수 있다.

즉, 최대, 최소 힙을 만드는 기준을 바꿀 수 있다

디폴트 인자로는 오름차순인 less<>가 인자로 전달된다.

 

compare comparator를 커스텀으로 정의해 사용할 수도 있다.

직접 compare를 구현하지 않고도, queue에서 꺼내서 음수로 바꿔서 다시 넣어 구현하는 방법도 있다.

(나는 원래 프로젝트를 진행할 때 어떻게든 완성을 목표로 온몸비틀기도 서슴치 않아하는데, 코딩 테스트에 있어서는 정해진대로 풀지 않으면 의미가 없다라고 생각이 점점 바뀌어 사고가 조금 유연해지지 못하게 바뀌었던 것 같다.

위 처럼 우선 순위 큐에서 꺼내어 역순으로 다시 출력해 오름차순에서 내림차순으로 혹은 내림차순에서 올름차순으로 바꾸는걸 이번에 생각하지 못했었다.)

꼼수인데, 이게 은근 코딩테스트 현장에서 쓸 수 있는 꼼수이기 때문에 이런 유연한 사고도 중요하고, 이런 꼼수들도 한번쯤은 알아두면 좋다고 하신다.

(다양한 풀이법을 생각할 수 있는 능력을 기르면 좋을 것 같았다.

추후에는 더 나아가 다양한 풀이법 중 어느 풀이가 더욱 효율적인지 알고, 적용하는 것을 넘어서 다른 사람들에게 납득이 가도록 설명할 수 있을 정도가 되면 좋겠다.)

 

<문제>

Q. 최소 힙 커스텀하게 구현하기

#include <iostream>
#include <queue>

using namespace std;

struct compare {
	bool operator() (const int& a, const int& b)
	{
		return a < b;
	}
};

int main()
{
	priority_queue<int, vector<int>, compare> pq;

	pq.push(1);
	pq.push(10);
	pq.push(8);
	pq.push(12);
	pq.push(17);
	pq.push(5);
	
	cout << pq.top();

	return 0;
}

 

compare 함수를 만들때 구조체를 만들어 연산자를 재정의하는 형태로 전달해주어야 한다.

 

2. 순열

순열 관련 문제에서 next_permutation, prev_permutation 를 사용하면 굉장히 편리하다.

주의할 점은 원본 데이터가 정렬 상태여야 한다.

(이전에 공부하고, 문제를 풀 때 몇 번 햇갈려 고생했던 덕분인지 이 주의사항은 좀처럼 까먹지 않아 다행이라 생각한다. ㅎㅎ)

 

<문제>

Q. 순열 거꾸로 출력해보기

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int main()
{
	vector<int> v = { 1,2,3 };

	sort(v.begin(), v.end(), greater<>());

	do
	{
		for (int x : v) cout << x << " ";
		cout << endl;

	} while (prev_permutation(v.begin(), v.end()));
}

 

3. K번째 값 찾기 

nth_element를 사용할 수 있다. 

파티션(pivot) 기반으로 n 번째로 작은 원소를 배열의 n번째 위치로 보낸 뒤, 그 왼쪽은 n 번째보다 작은 원소들, 오른쪽은 n번째 보다 큰원소들 이런식 으로 반 정렬을 해준다.

나중에 퀵 소트를 배울 때, 파티셔닝하는 로직과 매우 유사하다.

즉, 다른것들은 특정 원소 기준으로 왼쪽, 오른쪽으로만 정렬된다. 때문에 특정 번째 원소를 찾을 때 편하게 사용할 수 있다.

(처음 사용해보는 함수라 몇 번 안 사용해서 아직 익숙하지 않지만, 특정 번째의 값을 찾을 땐 정말 유용하게 쓰일 것 같다.

하지만, 해당 원소 기준으로만 정렬되는 것 같고.. 또, 내부 값중 특정 값들중에서 순서를 찾으려면 추가적인 작업이 필요해 마냥 만능은 아닌 느낌이었다.)

 

<문제>

Q. 배열에서 짝수 고려해서 3번째 큰 수 정리하기

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int main()
{
	vector<int> v = { 1, 2, 3, 4 };

	do
	{
		for (int i = 0; i < 3; ++i)
		{
			cout << v[i] << " ";
		}
		cout << endl;
	} while (next_permutation(v.begin() + 1, v.end()));


	return 0;
}

 

 

2. 언리얼 강의 정리


1. 아이템 스폰 및 레벨 데이터 관리하기

스폰 영역을 지정하고, 해당 영역 내에서 랜덤한 위치에 아이템을 스폰하는게 목표였다.

추가적으로 아이템마다 스폰 확률을 정해주어 스폰 확률을 조절할 수 있도록 하는게 목표이다.

 

<스폰 영역 지정하기>

스폰 영역을 구현하는 방법은 다양하게 있다.

  • 수학적 알고리즘을 이용해 영역을 디테일하게 잡는것
  • 스폰 액터를 배치하고 중점을 기준으로 계산하는 것
  • 컬리전을 붙여서 컬리전 크기를 이용하는 것 등등

수업에서는 단순히 Box CollisionComponent를 이용해 구현했다.

우리는 이 콜리전 내부를 바로 스폰 영역으로 잡을 것이다.

 

<Box Collision Component를 이용한 이유>

  • 에디터에서 시각적으로 영역을 볼 수 있다.
  • 위치 계산하기가 편리하다.
  • 충돌 처리도 할 수 있기 때문에 해당 영역에 들어오면 그때부터 스폰을 시작한다 등의 처리가 가능하다.
  • 엔진에서 제공해주는 CollisionComponent들은 최적화가 잘되어있어서 가볍기 때문에 이런 식으로 사용한다고해서 성능저하가 발생하진 않는다.

이러한 CollisionComponent들은 SpawnVolum이라고 명칭을 보통 붙인다.

(음.. 사용 목적 + Volume으로 사용하는 모양..? 반대로 어떤 트리거면 트리거를 붙이면 될 것 같다.)

 

위의 2번 항목에 연결되는 내용으로

BoxComponent의 GetScaledBoxExtent()를 사용하면, 박스 콜리전의 절반 크기를 반환해준다..!!

 

<아이템 확률 데이터 테이블 만들기 I>

확률이나 이런 수치들은 데이터 테이블을 따로 만들어서 관리하며 사용한다.

(json파일로 보통 많이 만드는 것 같다.

또 다른분께서 질문하시는 것을 옅들어보니.. 회사마다 전부 달라서 우린 거기에 맞춰나갈 준비만 해두면 될 것 같다.)

구조체는 None클래스로 생성을 해주고, USTRUCT()를 붙여준다.

USTRUCT(BlueprintType)
struct FItemSpawnRow : public FTableRowBase
{
GENERATED_BODY()
};

이런 구조로 변경해주어야 한다.

FTableRowBase를 반드시 상속 받아야하며, 보통 언리얼에서 struct는 F를 접두사로 붙여준다.

 

[ TSubclassOf<>()  Vs TSoftClassPtr ]

TSubclassOf<>() TSoftClassPtr 
하드 레퍼런스이다. 소프트 레퍼런스이다.
클래스가 항상 메모리에 로드된 상태에서 바로 접근한다. 클래스의 경로만 유지한다.
메모리에 로드해두고, 바로 접근하는 형태라는 것이다. 해당 클래스가 필요한 상황이되면 그때 메모리에 로드한다.

즉, 처음 레퍼런스를 가져올 때 메모리에 로드하느냐 하지 않느냐의 차이로 메모리에 사용하지 않는 것들을 올리는게 비효율 적이라서 상황에 맞게 사용해야 한다.

 

사용하지 않는 클래스를 메모리에 모두 올려두는건 비효율적이기 때문에 하드와 소프트 레퍼런스를 나눠둔 것.

우리도 소프트 레퍼런스를 쓰는게 좋지만, 사용하려면 몇가지 조치를 더 해주어야 해서 우선 하드 레퍼런스로 처리했다.

(우린 연습이니까 이런 방향으로 잡으신 것 같다.)

 

<아이템 확률 데이터 테이블 만들기 II>

데이터 테이블은 확률을 항목들을 모두 합치면 100이 되어야 한다.

데이터 테이블의 행이름(RowName)은 Key값으로 중복을 허용하지 않는 값이다.

참조를 할때 특징이 뒤에 접미사로 _C가 붙는다.

(알아두는게 좋다고 하신다.)

UDataTable*으로 데이터 테이블들을 변수로 가져올 수 있다.

FItemSpawnRow* ASpawnVolume::GetRandomItem() const
{
	if (!ItemDataTable) return nullptr;

	TArray<FItemSpawnRow*> AllRows;
	static const FString ContextString(TEXT("ItemSpawnContext"));
	ItemDataTable->GetAllRows(ContextString, AllRows);

// ...

GetAllRows() 주의사항 첫 인자에 문자열을 전달해주어야 하는데, 이는 디버깅을 위한 용도로 행 가져오기를 실패했을때, 어떤 행인지를 알려주기 위한 용도이다.

static const FString ContextString(TEXT("ItemSpawnContext")); 이러한 것을 첫번째 인자로 전달해주어야 한다.

 

우리가 사용한 랜덤은 누적 확률값 뽑기이다.

확률에 비례해서 선택을 보장해준다.

(실제 게임에서도 사용하는 알고리즘이라고 하신다.)

 

2. 캐릭터 체력 및 점수 관리 시스템 구현하기


확률을 위해서 데이터 테이블을 만들었었다.

사실 강사님께서는 동일한 작업이라서 제외는 시켰지만, 레벨별로 데이터 테이블도 따로 주어야 한다.

 

<캐릭터의 체력 및 점수 시스템 만들기>

 

PlayerState

ㄴ> GameMode가 관리해주는 클래스 중 하나인데, 각 플레이어마다의 정보를 관리해주는 클래스이다.

ㄴ> 하지만 우리는 싱글 게임이기 때문에 굳이 반드시 사용할 필요는 없다.

ㄴ> 하지만 멀티 게임으로 넘어갈 때 굉장히 중요해지는 클래스이다.

 

데미지 처리

ㄴ> 언리얼에서는 이미 데미지 시스템을 제공하고 있다.

ㄴ> 이걸 최대한 활용하는게 나은 경우가 많다.

ㄴ> UGamePlayStatics::ApplyDamage() : 데미지를 입힌다.

ㄴ> Statics 클래스이기 때문에 객체 없이 호출이 가능하다.

ㄴ> AActor::TakeDamage() : 데미지를 받는다. ApplyDamage()로 어떤 액터가 데미지를 받았는지 알리고, 알려진 액터쪽에서는 TakeDamage()는 오버라이딩을 해서 액터마다 데미지 처리를 해주면된다.

 

virtual float TakeDamage(
float DamageAmount,
struct FDamageEvent const& DamageEvent,
class AController* EventInstigator,
AActor* DamageCauser) override;

DamageAmount : 데미지를 얼마나 입었나

DamageEvent : 데미지 이벤트에 전달할 추가적인 정보를 구조체에 담아보낸다. (스킬 같은 경우)

EventInstigator : 데미지를 누가 입혔는지 (몬스터나, 상대 플레이어)

DamageCauser : 데미지를 일으킨 오브젝트

ActualDamage는 순수 데미지를 전달 받아 플레이어의 방어력등으로 차감해 다시 최종 데미지를 return해주는 형태로 사용하기 위한 것이다.

 

UGameplayStatics::ApplyDamage(
Actor,					// 대미지를 받을 객체
ExplosionDamage,			// 데미지를 몇을 줬는지
nullptr,				// 대미지 발생 대상(컨트롤러)은 누구인지
this,					// 대미지를 발생한 오브젝트는 무엇인지
UDamageType::StaticClass());		// 대미지 유형 클래스 전달 (가장 기본적인 것으로 전달해주었음)

이제 점수를 획득할 수 있도록 점수관리 시스템을 만들어 주어야 한다.

PlayerState는 플레이어마다 고유한 정보를 저장하고 관리해주는 클래스라고 해주었다.

GameState는 게임 전역 정보를 저장하고 관리하는 곳이다.

ㄴ> 때문에 우리 점수 시스템이나 웨이브 시스템의 경우에도 이 GameState에서 관리하도록 할 것이다.

 

GameStateBase - 간단한 게임시스템이나 싱글 게임에서 사용한다.

GameState - 멀티플레이어를 고려했을 상황에서 사용

우리가 만든 GameState를 GameMode에 셋팅해주어야 한다.

이제 coin에서 우리가 만든 게임 스테이트에 접근해야하는데, 이때는 GetWorld()로 월드에 먼저 접근해주어야 한다.

이후에는 GetGameState()를 호출해 월드의 게임스테이트를 가져와 주면 된다.

 

3. 마무리


일단 내일부터 바로 과제 들어가야하니까.. 할일 정리를 조금 해보자면,

 

0. 프로젝트 셋팅하기

    I. 이전에 만들었던 것들을 활용하기 위해서 환경 구축하기 (옮긴다거나 등등)

1. 아이템 인터페이스 만들기

     I. 아이템 인터페이스를 만든다.

     II. 아이템은 회복 포션, 코인, 지뢰, 속도를 느리게하는 포션, 플레이어 주변을 돌아다니는 미니 드론 생성해주는 아이템 등을 구현

2. 게임 로직 구현

     I. GameMode

     II. GameState

     III. GameInstance 등

3. UI 작업하기

     I. 코인 획득량

     II. 시간 흐름

     III. Wave 표시

 

프로젝트 내용 기획?

3D 공간 상에서 드론을 움직여 밀려오는 적 드론을 격추시켜야 다음 레벨로 진행되는 형식

우선 우리가 AI는  도입하지 못했기 때문에, 아이템으로 진행하고, 또한 코인을 모으는 형태로 진행할 것이다.

추후에 시간이 충분하다면, AI는 못넣어도 Monster를 만들어 피격당해 죽는 것, 드론에서 총알을 발사하는 것을 구현할 것이다.

또, 만들고 싶은 아이템이 있는데, 이것은 만들면 보여줘야겠다 ㅎㅎ >ㅅ<b

'프로그래밍 > Unreal 부트캠프' 카테고리의 다른 글

TIL 2025.02.12 기록  (0) 2025.02.12
TIL 2025.02.11 기록  (0) 2025.02.11
TIL 2025.02.06 기록  (1) 2025.02.06
TIL 2025.02.05 기록  (0) 2025.02.05
TIL 2025.02.04 기록  (0) 2025.02.04