대난투 팀 프로젝트(Project_CS) - 1

정완훈·2025년 4월 2일

언리얼 엔진: 모드별 GameMode 구현 및 초기 개발 단계 관리

언리얼 엔진에서 GameMode는 게임의 규칙과 흐름을 정의하는 핵심 클래스다. 프로젝트가 복잡해지면 게임의 각기 다른 상태나 모드(메인 메뉴, 캐릭터 선택, 인게임 플레이 등)에 맞는 전용 GameMode를 구현하는 게 효율적이다. 이 포스트에서는 공통 기반 클래스(ACSGameModeBase)를 상속받아서 싱글 플레이어 게임의 '캐릭터 선택'과 '튜토리얼 플레이'를 위한 각각의 GameMode(CSSingleGameMode, CSTutorialGameMode)를 구현하는 과정을 다룬다.

또한 게임 개발 초기 단계에서 GameState, PlayerController 등 아직 완전히 구현되지 않은 다른 클래스들에 대한 의존성을 관리하면서 GameMode의 기본 구조를 먼저 개발하는 실용적인 접근 방식을 소개한다.

1. 기본 GameMode (ACSGameModeBase) 정의

모든 인게임 레벨 GameMode의 기반이 될 ACSGameModeBase 클래스를 먼저 정의한다. 이 클래스는 게임의 매치 타입(EMatchType: 싱글, 협동, 대전)과 매치 단계(EMatchPhase: 대기, 진행 중, 종료)를 관리하고, 각 매치 타입에 따른 초기화 로직(InitMatchLogic, InitSinglePlayLogic 등)을 호출하는 기본 구조를 제공한다.

// CSGameModeBase.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameMode.h"
#include "CSTypes/CSGameTypes.h" // EMatchType, EMatchPhase enum 포함
#include "CSGameModeBase.generated.h"

UCLASS()
class CS_API ACSGameModeBase : public AGameMode
{
	GENERATED_BODY()

public:
	ACSGameModeBase();
	virtual void BeginPlay() override;
	virtual void PostLogin(APlayerController* NewPlayer) override; // 플레이어 로그인 시 처리
	virtual void HandleStartGame(); // 게임 시작 처리 (MatchPhase 변경)
	virtual void HandleEndGame();   // 게임 종료 처리 (MatchPhase 변경)

protected:
	UPROPERTY(BlueprintReadOnly, Category="GameMode")
	EMatchType MatchType; // 게임 인스턴스로부터 설정됨

	UPROPERTY(BlueprintReadOnly, Category="GameMode")
	EMatchPhase MatchPhase; // 현재 게임 단계

	virtual void InitMatchLogic(); // MatchType에 따라 분기

	// 각 MatchType별 초기화 (파생 클래스에서 오버라이드)
	virtual void InitSinglePlayLogic() {}
	virtual void InitVersusLogic() {}
	virtual void InitCoopLogic() {}

	void SetMatchPhase(EMatchPhase NewPhase); // MatchPhase 변경 및 GameState에 전파
};

// CSGameModeBase.cpp 일부
#include "GameModes/CSGameModeBase.h"
#include "GameStates/CSGameStateBase.h" // 기본 GameState 클래스
#include "GameInstance/CSGameInstance.h" // MatchType 가져오기
#include "Kismet/GameplayStatics.h"

ACSGameModeBase::ACSGameModeBase()
{
	// 기본 GameState 설정
	GameStateClass = ACSGameStateBase::StaticClass();
}

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

	// GameInstance에서 MatchType 가져오기
	if (const UCSGameInstance* GI = GetGameInstance<UCSGameInstance>())
	{
		MatchType = GI->MatchType;
	}

	InitMatchLogic(); // MatchType에 맞는 초기화 실행
	SetMatchPhase(EMatchPhase::EMP_Waiting); // 초기 상태는 Waiting
}

// ... 기타 함수 구현 ...

2. 캐릭터 선택 GameMode (CSSingleGameMode) 구현

싱글 플레이어 모드의 캐릭터 선택 레벨을 위한 GameMode다. 이 레벨의 주요 역할은 플레이어에게 캐릭터 선택 UI를 제공하고, 선택이 완료되면 실제 게임 플레이 레벨(튜토리얼 등)로 이동시키는 것이다.

주요 설계:

  • UI 처리: 캐릭터 선택 UI 표시는 PlayerController의 책임으로 분리한다. GameMode는 서버에만 존재하고 UI는 클라이언트 요소이므로, PlayerControllerBeginPlay 등에서 자신의 UI를 직접 생성하고 관리하는 구조가 더 적합하다. 그래서 CSSingleGameModePostLogin에서는 UI 관련 직접 호출을 하지 않는다.
  • 레벨 전환: 플레이어가 캐릭터 선택을 완료하면 PlayerController가 서버 RPC를 통해 GameMode의 RequestStartGameplayLevel 함수를 호출하도록 설계한다. 이 함수는 선택된 캐릭터 정보를 저장(아직 미구현)하고 다음 레벨을 로드한다.
// CSSingleGameMode.h
#pragma once

#include "CoreMinimal.h"
#include "GameModes/CSGameModeBase.h"
#include "CSSingleGameMode.generated.h"

UCLASS()
class CS_API ACSSingleGameMode : public ACSGameModeBase
{
	GENERATED_BODY()

public:
	ACSSingleGameMode();

	// PlayerController에서 호출되어 다음 게임 레벨 시작을 요청한다.
	UFUNCTION(BlueprintCallable, Category = "Game Flow")
	void RequestStartGameplayLevel(APlayerController* SelectingPlayer);

protected:
	virtual void InitSinglePlayLogic() override;
	virtual void PostLogin(APlayerController* NewPlayer) override;

	// 다음으로 이동할 레벨 이름 (에디터에서 설정 가능)
	UPROPERTY(EditDefaultsOnly, Category = "Game Flow")
	FName NextLevelName = FName("TutorialLevel");
};

// CSSingleGameMode.cpp (주요 부분)
#include "GameModes/CSSingleGameMode.h"
#include "Controller/CSPlayerController.h"
#include "Kismet/GameplayStatics.h"

ACSSingleGameMode::ACSSingleGameMode()
{
	DefaultPawnClass = nullptr; // 캐릭터 선택 레벨에서는 Pawn이 필요 없을 수도 있다.
	PlayerControllerClass = ACSPlayerController::StaticClass();
	// GameStateClass = ACSSingleGameState::StaticClass(); // 필요하면 설정한다.
}

// ... InitSinglePlayLogic, PostLogin 구현 ...

void ACSSingleGameMode::RequestStartGameplayLevel(APlayerController* SelectingPlayer)
{
	// ... PlayerController 유효성 검사 ...
	UE_LOG(LogTemp, Log, TEXT("Player %s requested level: %s"), *SelectingPlayer->GetName(), *NextLevelName.ToString());

	// --- 캐릭터 선택 정보 저장 로직 (현재는 주석 처리) ---
	// TODO: 이 부분은 GameInstance 또는 PlayerState에 선택된 캐릭터 정보를 저장하는 로직이 필요하다.
	// PlayerState나 GameInstance에 'SelectedCharacterClass' 같은 변수가 정의되어야 한다.
	/*
	UCSGameInstance* GI = GetGameInstance<UCSGameInstance>(); // 예시: GameInstance 사용
	if (GI) { GI->SelectedCharacterClass = ...; }

	ACSPlayerState* PS = SelectingPlayer->GetPlayerState<ACSPlayerState>(); // 예시: PlayerState 사용
	if (PS) { PS->SetSelectedCharacter(...); }
	*/
	// --- 캐릭터 선택 정보 저장 로직 끝 ---

	UGameplayStatics::OpenLevel(this, NextLevelName); // 다음 레벨 로드
}

3. 튜토리얼 GameMode (CSTutorialGameMode) 구현

실제 게임 플레이가 이루어지는 튜토리얼 레벨을 위한 GameMode다. 이 GameMode는 캐릭터 선택 레벨에서 플레이어가 선택한 캐릭터를 스폰하고, 튜토리얼 진행 규칙을 관리한다.

주요 설계:

  • 플레이어 스폰: HandleStartingNewPlayer 함수를 오버라이드해서 플레이어가 레벨에 진입(또는 리스폰)할 때, GameInstancePlayerState에 저장된 캐릭터 선택 정보를 바탕으로 올바른 Pawn 클래스를 스폰한다. 이를 위해 GetDefaultPawnClassForController 함수도 오버라이드해서 선택된 클래스를 반환하도록 한다. (선택 정보 로드 부분은 현재 주석 처리)
  • 게임 시작: HandleMatchIsWaitingToStart 함수를 오버라이드해서 게임 시작 준비가 되었을 때 기반 클래스의 HandleStartGame 함수를 호출함으로써 MatchPhaseEMP_Playing으로 변경한다.
  • Fallback Pawn: 캐릭터 선택 정보를 가져오지 못하는 예외 상황에 대비해서 스폰할 기본 Pawn 클래스(FallbackDefaultPawnClass)를 지정할 수 있도록 한다.
// CSTutorialGameMode.h
#pragma once

#include "CoreMinimal.h"
#include "GameModes/CSGameModeBase.h"
#include "CSTutorialGameMode.generated.h"

UCLASS()
class CS_API ACSTutorialGameMode : public ACSGameModeBase
{
	GENERATED_BODY()

public:
	ACSTutorialGameMode();
	virtual void HandleMatchIsWaitingToStart() override; // 게임 시작 준비 완료 시
	virtual UClass* GetDefaultPawnClassForController_Implementation(AController* InController) override; // 스폰할 Pawn 클래스 결정

protected:
	virtual void InitSinglePlayLogic() override;
	virtual void PostLogin(APlayerController* NewPlayer) override;
	virtual void RestartPlayer(AController* NewPlayer) override; // 플레이어 리스폰 처리
	virtual void HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer) override; // 최초 스폰 처리

	// 캐릭터 선택 정보 로드 실패 시 사용할 기본 Pawn 클래스
	UPROPERTY(EditDefaultsOnly, Category = "Defaults")
	TSubclassOf<APawn> FallbackDefaultPawnClass;
};

// CSTutorialGameMode.cpp (주요 부분)
#include "GameModes/CSTutorialGameMode.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/PlayerStart.h"
#include "Controller/CSPlayerController.h"
// #include "GameStates/CSTutorialGameState.h" // 필요하면 포함한다.

ACSTutorialGameMode::ACSTutorialGameMode()
{
	// ... 기본 설정 ...
	// GameStateClass = ACSTutorialGameState::StaticClass(); // 필요하면 설정한다.
}

// ... InitSinglePlayLogic, PostLogin, RestartPlayer 구현 ...

void ACSTutorialGameMode::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
{
	UClass* PawnClass = GetDefaultPawnClassForController(NewPlayer); // 선택 정보 로드 시도
	// ... PawnClass 유효성 검사 및 Fallback 처리 ...
    if (!PawnClass) return;

	AActor* StartSpot = FindPlayerStart(NewPlayer);
	// ... 스폰 위치/회전 계산 ...
	APawn* NewPawn = GetWorld()->SpawnActor<APawn>(PawnClass, ...); // Pawn 스폰
	if (NewPawn)
	{
		NewPlayer->Possess(NewPawn); // Pawn에 빙의
	}
}

void ACSTutorialGameMode::HandleMatchIsWaitingToStart()
{
	HandleStartGame(); // MatchPhase를 Playing으로 변경
}

UClass* ACSTutorialGameMode::GetDefaultPawnClassForController_Implementation(AController* InController)
{
	// --- 캐릭터 선택 정보 가져오는 로직 (현재는 주석 처리) ---
	// TODO: GameInstance 또는 PlayerState에서 캐릭터 선택 정보를 가져와 반환해야 한다.
	/*
	UCSGameInstance* GI = GetGameInstance<UCSGameInstance>(); // 예시: GameInstance
	if (GI && GI->SelectedCharacterClass) { return GI->SelectedCharacterClass; }

	ACSPlayerState* PS = InController ? InController->GetPlayerState<ACSPlayerState>() : nullptr; // 예시: PlayerState
	if (PS && PS->SelectedCharacterClass) { return PS->SelectedCharacterClass; }
	*/
	// --- 캐릭터 선택 정보 가져오는 로직 끝 ---

	return FallbackDefaultPawnClass; // 정보 없으면 Fallback 반환
}

4. 초기 개발 단계의 의존성 관리

게임 개발 초기에는 모든 클래스가 한 번에 구현되지 않는다. 위 코드 예시처럼 GameState, PlayerController, PlayerState 등의 기능이 아직 구현되지 않았더라도 GameMode의 기본 구조 개발을 진행할 수 있다.

접근 방식:

  1. 의존성 식별: GameMode 코드 중 아직 구현되지 않은 다른 클래스의 변수나 함수를 사용하는 부분을 식별한다. (예: PlayerStateSelectedCharacterClass 변수 접근)
  2. 주석 처리: 식별된 부분을 // TODO: 와 같은 주석과 함께 임시로 주석 처리한다. 이렇게 하면 코드가 컴파일되고 GameMode의 다른 부분을 테스트할 수 있다.
  3. 필요 기능 명시: 주석 처리된 부분이나 관련 로직을 위해 다른 클래스에 추가되어야 할 변수 및 함수들을 명확히 목록화한다. 이는 향후 개발을 위한 로드맵 역할을 한다.

향후 필요한 정의 (요약):

  • ACSPlayerController:
    • 캐릭터 선택 완료 시 서버에 알리는 RPC 함수 (예: Server_RequestStartGameplayLevel).
    • 튜토리얼 목표 달성 등 게임 이벤트 발생 시 서버에 알리는 RPC 함수.
    • (Client) UI를 생성하고 표시/업데이트하는 로직 (예: BeginPlay에서 위젯 생성, GameState 변경 시 UI 업데이트).
  • ACSGameStateBase 및 파생 클래스 (ACSSingleGameState, ACSTutorialGameState 등):
    • (Replicated) 게임의 현재 상태 변수 (예: MatchPhase - Base에 이미 있음, CurrentTutorialObjectiveID).
    • (Replicated) 클라이언트 UI 표시에 필요한 데이터 (예: AvailableCharacterClasses).
    • 게임 규칙/상태 관리 함수 (예: LoadAvailableCharacters, SetupTutorialObjectives).
  • ACSPlayerState (사용한다면):
    • (Replicated) 플레이어별로 유지되어야 하는 상태 변수 (예: SelectedCharacterClass, TutorialProgressIndex). 리플리케이션 설정이 중요하다.
  • UCSGameInstance (사용한다면):
    • 게임 세션 동안 유지되어야 하는 정보 (예: SelectedCharacterClass). 싱글플레이어에서는 간편하지만 상태 유지 및 전달 방식에 주의가 필요하다.

결론

모드별로 특화된 GameMode를 구현하고 개발 초기 단계에서 의존성을 체계적으로 관리하면 복잡한 게임 개발 과정을 효율적으로 만들 수 있다. 기반 클래스를 활용해서 공통 로직을 재사용하고, 아직 구현되지 않은 부분은 주석과 TODO 리스트로 관리하면서 점진적으로 개발을 진행하는 방식으로 안정적인 프로젝트 구조를 구축할 수 있다.

0개의 댓글