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

TIL 2025.02.03 기록

by Rozentea 2025. 2. 3.

0. 개요


오늘도 감기가 아직 다 낫지 않아서.. 과제 위주로 빨리빨리 정리하고 넘어갈 것이다.. ㅠ

우선 과제의 경우.. 드론 움직임을 제외하면 오늘 모두 마무리 지었다...

내일은 드론 마무리하고, 레벨을 완성하면 될 것 같다.

퍼즐 레벨 완성까지 할 수 있을지.. 자신은 크게 없지만.. 그래도 우선 오늘 최대한 건강 회복하고 내일 달려봐야할 것 같다..

 

그래도 레벨 디자인은 어느정도 구상은 해뒀으니까 내일... 오전 중으로 드론 작업만 끝난다면 불가능하진 않을 것 같다.

물론... Selectable 인터페이스, PuzzleSolved 등등.. 구현을 추가적으로 하긴해야 하지만..

 

1. 과제


1. 챌린지반 과제

<문제>
Q. n개의 정수를 입력받아 벡터에 저장한 후, 연산 결과를 출력하세요. 사용자는 q개의 질의를 입력할 수 있게 합니다. 각 질의는 두 개의 정수 l과 r로 구성되며, 이는 벡터의 구간 [l, r]의 합을 계산하라는 의미입니다.

<입력 예제>
5 // 벡터 크기
1 2 3 4 5 // 벡터에 저장되는 정수들
3 // 질의 개수
1 3 // 질의 1
2 5 // 질의 2
1 5 // 질의 3

<출력 결과>
6 // 1 3에 대한 출력 (1, 2, 3을 더해서 6)
14 // 2 5에 대한 출력 (2, 3, 4, 5를 더해서 14)
15 // 1 5에 대한 출력 (1, 2, 3, 4, 5를 더해서 15)
#include <iostream>
#include <vector>
#include <numeric>

using namespace std;

int main()
{
	int N = 0, Q = 0, l = 0, r = 0;
	cin >> N;
	vector<int> vec_Arr(N, 0);

	for (int i = 0; i < N; ++i)
	{
		cin >> vec_Arr[i];
	}

	cin >> Q;
	for (int i = 0; i < Q; ++i)
	{
		cin >> l >> r;
		l -= 1;
		cout << accumulate(vec_Arr.begin() + l, vec_Arr.begin() + r, 0);
	}

	return 0;
}

accumulate()를 처음으로 알게되었다.

numeric 헤더를 참조하면 사용할 수 있다.

주의할 점은 2번째 인자인 범위 끝 지점은 end iter때 처럼 +1한 곳을 전달해야 한다.

 

2. 랜덤 스포너 디버그 드로우 완성하기

<Editor Tick 사용>

우선 저번주에는 Play를 해야지 tick이 돌아 디버그 박스가 나왔다.

때문에 에디터에서도 tick을 돌릴 수 있도록 변경해 에디터에서만 디버그 박스가 미로가 생성될 위치를 표현해주고, Play하면, 사라지도록 만들것이다.

// .... .h
virtual bool ShouldTickIfViewportsOnly() const override;


// ... .cpp

void ARandomMazeSpawner::Tick(float DeltaTime)
{
#if WITH_EDITOR
	// 에디터 Tick
	if (nullptr != GetWorld() && GetWorld()->WorldType == EWorldType::Editor)
	{
		if (nullptr == BPSpawnPlatformRef)
		{
			DrawDebugBox(GetWorld(), GetActorLocation(),FVector(100.f, 100.f, 100.f), FColor::Cyan);
		}
		else
		{
			FVector BounceExtent = BPSpawnPlatformRef->GetDefaultObject<AFallPlatform>()->GetComponentByClass<UStaticMeshComponent>()->GetStaticMesh()->GetBounds().BoxExtent;
			FVector StandardLocation = GetActorLocation() + FVector(BounceExtent.X, BounceExtent.Y, 0.f);
			float AccOffset_Y = 0.0f;
			for (int i = 0; i < Height; ++i)
			{
				float AccOffset_X = 0.0f;
				for (int j = 0; j <Width; ++j)
				{
					FVector Offset = FVector(AccOffset_X + j * BounceExtent.X * 2, AccOffset_Y + i * BounceExtent.Y * 2, 0.f);
					DrawDebugBox(GetWorld(),  StandardLocation + Offset,FVector(BounceExtent.X, BounceExtent.Y, BounceExtent.Z), FColor::Cyan);
					AccOffset_X += Offset_X;
				}
				AccOffset_Y += Offset_Y;
			}
		}
		return;
	}
#endif
	Super::Tick(DeltaTime);
	
}


bool ARandomMazeSpawner::ShouldTickIfViewportsOnly() const
{
	//return Super::ShouldTickIfViewportsOnly();
	if (nullptr != GetWorld() && GetWorld()->WorldType == EWorldType::Editor)
	{
		return true;
	}
	else
	{
		return false;
	}
}

Editor Tick은 virtual bool ShouldTickIfViewportsOnly() const override; 를 생성해 tick에서 if 전처리기를 이용해 사용하면 된다.

 

<Get Bounce()로 실제 크기 알아오기>

다음으로는 액터의 실제 크기를 알아와야 한다.

그래야 디버그 박스를 제대로 그릴 수 있기 때문이다.

이전 블루 프린트를 이용해 레이저 퍼즐을 만들 때 사용했던 Get Bounces를 호출하는 함수를 찾아 호출해주면 된다.

FVector BounceExtent = BPSpawnPlatformRef->GetDefaultObject<AFallPlatform>()->GetComponentByClass<UStaticMeshComponent>()->GetStaticMesh()->GetBounds().BoxExtent;

방법은 간단하다. StaticMesh에 접근해서 GetBounce로 가져와 주면 된다.

 

이때, BPSpawnPlatformRef는 TSubclassOf<> 타입인데, 해당 타입으로는 AActor로 캐스팅을 실패한다.

생각해보니 TSubclassOf<> 타입은 단순히 원본 포인터?를 가지고 있는 객체 같은 느낌인것 같았다. (사용처는 확실히 다르겠지만 스마트 포인터 처럼 실제 포인터는 본인이 가지고 있는 느낌?)

때문에 GetDefaultObject라는 함수를 통해 실제 T타입의 오브젝트를 가져와 주었다.

확인해보니, 실제 크기와 동일하게 잘 나오는 것을 확인할 수 있었다.

 

다음으로는 발판 생성 위치를 미리 보여주기만 하면 된다..!

오른쪽 사진은 기즈모 위치가 마음에 안들어 offset을 주어서 피벗을 조정해주었다.

 

자.. 이제 준비는 다 끝났다..!! 이제 생성만 해주면 된다..!

  1. 안떨어지는 발판은 머티리얼 색을 붉은색으로 변경해 줌 (지금은 잘 되는지 확인해야하니까)
  2. 3by5일때는 직선으로만 나와서.. 7by7로 테스트함

랜덤하게 생성도 되고, 디버그 드로우만큼의 영역에서 잘 생성되는 것을 확인했다.

 

<버그.. 수정.. ;ㅅ;>

액터가 떨어져 보이지만 사실 발판간의 offset이 0이기 때문에 낑겨서 안떨어지는 문제 발생.

음.. 변수로 offset을 줄 수 있도록 만들어서 offset만큼 떨어진 곳에 생성해주도록 수정

Offset 설정만큼 잘 떨어지는 것을 확인

offet만 잘 주면, 밟자마자 떨어져서 이전 문제도 크게 상관은 없을 것 같다..

다만, 마음에 안들기도 하고 근본적인 해결은 아니라서 슬프다.. ㅠ

 

3. 과제 6번 레벨 완성하기..!

영상을 찍고 해야하니까 레벨을 더 크게 만들어서 각 구역을 만들고, 구역마다 만든 것 들을 배치해 보여주도록 했다.

설명을 잘 할 수 있도록 맵을 좀 크게 만들고, 텍스트를 이용해서 각 영역이 어떤 액터들에 대한 설명인지 보이도록 해주었다.

 

4. 과제 7번 입성....! 기반 완성하기

<6번 과제의 발판 및 레이저 이주하기>

헤더 파일의 SPARTA_HOMEWORK_06_API를 7로 변경하고, 헤더, 소스파일을 옮겨주었다.

뿐만 아니라 사용해야할 재질, 텍스처 등등을 이주해주었다.

이후에는 블루 프린트로 한번 더 랩핑해 배치하면 사용할 수 있도록 준비를 해두었다.

 

<IA, IMC, Controller, GameMode 만들기>

7번 과제는 폰을 이용해 캐릭터를 만들어 움직임을 제어하고, 드론 움직임까지 만드는 것이다.

때문에 우선, 입력을 받을 수 있도록 IA와 IMC를 생성해주고, Controller를 만들어 주었다.

또.. 어쨌든 우린 간단한 퍼즐 게임을 만들 것이기 때문에 GameMode도 있으면 좋을 것 같아서 만들어 프로젝트 셋팅까지 마쳐두었다.

 

5. 폰 클래스로 캐릭터 만들기


<이동, 카메라 회전, 달리기 구현>

사실... 캐릭터까지는 다 완성하고 녹화한거라 애니메이션까지다 적용되어 있다.. 헤헿..

 

아무튼 이동, 카메라 회전, 달리기 구현까지 마쳐두었다.

 

<점프 구현>

점프를 구현하기 위해서는 중력, 그라운드 체크가 필요했고, addfroce를 어떻게 해줘야 할지도 고민이었다.

결과적으론 addforce의 경우 Z축 방향 FVector를 정규화한 뒤, jumpforce를 곱해 전달해주었다.

그라운드 체크의 경우 플레이어가 공중인지, 땅인지 알기 위한 bool 변수이고, 중력의 경우 언리얼 기준인 980.0f cm/s^2을 기반으로 구현했다.

중력의 경우 구현자체는 쉬웠다. Z방향으로 빼주기만 하면 되니까..

if (!bIsGround)
	{
		JumpSpeed -= Gravity * DeltaTime;
		NewLocation += (FVector::UpVector * JumpSpeed * DeltaTime);
	}

 

<버그 수정>

사실 버그는 아니긴한데.. 이번 과제에서 조건중 하나가 물리 시뮬레이션을 키지 않고, 움직임을 설계하는 것이었다.

하지만 문제는.. 물리 시뮬레이션을 키지 않으니 충돌처리가 안되어서 액터들을 뚫고지나가는 불상사가 발생했다.....

 

과제만 한다면 문제가 없지만, 나는 최종 목표가 레이저 퍼즐게임을 만드는 것이었기 때문에 굉장히 난처해졌다.. ㅠ

 

그래서 그냥 캐릭터를 2개로 나누어서, 과제용과 게임 플레이용 캐릭터를 나눌까 고민했다.

하지만, 이번 기회에 시도해보는 것도 재밌을 것 같아서 그냥 충돌처리를 만드는 방향으로 갔다.

 

이부분이 굉장히 굉장히 머리가 아팠는데... 코드가 길어서 일단 정리해서 말해보자면,

미끄러짐 벡터를 이용한 밀림 처리, 부딪힌 방향과 반대 방향으로 밀어주는 offset 주기, 이러한 처리를 해주기 위한 스윕트레이싱 등등.. 굉장히 다양한 것을 사용했다... ;ㅅ;

 

▽코드

더보기
void APawnCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	bPrevMove = false;


	if (bMove || !bIsGround)
	{
		FVector NewLocation = CalNewLocation(DeltaTime);

		// 충돌 검사
		FHitResult HitResult;
		FCollisionQueryParams Params;
		Params.AddIgnoredActor(this); // 자기 자신은 충돌 검사에서 제외
		FVector StartPoint = GetActorLocation();
		StartPoint.Z -= (CapsuleComp->GetUnscaledCapsuleHalfHeight() * 0.5f) + 15.0f;

		//DrawDebugSphere(GetWorld(), StartPoint, 15, 26, FColor::Red);
		
		bool bHit = GetWorld()->SweepSingleByChannel(
			HitResult,             // 충돌 결과 저장
			StartPoint,				// 시작 위치
			NewLocation,             // 목표 위치
			FQuat::Identity,         // 회전 없음
			ECC_Pawn,                // 충돌 채널 (Pawn으로 설정)
			FCollisionShape::MakeSphere(30.0f), // 충돌 체크 모양 (구)
			Params
		);

		if (!bHit) 
		{
			SetActorLocation(NewLocation); // 충돌이 없으면 이동
		}
		else 
		{
			// 슬라이딩 처리: 벽의 법선(Normal)과 이동 방향을 사용하여 새로운 이동 벡터 계산
			FVector Direction = (GetActorLocation() - NewLocation) * -1;
			Direction = Direction.GetSafeNormal();
			FVector SlideDirection = FVector::VectorPlaneProject(Direction, HitResult.Normal);
			FVector SlideLocation = GetActorLocation() + (SlideDirection * MaxWalkSpeed * DeltaTime);

			// 슬라이딩 방향으로 재충돌 검사
			bool bSlideHit = GetWorld()->SweepSingleByChannel(
				HitResult,
				StartPoint,
				SlideLocation,
				FQuat::Identity,
				ECC_Pawn,
				FCollisionShape::MakeSphere(30.0f),
				Params
			);

			// 해결책 1: 벽에 완전히 정면으로 부딪힌 경우는 이동하지 않음
			if (FVector::DotProduct(Direction, HitResult.Normal) < -0.9f) {}
			else
			{
				if (!bSlideHit) 
				{
					// 슬라이딩 방향으로 이동 허용
					SetActorLocation(SlideLocation);
				}
				else 
				{
					UE_LOG(LogTemp, Warning, TEXT("벽에 완전히 막혀 이동 불가!"));
					FVector SmallOffset = HitResult.Normal * 2.0f;
					SetActorLocation(GetActorLocation() + SmallOffset);
				}
			}
		}
	}
	
	FVector CurrentLocation = GetActorLocation();
	float FrameDistance = FVector::Dist(PreviousLocation, CurrentLocation); 
	CurrentSpeed = FrameDistance / DeltaTime; // 실제 이동 속도
	
	TotalVelocity = (CurrentLocation - PreviousLocation) / DeltaTime;
	PreviousLocation = CurrentLocation;

	CheckGround();
}

FVector APawnCharacter::CalNewLocation(float DeltaTime)
{
	FVector NewLocation = GetActorLocation();
	TotalVelocity = FVector::ZeroVector;
	
	if (bMove)
	{
		float Speed = MaxWalkSpeed * DeltaTime;
		if (!bIsGround && bJumpFirst)
		{
			Speed *= AirSpeedMultiplier;
		}
		ControlInputVector = ControlInputVector.GetSafeNormal();		
		//TotalVelocity += (ControlInputVector * Speed);
		NewLocation += (Internal_ConsumeMovementInputVector() * Speed);

		bMove = false;
		bPrevMove = true;
	}

	if (!bIsGround)
	{
		JumpSpeed -= Gravity * DeltaTime;
		//TotalVelocity += (FVector::UpVector * JumpSpeed * DeltaTime);
		NewLocation += (FVector::UpVector * JumpSpeed * DeltaTime);
	}
	
	return NewLocation;
}

 

<애니메이션 넣어주기>

마지막으로 애니메이션을 넣어주었다.

애니메이션을 넣는 것도 꽤 굉장히 에러 사항이 많았는데... 아직 스테이트 머신을 잘 못쓰는 것 같다...

어떤 시점에 애니메이션을 넘겨주는게 맞는지 너무 햇갈려서 골머리를 썩혔다...

특히.. Falling과 Land 부분이 애매했다..

 

거기다가 지금 아직 수정하지 않은 부분이 있는데, 점프를 쉼없이 계속할 경우 Falling 모션이 끝나지 않았는데, Jump모션이 나와야되면서 애니메이션이 꼬이는 문제가 발생했다..

 

시간이 너무 오래 걸릴 것 같아서.. 우선 이부분은 넘기도록 했다.. ㅠㅠ

 

<완성한 모습!!!!>

굉장히.. 짧네... 충격적이다.. ;ㅅ;

아무튼.. 충돌 처리나 애니메이션 부분이 아쉬운게 많지만 그래도 이젠 조금 봐줄만하게는 나온 것 같았따.. ;ㅅ;

 

과제는 충돌처리나 그런건 없었으니까!! 구현 했잖아 한잔해! ;ㅅ;

의미 있었어..!

 

6. 폰 클래스로 드론 만들기


<계획>

우선 계획부터 이야기 해보자면,

플레이어가 4번 키를 입력하면, 드론이 생성되고, 해당 드론을 플레이어가 조종하도록 구현할 것이다.

그러기 위해서는 우선

1. 런타임 중에 입력 매핑 컨텍스트와 폰 빙의를 변경할 수 있어야 한다.

2. 드론에 알맞는 입력 액션 만들기

3. 충돌 처리

4. 중력 구현 (과제에서 요구사항..)

5. 드론 처럼 움직이기 위한 회전 및 조작감 (이건 좀... 어떻게 해야 조작감이 괜찮을지 감이 안온다..;;)

 

우선 계획을 바탕으로 구현하려면 이런 것들이 필요하다고 떠올랐다.

 

<컴포넌트 붙이기>

우선 컴포넌트는 이렇게 붙여주었다.

사실 캐릭터랑 다른 점이 스켈레탈 메시 대신 스태틱 메시를 썼다는 점뿐이다..

 

<입력 액션, 매핑 만들기>

정렬을 까먹고 안했어서 조금 해맷는데... 음... 저거하니 잘 된당..! ㅎㅎ

 

<런타임 중 컨트롤러 변경 및 빙의 변경>

void APawnCharacter::SpawnDrone(const FInputActionValue& value)
{
	if (value.Get<bool>())
	{
		FVector NewLocation = GetActorLocation();
		NewLocation += GetActorForwardVector() * 100.0f;
		ADronePawn* newDrone = GetWorld()->SpawnActor<ADronePawn>(BPDroneRef, NewLocation, FRotator::ZeroRotator);
		Cast<ASpartaPlayerController>(GetController())->ChangeMappingContext(1);
		Cast<ASpartaPlayerController>(GetController())->ChangePossess(newDrone);
	}
}

드론을 스폰하는 키를 4번으로 지정했고, 해당 입력 액션에 바인딩 해준 함수에서 컨트롤러 변경 및 빙의를 변경해주도록 구현했다.

처음에 빙의를 먼저 변경하고, 컨트롤러를 변경했는데.. 생각해보니 빙의를 해제했다는 것은 해당 캐릭터에는 더이상 컨트롤러가 없다는 것이라서 GetController()를 할 때, nullptr이 반환되어 터졌었다.. ;ㅅ;

당연한 것을 생각을 너무 못한게 아쉬웠다..

void ASpartaPlayerController::ChangeMappingContext(int Type)
{
	 if (ULocalPlayer* LocalPlayer = GetLocalPlayer())
	 {
	 	if (UEnhancedInputLocalPlayerSubsystem* SubSystem =
	 		LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
	 	{
	 		if (nullptr != InputMappingContext)
	 		{
	 			SubSystem->ClearAllMappings();
	 			// Character
	 			if (Type == 0)
	 			{
	 				// 0은 가장 높은 우선순위
	 				// 다른 IMC들과 겹치는 특수한 상황이 있다면 이 우선순위를 체크해 우선순위가 높은 것으로 할당한다.
	 				SubSystem->AddMappingContext(InputMappingContext, 0);
	 			}
	 			// Drone
	 			else if (Type == 1)
	 			{
	 				SubSystem->AddMappingContext(DronInputMappingContext, 1);
	 			}
	 		}
	 	}
	 }
}
void ASpartaPlayerController::ChangePossess(APawn* NewPawn)
{
	if (nullptr != NewPawn)
	{
		PlayerPawn = GetPawn();
		UnPossess();  // 기존 Pawn에서 빙의 해제
		Possess(NewPawn); // 새로운 Pawn에 빙의

		if (ADronePawn* DronePawn = Cast<ADronePawn>(NewPawn))
		{
			DronePawn->SetEnhancedInput();
		}
	}
}

이렇게 컨트롤러와 매핑 컨텍스트, 빙의를 런타임 중에 변경해주었다.

 

물론 지금은 캐릭터에서 드론으로만 변경하는 중이라 미완성인데, 캐릭터는 삭제하지 않을 것이기 때문에 캐릭터 액터의 주소를 어디에 저장하고 있어야 할 것 같다. 레벨에서 찾아도는 되는데, 레벨에서 액터 탐색이 효율적이진 않을 것 같다..

탐색 시간이 오래걸리지 않을까..?

 

<여기까지 완성 영상!!>

이러면.. 이제 드론 기능 위주로 쭉쭉 뽑아내면 되니까 오래는 안걸릴 것 같다..

아무튼..!! 여기까지 해냈다!

중간 중간 오래걸렸던 부분들이 많았는데, 작업 속도가 나쁘지 않았던 것 같다.. ㅎㅎ

 

2. 마무리


원래는 한번에 들었던 강의들을 하루에 한 강의씩 정리하려 했는데.. 불가능할 것 같다.. ㅠ

설 연휴 + 감기 이슈가 너무 크리티컬 한것 같다... ㅠ

설 연휴 때 좀 했어야 했는데... 내가 잘못했던 것 같다..

그래도 기한내에 완성해 과제를 제출할 수 있을 것 같아서 다행이라 생각한다.. ㅎㅎ

 

어서 자고 낼 또 이어서 후다닥 해야지..

지금 기침이 점점 심해진당.. 잘 시간인가보당..

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

TIL 2025.02.05 기록  (0) 2025.02.05
TIL 2025.02.04 기록  (0) 2025.02.04
TIL 2025.01.31 기록  (0) 2025.01.31
TIL 2025.01.24 기록  (0) 2025.01.24
TIL 2025.01.23 기록  (0) 2025.01.23