본문 바로가기

개인 프로젝트(과제)

8번과제 장애물 만들기2, 도전 과제 1번

장애물을 만들기 전에 레벨 3 트로피에 다가가면 Clear!!! 가 출력되도록 미리 구현을 해놓고 나중에 Clear UI를 만들어서 띄울 예정이다.

그리고 동전을 너무 안먹고 달리기만 하면 제한시간이 의미가 없는 것 같아서 총 점수가 500점 미만이면 트로피에 다가가도 게임오버가 되도록 구현을 했다.

테스트를 해보니 500점 미만일 때 게임오버가 되고 500점 이상일 때 Clear!!가 출력이 잘 됐다.

 

이제 마지막 장애물인 벽에서 왕복하는 장애물을 만들것이다.

액터를 생성하고 InterpToMovement로 왕복하도록 Behavior Type을 PingPong으로 설정했다.

그리고 지형만큼 움직이도록 움직이는 거리를 설정하고 장애물마다 속도를 다르게 하기 위해 Duration을 랜덤한 값을 받도록 설정했다.

이제 맵에 배치하고 테스트를 해보니 모두 속도가 다르게 잘 작동을 했다.

이제 레벨 1, 2, 3 맵과 장애물은 모두 완성했고 도전과제를 구현해보려 한다.


도전 과제 1번 - 아이템 상호작용 로직 고도화

  • 새로운 부정적 아이템 효과를 최소 두 가지 추가해 보세요. (단순 점수 감소 외에 색다른 디버프)
    • SlowingItem: 플레이어가 일정 시간 동안 이동 속도 50% 감소
    • ReverseControlItem: 일정 시간 동안 W키가 뒤로, A키가 오른쪽 등 컨트롤 반전
    • BlindItem: 일정 시간 화면 시야가 제한되거나, 카메라 회전이 제한
  • 아이템 효과는 중첩될 수 있습니다.
  • 효과의 시작/끝, 중첩 상황 등을 UI를 통해 표시하여 플레이어가 언제 디버프가 풀리는지 인지할 수 있도록 해주세요.

도전 과제 1번 - 아이템 상호작용 로직 고도화

예시로 들어준 아이템 중 일정시간 이동속도를 느리게하는 아이템과 키 입력이 반대로 되는 아이템을 구현해보려 한다.

 

BaseItem 클래스를 상속 받는 SlowItem 클래스를 만들고 지속시간을 담당할 변수 SlowTime과 타이머핸들을 선언했다.

#pragma once

#include "CoreMinimal.h"
#include "BaseItem.h"
#include "SlowItem.generated.h"

UCLASS()
class UNREAL_CPP_STUDY_API ASlowItem : public ABaseItem
{
	GENERATED_BODY()
	
public:
	ASlowItem();

	UPROPERTY(EditAnywhere, Category = "Time")
	float SlowTime;

	FTimerHandle SlowingTimerHandle;

	virtual void ActivateItem(AActor* Activator) override;
};

JinCharacter의 bool 변수 bIsSlowing을 선언하고 SlowItem을 활성화 했을 때 bIsSlowing을 true로 변경하고 몇 초 후 false로 변환하는 로직을 구현해놓고 bIsSlowing이 true일 때 캐릭터의 속도가 0.5배가 되도록 구현했다.

// JinCharacter.h
	UFUNCTION()
	void SetFalsebIsSlowing();
    
	bool bIsSlowing; // bool 변수 선언
// JinCharacter.cpp
void AJinCharacter::SetFalsebIsSlowing()
{
	bIsSlowing = false;
}

void AJinCharacter::StartSprint(const FInputActionValue& value)
{
	if (GetCharacterMovement() && !bIsSlowing)
	{
		GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
	}
	else if (GetCharacterMovement() && bIsSlowing)
	{
		GetCharacterMovement()->MaxWalkSpeed = SprintSpeed * 0.5;
	}
}

void AJinCharacter::StopSprint(const FInputActionValue& value)
{
	if (GetCharacterMovement() && !bIsSlowing)
	{
		GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
	}
	else if (GetCharacterMovement() && bIsSlowing)
	{
		GetCharacterMovement()->MaxWalkSpeed = NormalSpeed * 0.5;
	}
}
// SlowItem.cpp
#include "SlowItem.h"
#include "JinCharacter.h"

ASlowItem::ASlowItem()
{
	SlowTime = 2.0f;
}

void ASlowItem::ActivateItem(AActor* Activator)
{
	Super::ActivateItem(Activator);

	if (AJinCharacter* PlayerCharacter = Cast<AJinCharacter>(Activator))
	{
		PlayerCharacter->bIsSlowing = true;
		GetWorld()->GetTimerManager().SetTimer(
			SlowingTimerHandle,
			PlayerCharacter,
			&AJinCharacter::SetFalsebIsSlowing,
			SlowTime,
			false
		);
	}
	DestroyItem();
}

속도가 느려짐을 나타내기 위해 달팽이 스태틱 메시 에셋을 다운받아서 SlowItem에 적용했다.

맵에 배치하고 테스트를 해보니 2초동안 이동속도가 절반이 됐다가 다시 돌아오는게 확인이 됐다.

이제 랜덤하게 스폰이 되도록 데이터테이블에 SlowItem을 추가를 하고 각 아이템 확률을 조정했다.

실행을 해보니 스폰이 잘 되지만 현재 코드로는 달리기를 할 때만 SlowItem 효과가 적용이 되어 그냥 걸어가면서 먹으면 속도에 변화가 없는 문제가 발견됐다.

그래서 JinCharacter의 Move 함수에도 bIsSlowing이 true일 때 스피드가 0.5배가 되도록 구현했다.

// JinCharacter.cpp
void AJinCharacter::Move(const FInputActionValue& value)
{
	if (!Controller) return;

	const FVector2D MoveInput = value.Get<FVector2D>();

	if (bIsSlowing)
	{
		GetCharacterMovement()->MaxWalkSpeed = NormalSpeed * 0.5;
	}
	else
	{
		GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
	}

	if (!FMath::IsNearlyZero(MoveInput.X))
	{
		AddMovementInput(GetActorForwardVector(), MoveInput.X);
	}

	if (!FMath::IsNearlyZero(MoveInput.Y))
	{
		AddMovementInput(GetActorRightVector(), MoveInput.Y);
	}
}

이제 디버프 효과가 남아있는 시간을 UI에 표시해야 하는데 기존에 JinGameState에 구현해놨던 UpdateHUD 함수에 로직을 추가하려한다.

우선 위젯블루프린트에 디버프를 표시할 텍스트블럭을 추가했다.

그리고 Visibility를 Hidden으로 설정해두고 JinCharacter의 bIsSlowing이 true일 때 보이도록 설정했다.

if (AJinCharacter* JinCharacter = Cast<AJinCharacter>(JinPlayerController->GetPawn()))
{
	if (UTextBlock* HPText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("HP"))))
	{
		HPText->SetText(FText::FromString(FString::Printf(TEXT("HP : %.f / %.f"), JinCharacter->GetHealth(), JinCharacter->GetMaxHealth())));
	}

	if (UTextBlock* SlowText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("Slow"))))
	{
		if(JinCharacter->bIsSlowing)
		{
			SlowText->SetVisibility(ESlateVisibility::Visible);
		}
		else
		{
			SlowText->SetVisibility(ESlateVisibility::Hidden);
		}
	}
}

그리고 SlowItem의 TimerHandle을 가져와서 남아있는 지속시간을 표시하려 했는데 SlowItem 객체를 어떤 방법으로 가져와야할 지 모르겠어서 기존의 SlowItem의 ActivateItem 함수 로직을 JinCharacter의 함수로 옮기고 ActivateItem에서는 JinCharacter 함수를 호출하도록 수정했다.(SlowItem의 타이머핸들을 JinCharacter로 옮기기 위해서 수정을 했다.)

// JinCharacter.h
	FTimerHandle SlowingTimerHandle;
	void ActivateSlowItem();
// JinCharacter.cpp
void AJinCharacter::ActivateSlowItem()
{
	bIsSlowing = true;
	GetWorld()->GetTimerManager().SetTimer(
		SlowingTimerHandle,
		this,
		&AJinCharacter::SetbFalseIsSlowing,
		2.0f,
		false
	);
}
// SlowItem.cpp
#include "SlowItem.h"
#include "JinCharacter.h"

void ASlowItem::ActivateItem(AActor* Activator)
{
	Super::ActivateItem(Activator);

	if (AJinCharacter* PlayerCharacter = Cast<AJinCharacter>(Activator))
	{
		PlayerCharacter->ActivateSlowItem(); // 함수 호출만함
	}
	DestroyItem();
}

이렇게 하니 디버프 아이템이 효과가 지속중일때만 남은 지속시간이 보이고 효과가 끝나면 사라졌다.

 

이제 키 입력이 반대로 되는 아이템을 만들어 보려 한다.

테스트로 키 입력을 받는 로직에 -1을 곱해서 실행을 해보니 키 입력과 반대로 움직였다. 이를 잘 활용하면 될 것 같다.

// JinCharacter.cpp
void AJinCharacter::Move(const FInputActionValue& value)
{
	if (!Controller) return;

	const FVector2D MoveInput = value.Get<FVector2D>();

	if (bIsSlowing)
	{
		GetCharacterMovement()->MaxWalkSpeed = NormalSpeed * 0.5;
	}
	else
	{
		GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
	}

	if (!FMath::IsNearlyZero(MoveInput.X))
	{
		AddMovementInput(GetActorForwardVector(), MoveInput.X * -1);
	}

	if (!FMath::IsNearlyZero(MoveInput.Y))
	{
		AddMovementInput(GetActorRightVector(), MoveInput.Y * -1);
	}
}

BaseItem을 상속받는 클래스를 만들었다. 이름은 ReverseControlItem으로 지었다.

이 아이템도 SlowItem과 비슷하게 JinCharacter에 bool 변수를 선언하고 아이템이 지속중일때 true, 지속중이 아닐때 false로 해서 디버프 효과를 주면 될 것 같다는 생각이 들었다.

// JinCharacter.h
	bool bIsReverseControl;
	FTimerHandle ReverseControlTimerHandle;
    
	void ActivateReverseControlItem();

bIsSlowing의 초기값이 설정되지 않았길래 bIsReverseControl과 같이 false로 설정했다.

// JinCharacter.cpp
// 생성자
	bIsSlowing = false;
	bIsReverseControl = false;

그리고 ReverseControlItem에서 JinCharacter의 ActivateReverseControlItem 함수를 호출하도록 구현했다.

// ReverseControlItem.h
#pragma once

#include "CoreMinimal.h"
#include "BaseItem.h"
#include "ReverseControlItem.generated.h"

UCLASS()
class UNREAL_CPP_STUDY_API AReverseControlItem : public ABaseItem
{
	GENERATED_BODY()
	
public:
	virtual void ActivateItem(AActor* Activator) override;
};
// ReverseControlItem.cpp
#include "ReverseControlItem.h"
#include "JinCharacter.h"

void AReverseControlItem::ActivateItem(AActor* Activator)
{
	Super::ActivateItem(Activator);

	if (AJinCharacter* JinCharacter = Cast<AJinCharacter>(Activator))
	{
		JinCharacter->ActivateReverseControlItem();
	}
	DestroyItem();
}

이제 ActivateReverseControlItem 함수에서 bIsReverseControl 변수를 true로 바꾸고 몇 초 후 false로 바뀌는 로직을 구현하면 된다.

// JinCharacter.h
	void SetbIsReverseControlFalse();
// JinCharacter.cpp
void AJinCharacter::SetbIsReverseControlFalse()
{
	bIsReverseControl = false;
}

void AJinCharacter::ActivateReverseControlItem()
{
	bIsReverseControl = true;
	GetWorld()->GetTimerManager().SetTimer(
		ReverseControlTimerHandle,
		this,
		&AJinCharacter::SetbIsReverseControlFalse,
		2.0f,
		false
	);
}

그리고 Move함수에서 bIsReverseControl이 true면 -1을 곱하고 false면 그대로 움직이도록 했다.

void AJinCharacter::Move(const FInputActionValue& value)
{
	if (!Controller) return;

	const FVector2D MoveInput = value.Get<FVector2D>();

	if (bIsSlowing)
	{
		GetCharacterMovement()->MaxWalkSpeed = NormalSpeed * 0.5;
	}
	else
	{
		GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
	}

	if (!FMath::IsNearlyZero(MoveInput.X))
	{
		if(bIsReverseControl)
		{
			AddMovementInput(GetActorForwardVector(), MoveInput.X * -1);
		}
		else
		{
			AddMovementInput(GetActorForwardVector(), MoveInput.X);
		}
	}

	if (!FMath::IsNearlyZero(MoveInput.Y))
	{
		if (bIsReverseControl)
		{
			AddMovementInput(GetActorRightVector(), MoveInput.Y * -1);
		}
		else
		{
			AddMovementInput(GetActorRightVector(), MoveInput.Y);
		}
	}
}

테스트를 해보니 잘 작동이 됐다. 이제 데이터테이블에 ReverseControl 아이템을 추가하고 UI에 지속시간을 띄우면 도전과제 1번은 끝이 난다.

위젯블루프린트에 Reverse 텍스트블럭을 추가했다.

JinGameState의 HUDUpdate 함수에서 SlowItem과 똑같은 로직을 구현했다.

// JinGameState.cpp
// HUDUpdate 함수
if (UTextBlock* ReverseText = Cast<UTextBlock>(HUDWidget->GetWidgetFromName(TEXT("Reverse"))))
{
	if (JinCharacter->bIsReverseControl)
	{
		float RemainingReverseTime = GetWorldTimerManager().GetTimerRemaining(JinCharacter->ReverseControlTimerHandle);
		ReverseText->SetVisibility(ESlateVisibility::Visible);
		ReverseText->SetText(FText::FromString(FString::Printf(TEXT("Reverse: %.1f"), RemainingReverseTime)));
	}
	else
	{
		ReverseText->SetVisibility(ESlateVisibility::Hidden);
	}
}