내배캠/Unreal Engine

Game State와 Game Mode

동그래님 2025. 2. 4. 20:36

 

 

언리얼 엔진에서 게임의 흐름과 데이터 관리를 위한 전역적 상태를 다루는 클래스에는 보통 Game State와 Game Mode 두 가지로 나뉘게 된다.

아래에서 GameState와 GameMode 그리고 게임의 레벨 간 데이터를 유지하는데 사용되는 GameInstance까지 정리하려한다.
그리고 싱글 플레이에 적합한 GameState를 예시로 추가적인 정리를 하겠다.

 

📌 GameState

GameState는 현재 게임의 전역 상태를 관리하는 클래스다. 클라이언트와 서버 간에 동기화되는 상태 정보를 담고 있다.

레벨이 전환될 때마다 새로 GameState가 생기기 때문에, 레벨이 전환되더라도 유지되어야 하는 데이터는 GameInstance를 사용한다.

📍 GameState에서 주로 관리하는 데이터

  • 점수, 플레이어 레벨, 남은 시간, 게임 종료 여부 등의 정보
  • 모든 플레이어에게 공유되어야 하는 전역 상태

🟢 GameState를 사용하는 이유

  • 게임 상태(점수, 시간 등)를 모든 클라이언트가 동일하게 참조 가능
  • 싱글플레이에서도 게임의 흐름을 관리하는 데 유용

📌 GameMode

GameMode는 게임 규칙과 로직을 정의하는 서버 전용 클래스다.
게임의 흐름과 승패 조건, 규칙을 제어하는 핵심 역할을 하며, 주로 서버에서 실행된다.

🔴 클라이언트가 GameMode에 접근할 수 없는 이유

  • GameMode는 서버에서만 동작하기 때문에 클라이언트에서 접근하려 하면 값이 동기화되지 않아 복잡한 문제가 생길 수 있다.
  • 따라서, 클라이언트도 알아야 할 정보는 GameState에 저장하는 것이 일반적이다.

📌 GameInstance

GameInstance는 게임의 전역 싱글톤 객체로, 레벨 간 데이터를 유지하는 데 사용된다.
레벨 전환 시에도 삭제되지 않기 때문에 전체 게임 데이터를 저장하는 데 적합하다.

📍 GameInstance에서 주로 관리하는 데이터

  • 총 점수, 진행 중인 레벨 정보, 설정 데이터 등
  • 한 번 초기화되면 게임이 종료될 때까지 유지된다.

🟢 싱글플레이어에서 유용한 이유

  • 한 레벨에서 다른 레벨로 넘어가더라도 데이터를 유지
  • 중간 저장 및 글로벌 데이터 관리에 적합

 


 

📌 GameState.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameState.h"
#include "SZ_GameState.generated.h"

UCLASS()
class STRIKEZONE_API ASZ_GameState : public AGameState
{
	GENERATED_BODY()
	
public:
	ASZ_GameState();

	virtual void BeginPlay() override;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Score")
	int32 Score;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Coin")
	int32 SpawnedCoinCount;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Coin")
	int32 CollectedCoinCount;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Level")
	float LevelDuration;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Level")
	int32 CurrentLevelIndex;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Level")
	int32 MaxLevels;
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Level")
	TArray<FName> LevelMapNames;

	FTimerHandle LevelTimerHandle;

	UFUNCTION(BlueprintPure, Category = "Score")
	int32 GetScore() const { return Score; }
	UFUNCTION(BlueprintCallable, Category = "Score")
	void AddScore(int32 Amount);
	UFUNCTION(BlueprintCallable, Category = "Level")
	void OnGameOver();

	void StartLevel();
	void OnLevelTimeUp();
	void OnCoinCollected();
	void EndLevel();
};

 

 

📌 GameState.cpp

#include "SZ_GameState.h"
#include "Kismet/GameplayStatics.h"
#include "ItemSpawnVolume.h"
#include "CoinItem.h"
#include "SZ_GameInstance.h"

ASZ_GameState::ASZ_GameState()
{
	Score = 0;
	SpawnedCoinCount = 0;
	CollectedCoinCount = 0;
	LevelDuration = 30.0f;
	CurrentLevelIndex = 0;
	MaxLevels = 3;
}

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

	StartLevel();
}

void ASZ_GameState::AddScore(int32 Amount)
{
	if (UGameInstance* GameInstance = GetGameInstance())
	{
		USZ_GameInstance* SZ_GameInstance = Cast<USZ_GameInstance>(GameInstance);
		if (SZ_GameInstance)
		{
			SZ_GameInstance->AddToScore(Amount);
		}
	}
}

void ASZ_GameState::StartLevel()
{
	if (UGameInstance* GameInstance = GetGameInstance())
	{
		USZ_GameInstance* SZ_GameInstance = Cast<USZ_GameInstance>(GameInstance);
		if (SZ_GameInstance)
		{
			CurrentLevelIndex = SZ_GameInstance->CurrentLevelIndex;
		}
	}
	SpawnedCoinCount = 0;
	CollectedCoinCount = 0;

	TArray<AActor*> FoundVolumes;
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), AItemSpawnVolume::StaticClass(), FoundVolumes);

	const int32 ItemToSpawn = 40;
	for (int32 i = 0; i < ItemToSpawn; ++i)
	{
		if (FoundVolumes.Num() > 0)
		{
			AItemSpawnVolume* SpawnVolume = Cast<AItemSpawnVolume>(FoundVolumes[0]);
			if (SpawnVolume)
			{
				AActor* SpawnedActor = SpawnVolume->SpawnRandomItem();
				if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass()))
				{
					SpawnedCoinCount++;
				}

			}
		}
	}

	GetWorldTimerManager().SetTimer
	(
		LevelTimerHandle,
		this,
		&ASZ_GameState::OnLevelTimeUp,
		LevelDuration,
		false
	);
}

void ASZ_GameState::OnLevelTimeUp()
{
	EndLevel();
}

void ASZ_GameState::OnCoinCollected()
{
	CollectedCoinCount++;
	
	if (SpawnedCoinCount > 0 && CollectedCoinCount >= SpawnedCoinCount)
	{
		EndLevel();
	}
}

void ASZ_GameState::EndLevel()
{
	GetWorldTimerManager().ClearTimer(LevelTimerHandle);

	if (UGameInstance* GameInstance = GetGameInstance())
	{
		USZ_GameInstance* SZ_GameInstance = Cast<USZ_GameInstance>(GameInstance);
		if (SZ_GameInstance)
		{
			AddScore(Score);
			CurrentLevelIndex++;
			SZ_GameInstance->CurrentLevelIndex = CurrentLevelIndex;
		}
	}

	if (CurrentLevelIndex >= MaxLevels)
	{
		OnGameOver();
		return;
	}
	
	if (LevelMapNames.IsValidIndex(CurrentLevelIndex))
	{
		UGameplayStatics::OpenLevel(GetWorld(), LevelMapNames[CurrentLevelIndex]);
	}
	else
	{
		OnGameOver();
	}
}

void ASZ_GameState::OnGameOver()
{
	// 게임 종료 로직
}

 

✅ GameState 동작 흐름

레벨이 전환되면 GameState가 새롭게 생성되며 BeginPlay부터 다시 실행하게 된다.
  1. BeginPlay( ) → StartLevel( ) 호출
  2. StartLevel( ) 동작
    • GameInstance에서 현재 레벨 정보를 가져와 GameState의 CurrentLevelIndex에 저장
    • 월드에 스폰되어있는 SpawnVolume을 가져와, SpawnRandomItem( ) 호출
    • 스폰된 액터가 코인일 경우 코인 개수 증가
    • LevelDuration 제한시간이 지나면 OnLevelTimeUp( ) 호출
  3. OnLevelTimeUp( ) → EndLevel( ) 호출
  4. EndLevel( ) 동작
    • 타이머 초기화
    • AddScore( ) 호출
      • GameInstance가 관리하는 TotalScore에 획득한 점수 합산
    • 현재 레벨을 증가시키고, 이를 GameInstance에도 반영
    • 현재 레벨이 마지막 레벨이면 OnGameOver( ) 호출하여 게임 종료
    • 마지막 레벨이 아니라면 UGameplayStatics::OpenLevel( ) 호출하여 다음 레벨로 전환
  5. OnGameOver( ) 동작
    • 게임 종료 로직

 

✅ Coin 클래스에서 호출하는 함수

AddScore( ): Coin 클래스에서 GameInstance에서 관리하는 TotalScore에 획득한 점수 합산 로직 호출
OnCoinCollected( ): Coin 클래스에서 코인 획득 개수 증가 및 레벨에 있는 코인 모두 획득시 레벨 종료 로직 호출
// 코인에 오버랩된 객체가 Player일 때 호출되는 함수
void ACoinItem::ActivateItem(AActor* Activator)
{
	if (Activator && Activator->ActorHasTag("Player"))
	{
		if (UWorld* World = GetWorld())
		{
			if (ASZ_GameState* GameState = World->GetGameState<ASZ_GameState>())
			{
				GameState->AddScore(PointValue);
				GameState->OnCoinCollected();
			}
		}
		DestroyItem();
	}
}

 


 

📌 사용했던 언리얼 주요 함수 정리

 

🔎 UGameplayStatics 클래스 관련 메서드

UGameplayStatics는 게임 실행 중 유용하게 사용할 수 있는 정적 유틸리티 함수들을 제공한다.

 

📍 UGameplayStatics::GetAllActorsOfClass

static void GetAllActorsOfClass(const UObject* WorldContextObject, TSubclassOf<AActor> ActorClass, TArray<AActor*>& OutActors);

 

🟢 주요 특징

  • 주어진 특정 클래스 타입의 모든 액터를 월드에서 가져와 배열에 저장한다.
  • 월드에 존재하는 모든 액터를 대상으로 검색하므로 성능에 주의해야 한다.

 

📍 UGameplayStatics::OpenLevel

static bool OpenLevel(const UObject* WorldContextObject, FName LevelName, bool bAbsolute = true, FString Options = FString());

🟢 주요 특징

  • 주어진 레벨 이름에 따라 레벨 전환
  • 옵션을 설정해 추가적인 파라미터 전달 가능

 

🔎AActor 관련 메서드

📍 AActor::Is A

bool IsA(UClass* Class) const;

🟢 주요 특징

  • 동적 캐스팅 없이 클래스 확인 가능
  • 특정 오브젝트 타입에만 로직을 적용할 때 유용