C++와 Unreal Engine으로 3D 게임 개발

C++와 Unreal Engine으로 3D 게임 개발 3-5

jh009 2026. 6. 22. 12:13

3-5. 게임 루프 설계를 통한 게임 흐름 제어하기

1. GameState를 이용한 게임 루프 구현

 

언리얼 엔진에서 게임 루프나 전역 상태를 관리할 때 대표적으로 고려되는 클래스 GameState, GameMode

 

GameMode를 쓰는 이유

  • 서버 전용 로직
  • 게임 규칙 (팀 배정, 승패 조건, 플레이어 스폰 등)을 서버에서 제어하는 데 사용
  • 클라이언트는 GameMode에 직접 접근 X
  • 클라이언트도 알아야 하는 정보 (예시: 남은 시간, 현재 점수 등)를 GameMode에만 두면 복잡해짐

멀티플레이를 고려할 때 많이 사용하는 방식

중요 규칙 로직 → GameMode

서버-클라이언트가 공통으로 알아야 하는 상태 → GameState

 

GameState를 쓰는 이유

  • 게임 전반에 걸쳐 모든 플레이어가 공유해야 하는 상태를 담는 클래스
  • 전역 상태가 필요할 경우 GameState를 활용
  • GameState 객체는 게임이 시작될 때 서버에서 생성
  • 클라이언트는 이를 복제 받아서 똑같은 정보를 읽을 수 있음 → 서버와 클라이언트 모두 동일한 정보를 가지게 됨

SpawnVolume 클래스 스폰 데이터 반환 수정

스폰된 아이템의 정보 (코인이 맞는지, 혹은 다른 아이템인지)를 추후 GameState에서 카운팅을 하게 해야함

 

기존: 스폰 함수가 void를 반환

수정: 스폰 함수가 스폰된 AActor*를 반환해야됨

 

SpawnRandomItem() 이 스폰된 액터 포인터를 반환하도록 수정, 그 액터가 코인인지 확인할 수 있게 만들기

SpawnVolume.h

 

SpawnVolume.cpp


GameState 기반의 게임 루프 구현

GameStateBase를 상속한 SpartaGameStateBase는 삭제, GameState를 상속한 SpartaGameState를 생성

→ GameMode 버전과의 일관성을 위함

  • SpartaGameStateBase.h 와 cpp 에 있는 내용을 SpartaGameState에 복붙 후 수정
  • 파일에서 SpartaGameStateBase.h 와 cpp 삭제

SpartaGameState.h

 

SpartaGameState.cpp

 

맵 전환 (OpenLevel) 시 주의

UGameplayStatics::OpenLevel 호출 → 지금 월드가 제거 → 새로운 맵이 로드 → BeginPlay() 다시 실행

이때 GameState도 새로 생성되기 때문에, 이전 레벨에서 유지하던 변수가 모두 초기화될 수 있음

 

로직 함수

BeginPlay(): 게임 시작 시 StartLevel() 호출

 

StartLevel():

  1. 코인 개수들 초기화 (SpawnedCoinCount=0, CollectedCoinCount=0)
  2. 스폰 볼륨들을 찾아서 40개 아이템 스폰(반복)
    • 만약 SpawnRandomItem()이 ACoinItem을 반환하면 SpawnedCoinCount++
  3. 30초 타이머 설정 (OnLevelTimeUp 호출)
  • OnLevelTimeUp(): 30초가 만료되면 레벨 종료(EndLevel())
  • OnCoinCollected(): 코인 아이템을 먹을 때마다 호출
    • CollectedCoinCount++
    • CollectedCoinCount >= SpawnedCoinCount 이면, 즉시 EndLevel()
  • EndLevel()
    1. 현재 레벨 타이머 정리
    2. CurrentLevelIndex++
    3. 만약 CurrentLevelIndex >= MaxLevels 이면 OnGameOver()
      • 아니면 다음 레벨 StartLevel()
  • OnGameOver()
    • GameOver 로그 출력 (혹은 UI 호출 등)

코인 아이템 점수 획득 로직 수정

 

CoinItem → 플레이어가 닿았을 때 (ActivateItem) 점수를 획득하고 자기 자신을 제거하는 구조

여기서 추가로 코인을 하나 더 먹었다고 GameState에게 알려야 함

// CoinItem.cpp
#include "CoinItem.h"
#include "Engine/World.h"
#include "SpartaGameState.h"

ACoinItem::ACoinItem()
{
		PointValue = 0;
		ItemType = "DefaultCoin";
}

void ACoinItem::ActivateItem(AActor* Activator)
{
	if (Activator && Activator->ActorHasTag("Player"))
	{
		if (UWorld* World = GetWorld())
		{
			if (ASpartaGameState* GameState = World->GetGameState<ASpartaGameState>())
			{
				GameState->AddScore(PointValue);
				// 추가
				GameState->OnCoinCollected();
			}
		}
		
		DestroyItem();
	}
}

2. Game Instance를 활용한 데이터 유지하기

레벨 전환 시 맵 내에서 생성된 대부분의 객체가 처음부터 다시 생성이 됨

 

이전 레벨에서 획득한 점수, 플레이어 상태 등을 모든 레벨에 걸쳐 유지하고 싶을 때 쓰는 방법

  1. Game Instance
    • 프로젝트가 시작될 때부터 애플리케이션이 완전히 종료될 때까지 유일하게 계속 살아있는 객체
    • 맵이 전환되어도 파괴 X, 여기서 전역 데이터를 유지 가능
  2. Seamless Travel
    • 멀티플레이 환경에서 주로 사용되는 레벨 전환 방식
    • GameState, PlayerController 등을 파괴하지 않고 그대로 다음 맵으로 넘어가는 기능
    • 대부분의 객체를 유지할 수 있지만, 설정과 로직이 조금 더 복잡함
    • 싱글 플레이 전용 간단 프로젝트라면 GameInstance 를 사용하기 쉬움

Game Instance 생성 및 변수 선언

C++ Class → Game Instance 생성

SpartaGameInstance.h
SpartaGameInstance.cpp


인스턴스 값을 기존에 스테이트에 추가

// SpartaGameState.cpp

#include "SpartaGameInstance.h" // 추가

// 삭제
// Score += Amount;                    
// UE_LOG(LogTemp, Warning, TEXT("Score: %d"), Score); 

void ASpartaGameState::AddScore(int32 Amount)
{
	// 추가
	if (UGameInstance* GameUnstance = GetGameInstance())
    {
    	USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GameInstance);
        if(SpartaGameInstance)
        {
        	SpartaGameInstance->AddToScore(Amount);
        }
    }
}

void ASpartaGameState::StartLevel()
// CurrentLevelIndex 추가
{
	if (UGameInstance* GameUnstance = GetGameInstance())
    {
    	USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GameInstance);
        if(SpartaGameInstance)
        {
        	CurrentLevelIndex = SpartaGameInstance->CurrentLevelIndex;
        }
    }
.
.
.
void ASpartaGameState::EndLevel()
{		
	GetWorldTimerManager().ClearTimer(LevelTimerHandle);
	
    // 추가 및 수정
	if (UGameInstance* GameUnstance = GetGameInstance())
    {
    	USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GameInstance);
        if(SpartaGameInstance)
        {
        	AddScore(Score);
            CurrentLevelIndex++;
        	SpartaGameInstance->CurrentLevelIndex = CurrentLevelIndex
        }
    }

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

 

빌드 이후 

BP_SpartaGameInstance 생성

 

Project Settings → Maps & Modes → 맨 아래 Game Instance → BP_SpartaGameInstance 선택


전체 게임 루프 요약

  1. 게임 실행
    • GameInstance 생성, GameMode/GameState 생성, 첫 레벨 로드
  2. BeginPlay()
    • ASpartaGameState::BeginPlay() → StartLevel()
    • 스폰 볼륨(SpawnVolume)에서 40개 아이템 스폰
    • 코인 개수 추적(SpawnedCoinCount)
    • 30초 타이머 시작
  3. 플레이어가 코인 획득
    • CoinItem::ActivateItem()에서 GameState->AddScore(), OnCoinCollected()
    • 모든 코인을 모으면 즉시 EndLevel()
  4. 레벨 종료
    • EndLevel()에서 CurrentLevelIndex++
    • 남은 레벨이 있으면 UGameplayStatics::OpenLevel(...)로 다음 맵 로드
    • 더 이상 레벨이 없으면 OnGameOver()
  5. 다음 맵 로드 시
    • 새로운 GameState가 생성 → 다시 BeginPlay() → StartLevel()
    • 이전 레벨에서 유지하고 싶은 정보는 GameInstance나 “Seamless Travel” 등을 통해 별도로 관리해야 함
  6. Game Over
    • 로그 출력 (추후 UI 표시로 전환)