본 캠프 TIL

26.07.08 TIL / 팀플 슈터 게임 6일차

jh009 2026. 7. 8. 22:23

오늘

오전 작업: GameMode 데이터 관리, UI 데이터 동기화 로직 최적화 코드 짜기

  • 델리게이트(Delegate): 공지사항 게시판
  • 바인딩(Bind): 게시판 구독
  • 브로드캐스트(Broadcast): 게시판에 알림을 보내기

작성 순서

  • 체력
  • 탄약
  • 킬 카운트
  • 미션
  • 게임클리어
  • 게임오버

GameMode.h 추가사항

위젯 클래스 사용을 위함

#include "Blueprint/UserWidget.h”

델리게이트 추가

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChanged, float, NewHealthPercent);

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnAmmoChanged, int32, CurrentAmmo, int32, MaxAmmo);

public:

virtual void BeginPlay() override; [추가 사항]

 

UPROPERTY (EditDefaultsOnly, Category = "UI”)

TSubclassOf<UUserWidget>

StartWidgetClass; / HUDWidgetClass; / ResultWidgetClass; 3개 추가

[3개 추가 후 밑에 추가] [현재 화면에 있는 위젯]

UPROPERTY() UUserWidget* CurrentWidget;

 

UI 전환 함수

void ChangeWidget(TSubclassOf<UUserWidget> NewWidgetClass);

 

[UI 업데이트 이벤트 바인딩]

UPROPERTY(BlueprintAssignable, Category = "Game Events")

  • FOnHealthChanged OnHealthChanged;
  • FOnAmmoChanged OnAmmoChanged;
  • FOnKillCountChanged OnKillCountChanged;
  • FOnMissionUpdated OnMissionUpdated;
  • FOnGameEnded OnGameEnded;

[게임 데이터, 상태 관리]

UPROPERTY(BlueprintReadOnly, Category = "Game Data")

  • float Health;
  • int32 CurrentAmmo;
  • int32 MaxAmmo;
  • float CurrentTime;
  • int32 KillCount;
  • int32 TargetScore;

Category = "Game State")

  • bool bIsGameClear;
  • bool bIsGameOver;

UFUNCTION(BlueprintCallable, Category = "Game Logic") [추가]

void StartGame(); [StartGame만 추가하기

 


J_GameMode.cpp 추가사항

BeginPlay

 

[게임 시작 시 시작 화면 위젯을 띄움]
StartGame(); [삭제]

 

ChangeWidget(StartWidgetClass); [추가]

 

void AJ_GameMode::ChangeWidget [추가]

void AJ_GameMode::ChangeWidget(TSubclassOf<UUserWidget> NewWidgetClass)
{
    if (CurrentWidget) 
    {
        CurrentWidget->RemoveFromParent();
    }
    
    if (NewWidgetClass) 
    {
        CurrentWidget = CreateWidget<UUserWidget>(GetWorld(), NewWidgetClass);
        if (CurrentWidget) 
        {
            CurrentWidget->AddToViewport();
        }
    }
}

StartGame()

Health = 1.0f;

CurrentAmmo = 10;

MaxAmmo = 20;
CurrentTime = 60.0f;

KillCount = 0;

bIsGameClear = false;

bIsGameOver = false;

 

[데이터 초기화 시 UI 동기화]

OnHealthChanged.Broadcast(Health);
OnAmmoChanged.Broadcast(CurrentAmmo, MaxAmmo);
OnKillCountChanged.Broadcast(KillCount);

 

[게임 HUD 로드]

ChangeWidget(HUDWidgetClass);


EndGame()

GetWorldTimerManager().ClearTimer(GameTimerHandle);

ChangeWidget(ResultWidgetClass); [게임 HUD 로드]

OnGameEnded.Broadcast(bIsGameClear);

 

코드 순서

RestartGame()

AddKill()

CheckVictoryCondition()

CheckDefeatCondition()

HandleTimer()


J_UserWidget.h

UPROPERTY(meta = (BindWidget)) UTextBlock* ResultText; [삭제]

UFUNCTION() void ShowResultUI(bool bIsGameClear); [삭제]


J_UserWidget.cpp

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

    // 초기값 설정
    UpdateKillCountUI(0);
    if (HealthBar) HealthBar->SetPercent(1.0f);
    if (AmmoText) AmmoText->SetText(FText::FromString(TEXT("Ammo: 0")));

    AJ_GameMode* GM = Cast<AJ_GameMode>(UGameplayStatics::GetGameMode(GetWorld()));
    if (GM)
    {
        // [수정] 결과 관련 바인딩 제거 및 체력/탄약 이벤트 바인딩 추가
        GM->OnKillCountChanged.AddDynamic(this, &UJ_UserWidget::UpdateKillCountUI);
        GM->OnMissionUpdated.AddDynamic(this, &UJ_UserWidget::UpdateMissionUI);
        
        // [추가] 델리게이트 바인딩
        GM->OnHealthChanged.AddDynamic(this, &UJ_UserWidget::UpdateHealthUI);
        GM->OnAmmoChanged.AddDynamic(this, &UJ_UserWidget::UpdateAmmoUI);
    }
}
// [전체 삭제]
void UJ_UserWidget::ShowResultUI(bool bIsGameClear)
{
    if (ResultText)
    {
        ResultText->SetVisibility(ESlateVisibility::Visible);
        ResultText->SetText(bIsGameClear
            ? FText::FromString(TEXT("MISSION CLEAR!"))
            : FText::FromString(TEXT("..GAME OVER..")));
    }
}

 오후 작업: 코드 수정 및 테스트 진행


현재는 UI 로직 분리 작업 중..

 

J_MainMenuWidget

// J_StartWidget.h

#pragma once

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

UCLASS()
class CH3_TEAM6_PROJECT_API UJ_StartWidget : public UUserWidget
{
    GENERATED_BODY()

public:
    // 블루프린트 버튼에서 호출할 함수 (OnStartClicked)
    UFUNCTION(BlueprintCallable, Category = "UI") 
    void OnStartClicked();
};
// J_StartWidget.cpp

#include "J_StartWidget.h"
#include "J_GameMode.h"
#include "Kismet/GameplayStatics.h"

void UJ_StartWidget::OnStartClicked()
{
    // 게임 모드를 가져와서 StartGame() 호출
    AJ_GameMode* GM = Cast<AJ_GameMode>(UGameplayStatics::GetGameMode(GetWorld()));
    if (GM)
    {
        GM->StartGame();
    }
}

 

J_ResultWidget [결과 화면]

// J_ResultWidget.h

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/TextBlock.h"
#include "J_ResultWidget.generated.h"

UCLASS()
class CH3_TEAM6_PROJECT_API UJ_ResultWidget : public UUserWidget
{
    GENERATED_BODY()

protected:
    // 블루프린트에서 만든 텍스트 컴포넌트와 바인딩할 변수
    UPROPERTY(meta = (BindWidget)) UTextBlock* ResultTitle;

public:
    // 결과 텍스트를 설정하는 함수
    UFUNCTION(BlueprintCallable, Category = "UI") 
    void SetResultText(bool bIsGameClear);

    // 재시작 버튼 클릭 시 호출할 함수
    UFUNCTION(BlueprintCallable, Category = "UI") 
    void OnRestartClicked();
};
// J_ResultWidget.cpp

#include "J_ResultWidget.h"
#include "J_GameMode.h"
#include "Kismet/GameplayStatics.h"

void UJ_ResultWidget::SetResultText(bool bIsGameClear)
{
    if (ResultTitle)
    {
        ResultTitle->SetText(bIsGameClear 
            ? FText::FromString(TEXT("MISSION CLEAR!")) 
            : FText::FromString(TEXT("..GAME OVER..")));
    }
}

void UJ_ResultWidget::OnRestartClicked()
{
    AJ_GameMode* GM = Cast<AJ_GameMode>(UGameplayStatics::GetGameMode(GetWorld()));
    if (GM)
    {
        GM->RestartGame();
    }
}

 

WBP_MainMenu 생성 → 부모 클래스 J_MainMenuWidget설정

OnClicked 이벤트에 위에서 만든 OnStartClicked 함수 연결

 

WBP_Result 생성 → 부모 클래스 J_ResultWidget설정

텍스트 블록의 이름 ResultTitle

 

Event Construct →GameMode를 캐스팅

bIsGameClear 값을 가져오기, SetResultText를 호출하도록 노드 구성

 

재시작 버튼의 OnClicked에 OnRestartClicked 함수 연결

 

BP_GameMode 설정

StartWidgetClass →WBP_StartScreen, ResultWidgetClass

WBP_Result할당

 

현재 코드까지 다 짜놓은 상태인데 에디터 UI가 말썽이다. 계속 UI가 동시에 뜨기 때문에 문제 해결 필요함


회고

팀플의 중요성은 서로에 대한 배려와 신뢰, 각자의 맡은 바 책임을 다하는 데 있다는 것을 이번 기회에 다시금 깨달았다.

혼자서 많은 부분을 소화하며 언리얼 엔진의 전체적인 파이프라인을 깊게 들여다볼 수 있는 기회가 되었지만,

한편으로는 팀원들과 더 적극적으로 소통하여 프로젝트의 균형을 맞추는 노력이 얼마나 중요한지 배울 수 있었다.

 

특히, 특정 업무 공백이 발생했을 때,

프로젝트가 유기적으로 굴러갈 수 있게 의존성재조정하며 전체 파이프라인을 운영하는 법을 익혔다.

 

이번 경험을 통해 기술적인 구현뿐만 아니라,

팀 프로젝트에서의 리스크 관리소통 방식이 결과물의 퀄리티를 결정짓는 요소임을 체감했다.

 

24일이라는 데드라인이 설정된 상황에서 팀 전체의 업무 공백을 채우며, 프로젝트를 완수해야 한다는 압박감을 경험했다.