[UE5] 시퀀스 & 다이얼로그 매니저 구현 / 데이터 시트 설명서 제작

연하·2024년 8월 6일
0

Trapper

목록 보기
29/32

시퀀스 매니저 구현

시퀀스들의 정보를 가지고 있고, 시퀀스마다 타입을 붙혀준 뒤 그 타입을 인자로 받아 재생시켜주는 시퀀스 매니저를 만들어주었다.

UENUM(BlueprintType)
enum class ESequenceType : uint8
{
	Opening							UMETA(DisplayName = "Opening"),
	TutorialCameraMove				UMETA(DisplayName = "TutorialCameraMove"),
	FirstWave						UMETA(DisplayName = "FirstWave"),
	BonusWave						UMETA(DisplayName = "BonusWave"),
	Ending							UMETA(DisplayName = "Ending"),
};

enum class를 사용해 각각의 시퀀스마다 이름을 붙혀주었다.

UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TMap<ESequenceType, class ULevelSequence*> SequenceMap;

맵을 만들어주고, 블루프린트에서 직접 시퀀스를 설정할 수 있도록 해줬다.

UFUNCTION(NetMulticast, Reliable)
void MulticastRPCPlaySequence(ESequenceType Type);
void ASequenceManager::MulticastRPCPlaySequence_Implementation(ESequenceType Type)
{
	ULevelSequence** Sequence = SequenceMap.Find(Type);
	if (Sequence && *Sequence)
	{
		if (HasAuthority())
		{
			ATrapperGameState* TrapperGameState = GetWorld()->GetGameState<ATrapperGameState>();
			TrapperGameState->OnEventExecute.Broadcast(99);
		}

		FMovieSceneSequencePlaybackSettings PlaybackSettings;
		LevelSequencePlayer = ULevelSequencePlayer::CreateLevelSequencePlayer(GetWorld(), *Sequence, PlaybackSettings, LevelSequenceActor);
	
		if (LevelSequencePlayer && LevelSequenceActor)
		{
			// 종료 함수 바인딩
			if (HasAuthority())
			{
				FName* EndFunctionName = EndFunctionMap.Find(Type);
				if (EndFunctionName)
				{
					LevelSequencePlayer->OnFinished.AddDynamic(this, &ASequenceManager::OnSequenceFinished);
				}
			}
			
			LevelSequencePlayer->Play();
		}
	}
}

멀티캐스트 RPC 함수를 사용해 시퀀스 타입을 인자로 받아 시퀀스를 재생시켜주는 함수이다. 시퀀스 맵에서 타입에 따라 맞는 시퀀스를 불러와주고, 종료 함수를 바인딩한 뒤 시퀀스를 플레이한다. 서버일 경우 이벤트 코드 99번을 송신하는데, 플레이어 HUD를 숨김처리해주는 코드이다.

void ASequenceManager::OnSequenceFinished()
{
	for (const TPair<ESequenceType, FName>& Pair : EndFunctionMap)
	{
		if (LevelSequencePlayer->GetSequence() == SequenceMap[Pair.Key])
		{
			// 함수 이름을 통해 호출
			FName FunctionName = Pair.Value;
			UFunction* Function = FindFunction(FunctionName);
			if (Function)
			{
				ProcessEvent(Function, nullptr);

				ATrapperGameState* TrapperGameState = GetWorld()->GetGameState<ATrapperGameState>();
				TrapperGameState->OnEventExecute.Broadcast(98);
			}
		}
	}
}

시퀀스가 종료되었을 때 호출되는 함수이다. ProcessEvent 함수를 통해 실행된 시퀀스에 따라 시퀀스가 끝났을 때 각기 다르게 처리해준다.

// ASequenceManager.h

TMap<ESequenceType, FName> EndFunctionMap;
UFUNCTION() void OpeningSequenceFinished();
UFUNCTION() void TutorialCameraMoveFinished();
UFUNCTION() void FirstWaveFinished();
UFUNCTION() void BonusWaveFinished();

// ASequenceManager.cpp

void ASequenceManager::Initialize()
{
	EndFunctionMap.Add(ESequenceType::Opening, TEXT("OpeningSequenceFinished"));
	EndFunctionMap.Add(ESequenceType::TutorialCameraMove, TEXT("TutorialCameraMoveFinished"));
	EndFunctionMap.Add(ESequenceType::FirstWave, TEXT("FirstWaveFinished"));
	EndFunctionMap.Add(ESequenceType::BonusWave, TEXT("BonusWaveFinished"));
}

Initialize() 함수 안에서 EndFunctionMap에 호출해야 할 함수명들을 저장해둔다. 사실 모든 함수에서 이벤트 코드만 호출하기 때문에 이렇게 복잡하게 짤 필요는 없었지만, 다른 방식으로도 한번 설계해 보았다. 이벤트 코드로 호출하는 것과는 다르게, 시퀀스마다 이름을 지정해주었기 때문에 호출할때 헷갈리지 않아서 좋은 것 같다 :)

다시 위로 올라가서 시퀀스가 종료되어 OnSequenceFinished() 함수가 호출되면, 각 시퀀스에 맞는 종료 함수를 실행해주고, 98번 이벤트 코드를 전송해 플레이어 HUD를 다시 되돌려놓는다.

다이얼로그 매니저 구현

TObjectPtr<class UDataTable> DialogData;

ADialogManager::ADialogManager()
{
	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	static ConstructorHelpers::FObjectFinder<UDataTable> DialogTable(TEXT("/Script/Engine.DataTable'/Game/Blueprints/Data/DT_DialogData.DT_DialogData'"));
	if (DialogTable.Succeeded() && DialogTable.Object)
	{
		DialogData = DialogTable.Object;
	}
}

먼저, 생성 시점에 다이얼로그 데이터 테이블을 불러와줬다.

	/// --------------------------------
	///				Dialog 
	/// --------------------------------

	if (EventCode >= 500 && EventCode < 600)
	{
		DialogManager->MulticastRPCPlayDialog(EventCode);
	}

게임모드에서 500번대 이벤트 코드를 받으면 MulticastRPCPlayDialog() 함수를 호출한다.

void ADialogManager::MulticastRPCPlayDialog_Implementation(int32 DialogCode)
{
	if (!PlayerController)
	{
		return;
	}

	PlayDialog(DialogCode);
}

플레이어 컨트롤러가 있는 경우에만(BeginPlay 함수에서 받아온다) PlayDialog() 함수를 호출한다.

void ADialogManager::PlayDialog(int32 DialogCode)
{
	// 플레이어 대사 UI 활성화
	PlayerController->ShowPlayerDialog(true);
	bIsPlaying = true;

	DialogIndex++;

	// 다이얼로그 데이터를 불러오기 위한 이름 계산
	FString DialogDataName = FString::FromInt(DialogCode) + TEXT("_") + FString::FromInt(DialogIndex);
	FDialogInfo* Dialog = DialogData->FindRow<FDialogInfo>(*DialogDataName, FString());
	LastDialogCode = DialogCode;

	// 데이터가 있다면,
	if (Dialog)
	{
    	// Text UI를 설정해준다.
		PlayerController->SetPlayerText(GetCharacterName(Dialog->Character), Dialog->Dialog);
		
        // 다이얼로그에 이벤트 코드가 있을 경우 실행
		if (Dialog->EventCode.Num() != 0 && HasAuthority())
		{
			for (auto Code : Dialog->EventCode)
			{
				GetWorld()->GetGameState<ATrapperGameState>()->OnEventExecute.Broadcast(Code);
			}
		}

		// 마지막 대사의 경우 타이머를 설정한 뒤 다이얼로그 재생을 끝낸다
		if (Dialog->bIsEnd)
		{
			FTimerHandle EndHandle;
			GetWorldTimerManager().SetTimer(EndHandle, FTimerDelegate::CreateLambda([&]
				{	
                	// 대사 UI 비활성화, 인덱스 초기화
					PlayerController->ShowPlayerDialog(false);
					DialogIndex = 0;
					bIsPlaying = false;
				}
			), 1.0f, false, Dialog->Time);
		}
        // 마지막 대사가 아닐 경우, 다음 대사 진행
		else
		{
			FTimerHandle Handle;
			GetWorldTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
				{
					PlayDialog(LastDialogCode);
				}
			), 1.0f, false, Dialog->Time);
		}
	}
}

이것도 이쁘게 주석으로 정리해보았다..! 다이얼로그 매니저는 호출 당했을 때 저장하고 있는 대사만 출력해주면 되므로 간단하게 끝난다 :)

이벤트 작동 테스트

처음 설계대로 게임모드가 시퀀스 매니저와 퀘스트 매니저, 다이얼로그 매니저에서 보내준 이벤트 코드를 받아 전역으로 처리해주며 게임의 루프가 굴러가고 있다.

튜토리얼~첫번째 웨이브까지의 이벤트코드 수신 목록이다. 코드를 받는대로 잘 작동하는걸 확인할 수 있었다 :) 이제 틀은 모두 만들어졌으니, 이런식으로 쭉쭉 살을 붙혀나가면 메타루프도 금방 끝날 것 같다.

데이터 시트 설명서 제작

기획자분들이 웨이브 / 퀘스트 / 다이얼로그 관련해 수정할 수 있도록 설명서를 제작했다(드디어!).

예시와 함께 최대한 쉽게 설명하려고 노력해서 작성했다..!!

업로드중..

그리고 내 개인 노트에 정리해두었던 퀘스트 / 이벤트 코드 관련된 것들도 같이 작성해두었다. 겸사겸사 현재 적용 여부도 기획분들이 확인하실 수 있게끔 함께 정리해두었음 :)

[Trapper] 웨이브 & 퀘스트 & 다이얼로그 코드 정리 및 설명서

궁금하실분을 위해 준비했습니다..(뾰롱)


(뜬금)일기

갈수록 정리의 개념으로 글을 쓰고있게 되는 것 같다. 빨리 굴러가게 구현해야하니까 글 쓸 시간이..
과정을 남기는게 목표긴 했는데, 그래도 이렇게라도 남겨두는게 어디냐!!! 앞으로도 열심히 써야지 :)

프로젝트를 진행할 수록, 회사에서의 프로젝트 진행이 너무너무 궁금하다..
얼마나 체계적일까? 내가 설계하는게 효율적인지 비효율적인지 판단해줄 선배들이 잔뜩 계시는걸까..?!
기존 게임들은 도대체 어떻게 게임의 흐름을 관리하고, 기획자와 협업하고, 시스템을 만들고 그러는걸까..
너무너무너무 궁금하다 XD 빨리 배우고싶어!!!!!!!!(시끄러움)

0개의 댓글