상세 컨텐츠

본문 제목

Unreal C++ 게임 루프 및 UI 설계 후 코인 획득 게임 구현

Unreal C++

by hyunjunstar 2026. 4. 29. 23:22

본문

오늘은 Unreal C++ 과제로 기존 프로젝트에 게임 루프와 UI를 재설계 하는 작업을 진행했다.

 

GameState에서 레벨과 웨이브 흐름을 관리,

SpawnVolume과 데이터 테이블을 활용해서 아이템들이 확률 기반으로 스폰 되도록 구현 하였으며, 

HUD와 메인 메뉴를 별도 위젯 클래스로 분리하여 점수, 시간, 레벨, 웨이브, 게임오버 메뉴 등을 구현하였다.

 

게임 루프

 - GameState에서 레벨과 웨이브 진행 관리
 - 각 레벨마다 3개의 웨이브 진행
 - 웨이브별 제한 시간과 아이템 스폰 수 설정
 - 시간이 끝나거나 코인을 모두 획득하면 다음 웨이브로 이동
 - 모든 웨이브가 끝나면 다음 레벨로 이동
 - 마지막 레벨 종료 시 게임오버 메뉴 출력

아이템 스폰

 - SpawnVolume을 활용한 랜덤 위치 스폰
 - 데이터테이블 기반 아이템 확률 관리
 - 코인, 회복 아이템, 지뢰 아이템 스폰
 - 스폰된 아이템에 현재 레벨 난이도 적용

아이템

 - BaseItem에서 공통 충돌, 파티클, 사운드 처리
 - 코인 아이템 획득 시 점수 증가
 - 회복 아이템은 레벨이 올라갈수록 회복량 감소
 - 지뢰 아이템은 레벨이 올라갈수록 폭발 시간 감소 및 데미지 증가
 - 웨이브 전환 시 남은 파티클 정리

UI

 - SpartaHUDWidget으로 HUD 관리
 - 점수, 시간, 레벨, 웨이브 표시
 - 캐릭터 머리 위에 체력 텍스트와 HP바 표시
 - SpartaMainMenuWidget으로 메인 메뉴와 게임오버 메뉴 관리
 - 시작, 재시작, 메인 메뉴로 돌아가기, 종료 버튼 기능 연결

1. GameState - 웨이브, 레벨 시작 종료 및 파티클 정리

// SpartaGameState.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameState.h"
#include "SpartaGameState.generated.h"

class UParticleSystemComponent; // 파티클 컴포넌트 클래스 전방 선언

UCLASS()
class SPARTA_API ASpartaGameState : public AGameState
{
    GENERATED_BODY()

public:
    ASpartaGameState();

    virtual void BeginPlay() override;

    // 현재 점수 반환
    UFUNCTION(BlueprintPure, Category = "Score")
    int32 GetScore() const;

    // 점수 추가
    UFUNCTION(BlueprintCallable, Category = "Score")
    void AddScore(int32 Amount);

    // 게임 오버 메뉴 출력
    UFUNCTION(BlueprintCallable, Category = "Level")
    void OnGameOver();

    // 레벨 시작
    void StartLevel();

    // 레벨 시간 종료 처리
    void OnLevelTimeUp();

    // 코인 획득 처리
    void OnCoinCollected();

    // 레벨 종료 및 다음 레벨 이동
    void EndLevel();

    // HUD 갱신
    void UpdateHUD();

    // 웨이브 시작
    void StartWave();

    // 웨이브 시간 종료 처리
    void OnWaveTimeUp();

    // 웨이브 종료 및 다음 웨이브 이동
    void EndWave();

    // 파티클 등록
    void RegisterParticle(UParticleSystemComponent* Particle);

public:
    // 현재 레벨 점수
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Score")
    int32 Score;

    // 현재 웨이브에서 생성된 코인 수
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Coin")
    int32 SpawnedCoinCount;

    // 현재 웨이브에서 획득한 코인 수
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Coin")
    int32 CollectedCoinCount;

    // 현재 웨이브 제한 시간
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Level")
    float LevelDuration;

    // 현재 레벨 인덱스
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Level")
    int32 CurrentLevelIndex;

    // 전체 레벨
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Level")
    int32 MaxLevels;

    // 레벨 이동에 사용할 맵 이름 배열
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Level")
    TArray<FName> LevelMapNames;

    // 현재 웨이브 인덱스
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Wave")
    int32 CurrentWaveIndex;

    // 한 레벨의 최대 웨이브 수
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Wave")
    int32 MaxWaves;

protected:
    // 현재 웨이브에서 스폰할 아이템 수
    int32 ItemToSpawn;

    // 웨이브 제한 시간 타이머
    FTimerHandle LevelTimerHandle;

    // HUD 갱신 타이머
    FTimerHandle HUDUpdateTimerHandle;

    // 생성된 파티클 목록을 가져옴
    TArray<TWeakObjectPtr<UParticleSystemComponent>> ActiveParticles;

    // 등록된 파티클 제거
    void ClearActiveParticles();

    // 레벨에 따라 아이템 난이도 조절
    void ApplyItemDifficulty(AActor* SpawnedActor);
};
// SpartaGameState.cpp

#include "SpartaGameState.h"
#include "SpartaGameInstance.h"
#include "SpartaPlayerController.h"
#include "SpawnVolume.h"
#include "BaseItem.h"
#include "CoinItem.h"
#include "MineItem.h"
#include "HealingItem.h"
#include "SpartaHUDWidget.h"
#include "SpartaCharacter.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystemComponent.h"

// 게임 정보
ASpartaGameState::ASpartaGameState()
{
    Score = 0;
    
    LevelDuration = 0;
    ItemToSpawn = 0;

    CurrentWaveIndex = 0;
    MaxWaves = 3;
    
    CurrentLevelIndex = 0;
    MaxLevels = 3;
}

// ======================================== // 

// 게임 시작 시 레벨 시작 및 HUD 갱신 타이머 실행
void ASpartaGameState::BeginPlay()
{
    Super::BeginPlay();

    // 현재 레벨 시작
    StartLevel();

    // HUD를 0.1초마다 갱신
    GetWorldTimerManager().SetTimer(
        HUDUpdateTimerHandle,
        this,
        &ASpartaGameState::UpdateHUD,
        0.1f,
        true
    );
}

// ======================================== // 

// 현재 점수 반환
int32 ASpartaGameState::GetScore() const
{
    return Score;
}

// ======================================== // 

// GameInstance에 점수 누적
void ASpartaGameState::AddScore(int32 Amount)
{
    // 현재 GameInstance 정보 가져오기
    if (UGameInstance* GameInstance = GetGameInstance())
    {
        // 프로젝트 전용 GameInstance로 캐스팅
        if (USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GameInstance))
        {
            // 총점수에 점수 누적
            SpartaGameInstance->AddToScore(Amount);
        }
    }
}

// ======================================== // 

// 레벨 시작 HUD 표시, 현재 레벨 인덱스 불러오기, 1웨이브 시작
void ASpartaGameState::StartLevel()
{
    // 플레이어 컨트롤러 가져오기
    if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
    {
        // 프로젝트 전용 PlayerController로 캐스팅
        if (ASpartaPlayerController* SpartaPlayerController = Cast<ASpartaPlayerController>(PlayerController))
        {
            // 게임 HUD 표시
            SpartaPlayerController->ShowGameHUD();
        }
    }

    // GameInstance에서 현재 레벨 인덱스 가져오기
    if (UGameInstance* GameInstance = GetGameInstance())
    {
        if (USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GameInstance))
        {
            CurrentLevelIndex = SpartaGameInstance->CurrentLevelIndex;
        }
    }

    // 레벨 시작 시 코인 카운트 초기화
    SpawnedCoinCount = 0;
    CollectedCoinCount = 0;

    // 1웨이브부터 시작
    CurrentWaveIndex = 1;

    // 첫 웨이브 시작
    StartWave();
}

// ======================================== // 

// 웨이브 시작: 웨이브별 시간과 아이템 수 설정 후 아이템 스폰
void ASpartaGameState::StartWave()
{
    // 월드에 배치된 SpawnVolume 검색
    TArray<AActor*> FoundVolumes;
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASpawnVolume::StaticClass(), FoundVolumes);

    // 웨이브마다 코인 카운트 초기화
    SpawnedCoinCount = 0;
    CollectedCoinCount = 0;

    // 1웨이브 설정
    if (CurrentWaveIndex == 1)
    {
        // 화면에 웨이브 시작 메시지 출력
        if (GEngine)
        {
            GEngine->AddOnScreenDebugMessage(
                -1,
                2.0f,
                FColor::Green,
                FString::Printf(TEXT("Wave %d 시작"), CurrentWaveIndex)
            );
        }

        // 제한 시간, 아이템 개수 설정
        LevelDuration = 40.0f;
        ItemToSpawn = 40;

        // 설정된 개수만큼 아이템 스폰
        for (int32 i = 0; i < ItemToSpawn; i++)
        {
            // SpawnVolume이 존재하는지 확인
            if (FoundVolumes.Num() > 0)
            {
                // 첫 번째 SpawnVolume 사용
                ASpawnVolume* SpawnVolume = Cast<ASpawnVolume>(FoundVolumes[0]);
                if (SpawnVolume)
                {
                    // 랜덤 아이템 스폰
                    AActor* SpawnedActor = SpawnVolume->SpawnRandomItem();

                    // 스폰된 아이템에 레벨 난이도 적용
                    ApplyItemDifficulty(SpawnedActor);

                    // 스폰된 아이템이 코인이면 코인 수 증가
                    if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass()))
                    {
                        SpawnedCoinCount++;
                    }
                }
            }
        }
    }

    // 2웨이브 설정
    else if (CurrentWaveIndex == 2)
    {
        if (GEngine)
        {
            GEngine->AddOnScreenDebugMessage(
                -1,
                2.0f,
                FColor::Green,
                FString::Printf(TEXT("Wave %d 시작"), CurrentWaveIndex)
            );
        }

        LevelDuration = 35.0f;
        ItemToSpawn = 45;

        for (int32 i = 0; i < ItemToSpawn; i++)
        {
            if (FoundVolumes.Num() > 0)
            {
                ASpawnVolume* SpawnVolume = Cast<ASpawnVolume>(FoundVolumes[0]);
                if (SpawnVolume)
                {
                    AActor* SpawnedActor = SpawnVolume->SpawnRandomItem();

                    ApplyItemDifficulty(SpawnedActor);

                    if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass()))
                    {
                        SpawnedCoinCount++;
                    }
                }
            }
        }
    }

    // 3웨이브 설정
    else if (CurrentWaveIndex == 3)
    {
        if (GEngine)
        {
            GEngine->AddOnScreenDebugMessage(
                -1,
                2.0f,
                FColor::Green,
                FString::Printf(TEXT("Wave %d 시작"), CurrentWaveIndex)
            );
        }

        LevelDuration = 30.0f;
        ItemToSpawn = 50;

        for (int32 i = 0; i < ItemToSpawn; i++)
        {
            if (FoundVolumes.Num() > 0)
            {
                ASpawnVolume* SpawnVolume = Cast<ASpawnVolume>(FoundVolumes[0]);
                if (SpawnVolume)
                {
                    AActor* SpawnedActor = SpawnVolume->SpawnRandomItem();

                    ApplyItemDifficulty(SpawnedActor);

                    if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass()))
                    {
                        SpawnedCoinCount++;
                    }
                }
            }
        }
    }

    // 웨이브 제한 시간 타이머 설정
    GetWorldTimerManager().SetTimer(
        LevelTimerHandle,
        this,
        &ASpartaGameState::OnWaveTimeUp,
        LevelDuration,
        false
    );
}

// ======================================== // 

// 웨이브 시간이 끝났을 때 호출
void ASpartaGameState::OnWaveTimeUp()
{
    // 웨이브 종료
    EndWave();
}

// ======================================== // 

// 레벨 시간이 끝났을 때 호출
void ASpartaGameState::OnLevelTimeUp()
{
    // 레벨 종료
    EndLevel();
}

// ======================================== // 

// 코인 획득 시 획득 수를 증가시키고, 모두 획득하면 웨이브 종료
void ASpartaGameState::OnCoinCollected()
{
    // 획득한 코인 수 증가
    CollectedCoinCount++;

    // 현재 코인 획득 상황 로그 출력
    UE_LOG(LogTemp, Warning,
        TEXT("획득한 코인 : %d / %d"),
        CollectedCoinCount,
        SpawnedCoinCount
    );

    // 모든 코인을 획득하면 웨이브 종료
    if (SpawnedCoinCount > 0 && CollectedCoinCount >= SpawnedCoinCount)
    {
        EndWave();
    }
}

// ======================================== // 

// 웨이브 종료 남은 파티클/아이템 정리 후 다음 웨이브 또는 레벨 종료
void ASpartaGameState::EndWave()
{
    // 남아있는 파티클 제거
    ClearActiveParticles();

    // 웨이브 타이머 정리
    GetWorldTimerManager().ClearTimer(LevelTimerHandle);

    // 월드에 남아있는 모든 BaseItem 검색
    TArray<AActor*> Actors;
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), ABaseItem::StaticClass(), Actors);

    // 남은 아이템 제거
    for (AActor* Item : Actors)
    {
        if (ABaseItem* BaseItem = Cast<ABaseItem>(Item))
        {
            BaseItem->Destroy();
        }
    }

    // 다음 웨이브로 이동
    CurrentWaveIndex++;

    // 남은 웨이브가 있으면 다음 웨이브 시작
    if (CurrentWaveIndex <= MaxWaves)
    {
        StartWave();
    }
    // 모든 웨이브가 끝나면 레벨 종료
    else
    {
        EndLevel();
    }

}

// ======================================== // 

// 레벨 종료 남은 오브젝트 정리 후 다음 레벨 이동 또는 게임오버
void ASpartaGameState::EndLevel()
{
    // 남아있는 파티클 제거
    ClearActiveParticles();

    // 웨이브 타이머 정리
    GetWorldTimerManager().ClearTimer(LevelTimerHandle);

    // 월드에 남아있는 모든 BaseItem 검색
    TArray<AActor*> Actors;
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), ABaseItem::StaticClass(), Actors);

    // 남은 아이템 제거
    for (AActor* Item : Actors)
    {
        if (ABaseItem* BaseItem = Cast<ABaseItem>(Item))
        {
            // 아이템 내부 타이머 정리
            BaseItem->ClearTimer();

            // 아이템 제거
            BaseItem->Destroy();
        }
    }

    // GameInstance 가져오기
    if (UGameInstance* GameInstance = GetGameInstance())
    {
        // 프로젝트 전용 GameInstance로 캐스팅
        USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GameInstance);
        if (SpartaGameInstance)
        {
            // 현재 레벨 점수 반영
            AddScore(Score);

            // 다음 레벨 인덱스로 증가
            CurrentLevelIndex++;
            SpartaGameInstance->CurrentLevelIndex = CurrentLevelIndex;

            // 최대 레벨을 넘으면 게임오버
            if (CurrentLevelIndex >= MaxLevels)
            {
                OnGameOver();
                return;
            }

            // 다음 레벨 맵이 존재하면 이동
            if (LevelMapNames.IsValidIndex(CurrentLevelIndex))
            {
                UGameplayStatics::OpenLevel(GetWorld(), LevelMapNames[CurrentLevelIndex]);
            }
            // 맵 이름이 없으면 게임오버
            else
            {
                OnGameOver();
            }
        }
    }
}

// ======================================== // 

// 생성된 파티클을 GameState에 등록
void ASpartaGameState::RegisterParticle(UParticleSystemComponent* Particle)
{
    // 유효한 파티클만 배열에 저장
    if (Particle)
    {
        ActiveParticles.Add(Particle);
    }
}

// ======================================== // 

// 웨이브, 레벨 전환 시 남아있는 파티클 제거
void ASpartaGameState::ClearActiveParticles()
{
    // 등록된 파티클 순회
    for (TWeakObjectPtr<UParticleSystemComponent>& WeakParticle : ActiveParticles)
    {
        // 아직 살아있는 파티클만 제거
        if (WeakParticle.IsValid())
        {
            WeakParticle->DeactivateSystem();
            WeakParticle->DestroyComponent();
        }
    }

    // 배열 비우기
    ActiveParticles.Empty();
}

// ======================================== // 

// 레벨에 따라 아이템 난이도 조정
void ASpartaGameState::ApplyItemDifficulty(AActor* SpawnedActor)
{
    // 스폰 실패 시 종료
    if (!SpawnedActor)
    {
        return;
    }

    // 지뢰 아이템이면 폭발 시간/데미지 조정
    if (AMineItem* MineItem = Cast<AMineItem>(SpawnedActor))
    {
        MineItem->ApplyLevelDifficulty(CurrentLevelIndex);
    }

    // 회복 아이템이면 회복량 조정
    if (AHealingItem* HealingItem = Cast<AHealingItem>(SpawnedActor))
    {
        HealingItem->ApplyLevelDifficulty(CurrentLevelIndex);
    }
}

// ======================================== // 

// 게임 종료 후 메뉴 표시
void ASpartaGameState::OnGameOver()
{
    // 첫 번째 플레이어 컨트롤러 가져오기
    if (APlayerController* PlayerController = GetWorld()->GetFirstPlayerController())
    {
        // 프로젝트 전용 PlayerController로 캐스팅
        if (ASpartaPlayerController* SpartaPlayerController = Cast<ASpartaPlayerController>(PlayerController))
        {
            // 게임 일시정지
            SpartaPlayerController->SetPause(true);

            // 게임오버 메뉴 표시
            SpartaPlayerController->ShowMainMenu(true);
        }
    }

}

// ======================================== // 

// HUD에 시간, 점수, 레벨, 웨이브, 체력 정보 전달
void ASpartaGameState::UpdateHUD()
{   
    // 첫 번째 플레이어 컨트롤러 가져오기
    APlayerController* PlayerController = GetWorld()->GetFirstPlayerController();

    // 프로젝트 전용 PlayerController로 캐스팅
    ASpartaPlayerController* SpartaPlayerController = Cast<ASpartaPlayerController>(PlayerController);

    // 캐스팅 실패 시 종료
    if (!SpartaPlayerController)
    {
        return;
    }

    // HUD 위젯 가져오기
    USpartaHUDWidget* HUDWidget = SpartaPlayerController->GetHUDWidget();

    // HUD가 없으면 종료
    if (!HUDWidget)
    {
        return;
    }

    // 총점 기본값
    int32 TotalScore = 0;

    // GameInstance에서 총점 가져오기
    if (USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GetGameInstance()))
    {
        TotalScore = SpartaGameInstance->TotalScore;
    }

    // 남은 시간 계산
    float RemainingTime = GetWorldTimerManager().GetTimerRemaining(LevelTimerHandle);
    RemainingTime = FMath::Max(0.0f, RemainingTime);

    // 체력 기본값
    float Health = 0.0f;
    float MaxHealth = 100.0f;

    // 플레이어 캐릭터 가져오기
    if (ASpartaCharacter* PlayerCharacter = Cast<ASpartaCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0)))
    {
        Health = PlayerCharacter->GetHealth();
        MaxHealth = PlayerCharacter->GetMaxHealth();
    }

    // HUD 위젯에 표시할 값 전달
    HUDWidget->UpdateHUD(
        RemainingTime,
        TotalScore,
        CurrentLevelIndex,
        CurrentWaveIndex,
        Health,
        MaxHealth
    );
}

 

2. SpawnVolume - 데이터테이블 기반 아이템 랜덤 스폰

// SpawnVolume.h

#pragma once

#include "CoreMinimal.h"
// 아이템 데이터 테이블 구조체 인클루드
#include "ItemSpawnRow.h"
#include "GameFramework/Actor.h"
#include "SpawnVolume.generated.h"

class UBoxComponent;    // 박스 충돌 컴포넌트 클래스 전방 선언

UCLASS()
class SPARTA_API ASpawnVolume : public AActor
{
	GENERATED_BODY()
	
public:	
	ASpawnVolume();

    // 스폰 볼륨의 루트 컴포넌트
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
    USceneComponent* SceneComp;

    // 아이템이 생성될 범위를 나타내는 박스 컴포넌트
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spawning")
    UBoxComponent* SpawningBox;

    // 아이템 클래스와 스폰 확률을 저장한 데이터 테이블
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawning")
    UDataTable* ItemDataTable;

    // 데이터 테이블의 확률을 기준으로 랜덤 아이템을 스폰
    UFUNCTION(BlueprintCallable, Category = "Spawning")
    AActor* SpawnRandomItem();

    // 데이터 테이블에서 확률 계산으로 스폰할 아이템 Row 선택
    FItemSpawnRow* GetRandomItem() const;

    // 전달받은 아이템 클래스를 실제 월드에 생성
    AActor* SpawnItem(TSubclassOf<AActor> ItemClass);

    // SpawningBox 내부의 랜덤 위치 반환
    FVector GetRandomPointInVolume() const;
};
// SpawnVolume.cpp

#include "SpawnVolume.h"
#include "Components/BoxComponent.h"

// 스폰 볼륨 컴포넌트 초기화
ASpawnVolume::ASpawnVolume()
{
    // 매 프레임 Tick이 필요 없으므로 비활성화
    PrimaryActorTick.bCanEverTick = false;

    // 루트 컴포넌트 생성
    SceneComp = CreateDefaultSubobject<USceneComponent>(TEXT("SceneComponent"));
    SetRootComponent(SceneComp);

    // 아이템 스폰 범위로 사용할 박스 컴포넌트 생성
    SpawningBox = CreateDefaultSubobject<UBoxComponent>(TEXT("Spawning Box"));
    SpawningBox->SetupAttachment(SceneComp);

    // 데이터 테이블 기본값 초기화
    ItemDataTable = nullptr;
}

// ======================================== //

// 데이터 테이블 확률에 따라 랜덤 아이템 스폰
AActor* ASpawnVolume::SpawnRandomItem()
{
    // 확률 계산으로 선택된 데이터 Row 가져오기
    if (FItemSpawnRow* SelectedRow = GetRandomItem())
    {
        // Row에 저장된 아이템 클래스 가져오기
        if (UClass* ActualClass = SelectedRow->ItemClass.Get())
        {
            // 선택된 아이템을 실제로 월드에 스폰
            return SpawnItem(ActualClass);
        }
    }

    // 실패 시 nullptr 반환
    return nullptr;
}

// ======================================== //

// 데이터 테이블에서 확률 기반으로 아이템 Row 선택
FItemSpawnRow* ASpawnVolume::GetRandomItem() const
{
    // DataTable이 있어야 실행(없으면 바로 종료)
    if (!ItemDataTable) return nullptr;

    // 모든 Row를 저장할 배열
    TArray<FItemSpawnRow*> AllRows;

    // GetAllRows에 필요한 컨텍스트 문자열
    static const FString ContextString(TEXT("ItemSpawnContext"));

    // 데이터 테이블의 모든 Row 가져오기
    ItemDataTable->GetAllRows(ContextString, AllRows);

    // 데이터가 없으면 종료
    if (AllRows.IsEmpty()) return nullptr;

    // 전체 확률 합산용 변수
    float TotalChance = 0.0f;

    // 모든 아이템의 스폰 확률 합산
    for (const FItemSpawnRow* Row : AllRows)
    {
        // 아이템 목록이 유효한지 확인
        if (Row)
        {
            // 아이템들의 확률을 더해서 총합 계산
            TotalChance += Row->Spawnchance;
        }
    }

    // 0 ~ 전체 확률 사이 랜덤값 생성
    const float RandValue = FMath::FRandRange(0.0f, TotalChance);

    // 누적 확률 계산용 변수
    float AccumulateChance = 0.0f;

    // 누적 확률 방식으로 선택된 Row 찾기
    for (FItemSpawnRow* Row : AllRows)
    {
        // 현재 Row의 확률을 누적
        AccumulateChance += Row->Spawnchance;

        // 랜덤 값이 현재 누적 확률 구간에 포함되면 해당 아이템 선택
        if (RandValue <= AccumulateChance)
        {
            // 선택된 아이템을 반환
            return Row;
        }
    }

    // 선택 실패 시 nullptr 반환
    return nullptr;
}

// ======================================== //

// SpawningBox 내부의 랜덤 위치 반환
FVector ASpawnVolume::GetRandomPointInVolume() const
{
    // 박스의 실제 크기 가져오기
    FVector BoxExtent = SpawningBox->GetScaledBoxExtent();

    // 박스의 월드 위치 가져오기
    FVector BoxOrigin = SpawningBox->GetComponentLocation();

    // 박스 범위 안에서 랜덤 좌표 생성
    return BoxOrigin + FVector(
        FMath::FRandRange(-BoxExtent.X, BoxExtent.X),
        FMath::FRandRange(-BoxExtent.Y, BoxExtent.Y),
        FMath::FRandRange(-BoxExtent.Z, BoxExtent.Z)
    );
}

// ======================================== //

// 전달받은 아이템 클래스를 월드에 스폰
AActor* ASpawnVolume::SpawnItem(TSubclassOf<AActor> ItemClass)
{
    // 클래스가 없으면 스폰하지 않음
    if (!ItemClass) return nullptr;

    // 랜덤 위치에 아이템 액터 생성
    AActor* SpawnedActor = GetWorld()->SpawnActor<AActor>(
        ItemClass,
        GetRandomPointInVolume(),
        FRotator::ZeroRotator
    );

    // 생성된 액터 반환
    return SpawnedActor;
}


3. BaseItem - 아이템 공통 부모 클래스

// BaseItem.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemInterface.h"
#include "BaseItem.generated.h"

class USphereComponent; // 충돌 감지용 Sphere 컴포넌트 전방 선언
class UParticleSystemComponent; // 파티클 컴포넌트 전방 선언

UCLASS()
class SPARTA_API ABaseItem : public AActor, public IItemInterface
{
    GENERATED_BODY()

public:
    ABaseItem();

    // 파티클 삭제 타이머 정리
    void ClearTimer();

protected:
    // 아이템 종류 구분용 이름
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
    FName ItemType;

    // 아이템의 루트 씬 컴포넌트
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
    USceneComponent* Scene;

    // 플레이어와의 Overlap 감지용 충돌 컴포넌트
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
    USphereComponent* Collision;

    // 아이템 외형 표시용 스태틱 메시
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
    UStaticMeshComponent* StaticMesh;

    // 아이템 획득 시 재생할 파티클
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
    UParticleSystem* PickupParticle;

    // 아이템 획득 시 재생할 사운드
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
    USoundBase* PickupSound;

    // 파티클 삭제 타이머 핸들
    FTimerHandle DestroyParticleTimerHandle;

    // 아이템과 다른 액터가 겹치기 시작했을 때 호출
    virtual void OnItemOverlap(
        UPrimitiveComponent* OverlappedComp,
        AActor* OtherActor,
        UPrimitiveComponent* OtherComp,
        int32 OtherBodyIndex,
        bool bFromSweep,
        const FHitResult& SweepResult) override;

    // 아이템과 다른 액터의 겹침이 끝났을 때 호출
    virtual void OnItemEndOverlap(
        UPrimitiveComponent* OverlappedComp,
        AActor* OtherActor,
        UPrimitiveComponent* OtherComp,
        int32 OtherBodyIndex) override;

    // 아이템 효과 실행
    virtual void ActivateItem(AActor* Activator) override;

    // 아이템 타입 반환
    virtual FName GetItemType() const override;

    // 아이템 액터 제거
    void DestroyItem();

    // 생성된 파티클 제거
    void DestroyParticle(UParticleSystemComponent* Particle);
};
// BaseItem.cpp

#include "BaseItem.h"
#include "Components/SphereComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystemComponent.h"
#include "TimerManager.h"
#include "SpartaGameState.h"

// 아이템 기본 컴포넌트와 Overlap 이벤트 설정
ABaseItem::ABaseItem()
{
    PrimaryActorTick.bCanEverTick = false;

    // 루트 컴포넌트 생성 및 설정
    Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
    SetRootComponent(Scene);

    // 충돌 컴포넌트 생성 및 설정
    Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));

    // 충돌은 막지 않고 Overlap만 감지
    Collision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));

    // 루트 컴포넌트로 설정
    Collision->SetupAttachment(Scene);

    // 스태틱 메시 컴포넌트 생성 및 설정
    StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
    StaticMesh->SetupAttachment(Collision);
    StaticMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    // 메시가 불필요하게 충돌을 막지 않도록 하기 위해, 별도로 NoCollision 등으로 설정할 수 있음.

    // Overlap 이벤트 바인딩
    Collision->OnComponentBeginOverlap.AddDynamic(this, &ABaseItem::OnItemOverlap);
    Collision->OnComponentEndOverlap.AddDynamic(this, &ABaseItem::OnItemEndOverlap);

}

// ======================================== //

// 아이템과 다른 액터가 겹쳤을 때 호출
void ABaseItem::OnItemOverlap(
    UPrimitiveComponent* OverlappedComp,
    AActor* OtherActor,
    UPrimitiveComponent* OtherComp,
    int32 OtherBodyIndex,
    bool bFromSweep,
    const FHitResult& SweepResult)
{
    // OtherActor가 플레이어인지 확인 ("Player" 태그 활용)
    if (OtherActor && OtherActor->ActorHasTag("Player"))
    {
        // 화면에 디버그 메시지 출력
        GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, FString::Printf(TEXT("Overlap!!!")));
        
        // 아이템 사용 (획득) 로직 호출
        ActivateItem(OtherActor);
    }
}

// ======================================== //

// Overlap 종료 시 호출, 현재는 별도 기능 없음
void ABaseItem::OnItemEndOverlap(
    UPrimitiveComponent* OverlappedComp,
    AActor* OtherActor,
    UPrimitiveComponent* OtherComp,
    int32 OtherBodyIndex)
{
}

// ======================================== //

// 아이템 능력 실행
void ABaseItem::ActivateItem(AActor* Activator)
{
    // 획득 파티클이 있으면 생성
    if (PickupParticle)
    {
        UParticleSystemComponent* Particle = UGameplayStatics::SpawnEmitterAtLocation(
            GetWorld(),
            PickupParticle,
            GetActorLocation(),
            GetActorRotation(),
            false
        );

        // 파티클 생성 성공 시
        if (Particle)
        {
            // GameState에 파티클 등록
            if (ASpartaGameState* SpartaGameState = GetWorld()->GetGameState<ASpartaGameState>())
            {
                SpartaGameState->RegisterParticle(Particle);
            }

            // 파티클 안전 참조용 Weak Pointer 생성
            TWeakObjectPtr<UParticleSystemComponent> WeakParticle = Particle;

            // 지역 타이머 핸들 생성
            FTimerHandle LocalParticleTimerHandle;

            // 2초 뒤 파티클 제거
            GetWorld()->GetTimerManager().SetTimer(
                LocalParticleTimerHandle,
                FTimerDelegate::CreateLambda([WeakParticle]()
                    {
                        // 파티클이 아직 유효하면 제거
                        if (WeakParticle.IsValid())
                        {
                            WeakParticle->DeactivateSystem();
                            WeakParticle->DestroyComponent();
                        }
                    }),
                2.0f,
                false
            );
        }
    }

    // 획득 사운드가 있으면 재생
    if (PickupSound)
    {
        UGameplayStatics::PlaySoundAtLocation(
            GetWorld(),
            PickupSound,
            GetActorLocation()
        );
    }

    // 아이템 액터 제거
    DestroyItem();
}

// ======================================== //

// 아이템 타입 반환
FName ABaseItem::GetItemType() const
{
    return ItemType;
}

// ======================================== //

// 아이템 액터 제거
void ABaseItem::DestroyItem()
{
    Destroy();
}

// ======================================== //

// 파티클 제거 함수
void ABaseItem::DestroyParticle(UParticleSystemComponent* Particle)
{
    // 기존 파티클 타이머 정리
    ClearTimer();

    // 파티클이 유효하면 제거
    if (IsValid(Particle))
    {
        Particle->DestroyComponent();
    }
}

// ======================================== //

// 파티클 삭제 타이머 정리
void ABaseItem::ClearTimer()
{   
    // 월드가 유효하면 타이머 제거
    if (GetWorld())
    {
        GetWorld()->GetTimerManager().ClearTimer(DestroyParticleTimerHandle);
    }
}


4. MineItem - 지뢰 데미지 조절

// MineItem.h

#pragma once

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

UCLASS()
class SPARTA_API AMineItem : public ABaseItem
{
    GENERATED_BODY()

public:
    AMineItem();

    // 레벨에 따라 폭발 시간과 데미지 조정
    void ApplyLevelDifficulty(int32 LevelIndex);

protected:
    // 폭발 범위를 감지하는 충돌 컴포넌트
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|Component")
    USphereComponent* ExplosionCollision;

    // 폭발 시 재생할 파티클
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
    UParticleSystem* ExplosionParticle;

    // 폭발 시 재생할 사운드
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item|Effects")
    USoundBase* ExplosionSound;

    // 지뢰가 발동된 후 폭발까지 걸리는 시간
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mine")
    float ExplosionDelay;

    // 폭발 데미지를 적용할 범위
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mine")
    float ExplosionRadius;

    // 플레이어에게 줄 폭발 데미지
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mine")
    int ExplosionDamage;

    // 이미 발동된 지뢰인지 확인하는 변수
    bool bHasExploded;

    // 폭발 타이머 핸들
    FTimerHandle ExplosionTimerHandle;

    // 플레이어가 지뢰와 충돌했을 때 발동
    virtual void ActivateItem(AActor* Activator) override;

    // 실제 폭발 처리 함수
    void Explode();
};
// MineItem.cpp


#include "MineItem.h"
#include "Components/SphereComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystemComponent.h"
#include "TimerManager.h"
#include "SpartaGameState.h"

// 지뢰 기본값과 폭발 범위 컴포넌트 설정
AMineItem::AMineItem()
{
    // 기본 폭발 대기 시간
    ExplosionDelay = 3.0f;

    // 기본 폭발 범위
    ExplosionRadius = 300.0f;

    // 기본 폭발 데미지
    ExplosionDamage = 30.0f;

    // 아이템 타입 설정
    ItemType = "Mine";

    // 지뢰 발동 여부 초기화
    bHasExploded = false;

    // 폭발 범위 감지용 컴포넌트 생성
    ExplosionCollision = CreateDefaultSubobject<USphereComponent>(TEXT("ExplosionCollision"));
    // 폭발 범위 설정
    ExplosionCollision->InitSphereRadius(ExplosionRadius);
    // Overlap 감지용 충돌 프로파일 설정
    ExplosionCollision->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
    // Scene 컴포넌트에 부착
    ExplosionCollision->SetupAttachment(Scene);
}

// ======================================== //

// 레벨에 따라 지뢰 난이도 조정
void AMineItem::ApplyLevelDifficulty(int32 LevelIndex)
{
    // 레벨이 오를수록 폭발 시간이 짧아짐, 최소 0.5초 유지
    ExplosionDelay = FMath::Max(0.5f, 1.5f - LevelIndex * 0.5f);

    // 레벨이 오를수록 데미지 증가
    ExplosionDamage = 30 + LevelIndex * 10;
}

// ======================================== //

// 플레이어가 지뢰에 닿았을 때 호출
void AMineItem::ActivateItem(AActor* Activator)
{
    // 이미 발동된 지뢰면 중복 실행 방지
    if (bHasExploded) return;

    // 지뢰 발동 상태로 변경
    bHasExploded = true;

    // 기본 충돌을 꺼서 중복 Overlap 방지
    if (Collision)
    {
        Collision->SetCollisionEnabled(ECollisionEnabled::NoCollision);
    }

    // 일정 시간 후 폭발 실행
    GetWorld()->GetTimerManager().SetTimer(
        ExplosionTimerHandle,
        this,
        &AMineItem::Explode,
        ExplosionDelay,
        false
    );

    // 지뢰 발동 사운드 재생
    if (PickupSound)
    {
        UGameplayStatics::PlaySoundAtLocation(
            GetWorld(),
            PickupSound,
            GetActorLocation()
        );
    }
}

// ======================================== //

// 지뢰 폭발 처리
void AMineItem::Explode()
{
    // 생성된 폭발 파티클 저장용
    UParticleSystemComponent* Particle = nullptr;

    // 폭발 파티클 생성
    if (ExplosionParticle)
    {
        Particle = UGameplayStatics::SpawnEmitterAtLocation(
            GetWorld(),
            ExplosionParticle,
            GetActorLocation(),
            GetActorRotation(),
            false
        );
    }

    // 폭발 사운드 재생
    if (ExplosionSound)
    {
        UGameplayStatics::PlaySoundAtLocation(
            GetWorld(),
            ExplosionSound,
            GetActorLocation()
        );
    }

    // 폭발 범위 안에 있는 액터 배열
    TArray<AActor*> OverlappingActors;

    // 폭발 범위에 겹쳐 있는 액터 가져오기
    ExplosionCollision->GetOverlappingActors(OverlappingActors);

    // 겹쳐 있는 액터 순회
    for (AActor* Actor : OverlappingActors)
    {
        // 플레이어 태그가 있는 액터만 데미지 적용
        if (Actor && Actor->ActorHasTag("Player"))
        {
            UGameplayStatics::ApplyDamage(
                Actor,
                ExplosionDamage,
                nullptr,
                this,
                UDamageType::StaticClass()
            );
        }
    }

    // 파티클이 생성되었을 경우
    if (Particle)
    {
        // GameState에 파티클 등록
        if (ASpartaGameState* SpartaGameState = GetWorld()->GetGameState<ASpartaGameState>())
        {
            SpartaGameState->RegisterParticle(Particle);
        }

        // 파티클 안전 참조용 Weak Pointer
        TWeakObjectPtr<UParticleSystemComponent> WeakParticle = Particle;

        // 파티클 삭제용 지역 타이머
        FTimerHandle LocalParticleTimerHandle;

        // 2초 뒤 파티클 제거
        GetWorld()->GetTimerManager().SetTimer(
            LocalParticleTimerHandle,
            FTimerDelegate::CreateLambda([WeakParticle]()
                {
                    // 파티클이 아직 유효하면 제거
                    if (WeakParticle.IsValid())
                    {
                        WeakParticle->DeactivateSystem();
                        WeakParticle->DestroyComponent();
                    }
                }),
            2.0f,
            false
        );
    }

    // 폭발 후 지뢰 제거
    DestroyItem();
}


5. HealingItem - 회복량 조절

// HealingItem.h

#pragma once

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

UCLASS()
class SPARTA_API AHealingItem : public ABaseItem
{
    GENERATED_BODY()

public:
    AHealingItem();

    // 레벨에 따라 회복량 조정
    void ApplyLevelDifficulty(int32 LevelIndex);

protected:
    // 회복량
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Healing")
    int32 HealAmount;

    // 플레이어가 아이템을 획득했을 때 회복 효과 실행
    virtual void ActivateItem(AActor* Activator) override;
};
// HealingItem.cpp

#include "HealingItem.h"
#include "SpartaCharacter.h"


// 회복 아이템 기본값 설정
AHealingItem::AHealingItem()
{
    HealAmount = 20.0f;
    ItemType = "Healing";
}

// ======================================== //

// 레벨에 따라 회복량 조정
void AHealingItem::ApplyLevelDifficulty(int32 LevelIndex)
{
    // 레벨이 올라갈수록 회복량 감소, 최소 10 유지
    HealAmount = FMath::Max(10, 30 - LevelIndex * 5);
}

// ======================================== //

// 플레이어가 회복 아이템을 획득했을 때 호출
void AHealingItem::ActivateItem(AActor* Activator)
{
    // 부모 클래스의 공통 효과 실행
    // 파티클, 사운드 재생 및 아이템 제거 처리
    Super::ActivateItem(Activator);

    // 획득한 액터가 플레이어인지 확인
    if (Activator && Activator->ActorHasTag("Player"))
    {
        // 플레이어 캐릭터로 캐스팅
        if (ASpartaCharacter* PlayerCharacter = Cast<ASpartaCharacter>(Activator))
        {
            // 체력 회복 적용
            PlayerCharacter->AddHealth(HealAmount);
        }

        // 아이템 제거
        DestroyItem();
    }
}


6. PlayerController - 메뉴/HUD 위젯 전환

// SpartaPlayerController.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "SpartaPlayerController.generated.h"

class UInputMappingContext; // IMC 관련 전방 선언
class UInputAction; // IA 관련 전방 선언
class USpartaHUDWidget; // HUDWidget 클래스 전방 선언
class USpartaMainMenuWidget; // 메인메뉴Widget 클래스 전방 선언

UCLASS()
class SPARTA_API ASpartaPlayerController : public APlayerController
{
    GENERATED_BODY()

public:
    ASpartaPlayerController();

    // 에디터 입력 매핑 컨텍스트
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
    UInputMappingContext* InputMappingContext;

    // 이동 입력 액션
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
    UInputAction* MoveAction;

    // 점프 입력 액션
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
    UInputAction* JumpAction;

    // 시점 회전 입력 액션
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
    UInputAction* LookAction;

    // 달리기 입력 액션
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
    UInputAction* SprintAction;

    // HUD 위젯 블루프린트 클래스
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HUD")
    TSubclassOf<USpartaHUDWidget> HUDWidgetClass;

    // 실제 생성된 HUD 위젯 인스턴스
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "HUD")
    USpartaHUDWidget* HUDWidgetInstance;

    // HUD 위젯 반환
    UFUNCTION(BlueprintPure, Category = "HUD")
    USpartaHUDWidget* GetHUDWidget() const;

    // 메인 메뉴 위젯 블루프린트 클래스
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Menu")
    TSubclassOf<USpartaMainMenuWidget> MainMenuWidgetClass;

    // 실제 생성된 메인 메뉴 위젯 인스턴스
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Menu")
    USpartaMainMenuWidget* MainMenuWidgetInstance;

    // 게임 HUD 표시
    UFUNCTION(BlueprintCallable, Category = "Menu")
    void ShowGameHUD();

    // 메인 메뉴 또는 게임오버 메뉴 표시
    UFUNCTION(BlueprintCallable, Category = "Menu")
    void ShowMainMenu(bool bIsRestart);

    // 게임 시작 또는 재시작
    UFUNCTION(BlueprintCallable, Category = "Menu")
    void StartGame();

    // 메인 메뉴 맵으로 이동
    UFUNCTION(BlueprintCallable, Category = "Menu")
    void ReturnToMainMenu();

    // 게임 종료
    UFUNCTION(BlueprintCallable, Category = "Menu")
    void QuitGame();

protected:
    // 게임 시작 시 입력 매핑 및 메뉴 표시 처리
    virtual void BeginPlay() override;
};
// SpartaPlayerController.cpp

#include "SpartaPlayerController.h"
#include "EnhancedInputSubsystems.h"
#include "SpartaHUDWidget.h"
#include "SpartaMainMenuWidget.h"
#include "SpartaGameState.h"
#include "Components/TextBlock.h"
#include "SpartaGameInstance.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetSystemLibrary.h"

// 포인터 변수 기본값 초기화
ASpartaPlayerController::ASpartaPlayerController()
    : InputMappingContext(nullptr),
    MoveAction(nullptr),
    JumpAction(nullptr),
    LookAction(nullptr),
    SprintAction(nullptr),
    HUDWidgetClass(nullptr),
    HUDWidgetInstance(nullptr),
    MainMenuWidgetClass(nullptr),
    MainMenuWidgetInstance(nullptr)
    // 아직 에디터에서 값이 할당되지 않아서
    // 이상한 값이 들어갈 수도 있으니 기본값을 nullptr로 초기화
{
}

// ======================================== //

// 게임 시작 시 입력 매핑과 메뉴 표시 처리
void ASpartaPlayerController::BeginPlay()
{
    Super::BeginPlay();

    // 현재 PlayerController에 연결된 Local Player 객체를 가져옴    
    if (ULocalPlayer* LocalPlayer = GetLocalPlayer())
    {
        // Local Player에서 Enhanced Input Subsystem 가져오ㅁ
        if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
            LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
        {
            // 입력 매핑 컨텍스트가 있으면 적용
            if (InputMappingContext)
            {
                // Subsystem을 통해 IMC를 활성화(입력 매핑 적용)
                // 0은 가장 높은 우선순위(Priority)
                Subsystem->AddMappingContext(InputMappingContext, 0);
            }
        }
    }

    // 현재 맵 이름 확인
    FString CurrentMapName = GetWorld()->GetMapName();

    // 메뉴 레벨이면 메인 메뉴 표시
    if (CurrentMapName.Contains("MenuLevel"))
    {
        ShowMainMenu(false);
    }
}

// ======================================== //

// 메인 메뉴 또는 게임오버 메뉴 표시
void ASpartaPlayerController::ShowMainMenu(bool bIsRestart)
{
    // 기존 HUD 제거
    if (HUDWidgetInstance)
    {
        HUDWidgetInstance->RemoveFromParent();
        HUDWidgetInstance = nullptr;
    }

    // 기존 메뉴 제거
    if (MainMenuWidgetInstance)
    {
        MainMenuWidgetInstance->RemoveFromParent();
        MainMenuWidgetInstance = nullptr;
    }

    // 메뉴 위젯 클래스가 설정되어 있으면 생성
    if (MainMenuWidgetClass)
    {
        MainMenuWidgetInstance = CreateWidget<USpartaMainMenuWidget>(this, MainMenuWidgetClass);
        if (MainMenuWidgetInstance)
        {
            // 메뉴를 화면에 추가
            MainMenuWidgetInstance->AddToViewport();

            // 마우스 커서 표시 및 UI 입력 모드 설정
            bShowMouseCursor = true;
            SetInputMode(FInputModeUIOnly());

            // 총점수 기본값
            int32 TotalScore = 0;

            // GameInstance에서 총점 가져오기
            if (USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(UGameplayStatics::GetGameInstance(this)))
            {
                TotalScore = SpartaGameInstance->TotalScore;
            }

            // 메뉴 상태 설정
            MainMenuWidgetInstance->SetupMenu(bIsRestart, TotalScore);
        }
    }
}

// ======================================== //

// 게임 HUD 표시
void ASpartaPlayerController::ShowGameHUD()
{
    // 기존 HUD 제거
    if (HUDWidgetInstance)
    {
        HUDWidgetInstance->RemoveFromParent();
        HUDWidgetInstance = nullptr;
    }

    // 기존 메뉴 제거
    if (MainMenuWidgetInstance)
    {
        MainMenuWidgetInstance->RemoveFromParent();
        MainMenuWidgetInstance = nullptr;
    }

    // HUD 위젯 클래스가 설정되어 있으면 생성
    if (HUDWidgetClass)
    {
        HUDWidgetInstance = CreateWidget<USpartaHUDWidget>(this, HUDWidgetClass);
        if (HUDWidgetInstance)
        {
            // HUD를 화면에 추가
            HUDWidgetInstance->AddToViewport();

            // 마우스 커서 숨김 및 게임 입력 모드 설정
            bShowMouseCursor = false;
            SetInputMode(FInputModeGameOnly());
        }

        // HUD 생성 직후 한 번 갱신
        ASpartaGameState* SpartaGameState = GetWorld() ? GetWorld()->GetGameState<ASpartaGameState>() : nullptr;
        if (SpartaGameState)
        {
            SpartaGameState->UpdateHUD();
        }
    }
}

// ======================================== //

// 게임 시작 또는 재시작
void ASpartaPlayerController::StartGame()
{
    // GameInstance 점수와 레벨 인덱스 초기화
    if (USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(UGameplayStatics::GetGameInstance(this)))
    {
        SpartaGameInstance->CurrentLevelIndex = 0;
        SpartaGameInstance->TotalScore = 0;
    }

    // 첫 번째 레벨로 이동
    UGameplayStatics::OpenLevel(GetWorld(), FName("Level1"));

    // 일시정지 해제
    SetPause(false);
}

// ======================================== //

// HUD 위젯 인스턴스 반환
USpartaHUDWidget* ASpartaPlayerController::GetHUDWidget() const
{
    return HUDWidgetInstance;
}

// ======================================== //

// 메인 메뉴 맵으로 이동
void ASpartaPlayerController::ReturnToMainMenu()
{
    UGameplayStatics::OpenLevel(GetWorld(), FName("MenuLevel"));
}

// ======================================== //

// 게임 종료
void ASpartaPlayerController::QuitGame()
{
    UKismetSystemLibrary::QuitGame(this, this, EQuitPreference::Quit, false);
}


7. HUDWidget / MainMenuWidget - UI 관리

// SpartaHUDWidget.h

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "SpartaHUDWidget.generated.h"

class UTextBlock;   // HUD에 표시할 텍스트 위젯 전방 선언
class UProgressBar; // 체력바 표시용 ProgressBar 전방 선언

UCLASS()
class SPARTA_API USpartaHUDWidget : public UUserWidget
{
	GENERATED_BODY()

public:
    // GameState에서 전달받은 값으로 HUD를 갱신
    void UpdateHUD(
        float RemainingTime,
        int32 TotalScore,
        int32 LevelIndex,
        int32 WaveIndex,
        float Health,
        float MaxHealth
    );

protected:
    // 남은 시간 표시 텍스트
    UPROPERTY(meta = (BindWidget))
    UTextBlock* TimeValue;

    // 점수 표시 텍스트
    UPROPERTY(meta = (BindWidget))
    UTextBlock* ScoreValue;

    // 현재 레벨 표시 텍스트
    UPROPERTY(meta = (BindWidget))
    UTextBlock* Level;

    // 현재 웨이브 표시 텍스트
    UPROPERTY(meta = (BindWidget))
    UTextBlock* Wave;

    // 체력 수치 표시 텍스트
    UPROPERTY(meta = (BindWidget))
    UTextBlock* HealthValue;

    // 체력 비율 표시 바
    UPROPERTY(meta = (BindWidget))
    UProgressBar* HealthBar;
};
// SpartaHUDWidget.cpp

#include "SpartaHUDWidget.h"
#include "Components/TextBlock.h"
#include "Components/ProgressBar.h"

// HUD에 표시할 값들을 갱신하는 함수
void USpartaHUDWidget::UpdateHUD(
    float RemainingTime,
    int32 TotalScore,
    int32 LevelIndex,
    int32 WaveIndex,
    float Health,
    float MaxHealth
)
{
    // 남은 시간 텍스트 갱신
    if (TimeValue)
    {
        TimeValue->SetText(FText::FromString(FString::Printf(TEXT("Time: %.1f"), RemainingTime)));
    }

    // 점수 텍스트 갱신
    if (ScoreValue)
    {
        ScoreValue->SetText(FText::FromString(FString::Printf(TEXT("Score: %d"), TotalScore)));
    }

    // 레벨 텍스트 갱신
    // LevelIndex는 0부터 시작하므로 화면에는 +1해서 표시
    if (Level)
    {
        Level->SetText(FText::FromString(FString::Printf(TEXT("Level: %d"), LevelIndex + 1)));
    }

    // 웨이브 텍스트 갱신
    if (Wave)
    {
        Wave->SetText(FText::FromString(FString::Printf(TEXT("Wave: %d"), WaveIndex)));
    }

    // 체력 수치 텍스트 갱신
    if (HealthValue)
    {
        HealthValue->SetText(FText::FromString(FString::Printf(TEXT("HP: %.0f / %.0f"), Health, MaxHealth)));
    }

    // 체력바 비율 갱신
    if (HealthBar)
    {
        // 최대 체력이 0보다 클 때만 비율 계산
        const float HealthPercent = MaxHealth > 0.0f ? Health / MaxHealth : 0.0f;

        // ProgressBar는 0.0 ~ 1.0 비율로 표시
        HealthBar->SetPercent(HealthPercent);
    }
}
// SpartaMainMenuWidget.h

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "SpartaMainMenuWidget.generated.h"

class UTextBlock;
class UButton;

UCLASS()
class SPARTA_API USpartaMainMenuWidget : public UUserWidget
{
    GENERATED_BODY()

public:
    void SetupMenu(bool bIsRestart, int32 TotalScore);

protected:
    UPROPERTY(BlueprintReadOnly, meta = (BindWidget), Category = "Menu")
    UTextBlock* StartButtonText;

    UPROPERTY(BlueprintReadOnly, meta = (BindWidget), Category = "Menu")
    UTextBlock* GameOverText;

    UPROPERTY(BlueprintReadOnly, meta = (BindWidget), Category = "Menu")
    UTextBlock* TotalScoreText;

    UPROPERTY(BlueprintReadOnly, meta = (BindWidget), Category = "Menu")
    UButton* MainMenuButton;
};
// SpartaMainMenuWidget.cpp

#include "SpartaMainMenuWidget.h"
#include "Components/TextBlock.h"
#include "Components/Button.h"

void USpartaMainMenuWidget::SetupMenu(bool bIsRestart, int32 TotalScore)
{
    if (StartButtonText)
    {
        StartButtonText->SetText(FText::FromString(bIsRestart ? TEXT("Restart") : TEXT("Start")));
    }

    if (GameOverText)
    {
        GameOverText->SetVisibility(bIsRestart ? ESlateVisibility::Visible : ESlateVisibility::Hidden);
    }

    if (TotalScoreText)
    {
        TotalScoreText->SetVisibility(bIsRestart ? ESlateVisibility::Visible : ESlateVisibility::Hidden);

        if (bIsRestart)
        {
            TotalScoreText->SetText(FText::FromString(
                FString::Printf(TEXT("Total Score: %d"), TotalScore)
            ));
        }
    }

    if (MainMenuButton)
    {
        MainMenuButton->SetVisibility(bIsRestart ? ESlateVisibility::Visible : ESlateVisibility::Hidden);
    }
}

 

관련글 더보기