언리얼엔진 학습 및 과제

소울라이크 팀 프로젝트 Desecration : 시작부터 MVP까지 (4)

park94128 2026. 3. 23. 23:47

● 클래스 선택 레벨

타이틀 화면 레벨은 이정도로 하고 클래스 선택 레벨에서 기능을 구현할 차례다. 레벨 자체는 타이틀 화면의 기능 구현을 위해 이미 생성해뒀지만 구현한 것은 아무것도 없으니 그냥 빈 깡통 수준이다.

 

플레이어 클래스를 선택하는 기능이라서 들어가는 기능은 다르겠지만 여기도도 UI 외에는 출력할 것이 없기 때문에 대략적인 C++ 클래스 구조는 타이틀 레벨과 다르지 않다. 게임 인스턴스는 이미 만든 상태라서 이를 제외한 나머지를 만들면 된다.

 

● 게임 모드

UCLASS()
class DESECRATION_API AT3SelectClassGameMode : public AGameModeBase
{
	GENERATED_BODY()
	
protected:
	virtual void BeginPlay() override;
	
public:
	//튜토리얼 시작
	void TutorialStart();
	
	//타이틀 레벨로 돌아가기
	void ReturnToTitleLevel();
	
private:
	//게임 인스턴스
	UPROPERTY()
	TObjectPtr<UT3GameInstance> T3GameInstance;
};

지금 게임 모드에서는 튜토리얼 시작 기능과 타이틀로 돌아가는 기능만 있으면 된다.

 

void AT3SelectClassGameMode::BeginPlay()
{
	Super::BeginPlay();
	
	//게임 인스턴스
	T3GameInstance = Cast<UT3GameInstance>(GetGameInstance());
	if (!T3GameInstance)
	{
		UE_LOG(LogTemp, Error, TEXT("%s : T3GameInstance is NULL"), *GetNameSafe(this));
		return;
	}
}

void AT3SelectClassGameMode::TutorialStart()
{
	UGameplayStatics::OpenLevel(CurrentWorld, TEXT("T3TutorialLevel"));
}

void AT3SelectClassGameMode::ReturnToTitleLevel()
{
	UGameplayStatics::OpenLevel(CurrentWorld, TEXT("T3TitleLevel"));
}

타이틀 화면의 게임모드 처럼 시작시 게임 인스턴스를 캐싱한다. 튜토리얼 시작은 튜토리얼 레벨을 열고, 타이틀로 돌아가기는 타이틀 레벨을 연다. 튜토리얼 레벨은 다른 분이 만들고 있으니 임시로 예상하여 이름을 넣었다.

 

● 플레이어 컨트롤러

UCLASS()
class DESECRATION_API AT3SelectClassPlayerController : public APlayerController
{
	GENERATED_BODY()
	
protected:
	virtual void BeginPlay() override;
	
public:
	//튜토리얼 시작
	void TutorialStart(const FString& PlayerName, const int32 ClassNum);
	
	//타이틀 화면으로
	void ReturnToTitleLevel();
	
private:	
	//게임 모드
	UPROPERTY()
	TObjectPtr<AT3SelectClassGameMode> SelectClassGameMode;
};

타이틀 화면의 플레이어 컨트롤러처럼 여기서도 위젯과 게임모드를 중계하는 역할이다. 게임 모드에서 선언한 함수에 대응되도록 함수를 만든다. 다만 기획상 선택한 클래스와 이름이 필요하기에 튜토리얼 시작 함수에 매개변수를 넣었다.

 

void AT3SelectClassPlayerController::BeginPlay()
{
	Super::BeginPlay();
	
	//마우스 사용
	bShowMouseCursor = true;
	const FInputModeUIOnly InputModeUIOnly;
	SetInputMode(InputModeUIOnly);
	
	//게임 모드
	SelectClassGameMode = Cast<AT3SelectClassGameMode>(GetWorld()->GetAuthGameMode());
	if (!SelectClassGameMode)
	{
		UE_LOG(LogTemp, Error, TEXT("%s : TitleGameMode is NULL"), *GetNameSafe(this));
		return;
	}
}

UI만 존재하는 레벨이니 여기서도 마우스 커서를 보이게 하고 입력 모드를 UI 전용으로 한다.

사실 이미 타이틀 화면에서 입력 모드를 UI 전용으로 했으니 여기서는 할 필요가 없는데, 테스트를 위해 이 레벨에서 바로 시작할 때 마우스가 간혹 사라지는 현상이 있어서 이렇게 했다.

 

void AT3SelectClassPlayerController::TutorialStart(const FString& PlayerName, const int32 ClassNum)
{	
	UE_LOG(LogTemp, Warning, TEXT("Name : %s, Class : %d"), *PlayerName, ClassNum);
	SelectClassGameMode->TutorialStart();
}

void AT3SelectClassPlayerController::ReturnToTitleLevel()
{
	SelectClassGameMode->ReturnToTitleLevel();
}

아직 이름이나 선택 클래스를 처리할 방법이 없으니 로그로 띄우는 용도로만 사용한다. 그 외에는 각 함수는 게임 모드에 대응되는 함수를 호출하도록 만든다.

 

● UI

UCLASS()
class DESECRATION_API UT3SelectClassWidget : public UUserWidget
{
	GENERATED_BODY()
	
protected:
	virtual void NativeConstruct() override;
	
private:
	//클래스 선택 버튼1
	UFUNCTION()
	void OnClickSelectClassButton1();
	
	//클래스 선택 버튼2
	UFUNCTION()
	void OnClickSelectClassButton2();
	
	//클래스 선택 버튼3
	UFUNCTION()
	void OnClickSelectClassButton3();
	
	//클래스 선택
	void SelectClass(int32 ClassNum);
	
	//선택 버튼
	UFUNCTION()
	void OnClickSelectButton();
	
	//타이틀로 돌아가기
	UFUNCTION()
	void ReturnToTitle();
	
	//클래스 버튼1
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr<UButton> SelectClassButton1;
	
	//클래스 버튼2
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr<UButton> SelectClassButton2;
	
	//클래스 버튼3
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr<UButton> SelectClassButton3;
    
	//선택 버튼
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr<UButton> SelectButton;
    
	//돌아가기 버튼
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr<UButton> ReturnButton;
	
	//플레이어 컨트롤러
	UPROPERTY()
	TObjectPtr<AT3SelectClassPlayerController> SelectClassPlayerController;
	
	//마지막으로 선택한 클래스
	int32 SelectedPlayerClass;
};

미리 캐싱할 플레이어 컨트롤러와 각 버튼마다 연결할 변수 및 대응되는 함수를 선언하느라 헤더가 다소 길어졌다. 내가 짠 코드지만 영 마음에 들지 않는다. 이걸 최적화할 방법을 생각해볼 필요가 있다. 하지만 당장 떠오르는 해결책이 없으니 일단 넘어가야겠다.

 

void UT3SelectClassWidget::NativeConstruct()
{
	Super::NativeConstruct();
	
	//플레이어 컨트롤러
	SelectClassPlayerController = Cast<AT3SelectClassPlayerController>(GetOwningPlayer());
	if (!SelectClassPlayerController)
	{
		UE_LOG(LogTemp, Error, TEXT("%s : SelectClassPlayerController가 NULL"), *GetNameSafe(this));
		return;
	}
	
	//선택 버튼은 비활성화 상태로 시작
	SelectButton->SetIsEnabled(false);
	
	//버튼 바인딩
	SelectClassButton1->OnClicked.AddDynamic(this, &ThisClass::OnClickSelectClassButton1);
	SelectClassButton2->OnClicked.AddDynamic(this, &ThisClass::OnClickSelectClassButton2);
	SelectClassButton3->OnClicked.AddDynamic(this, &ThisClass::OnClickSelectClassButton3);
	SelectButton->OnClicked.AddDynamic(this, &ThisClass::OnClickSelectButton);
	ReturnButton->OnClicked.AddDynamic(this, &ThisClass::ReturnToTitle);
    
	//그외 나머지 초기화
	SelectedPlayerClass = -1;
}

NativeConstruct에서는 플레이어 컨트롤러 캐싱 및 버튼 바인딩 작업을 한다.

 

void UT3TitleLevelWidget::OnClickSelectClassButton1()
{
	SelectClass(1);
}

void UT3TitleLevelWidget::OnClickSelectClassButton2()
{
	SelectClass(2);
}

void UT3TitleLevelWidget::OnClickSelectClassButton3()
{
	SelectClass(3);
}

void UT3TitleLevelWidget::SelectClass(int32 ClassNum)
{
	//이전에 누른 버튼과 동일하면 선택 버튼을 누른 것으로 간주
	if (ClassNum == SelectedPlayerClass)
	{
		OnClickSelectButton();
		return;
	}
	
	//선택 버튼 활성화
	SelectButton->SetIsEnabled(true);
	
	//누른 버튼 기억
	SelectedPlayerClass = ClassNum;
}

각 클래스 버튼의 함수는 하나의 클래스 선택 함수로 이어진다. 이 함수에서는 이전에 누른 버튼의 번호를 기억한 다음 다시 버튼을 누를 때 그게 동일한 버튼이면 선택 버튼의 함수를 호출하도록 했다.

그리고 클래스 버튼 중 아무거나 눌렀다면 선택 버튼을 활성화한다.

 

void UT3SelectClassWidget::OnClickSelectButton()
{
	SelectClassPlayerController->TutorialStart(TEXT(""), SelectedPlayerClass);
}
	

void UT3SelectClassWidget::ReturnToTitle()
{
	SelectClassPlayerController->ReturnToTitleLevel()
}

선택 버튼과 타이틀로 돌아가기 버튼의 함수는 플레리어 컨트롤러에서 대응되는 함수를 호출한다. 이름 입력은 아직 구현하지 않았으니 튜토리얼 시작 함수의 첫 번째 매개변수는 임시로 빈 문자열을 넘긴다.

 

//T3SelectClassPlayerController.h

private:
	//클래스 선택 화면의 위젯
	UPROPERTY(EditDefaultsOnly, Category = "Widget", meta = (AllowPrivateAccess = true))
	TSubclassOf<UT3SelectClassWidget> SelectClassWidgetClass;
	
	UPROPERTY()
	TObjectPtr<UT3SelectClassWidget> SelectClassWidgetInstance;
//T3SelectClassPlayerController.cpp

void AT3SelectClassPlayerController::BeginPlay()
{
	Super::BeginPlay();
	
    
	...
    
	
	//위젯 생성
	SelectClassWidgetInstance = CreateWidget<UT3SelectClassWidget>(this, SelectClassWidgetClass);
	if (!SelectClassWidgetInstance)
	{
		UE_LOG(LogTemp, Error, TEXT("%s : Failed to create TitleLevelWidget"), *GetNameSafe(this));
		return;
	}
	SelectClassWidgetInstance->AddToViewport();
}

위젯 클래스를 만들었으니 플레이어 컨트롤러에서는 해당 위젯을 화면에 출력하도록 한다.