이제 플레이어가 죽었을 때, 따로 애니메이션이 출력되면서 결산 내용을 보이게 하도록 하는 것이 목표이다.
전체 흐름은 다음과 같다.
결산 위젯이 받는 페이로드는 두 개의 USTRUCT로 분리되어 있다.
// PDRaidStats.h
USTRUCT(BlueprintType)
struct PROJECTD_API FPDRaidStats
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "PD|Raid")
int32 Kills = 0;
UPROPERTY(BlueprintReadOnly, Category = "PD|Raid")
float SurvivalSeconds = 0.f;
// 성공 시 +, 실패 시 −. 부호 없이 그대로 표시
UPROPERTY(BlueprintReadOnly, Category = "PD|Raid")
int32 GoldDelta = 0;
UPROPERTY(BlueprintReadOnly, Category = "PD|Raid")
int32 ItemDelta = 0;
};
// 결산 위젯이 받는 한 줄 단위. PlayerName + 생존 여부 + 성과를 묶음
USTRUCT(BlueprintType)
struct PROJECTD_API FPDPlayerRaidEntryData
{
GENERATED_BODY()
UPROPERTY(BlueprintReadWrite, Category = "PD|Raid")
FString PlayerName;
UPROPERTY(BlueprintReadWrite, Category = "PD|Raid")
bool bSurvived = false;
UPROPERTY(BlueprintReadWrite, Category = "PD|Raid")
FPDRaidStats Stats;
};
FPDRaidStats는 개인 성과(킬, 골드 변화, 아이템 변화)만 담고, FPDPlayerRaidEntryData는 거기에 PlayerName + 생존 여부를 덧붙여 결산 한 줄에 들어가는 단위가 된다.
사망/탈출이 어디서 발생하든 APDGameMode::EndRaid(bool) 한 함수로 모이도록 되어있다.
// PDGameMode.cpp
void APDGameMode::EndRaid(bool bSuccess)
{
// 같은 프레임에 사망/탈출이 겹쳐도 결산은 한 번만 수행
if (CurrentRaidState == ERaidState::Ended) return;
SetRaidState(ERaidState::Ended);
UPDGameInstance* GI = GetGameInstance<UPDGameInstance>();
if (APlayerController* PC = GetWorld()->GetFirstPlayerController())
{
// 탈출 성공이면 인벤토리를 스태시로 합치고, 실패면 그대로 소실
if (bSuccess)
{
TransferPlayerInventoryToStash(PC);
}
if (APawn* Pawn = PC->GetPawn())
{
// 시큐어 컨테이너는 성공/실패 무관하게 항상 보존
if (UPDSecureContainerComponent* SecureContainer =
Pawn->FindComponentByClass<UPDSecureContainerComponent>())
{
SecureContainer->SaveSecureContainer();
}
}
}
if (GI) GI->SaveToDisk();
// BP 후처리 로직. 결산 위젯 삽입에 활용
OnRaidEnded(bSuccess);
}
흐름을 정리하자면 다음과 같다.
탈출과 사망의 진입 경로는 다음과 같이 둘 다 EndRaid로 수렴된다.
// 탈출 성공
void APDGameMode::RequestExtraction(APlayerController* PC)
{
if (!PC || CurrentRaidState != ERaidState::InProgress) return;
SetRaidState(ERaidState::Extracting);
EndRaid(true);
}
// 사망
void APDGameMode::OnPlayerDied(APlayerController* PC, AActor* Killer)
{
if (!PC) return;
if (CurrentRaidState == ERaidState::Ended) return;
EndRaid(false);
}
UPDActivatableBase를 상속받는, Frontend 레이어에 띄워지는 활성 위젯이다. HUD를 덮지 않고 그 위에 겹치도록 구성했다.
// PDRaidEndTransitionWidget.cpp
void UPDRaidEndTransitionWidget::Configure(bool bInSuccess, const TArray<FPDPlayerRaidEntryData>& Entries, float RaidDurationSeconds)
{
bSuccess = bInSuccess;
// bSuccess에 따라 메인/서브 텍스트와 액센트 컬러를 교체
if (Text_Main)
{
Text_Main->SetText(bSuccess ? SuccessMainText : FailureMainText);
Text_Main->SetColorAndOpacity(FSlateColor(bSuccess ? SuccessAccentColor : FailureAccentColor));
}
if (Text_Subtitle)
{
Text_Subtitle->SetText(bSuccess ? SuccessSubtitleText : FailureSubtitleText);
}
// 레이드 지속 시간을 MM:SS 포맷으로 변환
if (Text_RaidDuration)
{
const int32 TotalSeconds = FMath::Max(0, FMath::FloorToInt(RaidDurationSeconds));
const int32 Minutes = TotalSeconds / 60;
const int32 Seconds = TotalSeconds % 60;
Text_RaidDuration->SetText(FText::FromString(FString::Printf(TEXT("%02d:%02d"), Minutes, Seconds)));
}
// Entries가 비어있으면 GameState->PlayerArray로 fallback 빌드
TArray<FPDPlayerRaidEntryData> ResolvedEntries = Entries;
if (ResolvedEntries.Num() == 0)
{
BuildFallbackEntries(ResolvedEntries);
}
if (Box_PlayerEntries)
{
Box_PlayerEntries->ClearChildren();
}
// 자식 엔트리 인스턴싱은 BP에 위임 (CreateWidget + AddChild)
PopulateEntries(ResolvedEntries, EntryStaggerInterval);
// 장식 위젯들(머티리얼 파라미터 등)에 액센트 컬러를 BP가 적용
ApplyAccent(bSuccess ? SuccessAccentColor : FailureAccentColor);
}
Configure가 하는 일은 다음과 같이 정리된다.
RaidEnd 위젯이 활성화되면 바로 클릭을 받지 않고 Raid 종료 애니메이션이 끝난 후에 기지로 넘어가는 Travel을 수행할 지에 대한 입력을 받는 로직을 구현한다.
// PDRaidEndTransitionWidget.cpp
void UPDRaidEndTransitionWidget::NativeOnActivated()
{
Super::NativeOnActivated();
bReadyForInput = false;
bInputConsumed = false;
if (Text_ContinuePrompt)
{
Text_ContinuePrompt->SetVisibility(ESlateVisibility::Hidden);
}
if (Anim_IntroSequence)
{
PlayAnimation(Anim_IntroSequence);
}
else
{
OnIntroFinished();
}
}
FReply UPDRaidEndTransitionWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
// 애니메이션 끝나기 전까지 입력 무시
if (!bReadyForInput || bInputConsumed) return FReply::Unhandled();
if (InMouseEvent.GetEffectingButton() != EKeys::LeftMouseButton) return FReply::Unhandled();
bInputConsumed = true;
if (Anim_BlackFade) PlayAnimation(Anim_BlackFade);
else HandleTravelTrigger();
return FReply::Handled();
}
void UPDRaidEndTransitionWidget::OnIntroFinished()
{
bReadyForInput = true;
if (Text_ContinuePrompt) Text_ContinuePrompt->SetVisibility(ESlateVisibility::Visible);
}
void UPDRaidEndTransitionWidget::HandleTravelTrigger()
{
// 기지(Base)로 Travel
if (APDPlayerController* PDPC = Cast<APDPlayerController>(GetOwningPlayer()))
{
PDPC->Server_RequestBaseTravel();
}
}
흐름은 다음과 같다.
결산 한 줄에 해당하는 서브 위젯이다. PlayerName/생존 여부/Kills/Gold/Items를 한 줄에 표시하고, 부모 위젯에서 전달받은 지연 시간만큼 기다린 뒤 결산 내용이 하나씩 나타나는 애니메이션인 Anim_Reveal을 재생한다.
// PDPlayerRaidEntryWidget.h
UCLASS(Abstract, BlueprintType, meta = (DisableNativeTick))
class PROJECTD_API UPDPlayerRaidEntryWidget : public UUserWidget
{
GENERATED_BODY()
public:
// 결산 데이터로 라벨을 채우고 지연 시간 후 Anim_Reveal 재생
UFUNCTION(BlueprintCallable, Category = "PD|Entry")
void Configure(const FPDPlayerRaidEntryData& Data, float StaggerStartDelay);
protected:
UPROPERTY(EditDefaultsOnly, Category = "PD|Entry")
FText SurvivedText;
UPROPERTY(EditDefaultsOnly, Category = "PD|Entry")
FText DeathText;
UPROPERTY(EditDefaultsOnly, Category = "PD|Entry")
FLinearColor SurvivedColor = FLinearColor(0.357f, 0.753f, 0.922f);
UPROPERTY(EditDefaultsOnly, Category = "PD|Entry")
FLinearColor DeathColor = FLinearColor(0.878f, 0.471f, 0.337f);
UPROPERTY(Transient, BlueprintReadOnly, meta = (BindWidgetAnimOptional))
TObjectPtr<UWidgetAnimation> Anim_Reveal;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
TObjectPtr<UTextBlock> Text_PlayerName;
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> Text_Status;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
TObjectPtr<UTextBlock> Text_KillsValue;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
TObjectPtr<UTextBlock> Text_GoldValue;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
TObjectPtr<UTextBlock> Text_ItemsValue;
private:
FTimerHandle RevealTimerHandle;
void PlayRevealNow();
};
Configure에서 데이터를 라벨에 박은 뒤 FTimerHandle로 지연 시간 이후 Anim_Reveal을 재생하는 구조다. 부모 위젯이 Index * StaggerInterval을 넘기기 때문에 엔트리가 여러 개일 경우 순차적으로 한 줄씩 등장하는 연출이 자연스럽게 나온다.
생존/사망 상태는 Text_Status의 텍스트와 컬러로 표현되는데, 밝기 변조 없이 색 자체만 바꾸는 방식이라 다른 UI 요소와 일관성을 유지하도록 만들었다.
다음에는 결산 화면에 보여줄 애니메이션과 WBP 작업글을 정리할 예정이다.