내배캠/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부터 다시 실행하게 된다.
- BeginPlay( ) → StartLevel( ) 호출
- StartLevel( ) 동작
- GameInstance에서 현재 레벨 정보를 가져와 GameState의 CurrentLevelIndex에 저장
- 월드에 스폰되어있는 SpawnVolume을 가져와, SpawnRandomItem( ) 호출
- 스폰된 액터가 코인일 경우 코인 개수 증가
- LevelDuration 제한시간이 지나면 OnLevelTimeUp( ) 호출
- OnLevelTimeUp( ) → EndLevel( ) 호출
- EndLevel( ) 동작
- 타이머 초기화
- AddScore( ) 호출
- GameInstance가 관리하는 TotalScore에 획득한 점수 합산
- 현재 레벨을 증가시키고, 이를 GameInstance에도 반영
- 현재 레벨이 마지막 레벨이면 OnGameOver( ) 호출하여 게임 종료
- 마지막 레벨이 아니라면 UGameplayStatics::OpenLevel( ) 호출하여 다음 레벨로 전환
- 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;
🟢 주요 특징
- 동적 캐스팅 없이 클래스 확인 가능
- 특정 오브젝트 타입에만 로직을 적용할 때 유용