프로그래밍/Unreal 부트캠프

TIL 2025.02.12 기록

Rozentea 2025. 2. 12. 23:22

0. 개요


오늘은 한게 많아서 정리할게 많다..

 

챌린지반 수업을 듣고, 과제를 꽤 많이 진행하고, 팀프로젝트가 다가와서 회의를 진행했다..

 

이번에 회의가 너무 재밌어서 앞으로 프로젝트가 너무 기대된다.

 

일단 아직도 할게 많이 남았기 때문에 감상에 빠지기보다는 빠르게 빠르게 정리를 마치고 과제를 하러 가야겠다.

 

1. 챌린지반 수업


1.  연속 구간 합 구하기

partial_snum : 구간 합!

partial_snum(v.begin(), v.end(), psum.begin())

 

psum에 부분별 합들을 넣을 것인데, psum의 시작점부터 채워 넣겠다는 것이다. 이때, 구간 합을 구해 넣어준다.

이렇게하면, v의 0번 인덱스부터 v의 N번째 까지의 누적합이 채워넣어질건데, 이 누적합을 빼주면 구간 합을 구할 수 있다.

ㄴ> 이전에 정리했던 누적합이랑 동일한 내용.

(이런 함수가 있는지 몰랐다... ㅇㅁㅇ..)

 

2.  유니크 값 구하기

unique는 연속된 중복 원소를 제거하는데 사용하는데, 끝 위치 iterator를 반환하는 함수이다. 이 친구는 정렬이 필수인데, 연속된 중복 원소만 제거할 수 있기 때문이다. ㅠㅠ

sort(v.begin(), v.end());

auto  newEnd = unique(v.begin(), v.end());
verase(newEnd, vend());

 

3. 총합 구하기

accumulate

accumulate(v.begin(), v.end(), 0);

이때, 3번째 인자는 초기값이다.

 

accumulate(v.begin(), v.end(), 1, multiplies<int>());

을 하면 초기값을 1로 시작하는 총 곱을 구할 수 있다.

 

accumulate(v.begin(), v.end(), 0, [](int a, int b) { return a + b + 2; });

뿐만 아니라 람다 함수로 사용자 정의 계산을 할 수 있다.

총합을 구할 때 각 원소마다 2씩을 더해줘야한다고 생각하면 위처럼 할 수 있다.

 

4. 빠른 이진 탐색

lower_bound / upper_bound는 정렬된 구간에서 이진 탐색을 깔끔하게 제공해준다.

이진 탐색은 아래 사진과 같다.

위 그림 기반으로 {1, 4, 6, 9, 15, 20, 30}에서 6을 찾는다고 가정할 때,

  • 대상이 9보다 큰가? → 작다
  • 대상이 4보다 큰가? → 크다
  • 답은 6이다.

처럼 탐색하는 방법이다.

시간 복잡도는 O(long n)이다.

vector<int> v = {1, 2, 4, 4, 5, 7, 8};
auto lb = lower_bound(v.begin(), v.end(), 4);
auto ub = upper_bound(v.begin(), v.end(), 4);

upper_bound는 x이상의 값이 처음 나오는 위치를 찾는다.

반대로 lower_bound는 x이하의 값이 처음 나오는 위치를 찾느다.

즉, iterator 반환이다.

 

5.   원소 변환하기

transform 구간 내의 원소들을 내가 원하는 대로 변환 시켜주는 함수이다.

 

6. 팁

코테 내용이 요즘 어렵진 않게 나오는데, 실수하거나 특정 테스트케이스를 만족하지 못하면 탈락인 트랜드인것 같다.

 

7. 문제 풀이

🧭
Q1. N개의 정수로 이루어진 정렬된 수열이 주어집니다.
각 쿼리는 두 정수 A, B로 이루어져 있습니다.
각 쿼리마다 수열에서 A 이상 B 이하인 원소의 개수를 출력하세요. (15분)

입력
첫째 줄에 수열의 크기 N1 (1 ≤ N 100,000)이 주어집니다.
둘째 줄에 N개의 정수가 오름차순으로 주어집니다.
셋째 줄에 쿼리의 개수 Q(1 ≤ Q ≤ 10,000)가 주어집니다.
다음 Q개의 줄에 각각 두 정수 A, B가 주어집니다.
10
1 2 3 3 3 6 7 8 9 9

3
3 5
2 3
6 9​

출력
각 쿼리마다 A이상 B이하인 원소의 개수를 한 줄에 하나씩 출력합니다.
3 // 3 5 입력 시
4 // 2 3 입력 시
5 // 6 9 입력 시​
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int main()
{
	int VectorSize = 0;
	cin >> VectorSize;
	
	vector<int> v(VectorSize, 0);
	for (int i = 0; i < VectorSize; ++i)
	{
		cin >> v[i];
	}

	int QCnt = 0;
	cin >> QCnt;
	for (int i = 0; i < QCnt; ++i)
	{
		int A = 0, B = 0;
		cin >> A >> B;
		auto lb = lower_bound(v.begin(), v.end(), A);
		auto ub = upper_bound(v.begin(), v.end(), B);

		int Cnt = 0;
		for (; lb != ub; ++lb)
		{
			Cnt++;
		}
		cout << Cnt << endl;
	}

	return 0;
}

 

2. 과제 진행


1. UI 추가로 구현하기

< 속도 표시 UI 만들기 >

우측 하단에 속도 표시 UI를 만들어주었다.

 

아무래도 드론을 조종하는 것이기 때문에 어떤 속도를 나타내주는 UI가 있으면 좋겠다고 생각했다.

때문에 해당 UI 완성을 목표로 만들어보기로 했다.

언리얼에서 기본적으로 제공해주는 프로그래스바가 박스 형태만 제공하고 있었다… ㅠ

 

그렇다면 실제 게임에서는 어떻게 다양한 프로그래스바를 만들까? 궁금하게되었고 찾아보게 되었다.

 

찾아보니 이미 다른 사람이 만들어둔 플러그인을 사용하는 방법도 있었고, 머티리얼을 만들어서 적용하는 방법이 있었다.

언리얼에서는 UImage 클래스로 이미지를 띄울 수 있도록 지원하고 있다.

이름대로 Texture만 넣어줄 수 있을 것 같지만, 머티리얼도 넣어줄 수 있다…!!

 

때문에 나는 원형 바 형태의 머티리얼을 만들어 해당 머티리얼을 이미지에 전달해주도록 만들었다.

 

또, UI블록들을 하나하나 옮기는 것보다. 부모자식 관계를 설정해 부모를 옮기면 한번에 다 옮길 수 있도록 하고 싶었다.

때문에 컨버스 패널을 하나 더 추가해 해당 컴버스 자식들로 텍스트와 프로그래스바를 넣어서 하나의 UI 모듈(?)을 만들어 주었다.

 

<속도UI 머티리얼 만들기>

https://blueprintue.com/blueprint/ee4tsosw/

머티리얼의 경우 위 링크를 토대로 만들어 주었다.

 

음.. 위 프로그래스바는 원형 전체다 사용하기도 하고, 시작점이 마음에 안들었기 때문에 머티리얼 측에서 수정을 해주었다.

하지만 시작점의 경우 WBP에서 이미지 블럭의 디테일 탭에서 회전 설정이 가능하다는 것을 나중에 알게되었따… ;ㅅ;

 

if (UImage* SpeedBar_In = Cast<UImage>(HUDWidget->GetWidgetFromName(TEXT("SpeedBar_In"))))
{
	if (UMaterialInstanceDynamic* UIMaterial = SpeedBar_In->GetDynamicMaterial())
	{
		 UIMaterial->SetScalarParameterValue(TEXT("Percent"), DronePawn->GetSpeedPercentage());
		 UIMaterial->SetScalarParameterValue(TEXT("Thickness"), 0.2f);
	}
}							
if (UTextBlock* CurrentSpeedText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("CurrentSpeed"))))
{
	const int32 CurrentSpeed = FMath::Floor<int32>(DronePawn->GetCurrentSpeed() / 10.0f);
	FString CurrentSpeedString = "";
			
	CurrentSpeedString += FString::Printf(TEXT("%d"), CurrentSpeed);
	CurrentSpeedText->SetText(FText::FromString(CurrentSpeedString));
}

머티리얼의 파람을 바꾸었어야 했는데 WBP에서는 따로 공개가 되지 않아서 각 UI마다 동일한 머티리얼을 사용하더라도 따로 변수를 넣어주지 못했다.

(다시 생각해보니 당연한 것 같다. 원본 머티리얼은 하나고, 그걸 같이 사용중인 거니까.)

때문에 코드에서는 DynamicMaterial를 인스턴스해서 파람들을 변경해주면 된다.

또한 해당 UI는 0.1초 단위로 갱신해주면, 너무 뚝뚝 끊기는 느낌이 나기 때문에 Tick을 만들어 Tick에서 돌려주었다.

 

< Score UI 만들기 >

코인을 획득할 때, 점수만 바뀌는게 너무 밋밋해서 애니메이션을 주도록 만들었다.

강의에서 배웠던 UI애니메이션을 사용해 만들었다.

 

다만.. 문제가 조금 있는데..

한번에 여러 코인을 획득할 때, 새로운 UI가 생성되어 올라가는게 아니라 다시 애니메이션이 반복되어 초기화되는 문제가 생겼다.

 

하지만, 조원분들(중요!)께서 괜찮아보인다고 해주셔서 이대로 하기로했다.

또, 새로 만드는 방식으로 중첩되게 한다면, 글자가 겹쳐보여서 보기에 불편할것 같았다.

 

< Time UI 만들기 >

시간을 표시해주기 위해 UI를 만들어 주었다.

글씨가 너무 고정되어있지 못하고, 왔다갔다가 심해서 변경해야할 것 같다.

마찬가지로 해당 UI도 Tick에서 돌려주도록 하였다.

확실히 아까보다 보기가 훨씬 편해졌다.

단순히 마침표 부분과 분, 초, 밀리초를 구분지어서 TextBlock을 만들어주었다.

 

2.  아이템 스포너 다시 만들기

< 이유와 계획 >

볼륨 형태로 아이템을 스폰하는 것은 좋지만, 원하는 위치나 원하는 방향을 가지도록 만들기 어렵다.

때문에 내가 의도한 방향대로 플레이어가 게임을 진행하게 만들기가 매우 어려워보였다.

따라서 2가지 형태의 아이템 스포너를 만들 것이다.

  1. 기존과 동일한 박스 볼륩 형태의 스포너
  2. 기존과는 다른 스플라인 형태의 스포너

때문에 베이스인 ABaseSpawner를 만들고, IItemSpawner를 만들어 줄 것이다.

이 두가지를 상속받아 각각의 스포너를 만들어 줄 것이다.

 

< Base 스포너 만들기 >

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "BaseSpawner.generated.h"

UCLASS()
class SPARTA_HOMEWORK_08_API ABaseSpawner : public AActor
{
	GENERATED_BODY()

protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "BaseSpawn|Copmonent")
	USceneComponent* Scene;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "BaseSpawn|Property")
	UDataTable* SpawnTargetDataTable;
	
public:	
	ABaseSpawner();

protected:
	virtual void BeginPlay() override;

public:	
	virtual void Tick(float DeltaTime) override;

	template <typename T>
	T* GetRandomItem() const;

};

template <typename T>
T* ABaseSpawner::GetRandomItem() const
{
	if (!SpawnTargetDataTable) return nullptr;

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

	if (AllRows.IsEmpty()) return nullptr;

	float TotalChance = 0.0f;
	for (const T* Row : AllRows)
	{
		if (Row)
		{
			TotalChance += Row->SpawnChance;
		}
	}

	const float RandValue = FMath::FRandRange(0.0f, TotalChance);
	float AccumulatedChance = 0.0f;

	for (T* Row : AllRows)
	{
		AccumulatedChance += Row->SpawnChance;
		if (RandValue <= AccumulatedChance)
		{
			return Row;
		}
	}

	return nullptr;
}

BaseSpawner에서는 스폰 대상의 데이터 테이블을 지니도록 해주고, 해당 테이블에서 랜덤한 것을 가져올 수 있도록 해주었다.

이때, 가져올 대상이 어떤 구조체형태의 테이블인지 모르기 때문에 템플릿 함수로 구현해주었다.

 

< IItemSpawnerInterface 만들기 >

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "ItemSpawnerInterface.generated.h"

UINTERFACE(MinimalAPI)
class UItemSpawnerInterface : public UInterface
{
	GENERATED_BODY()
};


class SPARTA_HOMEWORK_08_API IItemSpawnerInterface
{
	GENERATED_BODY()

public:
	virtual AActor* SpawnRandomItem() = 0;
	virtual AActor* SpawnItem(const TSubclassOf<AActor>& ItemClass) = 0;
};

인터페이스쪽에서는 아이템을 랜덤스폰하는 함수와 아이템 스폰 함수를 반드시 만들도록 명시해주었다.

 

< BoxSpawner 만들기 >

박스 스포너는 기존과 크게 다르지 않아서 코드는 따로 정리하지 않겠다.

단순 코드 정리 느낌으로 진행했다.

 

수정하고 나서도 잘 나오는 것을 확인할 수 있다.

 

< LineSpawner 만들기 >

짜잔~~ 의도한대로 원하는 진행방향에 맞게 코인들이 생성되는 것을 확인할 수 있었다!!

FVector ALineItemSpawner::GetRandomPointInLine() const
{
	float SplineLength = SplineComp->GetSplineLength();

	float RandomDistance = FMath::RandRange(0.0f, SplineLength);

	// 해당 거리에서 월드 위치 가져오기
	FVector SpawnLocation = SplineComp->GetLocationAtDistanceAlongSpline(RandomDistance, ESplineCoordinateSpace::World);
	return SpawnLocation;
}

다른 것들은 크게 다르지 않고, GetLocationAtDistanceAlongSpline() 함수를 이용해 길이를 전달해서 해당 길이가 스플라인 상에서 어디에 위치하는지를 알아와 이 위치에 스폰해주었다.

 

음.. 조금 문제는 스폰할 때, 너무 붙어서 나오는 경우가 있다는 점이다.

 

< 스폰 아이템 갯수를 각각의 스포너들이 가지도록 수정하기 >

기존 아이템 스폰 개수를 게임 스테이트에서 임의로 넣어주고 있는데 이부분을 수정해주어야겠다고 생각했다.

 

5개로 지정해준대로 잘 생성되는 것을 확인할 수 있었다.

 

// void ASpartaGameState::StartLevel() ...
TArray<AActor*> FoundVolumes;
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), ABaseSpawner::StaticClass(), FoundVolumes);
	if(FoundVolumes.Num() > 0)
	{
		for (int idx = 0; idx < FoundVolumes.Num(); idx++)
		{
			IItemSpawnerInterface* SpawnVolume = Cast<IItemSpawnerInterface>(FoundVolumes[idx]);

			if (ABaseSpawner* Spawner = Cast<ABaseSpawner>(FoundVolumes[idx]))
			{
				const int32 ItemToSpawnCount = Spawner->GetSpawnCount();
			
				for (int32 i = 0; i < ItemToSpawnCount; i++)
				{
					if (SpawnVolume)
					{
						AActor* SpawnedActor = SpawnVolume->SpawnRandomItem();
						if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass()))
						{
							SpawnedCoinCount++;
						}
					}
				}
			}
		}
	}

ABaseSpawner에서 스폰 카운트를 변수로 지니게 만들고, 해당 스폰 목표 수 만큼 스폰을 해주도록 수정하였다.

 

< 아이템 스폰시 거리두기 >

FActorSpawnParameters Param;
Param.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding;

을 작성해두어서 사실 잘 될 것이라 생각했지만, 지금 Collision이 Mesh의 부모 컴포넌트이고, 해당 Collision컴포넌트가 OverlapAllDynamic 프리셋을 사용중이라.. 의미가 없었다.

때문에 위 영상은 BlockAllDynamic으로 변경해본 것인데, 의도한대로 나오긴 하지만 오버랩 이밴트가 발생하지 않는건 둘째더라도 옆으로 밀려 생성되는 문제가 있다.

 

때문에.. 이를 해결하기 위해서 생성된 액터들의 위치를 알고 있다가 특정 거리만큼 벌어진 곳에 생성하도록 구현하려한다.

추가적으로 두 가지 방법중 어느것이 더 좋을지 잘 생각되지 않아서 GPT를 이용한 결과 위와 같은 고려사항을 얻을 수 있었다.

음.. 사실 제일 큰 문제는 AActor*로 할시 댕글링 포인터가 남지 않을까였고, Location과 Scale관리시 사라진 액터에 대해서 데이터 삭제처리를 해야하는 문제가 있다.

 

하지만 지금 스폰의 경우 계속해서 스폰하는 것이 아닌 레벨이 시작할 때, 한번에 스폰하고 더이상 스폰하지 않는 상황이기 때문에 댕글링 포인터의 위험을 가지고가면서, 메모리 효율이 떨어지는 문제보다는 Location과 Scale을 기록해두는게 더 좋다고 판단했다.

 

심지어 레이스를 마치고, 한 바퀴를 돌아 시작점에 왔다면, 트리거 볼륨을 통해 Spawner들의 데이터 기록을 모두 초기화 해준 뒤, 생성하면 되기 때문에 큰 문제가 없어보인다.

AActor* ALineItemSpawner::SpawnItem(const TSubclassOf<AActor>& ItemClass)
{
	if (!ItemClass)
		return nullptr;
	
	FVector SpawnLocation = FVector::ZeroVector;
	FVector SpawnScale = FVector::ZeroVector;
	bool bValidLocation = false;

	// 최대 10번 반복하여 적절한 위치 찾기
	for (int Attempt = 0; Attempt < 10; Attempt++)
	{
		bValidLocation = true;

		FActorSpawnParameters Param;
		Param.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding;
		AActor* SpawnedItem = GetWorld()->SpawnActor<AActor>(
			ItemClass,
			GetRandomPointInLine(),
			FRotator::ZeroRotator, Param);

		SpawnLocation = SpawnedItem->GetActorLocation();
		FVector Origin;
		FVector BoxExtent;
		SpawnedItem->GetActorBounds(false, Origin, BoxExtent);
		SpawnScale = BoxExtent;
		
		if (SpawnedLocations.IsEmpty())
		{
			SpawnedLocations.Push(SpawnLocation);
			SpawnedScales.Push(SpawnScale);
			return SpawnedItem;
		}
		else
		{
			// 기존 액터들과 거리 체크
			for (int i = 0; i < SpawnedLocations.Num(); ++i)
			{
				if (SpawnedItem)
				{
					float Dist1 = FVector::Dist(SpawnLocation, SpawnedLocations[i]);
					float VectersScale = SpawnedScales[i].X;
					if (Dist1 < VectersScale + SpawnScale.X)
					{
						bValidLocation = false;
						SpawnedItem->Destroy();
						break;
					}
				}
			}
			if (bValidLocation)
			{
				SpawnedLocations.Push(SpawnLocation);
				SpawnedScales.Push(SpawnScale);
				return SpawnedItem;
			};
		}
	}

	return nullptr;
}

 

으악 나는 바부다..!

아무튼 음.. 지금 상황에서는 액터의 실제 크기를 X축만 고려하고 있기 때문에 박스 형태인 경우엔 판정을 다르게 주어야 한다.

음… 총 3번 비교하도록 하면 될 것 같다.

 

아무튼 다행히도 영상처럼 결과가 잘 나온다.

 

3. 마무리


회의는 오늘은 꼭 만들면 좋겠다 하는 기능을 정하고 기능들을 토대로 컨셉을 정했다.

회의가 깔끔히 진행되고,  다들 적극적으로 의견도 내주시고, 즐겁게 게임을 만들 준비를 했던거 같아 즐거운 마무리를 할 수 있었던 것 같다.

 

과제는.. 진행할게 너무 많이 남아서.. 잠을 조금 줄이고.. 마저 해야할 것 같다. ㅎㅎ 아쟈뵤!

 

때문에 오늘은 말을 조금 아끼고 이만 마무리 짓겠다.

 

귀여운 폰트는 어제 자기전에 치토스에서 이번에 마케팅용으로 무료 폰트를 만든게 스토리도 너무 귀엽고, 폰트도 마음에 들어서 한번 사용해 봤다. ㅎㅎ >ㅅ<b

https://www.cheetos.com/otherhand