지난번 필수 기능 구현에 이어, 이번에는 게임을 한층 더 다채롭게 만들어 줄 도전 과제들을 구현했다. 😵 플레이어에게 부정적인 효과를 주는 디버프 아이템을 추가하고, 웨이브가 진행될수록 맵 환경이 동적으로 변하도록 설계했다. 마지막으로 UI 애니메이션과 3D 위젯을 적용하여 시각적인 완성도를 높이는 작업까지 진행하며 C++ 코드와 블루프린트 에디터의 유기적인 연동 방식을 깊이 이해할 수 있었다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Components/WidgetComponent.h"
#include "SpartaCharacter.generated.h"
// 디버프 종류를 나타낼 열거형
UENUM(BlueprintType)
enum class EDebuffType : uint8
{
None,
Slow,
ReverseControl
};
// 현재 활성화된 디버프의 정보를 저장할 구조체
USTRUCT()
struct FActiveDebuff
{
GENERATED_BODY()
EDebuffType Type = EDebuffType::None; // 디버프 종류
FTimerHandle TimerHandle; // 디버프 지속시간을 제어할 타이머 핸들
};
UCLASS()
class SPARTAN_API ASpartaCharacter : public ACharacter
{
GENERATED_BODY()
public:
ASpartaCharacter();
// 디버프를 적용시키는 함수
void ApplyDebuff(EDebuffType DebuffType, float Duration);
protected:
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
// 조작 반전 효과를 위해 기존 이동 함수 수정
void MoveForward(float Value);
void MoveRight(float Value);
// 디버프 효과를 제거하는 함수들
void RemoveSlowDebuff();
void RemoveReverseControlDebuff();
private:
// 현재 적용 중인 디버프들을 관리하는 TMap
TMap<EDebuffType, FActiveDebuff> ActiveDebuffs;
float OriginalMaxWalkSpeed; // 원래 이동 속도를 저장할 변수
bool bIsControlsReversed; // 조작 반전 상태를 저장할 플래그
protected:
// 캐릭터 머리 위에 표시될 3D 위젯 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "UI")
UWidgetComponent* HealthBarWidgetComponent;
};
#include "SpartaCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/InputComponent.h"
#include "TimerManager.h"
using namespace std;
ASpartaCharacter::ASpartaCharacter()
{
bIsControlsReversed = false;
// 3D 헬스바 위젯 컴포넌트 생성
HealthBarWidgetComponent = CreateDefaultSubobject<UWidgetComponent>(TEXT("HealthBarWidget"));
HealthBarWidgetComponent->SetupAttachment(RootComponent);
HealthBarWidgetComponent->SetWidgetSpace(EWidgetSpace::Screen); // 위젯이 항상 화면을 바라보도록 설정
HealthBarWidgetComponent->SetDrawSize(FVector2D(150.f, 20.f));
HealthBarWidgetComponent->SetRelativeLocation(FVector(0.f, 0.f, 120.f)); // 캐릭터 머리 위로 위치 조정
}
void ASpartaCharacter::BeginPlay()
{
Super::BeginPlay();
// 게임 시작 시, 원래 최대 이동 속도를 저장
OriginalMaxWalkSpeed = GetCharacterMovement()->MaxWalkSpeed;
}
void ASpartaCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
PlayerInputComponent->BindAxis("MoveForward", this, &ASpartaCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &ASpartaCharacter::MoveRight);
}
void ASpartaCharacter::MoveForward(float Value)
{
// 조작 반전 디버프가 활성화 상태이면, 입력 값을 반대로 적용
if (bIsControlsReversed)
{
Value *= -1.0f;
}
AddMovementInput(GetActorForwardVector(), Value);
}
void ASpartaCharacter::MoveRight(float Value)
{
// 조작 반전 디버프가 활성화 상태이면, 입력 값을 반대로 적용
if (bIsControlsReversed)
{
Value *= -1.0f;
}
AddMovementInput(GetActorRightVector(), Value);
}
void ASpartaCharacter::ApplyDebuff(EDebuffType DebuffType, float Duration)
{
// 이미 같은 종류의 디버프가 걸려있으면, 기존 타이머를 초기화
if (ActiveDebuffs.Contains(DebuffType))
{
GetWorldTimerManager().ClearTimer(ActiveDebuffs[DebuffType].TimerHandle);
}
FActiveDebuff NewDebuff;
NewDebuff.Type = DebuffType;
switch (DebuffType)
{
case EDebuffType::Slow:
GetCharacterMovement()->MaxWalkSpeed = OriginalMaxWalkSpeed * 0.5f; // 이동 속도 50% 감소
// 지정된 Duration 이후 RemoveSlowDebuff 함수를 호출하는 타이머 설정
GetWorldTimerManager().SetTimer(NewDebuff.TimerHandle, this, &ASpartaCharacter::RemoveSlowDebuff, Duration, false);
break;
case EDebuffType::ReverseControl:
bIsControlsReversed = true; // 조작 반전 플래그 활성화
// 지정된 Duration 이후 RemoveReverseControlDebuff 함수를 호출하는 타이머 설정
GetWorldTimerManager().SetTimer(NewDebuff.TimerHandle, this, &ASpartaCharacter::RemoveReverseControlDebuff, Duration, false);
break;
}
ActiveDebuffs.Add(DebuffType, NewDebuff);
}
void ASpartaCharacter::RemoveSlowDebuff()
{
GetCharacterMovement()->MaxWalkSpeed = OriginalMaxWalkSpeed; // 원래 속도로 복구
ActiveDebuffs.Remove(EDebuffType::Slow);
}
void ASpartaCharacter::RemoveReverseControlDebuff()
{
bIsControlsReversed = false; // 조작 반전 플래그 비활성화
ActiveDebuffs.Remove(EDebuffType::ReverseControl);
}
#include "SpartaGameState.h"
#include "TimerManager.h"
#include "Engine/Engine.h"
#include "Kismet/GameplayStatics.h" // UGameplayStatics 사용을 위해 include
using namespace std;
// Wave 2 시작 시, 환경 변화를 적용하는 함수
void ASpartaGameState::EnableWave2()
{
const FString Msg = TEXT("Wave 2: Spike Traps Activated!");
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, Msg);
// 레벨에 배치된 특정 액터(스파이크 함정)들을 찾아 활성화하는 로직을 여기에 추가
}
// Wave 3 시작 시, 환경 변화를 적용하는 함수
void ASpartaGameState::EnableWave3()
{
const FString Msg = TEXT("Wave 3: Random Explosions Incoming!");
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, Msg);
// 3초마다 SpawnRandomExplosion 함수를 반복적으로 호출하는 타이머 설정
GetWorldTimerManager().SetTimer(
ExplosionTimerHandle,
this,
&ASpartaGameState::SpawnRandomExplosion,
3.0f, // 반복 주기
true // 반복 활성화
);
}
// 플레이어 주변에 무작위 폭발을 생성하는 함수
void ASpartaGameState::SpawnRandomExplosion()
{
APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(this, 0);
if (!PlayerPawn) return;
FVector PlayerLocation = PlayerPawn->GetActorLocation();
// 플레이어 주변의 무작위 위치에 폭발 생성
FVector ExplosionLocation = PlayerLocation + FVector(FMath::RandRange(-800.f, 800.f), FMath::RandRange(-800.f, 800.f), 0.f);
// 폭발 이펙트 및 데미지 적용 로직을 여기에 추가
}
타이머 핸들 관리의 중요성: 처음 디버프를 구현할 때, 같은 디버프 아이템을 연속해서 먹으면 타이머가 중첩되어 이상하게 동작하는 문제가 있었다. 🤔 ApplyDebuff 함수에서 새로운 디버프를 적용하기 전에 ActiveDebuffs 맵에 이미 같은 종류의 디버프가 있는지 확인하고, 있다면 GetWorldTimerManager().ClearTimer()로 기존 타이머를 명시적으로 제거해 주어야 했다. 상태를 관리할 때는 항상 초기화와 제거 로직을 꼼꼼히 체크해야 한다는 것을 배웠다.
C++ 컴포넌트와 블루프린트 위젯 연결: SpartaCharacter.cpp에서 UWidgetComponent를 추가하고 모든 설정을 마쳤는데도 캐릭터 머리 위에 아무것도 보이지 않았다. 알고 보니 C++에서 컴포넌트를 추가하는 것은 '틀'을 만드는 것이고, 어떤 위젯을 표시할지는 블루프린트 에디터에서 직접 지정해 주어야 했다. BP_SpartaCharacter 블루프린트를 열어 HealthBarWidgetComponent의 Details 패널에서 Widget Class를 미리 만들어 둔 WBP_HealthBar로 설정해주니 그제야 정상적으로 표시되었다. ✨
| 개념 | 설명 | 비고 |
|---|---|---|
FTimerManager | 특정 시간 후에 함수를 호출하거나, 주기적으로 함수를 반복 실행하도록 스케줄링하는 기능이다. | 디버프의 지속 시간을 제어하거나, Wave 3의 무작위 폭발처럼 반복적인 이벤트를 만들 때 필수적이다. |
UWidgetComponent | UMG 위젯을 3D 월드 공간에 렌더링할 수 있게 해주는 컴포넌트다. | 캐릭터 머리 위 체력바나, 상호작용 가능한 아이템 위의 안내 문구 등을 표시할 때 유용하게 사용된다. |
Enum과 TMap 활용 | Enum으로 상태(디버프 종류)를 명확히 정의하고, TMap으로 현재 활성화된 상태들을 관리했다. | 여러 종류의 상태가 중첩되거나 동시에 존재할 수 있는 복잡한 로직을 체계적으로 관리할 수 있다. |
| C++과 블루프린트 연동 | C++에서는 기능의 '뼈대'와 로직을 구현하고, 블루프린트에서는 '디자인'과 구체적인 값을 설정하는 방식으로 역할을 분담했다. | UWidgetComponent에 어떤 위젯을 쓸지, 캐릭터의 이동 속도 기본값이 얼마인지 등은 블루프린트에서 쉽게 수정할 수 있어 효율적이다. |