원래 계획은
캐릭터의 사망처리가 캐릭터와 플레이어 컨트롤러(게임오버위젯 띄우는 부분) 사이에서만 델리게이트로 연결하면 될 거라 생각하고 구현했는데 아니어서 수정했다...
게임모드가 게임의 규칙/흐름을 총괄하는 "심판" 이기 때문에 캐릭터 → 게임모드 → 컨트롤러 순으로 가야한다.
1️⃣ 플레이어 사망
→ GameMode가 델리게이트 수신
→ GameOver 위젯 표시
2️⃣ 스테이지 클리어 조건 달성
→ “출구 게이트” 활성화 + 제한시간 타이머 시작
→ 제한시간 내 게이트 통과하면 다음 스테이지 로드
⚠️못 들어가면 GameOver
[APppCharacter::OnDeath()] - Player 죽음
↓ (Delegate)
[APPPGameMode::OnGameOver()]
↓
[APppPlayerController::ShowGameOver()]
↓
[GameOverWidget 생성 + 화면 표시]
↓
[Return 버튼 클릭 → MainMenuLevel 로드]
[스테이지 완료]
↓
[GameMode::OpenGateAndStartTimer(T)]
↓ (T초 내)
[StageGate Overlap]
↓
[EnterNextStage]
----------------------------------
⚠️ (시간초과)
↓
[GameMode::OnExitTimeOver()]
↓
[ShowGameOver]

#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;
};
#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에 들어가서 부모 클래스 연결하기
#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();
}
#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!"));
}
}
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;
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();
}
위에 코드에 메모
// PlayerController.h
void ShowGameOver();
UFUNCTION()
void OnCharacterDead();
// PlayerController.cpp
// 캐릭터 사망 후 GameOverWidget 띄우기
void APppPlayerController::OnCharacterDead()
{
ShowGameOver();
}
원래는 캐릭터의 사망처리가 캐릭터와 플레이어 컨트롤러(게임오버위젯 띄우는 부분) 사이에서만 델리게이트로 연결하면 될 거라 생각하고 구현했는데 아니어서 수정했다...
게임모드가 게임의 규칙/흐름을 총괄하는 "심판" 이기 때문에 캐릭터 → 게임모드 → 컨트롤러 순으로 가야한다.