[AbyssDiver] 튜토리얼_GameMode만들기

칼든개구리·2025년 7월 26일
0
post-thumbnail

게임에서 튜토리얼은 플레이어가 게임의 세계에 자연스럽게 녹아들게 하는 첫 번째 관문입니다. 잘 만들어진 튜토리얼은 플레이어에게 긍정적인 첫인상을 심어주고, 게임의 핵심 메카닉을 효과적으로 전달합니다.

이번 포스팅에서는 저희가 개발 중인 Abyss Diver: Underworld의 튜토리얼 시스템을 담당하는 AADTutorialGameMode 클래스의 구조와 설계에 대해 자세히 살펴보겠습니다. 이 클래스는 상태 머신(State Machine) 패턴과 데이터 테이블(Data Table)을 활용하여 유연하고 확장 가능한 튜토리얼 흐름을 어떻게 관리하는지 보여주는 좋은 예시가 될 것입니다.


AADTutorialGameMode는 다음과 같은 핵심 설계 원칙을 기반으로 합니다.

상태 기반 흐름 제어: 튜토리얼의 각 단계를 ETutorialPhase라는 enum으로 정의하고, 현재 어떤 단계를 진행 중인지 GameState에 저장합니다. GameMode는 이 상태를 기반으로 적절한 로직을 호출하는 상태 머신의 역할을 합니다.

데이터 기반(Data-Driven) 설계: 튜토리얼의 각 단계에 필요한 텍스트, 표시 시간, 플레이어 액션 대기 여부 등의 상세 정보를 C++ 코드에 하드코딩하는 대신, DataTable에 정의합니다. 이를 통해 기획자가 코드를 수정하지 않고도 튜토리얼의 내용과 흐름을 쉽게 수정하고 테스트할 수 있습니다.

역할의 명확한 분리:

GameMode: 튜토리얼의 전체적인 흐름과 규칙을 관리합니다. (언제 다음 단계로 넘어갈지 결정)

GameState: 튜토리얼의 현재 상태(CurrentPhase)를 저장합니다. 모든 클라이언트가 이 상태를 공유해야 할 때 중요한 역할을 합니다.

PlayerController: 플레이어의 입력을 처리하고, 튜토리얼 UI를 화면에 표시하는 역할을 위임받습니다.

  1. 헤더 파일 (AADTutorialGameMode.h)
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameMode.h"
#include "Tutorial/TutorialEnums.h"
#include "ADTutorialGameMode.generated.h"

class UDataTable;

UCLASS()
class ABYSSDIVERUNDERWORLD_API AADTutorialGameMode : public AGameMode
{
    GENERATED_BODY()

public:
    AADTutorialGameMode();

    // 게임 시작 시 호출
    virtual void StartPlay() override;
    
    // 다음 튜토리얼 단계로 진행
    void AdvanceTutorialPhase();

protected:
    // 현재 단계에 맞는 로직 처리
    void HandleCurrentPhase();

    // 각 단계별 핸들러 함수들
    void HandlePhase_Movement();
    void HandlePhase_Sprint();
    void HandlePhase_Oxygen();
    void HandlePhase_Radar();
    void HandlePhase_Looting();
    void HandlePhase_Inventory();     
    void HandlePhase_Drone();
    void HandlePhase_LightToggle();   
    void HandlePhase_Items();
    void HandlePhase_OxygenWarning();
    void HandlePhase_Revival();
    void HandlePhase_Complete();

    // 특정 NPC를 스폰하는 함수 (블루프린트에서도 호출 가능)
    UFUNCTION(BlueprintCallable)
    void SpawnDownedNPC();

    // 자주 사용하는 플레이어 컨트롤러 캐싱
    UPROPERTY()
    TObjectPtr<class AADPlayerController> TutorialPlayerController;

    // 다음 단계로 자동 진행을 위한 타이머 핸들
    FTimerHandle StepTimerHandle;

    // 튜토리얼 데이터를 담고 있는 데이터 테이블
    UPROPERTY(EditAnywhere, Category = "Tutorial")
    TObjectPtr<UDataTable> TutorialDataTable;
};

헤더 파일에서는 튜토리얼의 각 단계를 처리할 HandlePhase_... 함수들과 전체 흐름을 제어하는 AdvanceTutorialPhase, HandleCurrentPhase 함수가 선언되어 있습니다.

특히 주목할 점은 UPROPERTY로 선언된 TutorialDataTable입니다. 이 변수를 통해 언리얼 에디터에서 튜토리얼 데이터를 담은 데이터 테이블 애셋을 손쉽게 연결할 수 있습니다.

  1. 소스 파일 (AADTutorialGameMode.cpp) - 흐름 제어
// 다음 튜토리얼 단계로 진행시키는 함수
void AADTutorialGameMode::AdvanceTutorialPhase()
{
    if (AADTutorialGameState* TutorialGS = GetGameState<AADTutorialGameState>())
    {
        ETutorialPhase CurrentPhase = TutorialGS->GetCurrentPhase();
        if (CurrentPhase != ETutorialPhase::Complete)
        {
            // 현재 Phase enum 값을 1 증가시켜 다음 Phase를 계산
            ETutorialPhase NextPhase = static_cast<ETutorialPhase>(static_cast<uint8>(CurrentPhase) + 1);
            // GameState에 다음 Phase 상태를 저장
            TutorialGS->SetCurrentPhase(NextPhase);

            // 새로운 Phase에 맞는 핸들러 함수 호출
            HandleCurrentPhase();
        }
    }
}

AdvanceTutorialPhase 함수는 외부(플레이어의 특정 행동, 타이머 만료 등)에서 호출될 때마다 튜토리얼 단계를 하나씩 진행시키는 역할을 합니다. GameState에 저장된 현재 단계를 가져와 다음 단계로 업데이트한 후, HandleCurrentPhase를 호출하여 실제 로직을 실행합니다.

// 현재 튜토리얼 단계에 맞는 로직을 처리하는 핵심 함수
void AADTutorialGameMode::HandleCurrentPhase()
{
    // ... 플레이어 컨트롤러 유효성 검사 ...

    if (AADTutorialGameState* TutorialGS = GetGameState<AADTutorialGameState>())
    {
        ETutorialPhase CurrentPhase = TutorialGS->GetCurrentPhase();

        // 현재 Phase에 따라 적절한 핸들러 함수 호출 (상태 머신의 분기 처리)
        switch (CurrentPhase)
        {
            case ETutorialPhase::Step1_Movement:      HandlePhase_Movement(); break;
            // ... 다른 단계들 ...
            case ETutorialPhase::Complete:            HandlePhase_Complete(); break;
            default:                                  break;
        }

        // 데이터 테이블에서 현재 단계에 맞는 데이터 검색
        if (TutorialDataTable)
        {
            // Enum 값을 FName으로 변환하여 Row 이름으로 사용
            const FString EnumString = UEnum::GetValueAsString(CurrentPhase);
            const FName RowName = FName(EnumString.RightChop(EnumString.Find(TEXT("::")) + 2));
            const FTutorialStepData* StepData = TutorialDataTable->FindRow<FTutorialStepData>(RowName, TEXT(""));

            // 데이터가 있고, 플레이어의 입력을 기다리지 않는 단계라면
            if (StepData && !StepData->bWaitForPlayerTrigger)
            {
                // 설정된 시간(DisplayDuration) 후에 자동으로 AdvanceTutorialPhase를 호출하는 타이머 설정
                GetWorldTimerManager().SetTimer(
                    StepTimerHandle,
                    this,
                    &AADTutorialGameMode::AdvanceTutorialPhase,
                    StepData->DisplayDuration,
                    false);
            }
            else // 플레이어의 특정 행동을 기다려야 하는 단계라면
            {
                // 기존 타이머를 제거하여 자동 진행을 막음
                GetWorldTimerManager().ClearTimer(StepTimerHandle);
            }
        }
    }
}

HandleCurrentPhase는 이 시스템의 심장입니다.

switch 문을 통해 현재 단계에 맞는 HandlePhase_... 함수를 호출하여 고유 로직(예: 아이템 스폰, UI 활성화 등)을 실행합니다.

TutorialDataTable에서 현재 단계에 해당하는 행(Row)을 찾습니다. (ETutorialPhase 열거형의 이름을 그대로 행 이름으로 사용하는 방식이 인상적입니다.)

찾아낸 데이터의 bWaitForPlayerTrigger 플래그를 확인합니다.

false일 경우: 단순 설명처럼 특정 시간 동안 보여주기만 하면 되는 단계입니다. SetTimer를 이용해 DisplayDuration 시간 후에 자동으로 AdvanceTutorialPhase를 호출합니다.

true일 경우: 플레이어가 'W'키를 누르거나, 특정 아이템을 줍는 등의 행동을 완료해야만 다음으로 진행되는 단계입니다. 이 경우 타이머를 설정하지 않고, 다른 곳에서 플레이어의 행동을 감지하여 AdvanceTutorialPhase를 직접 호출해주기를 기다립니다.

  1. 소스 파일 (AADTutorialGameMode.cpp) - 개별 단계 핸들러
void AADTutorialGameMode::HandlePhase_Movement()
{
    // 이동 튜토리얼에 필요한 로직 구현
    // 예: 플레이어가 일정 거리 이상 움직였는지 체크하는 로직 바인딩
}

void AADTutorialGameMode::HandlePhase_Looting()
{
    // 루팅 튜토리얼에 필요한 로직 구현
    // 예: 상호작용 가능한 아이템을 플레이어 근처에 스폰
}

void AADTutorialGameMode::HandlePhase_Revival()
{
    // 부활 튜토리얼에 필요한 로직 구현
    // 예: SpawnDownedNPC() 함수를 호출하여 쓰러진 NPC를 생성
}
//...

현재 각 HandlePhase_... 함수들은 비어있지만, 이 함수들이 각 튜토리얼 단계의 구체적인 내용을 구현하는 공간이 됩니다. 예를 들어 HandlePhase_Revival에서는 SpawnDownedNPC 함수를 호출하여 부활시켜야 할 대상을 맵에 배치하는 코드가 들어갈 것입니다.

전체적인 흐름 정리
1. 게임이 시작되면 StartPlay에서 첫 튜토리얼 단계를 설정하고 HandleCurrentPhase를 호출합니다.

2.HandleCurrentPhase는 switch문으로 해당 단계의 고유 로직을 실행합니다.

3.DataTable을 조회하여 bWaitForPlayerTrigger 값을 확인합니다.

4.(자동 진행) false이면, 타이머를 설정하고 시간이 되면 AdvanceTutorialPhase를 호출합니다. -> 2번으로 순환

5.(수동 진행) true이면, 시스템은 대기합니다. 플레이어가 미션(예: 아이템 줍기)을 완료하면, 해당 아이템의 로직이 GameMode의 AdvanceTutorialPhase를 직접 호출합니다. -> 2번으로 순환

이 과정은 ETutorialPhase::Complete 단계에 도달할 때까지 반복됩니다.

profile
메타쏭이

0개의 댓글