[UE5] 게임 모드 리팩토링 & 웨이브 로직 변경

연하·2024년 7월 30일
0

Trapper

목록 보기
27/32
post-thumbnail

계속 퀘스트 매니저가 모든 것들을 제어하는 식으로 고민해왔는데, 모든 곳에서 게임 흐름을 제어하는 이벤트가 발생한다는 것을 생각하니, 별도의 관리자가 있는게 훨씬 나을 것 같다는 판단이 섰다. 각각의 매니저들은 각각의 기능만 관리하도록 하고, 한 곳에서 이벤트 코드를 받아 제어하도록 하는게 좋겠다 싶었다. 어차피 게임 흐름은 서버에서 관여할 것이고, 굳이 게임 매니저를 따로 만들지 않고 게임 모드를 사용하는게 가장 적합할 것 같다는 결론이 섰음.

정비 시간 & 웨이브 상태변수 게임 스테이트로 이동

우선, 게임모드가 가지고 있던 게임의 상태를 게임 스테이트로 이관하기로 했다. 작업하는 틈틈히 옮겨놓긴 해서 그렇게 많진 않았다.

정비 시간

#pragma region Maintenance

// ATrapperGameState.h

// 몇번째 정비시간인지 체크
UPROPERTY(Replicated)
uint8 MaintenanceCount = 0;

UPROPERTY(ReplicatedUsing = OnRep_ChangeMaintenanceTimeLeft)
uint32 MaintenanceTimeLeft;

void AddMaintenanceTime(uint32 Value);

UFUNCTION()
void OnRep_ChangeMaintenanceTimeLeft();

UPROPERTY(ReplicatedUsing = OnRep_ChangeMaintenanceState)
uint32 bMaintenanceInProgress : 1;

UFUNCTION()
void OnRep_ChangeMaintenanceState();

#pragma endregion Maintenance

클라이언트의 UI를 설정하는데 필요한 남은 정비 시간과 정비 시간임을 알 수 있는 변수를 옮겨주고, 게임모드 내에서 사용하고 있는 기존 변수들을 게임 스테이트에서 꺼내쓰도록 바꿔주었다. 또한, 게임모드에서 RPC를 통해 동기화시켜주고 있는 UI를 게임스테이트 내에서 로컬로 처리하도록 변경했다.

정상적으로 UI가 동기화되는 모습.

웨이브

#pragma region Wave

public:

	UPROPERTY(ReplicatedUsing = OnRep_ChangeWaveTimeLeft)
	uint32 WaveTimeLeft;

	void SetWaveTime(float Value);

	UFUNCTION()
	void OnRep_ChangeWaveTimeLeft();

	UPROPERTY(Replicated)
	uint32 bWaveInProgress : 1;

	UPROPERTY(Replicated)
	uint32 Wave = 0;

	UPROPERTY(Replicated)
	uint32 SubWave = 1;

	UPROPERTY(Replicated)
	uint32 MaxWave = 20;

#pragma endregion Wave

웨이브도 정비 시간과 마찬가지로 똑같이 처리해주었다.

웨이브 로직 수정

기존 10웨이브가 20웨이브로 늘어났고, 커다란 웨이브 한 묶음이 끝나고 몬스터를 '모두' 잡아야 정비시간으로 넘어가게끔 바뀌었기 때문에 수정해 주어야 하고, 서브 웨이브 단계를 기획분들이 밸런싱하기 쉽게 변경하기로 했다(기존에도 고려하여 설계했지만, 조금 더 이해하기 쉽도록 수정).

USTRUCT()
struct FWaveInfo : public FTableRowBase
{
	GENERATED_USTRUCT_BODY()

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite) uint8 bLastSubWave;
	UPROPERTY(EditAnywhere, BlueprintReadWrite) uint8 bLastLargeWave;
	UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Skeleton;
	UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Mummy;
	UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Zombie;
	UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Debuffer;
	UPROPERTY(EditAnywhere, BlueprintReadWrite) float NextWaveLeftTime;
	UPROPERTY(EditAnywhere, BlueprintReadWrite) FText Memo;
};

기존 웨이브 테이블의 bUseThisWave 변수를 삭제하고, 서브웨이브의 마지막과 웨이브의 한 묶음(1~4웨이브)의 마지막을 알 수 있는 변수들을 추가해주었다.

void ATrapperGameMode::WaveStart()
{
	UE_LOG(LogQuest, Warning, TEXT("Wave %d-%d"), TrapperGameState->Wave, TrapperGameState->SubWave);

	ALevelScriptActor* LevelScriptActor = GetWorld()->GetLevelScriptActor();
	ATrapperScriptActor* MyLevelScriptActor = Cast<ATrapperScriptActor>(LevelScriptActor);
	if (MyLevelScriptActor)
	{
		MyLevelScriptActor->MulticastRPCPlaySystemSound(ESystemSound::WaveStart);
	}

	FWaveInfo CurrentWaveData;
	if (GetWaveData(CurrentWaveData))
	{
		SpawnMonster(CurrentWaveData);
	}
	else
	{
		UE_LOG(LogQuest, Warning, TEXT("Wave Data Error"));
	}

	TrapperGameState->bWaveInProgress = true;
	TrapperGameState->SetWaveTime(CurrentWaveData.NextWaveLeftTime);
	TrapperGameState->SetWaveInfo();

	if (CurrentWaveData.bLastLargeWave == true)
	{
		TrapperGameState->SubWave = 1;
		TrapperGameState->bWaveInProgress = false;
		TrapperGameState->SetWaveTime(0.f);
		//SetGameProgress(EGameProgress::Maintenance);
		//SetSkipIcon(true);
		return;
	}

	if (CurrentWaveData.bLastSubWave == true)
	{
		// 다음 웨이브로 넘어감
		TrapperGameState->Wave++;
		TrapperGameState->SubWave = 1;
		SetGameProgress(EGameProgress::Wave);
		return;
	}

	FTimerHandle WaveTimerHandle;
	GetWorldTimerManager().SetTimer(WaveTimerHandle, FTimerDelegate::CreateLambda([&]
		{
			// 서브 웨이브 계속 진행
			TrapperGameState->SubWave++;
			WaveStart();
		}
	), 1.0f, false, CurrentWaveData.NextWaveLeftTime);
}

초반부분은 기존 코드와 거의 동일하다. 만약 이전에 진행한 웨이브가 Large Wave 였다면 더이상 웨이브를 진행하지 않고 리턴하도록 했다. 마지막 Sub Wave일 경우 Wave를 증가시키고 Sub Wave를 1로 초기화한다. 모두 아닌 경우 타이머를 설정하고 Sub Wave를 증가시키고 재귀하도록 했다. 확실히 이전 로직보다는 깔끔한 느낌이다.

void ATrapperGameMode::GetThisWaveRemainingMonsterCount()
{
	bool ThisLargeWaveEnd = false;

	while(!ThisLargeWaveEnd)
	{
		for (uint32 k = 1; k < 6; k++)
		{
			FString WaveText = TEXT("Wave") + FString::FromInt(RemainingMonsterCountWave) + TEXT("_") + FString::FromInt(k);
			FWaveInfo* Data = WaveData->FindRow<FWaveInfo>(*WaveText, FString());
			if (!Data) continue;

			uint32 TotalMonster = 0;

			TotalMonster += Data->Skeleton;
			TotalMonster += Data->Mummy;
			TotalMonster += Data->Zombie;
			TotalMonster += Data->Debuffer;
			TotalMonster += Data->Boss;

			TrapperGameState->ChangeRemainingMonsterCount(TotalMonster);
			//UE_LOG(LogQuest, Warning, TEXT("This Wave %d-%d, Total Spawn Monster %d"), RemainingMonsterCountWave, k, TotalMonster);

			if (Data->bLastLargeWave)
			{
				ThisLargeWaveEnd = true;
			}
		}

		RemainingMonsterCountWave++;
	}
}

개인적으로는 데이터 구성을 바꿈으로써 변경된 이 함수가 마음에 들었다. 기존에는 다섯개 웨이브 단위로 하드코딩 되어있어서 혹시라도 웨이브 단위가 바뀌면 변경해주어야 했는데, Large Wave가 끝나면 계산을 끝내도록 해주었기 때문에 웨이브 단위가 모두 달라져도 상관이 없어졌다.

정상적으로 진행되고 있는 웨이브 테스트 스크린샷 :)

몬스터가 모두 잡히면 정비 시간으로 넘어가도록 하기

정비 단계와 웨이브 단계 모두 퀘스트가 있다. 정비단계 시간이 끝나거나 스킵했을 때 / 모든 몬스터 처치가 끝나면 퀘스트 매니저로 각각의 완료 퀘스트 코드를 넘긴 뒤, 퀘스트 매니저에서 다시 이벤트 코드를 게임모드로 보내는 식으로 변경해주려고 한다.

정비단계 퀘스트 완료

void ATrapperGameMode::MaintenanceStart()
{
	// 정비 시간 설정
	TrapperGameState->SetMaintenanceTime(MaintenanceTime);
	TrapperGameState->bMaintenanceInProgress = true;
	TrapperGameState->MaintenanceCount++;
	TrapperGameState->OnRep_ChangeMaintenanceState();

	// 웨이브에 출현할 몬스터 계산
	GetThisWaveRemainingMonsterCount();

	FTimerHandle MaintenanceTimerHandle;
	GetWorldTimerManager().SetTimer(MaintenanceTimerHandle, FTimerDelegate::CreateLambda([&]
		{
			// 정비시간을 스킵했을 경우 리턴
			if (bSkipMaintenance) return;

			SetSkipIcon(false);

			TrapperGameState->OnQuestExecute.Broadcast(99, true);

			// 다음 웨이브 시작
			TrapperGameState->Wave++;
			TrapperGameState->bMaintenanceInProgress = false;
			SetGameProgress(EGameProgress::Wave);
		}
	), 1.0f, false, MaintenanceTime);
}

TrapperGameState->OnQuestExecute.Broadcast(99, true); 델리게이트에 99번 완료 코드를 보내주면, 정비 퀘스트가 완료된다.

void ATrapperGameMode::SkipMaintenance()
{
	bSkipMaintenance = true;
	TrapperGameState->bMaintenanceInProgress = false;

	TrapperGameState->OnQuestExecute.Broadcast(99, true);
	SetSkipIcon(false);

	TrapperGameState->Wave++;
	SetGameProgress(EGameProgress::Wave);
}

SkipMaintenance() 함수에서도 호출하도록 넣어주었다.

웨이브 퀘스트 완료

게임 스테이트에 있는 CurrentMonsterCount(현재 스폰된 몬스터 수) , RemainingMonsterCount(Large Wave동안 스폰될 남은 몬스터 수) 수가 모두 0이 되어야 정비 시간으로 넘어간다.

void ATrapperGameState::ChangeMonsterCount(int32 Count)
{
	if (HasAuthority())
	{
		CurrentMonsterCount += Count;
		OnRep_ChangeCurrentMonster();

		if (CurrentMonsterCount == 0)
		{
			CheckAllWaveMonsterDie();
		}
	}
}

void ATrapperGameState::CheckAllWaveMonsterDie()
{
	if (CurrentMonsterCount == 0 && RemainingMonsterCount == 0)
	{
		OnQuestExecute.Broadcast(98, true);
	}
}

굳이 Tick에서 계속 검사할 필요 없이 남아있는 몬스터 수가 0이 될때마다 두 변수를 모두 체크해서 이벤트를 발생시켜주면 될 것 같으므로, 이렇게 구현해보았다.

아직 Skip UI는 정상적으로 뜨지 않지만, 모든 몬스터가 죽었을 때 퀘스트가 정상적으로 완료되고 정비 단계로 이동하는 것을 볼 수 있다.

퀘스트 완료 함수에 스킵 UI를 띄워주는 코드를 임시로 넣어주었다. 이제 커밋해두고, 게임모드에 이벤트 코드를 받아 처리하는 함수를 만들어 줄 것이다. 그럼 퀘스트 완료 시에 이벤트 코드를 게임모드쪽으로 전송하고, 이벤트 코드에 따라 게임모드는 게임의 진행도를 관리해주면 된다. (이론은 완벽해..)

0개의 댓글