[CH4-05] TitleLevel

김여울·2025년 9월 12일
0

내일배움캠프

목록 보기
75/114

문제

  • 레벨에는 애니메이션 시퀀스만 배치 가능
  • 게임모드 설정하면 Default Pawn Class 자동으로 생성돼서 캐릭터 시점으로 이동됨 → 게임모드, 컨트롤러를 TitleLevel 전용으로 만들기
  • CineCamera로 고정하기
  • EditText 컴파일 오류 → EditTextBox로 다시 만들기
  • 블루프린트 → 코드로 수정해서 데디 서버 & 다른 레벨에서 닉네임 사용할 수 있게 하기

작동 흐름

[BeginPlay] ─▶ Controller가 Title 위젯 생성 & 마우스 커서 보임
         └▶ CineCameraActor로 뷰 전환

[Player 입력]
  - Start 버튼 클릭 or Enter 키 입력
      └▶ Controller에 닉네임 전달
           ├─ GameInstance에 저장
           ├─ (서버 권한 있으면) ServerTravel("WaitingLevel")
           └─ (클라 단독 실행/테스트) OpenLevel("WaitingLevel")

C++ 클래스 만들기

  • GameMode: ATitleGameMode
  • PlayerController: ATitlePlayerController
  • Widget: UTitleLevelWidget (닉네임 입력/Start & Enter키)
  • GameInstance: UDCGameInstance (닉네임 보관해서 레벨 전환 후에도 유지)

GameInstance — 닉네임 저장 (레벨 이동 후에도 유지)

// DCGameInstance.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "DCGameInstance.generated.h"

/**
 *
 */
UCLASS()
class DC_API UDCGameInstance : public UGameInstance
{
	GENERATED_BODY()

public:
    // 김여울
    // 닉네임 저장/조회
    UFUNCTION(BlueprintCallable, Category = "Profile")
    void SetNickname(const FString& InNickname)
	{
        Nickname = InNickname;
	}

    UFUNCTION(BlueprintCallable, Category = "Profile")
    FString GetNickname() const
    {
        return Nickname;
    }

private:
    // 클라 로컬에 보관 (레벨 넘어가도 유지)
    UPROPERTY()
    FString Nickname;
};

블루프린트 클래스로 만들고
Project Settings → Maps & Modes → Game Instance Class 를 BP_DCGameInstance로 지정하기

GameMode — 타이틀 레벨 규칙

큰 로직 없으니까 GameModeBase로 만들기

TitleGameMode.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "TitleGameMode.generated.h"

/**
 * TitleLevel의 GameModeBase
 * 김여울
 */
UCLASS()
class DC_API ATitleGameMode : public AGameModeBase
{
	GENERATED_BODY()

public:
    ATitleGameMode();

protected:
    virtual void BeginPlay() override;

};

TitleGameMode.cpp

#include "TitleLevelGameMode.h"

ATitleLevelGameMode::ATitleLevelGameMode()
{
    // 필요 시 DefaultPawnClass = nullptr; 등으로 입력 막기 가능
}

void ATitleLevelGameMode::BeginPlay()
{
    Super::BeginPlay();
    // 규칙/세팅이 있으면 여기에. (현재는 컨트롤러에서 UI 처리)
}

PlayerController — 위젯/카메라/입력 모드 & WaitingLevel 전환

TitlePlayerController.h

#pragma once

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

/**
 * TitleLevel의 PlayerController
 * 김여울
 */
UCLASS()
class DC_API ATitlePlayerController : public APlayerController
{
	GENERATED_BODY()

public:
    ATitlePlayerController();

protected:
    virtual void BeginPlay() override;

public:
    // 위젯에서 호출 : 닉네임으로 대기실 입장 요청
    UFUNCTION(BlueprintCallable, Category = "Flow")
    void RequestEnterWaitingLevel(const FString& InNickname);

protected:
    // 서버에서 맵 이동 수행(전용 서버/리스닝 서버 대응)
    UFUNCTION(Server, Reliable)
    void ServerGoToWaitingLevel(const FString& InNickname);
    void ServerGoToWaitingLevel_Implementation(const FString& InNickname);

    // 테스트/싱글 실행 대비: 클라에서 로컬로 열어줌
    void ClientOpenWaitingLevelLocal();

    // UI 스폰
    void ShowTitleWidget();

private:
    // 위젯 블루프린트 할당용
    UPROPERTY(EditDefaultsOnly, Category = "UI")
    TSubclassOf<class UUserWidget> TitleWidgetClass;

    // 생성된 위젯 참조
    UPROPERTY()
    class UTitleLevelWidget* TitleWidgetRef;

};

TitlePlayerController.cpp

#include "Player/TitlePlayerController.h"
#include "Blueprint/UserWidget.h"
#include "UI/TitleLevelWidget.h"
#include "Kismet/GameplayStatics.h"
#include "CineCameraActor.h"
#include "Game/DCGameInstance.h"
#include "Game/DCGameInstance.h"

ATitlePlayerController::ATitlePlayerController()
{
    static ConstructorHelpers::FClassFinder<UUserWidget> WBP(TEXT("/Game/DC/UI/OutGame/WBP_TitleLevel.WBP_TitleLevel_C"));
    if (WBP.Succeeded())
    {
        TitleWidgetClass = WBP.Class;
    }
    bShowMouseCursor = true;
}

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

    // 타이틀 UI 표시
    ShowTitleWidget();

    // 시네 카메라로 뷰 전환 (레벨에 1개 배치)
    TArray<AActor*> Cameras;
    UGameplayStatics::GetAllActorsOfClass(this, ACineCameraActor::StaticClass(), Cameras);
    if (Cameras.Num() > 0)
    {
        SetViewTargetWithBlend(Cameras[0], 0.f);
    }

    // 입력모드 : Game & UI
    FInputModeGameAndUI InputMode;
    InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
    SetInputMode(InputMode);
}

void ATitlePlayerController::ShowTitleWidget()
{
    if (!TitleWidgetClass)
    {
        UE_LOG(LogTemp, Warning, TEXT("TitleWidgetClass 미지정: 에디터에서 ATitlePlayerController의 TitleWidgetClass 설정"));
        return;
    }

    if (!TitleWidgetRef)
    {
        UUserWidget* Created = CreateWidget<UUserWidget>(this, TitleWidgetClass);
        TitleWidgetRef = Cast<UTitleLevelWidget>(Created);
        if (TitleWidgetRef)
        {
            TitleWidgetRef->AddToViewport();
            UE_LOG(LogTemp, Log, TEXT("Title Widget Added To Viewport"));
        }
    }
}

void ATitlePlayerController::RequestEnterWaitingLevel(const FString& InNickname)
{
    UE_LOG(LogTemp, Warning, TEXT("▶ RequestEnterWaitingLevel called: %s"), *InNickname);
    // 닉네임 저장 (레벨 넘어가도 유지)
    if (UDCGameInstance* GI = GetGameInstance<UDCGameInstance>())
    {
        GI->SetNickname(InNickname);
    }

    // 전용/리스닝 서버라면 서버가 맵 이동
    if (HasAuthority())
    {
        UE_LOG(LogTemp, Warning, TEXT("▶ Server travel by HasAuthority"));
        ServerGoToWaitingLevel(InNickname); // 서버 자신 호출 (Authority)
        return;
    }

    // 클라에서 서버에서 RPC 요청
    ServerGoToWaitingLevel(InNickname);

    // 전용 서버에 아직 연결 안 된 로컬 테스트 환경 대비
    // 에디터 플레이(클라 단독)에서는 로컬 OpenLevel로도 열어줌
    if (!IsNetMode(NM_DedicatedServer))
    {
        UE_LOG(LogTemp, Warning, TEXT("▶ Local ClientOpenWaitingLevel"));
        ClientOpenWaitingLevelLocal();
    }
}

void ATitlePlayerController::ServerGoToWaitingLevel_Implementation(const FString& InNickname)
{
    UE_LOG(LogTemp, Warning, TEXT("▶ ServerGoToWaitingLevel_Implementation: %s"), *InNickname);
    // 서버에서만 실행되도록 보장
    if (!HasAuthority())
    {
        return; // 클라에서는 실행 안 함
    }

    UWorld* World = GetWorld();
    if (!World)
    {
        return;
    }

    // WaitingLevel로 맵 이동
    // Dedicated Server일 경우: 클라이언트는 이미 IP:Port로 접속해 있으므로
    // ServerTravel만 호출하면 접속 클라들이 자동으로 맵 이동
    World->ServerTravel(TEXT("/Game/DC/Maps/WaitingLevel.WaitingLevel"), true);
}

void ATitlePlayerController::ClientOpenWaitingLevelLocal()
{
    // 싱글/PIE 단독 실행시 편의용
    UGameplayStatics::OpenLevel(this, FName("WaitingLevel"));
}

Build.cs 파일에 “CinematicCamera”추가

Widget — 닉네임 입력 & Start/Enter 키 바인딩

TitleLevelWidget.h

#pragma once

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

/**
 * 닉네임 입력 & Start/Enter 키 바인딩
 * 김여울
 */
UCLASS()
class DC_API UTitleLevelWidget : public UUserWidget
{
	GENERATED_BODY()

public:
    virtual void NativeConstruct() override;

    // 위젯 구성 요소
    UPROPERTY(meta=(BindWidget), BlueprintReadOnly, meta=(AllowPrivateAccess="true"))
    UEditableTextBox* EditableTextBox_Nickname;

    UPROPERTY(meta=(BindWidget), BlueprintReadOnly, meta=(AllowPrivateAccess="true"))
    UButton* StartButton;

protected:
    // 버튼 클릭
    UFUNCTION()
    void OnStartClicked();

    // Enter Key - 텍스트 커밋
    UFUNCTION()
    void OnNameCommitted(const FText& Text, ETextCommit::Type CommitType);

    // 컨트롤러 헬퍼
    class ATitlePlayerController* GetTitlePC() const;
};

TitleLevelWidget.cpp

#include "UI/TitleLevelWidget.h"
#include "Components/EditableTextBox.h"
#include "Components/Button.h"
#include "Player/TitlePlayerController.h"

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

    // 버튼 클릭 바인딩
    if (StartButton)
    {
        StartButton->OnClicked.AddDynamic(this, &UTitleLevelWidget::OnStartClicked);
    }

    // Enter Key (텍스트 커밋) 바인딩
    if (EditableTextBox_Nickname)
    {
        // 입력 전에는 비워두고
        EditableTextBox_Nickname->SetText(FText::GetEmpty());
        EditableTextBox_Nickname->OnTextCommitted.AddDynamic(this, &UTitleLevelWidget::OnNameCommitted);
        EditableTextBox_Nickname->SetKeyboardFocus();
    }
}

void UTitleLevelWidget::OnStartClicked()
{
    FString Nickname = EditableTextBox_Nickname
        ? EditableTextBox_Nickname->GetText().ToString()
        : TEXT("");

    FString FinalName = Nickname.IsEmpty() ? TEXT("Player") : Nickname;

    if (ATitlePlayerController* PC = GetTitlePC())
    {
        UE_LOG(LogTemp, Warning, TEXT("▶ RequestEnterWaitingLevel with %s"), *Nickname);
        PC->RequestEnterWaitingLevel(Nickname);
    }
}

void UTitleLevelWidget::OnNameCommitted(const FText& Text, ETextCommit::Type CommitType)
{
    if (CommitType == ETextCommit::OnEnter)
    {
        OnStartClicked();   // 엔터 == 시작
    }
}

class ATitlePlayerController* UTitleLevelWidget::GetTitlePC() const
{
    return GetOwningPlayer<ATitlePlayerController>();
}

이후 WaitingLevel에서는 최소인원/Ready버튼 → 모두 Ready 시 게임 시작으로 이어가기

0개의 댓글