[CH3-05] GameOverWidget 구현, 델리게이트

김여울·2025년 8월 12일
0

내일배움캠프

목록 보기
58/114

원래 계획은

  • 캐릭터가 죽으면 자동으로 GameOver 화면이 뜨고
  • 마우스 커서 활성화 + 게임 일시정지
  • 버튼 클릭 시 MainMenuLevel로 복귀

캐릭터의 사망처리가 캐릭터와 플레이어 컨트롤러(게임오버위젯 띄우는 부분) 사이에서만 델리게이트로 연결하면 될 거라 생각하고 구현했는데 아니어서 수정했다...

게임모드가 게임의 규칙/흐름을 총괄하는 "심판" 이기 때문에 캐릭터 → 게임모드 → 컨트롤러 순으로 가야한다.

캐릭터 → 게임모드 → 컨트롤러

게임모드의 역할

  • 게임의 승패 조건, 스코어, 진행 상태를 관리
  • 서버 권한(Authority)만 가지는 경우가 많아서 멀티플레이에서도 신뢰 가능한 판정 가능
  • “플레이어 한 명 죽음 → 게임오버” 같은 전체 로직을 결정하는 곳

캐릭터 → 게임모드 연결 이유

  • 캐릭터는 “나는 죽었어”라는 사실만 알면 됨
  • 이후에 이게 게임오버인지, 부활인지, 팀 리스폰인지는 캐릭터가 아닌 게임모드가 판단해야 함
  • 캐릭터에서 직접 컨트롤러에 알려버리면 게임 규칙을 우회하게 돼서
    재사용성↓, 유지보수 어려움↑

컨트롤러가 덜 관여하는 이유

  • 컨트롤러는 플레이어 입력 처리 + HUD 표시 중심
  • 게임 진행 규칙을 결정하는 권한이 없음 (특히 서버에서)
  • 죽었을 때 HUD를 띄우는 건 컨트롤러가 하지만, “언제 띄울지”는 게임모드가 알려줘야 함

🧩 메뉴 구성

GameOver 조건

1️⃣ 플레이어 사망
→ GameMode가 델리게이트 수신
→ GameOver 위젯 표시

2️⃣ 스테이지 클리어 조건 달성
→ “출구 게이트” 활성화 + 제한시간 타이머 시작
→ 제한시간 내 게이트 통과하면 다음 스테이지 로드
⚠️못 들어가면 GameOver

1️⃣번 조건 만족 시

[APppCharacter::OnDeath()] - Player 죽음
       ↓ (Delegate)
[APPPGameMode::OnGameOver()]
       ↓
[APppPlayerController::ShowGameOver()]
       ↓
[GameOverWidget 생성 + 화면 표시]
       ↓
[Return 버튼 클릭 → MainMenuLevel 로드]

2️⃣번 조건 만족시

[스테이지 완료]
       ↓
[GameMode::OpenGateAndStartTimer(T)]
       ↓ (T초 내)
[StageGate Overlap]
       ↓
[EnterNextStage]

----------------------------------

⚠️ (시간초과)
       ↓
[GameMode::OnExitTimeOver()]
       ↓
[ShowGameOver]

GameOverWidget C++ 클래스 만들기

GameOverWidget.h

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/Button.h"
#include "GameOverWidget.generated.h"

/**
 *
 */

class APppCharacter;

UCLASS()
class PPP_API UGameOverWidget : public UUserWidget
{
	GENERATED_BODY()

protected:
    virtual void NativeConstruct() override;
    virtual void NativeDestruct() override;

    UFUNCTION()
    void OnReturnToMainMenuClicked();

public:
    UPROPERTY(meta = (BindWidget))
    UButton* Return_BTN;

    UFUNCTION()
    void HandlePlayerDeath();

private:
    APppCharacter* CachedCharacter;

};

GameOverWidget.cpp

#include "GameOverWidget.h"
#include "Components/Button.h"
#include "../Characters/PppCharacter.h"
#include "Kismet/GameplayStatics.h"

void UGameOverWidget::NativeConstruct()
{
    Super::NativeConstruct();

    if (Return_BTN)
    {
        Return_BTN->OnClicked.AddDynamic(this, &UGameOverWidget::OnReturnToMainMenuClicked);
    }
}

void UGameOverWidget::NativeDestruct
(
)
{
    if (Return_BTN)
    {
        Return_BTN->OnClicked.RemoveDynamic(this, &UGameOverWidget::OnReturnToMainMenuClicked);
    }
    Super::NativeDestruct();
}

void UGameOverWidget::OnReturnToMainMenuClicked()
{
    // MainMenuLevel로 돌아가기
    UGameplayStatics::OpenLevel(this, FName("MainMenuLevel"));
    UE_LOG(LogTemp, Log, TEXT("Return to Main Menu Clicked"));
}

void UGameOverWidget::HandlePlayerDeath()
{
    UE_LOG(LogTemp, Warning, TEXT("GameOverWidget detected: Player is Dead."));
}

C++ 클래스 만든 후
블루프린트의 Class Default에 들어가서 부모 클래스 연결하기

PlayerController 연결하기

PlayerController.h

#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "Components/Button.h"
#include "InputMappingContext.h"
#include "Blueprint/UserWidget.h"
#include "InputAction.h"
#include "PppPlayerController.generated.h"

class UInputMappingContext;
class UInputAction;
class UUserWidget;
class USoundBase;

UCLASS()
class PPP_API APppPlayerController : public APlayerController
{
    GENERATED_BODY()
public:
    APppPlayerController();
    
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI")
    TSubclassOf<UUserWidget> GameOverWidgetClass;
    
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Transient, Category = "UI")
    UUserWidget* GameOverWidgetInstance;
    
     // ====== 사운드 ======
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Audio")
    
    void ShowGameOver();
    UFUNCTION()
    void OnCharacterDead();
}

PlayerController.cpp

#include "PppPlayerController.h"
#include "GameFramework/HUD.h"
#include "PppCharacter.h"
#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
#include "Blueprint/UserWidget.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Components/TextBlock.h"
#include "Components/Button.h"

APppPlayerController::APppPlayerController()
    : InputMappingContext(nullptr)
    , PauseMenuIMC(nullptr)
    , PauseMenuAction(nullptr)
    , MainMenuWidgetClass(nullptr)
    , PauseMenuWidgetClass(nullptr)
    , GameOverWidgetClass(nullptr)
    , MainMenuWidgetInstance(nullptr)
    , PauseMenuWidgetInstance(nullptr)
    , GameOverWidgetInstance(nullptr)
    , QuitSound(nullptr)
{
}

void APppPlayerController::ShowGameOver()
{
    if (!GameOverWidgetInstance && GameOverWidgetClass)
    {
        GameOverWidgetInstance = CreateWidget<UUserWidget>(this, GameOverWidgetClass);
        if (GameOverWidgetInstance)
        {
            GameOverWidgetInstance->AddToViewport();
            SetInputMode(FInputModeUIOnly());
            bShowMouseCursor = true;
            SetPause(true);
        }
    }
}

// 캐릭터 사망 후 GameOverWidget 띄우기
void APppPlayerController::OnCharacterDead()
{
    ShowGameOver();
}

델리게이트

// PlayerController.h
UFUNCTION()
void OnCharacterDead();

// PlayerController.cpp
// 캐릭터 사망 후 GameOverWidget 띄우기
void APppPlayerController::OnCharacterDead()
{
    ShowGameOver();
}

캐릭터 사망 시 델리게이트 호출

// Character.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCharacterDead);

UPROPERTY(BlueprintAssignable)
FOnCharacterDead OnCharacterDead;
    
// Character.cpp
void APppCharacter::OnDeath()
{

	APPPGameState* PPPGameState = GetWorld() ? GetWorld()->GetGameState<APPPGameState>() : nullptr;
	if (PPPGameState)
	{
	    OnCharacterDead.Broadcast();
	    UE_LOG(LogTemp, Warning, TEXT("You Died!"));
	}

}

GameMode에서 델리게이트 바인딩

GameMode.h

public: 
	/** 플레이어 사망 처리 */
	UFUNCTION(BlueprintCallable)
	void OnPlayerDeath();

	UFUNCTION()
	void OnGameOver();

	// 라운드 클리어 직후 출구 제한시간 시작 (시간 내 못 들어가면 GameOver)
	UFUNCTION(BlueprintCallable, Category="Stage")
	void StartExitWindow();

	// 게이트 겹침 시 호출(트리거나 BP에서 호출)
	UFUNCTION(BlueprintCallable, Category="Gate")
	void EnterNextStage();

	// 출구 제한시간 초과 시(GameState에서 콜백)
	UFUNCTION()
	void OnExitTimeOver();

protected:
    // 출구 제한시간 기본값(클리어 후 StartExitWindow 호출 시 쓰는 값)
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Gate|Timer", meta=(ClampMin="1.0"))
    float ExitWindowSeconds = 30.f;

    // 태그로 씬의 게이트 찾기(BP 트리거나 액터에 "StageGate" 태그 부여)
    UPROPERTY(EditAnywhere, Category="Gate")
    FName ExitGateTag = TEXT("StageGate");

    // 씬의 게이트 참조(자동 캐싱)
    UPROPERTY(VisibleAnywhere, Category="Gate")
    AActor* ExitGate = nullptr;

    // 출구 오픈 상태
    UPROPERTY(VisibleAnywhere, Category="Gate")
    bool bGateOpen = false;

GameMode.cpp

void APPPGameMode::BeginPlay()
{
    Super::BeginPlay();

    // ...
    
    // 출구 게이트 캐싱 (Tag: ExitGateTag), 기본 비표시/충돌 off
    {
        TArray<AActor*> Found;
        UGameplayStatics::GetAllActorsWithTag(this, ExitGateTag, Found);
        ExitGate = Found.Num() > 0 ? Found[0] : nullptr;

        if (ExitGate)
        {
            ExitGate->SetActorHiddenInGame(true);
            ExitGate->SetActorEnableCollision(false);
        }
        else
        {
            UE_LOG(LogGame, Warning, TEXT("ExitGate(Tag=%s) not found."), *ExitGateTag.ToString());
        }
    }
}

void APPPGameMode::OnPlayerDeath()
{
    UE_LOG(LogGame, Error, TEXT("플레이어 사망"));
    SetGameState(EGameState::GameOver);
}

void APPPGameMode::OnRoundCleared()
{
    UE_LOG(LogGame, Log, TEXT("라운드 클리어 - 다음 단계 준비"));

    // 라운드 수 증가 및 점수 초기화
    if (APPPGameState* GS = GetGameState<APPPGameState>())
    {
        const int32 NewRound = GS->CurrentRound + 1;
        GS->SetCurrentRound(NewRound);
        GS->ResetScore();
    }

    // 출구 제한시간 시작 + 게이트 오픈
    StartExitWindow();

    // 기존: StartRound()를 즉시 호출
    // 변경: 자동 시작을 막고, 다음 층으로 이동 가능 신호만 브로드캐스트
    OnRoundClearedSignal.Broadcast();

    // 자동으로 올리고 싶다면 아래처럼 사용:
    // CurrentFloor = FMath::Clamp(CurrentFloor + 1, 1, MaxFloors);
    // StartRound();
}

void APPPGameMode::OnGameOver()
{
    UE_LOG(LogGame, Warning, TEXT("게임 오버"));

    if (APppPlayerController* PC = Cast<APppPlayerController>(UGameplayStatics::GetPlayerController(this, 0)))
    {
        PC->ShowGameOver();	// GameMode에서 GameOver 호출 처리
    }
}

void APPPGameMode::StartExitWindow()
{
    bGateOpen = true;

    if (ExitGate)
    {
        ExitGate->SetActorHiddenInGame(false);
        ExitGate->SetActorEnableCollision(true);
    }

    if (APPPGameState* GS = GetGameState<APPPGameState>())
    {
        GS->StartRoundTimer(StageTimerSeconds);   // 시간이 끝나면 GameState→EndRound()→HasTimedOut() 경로로 GameOver 자동
    }

    UE_LOG(LogGame, Log, TEXT("출구 제한시간 시작: %.1f초"), StageTimerSeconds);
}

void APPPGameMode::EnterNextStage()
{
    if (!bGateOpen)
    {
        return; // 아직 게이트 안열림
    }

    // 타이머 정지
    if (APPPGameState* GS = GetGameState<APPPGameState>())
    {
        GS->StopRoundTimer();
    }

    // 게이트 닫기
    bGateOpen = false;
    if (ExitGate)
    {
        ExitGate->SetActorEnableCollision(false);
        ExitGate->SetActorHiddenInGame(true);
    }

    // 다음 라운드 시작(또는 맵 로드로 교체 가능)
    StartRound();
}

void APPPGameMode::OnExitTimeOver()
{
    if (!bGateOpen)
    {
        return; // 이미 통과했을 수 있음
    }

    UE_LOG(LogGame, Warning, TEXT("출구 제한시간 초과 → GameOver"));
    bGateOpen = false;

    if (ExitGate)
    {
        ExitGate->SetActorEnableCollision(false);
        ExitGate->SetActorHiddenInGame(true);
    }

    OnGameOver();	
}

GameMode에서 GameOver 호출 처리

위에 코드에 메모


PlayerController에서 GameOver 호출

// PlayerController.h
void ShowGameOver();
    UFUNCTION()
    void OnCharacterDead();

// PlayerController.cpp
// 캐릭터 사망 후 GameOverWidget 띄우기
void APppPlayerController::OnCharacterDead()
{
    ShowGameOver();
}

💭

원래는 캐릭터의 사망처리가 캐릭터와 플레이어 컨트롤러(게임오버위젯 띄우는 부분) 사이에서만 델리게이트로 연결하면 될 거라 생각하고 구현했는데 아니어서 수정했다...

게임모드가 게임의 규칙/흐름을 총괄하는 "심판" 이기 때문에 캐릭터 → 게임모드 → 컨트롤러 순으로 가야한다.

0개의 댓글