3-5. 게임 루프 설계를 통한 게임 흐름 제어하기
1. GameState를 이용한 게임 루프 구현
언리얼 엔진에서 게임 루프나 전역 상태를 관리할 때 대표적으로 고려되는 클래스 GameState, GameMode
GameMode를 쓰는 이유
- 서버 전용 로직
- 게임 규칙 (팀 배정, 승패 조건, 플레이어 스폰 등)을 서버에서 제어하는 데 사용
- 클라이언트는 GameMode에 직접 접근 X
- 클라이언트도 알아야 하는 정보 (예시: 남은 시간, 현재 점수 등)를 GameMode에만 두면 복잡해짐
멀티플레이를 고려할 때 많이 사용하는 방식
중요 규칙 로직 → GameMode
서버-클라이언트가 공통으로 알아야 하는 상태 → GameState
GameState를 쓰는 이유
- 게임 전반에 걸쳐 모든 플레이어가 공유해야 하는 상태를 담는 클래스
- 전역 상태가 필요할 경우 GameState를 활용
- GameState 객체는 게임이 시작될 때 서버에서 생성
- 클라이언트는 이를 복제 받아서 똑같은 정보를 읽을 수 있음 → 서버와 클라이언트 모두 동일한 정보를 가지게 됨
SpawnVolume 클래스 스폰 데이터 반환 수정
스폰된 아이템의 정보 (코인이 맞는지, 혹은 다른 아이템인지)를 추후 GameState에서 카운팅을 하게 해야함
기존: 스폰 함수가 void를 반환
수정: 스폰 함수가 스폰된 AActor*를 반환해야됨
SpawnRandomItem() 이 스폰된 액터 포인터를 반환하도록 수정, 그 액터가 코인인지 확인할 수 있게 만들기




GameState 기반의 게임 루프 구현
GameStateBase를 상속한 SpartaGameStateBase는 삭제, GameState를 상속한 SpartaGameState를 생성
→ GameMode 버전과의 일관성을 위함
- SpartaGameStateBase.h 와 cpp 에 있는 내용을 SpartaGameState에 복붙 후 수정
- 파일에서 SpartaGameStateBase.h 와 cpp 삭제






맵 전환 (OpenLevel) 시 주의
UGameplayStatics::OpenLevel 호출 → 지금 월드가 제거 → 새로운 맵이 로드 → BeginPlay() 다시 실행
이때 GameState도 새로 생성되기 때문에, 이전 레벨에서 유지하던 변수가 모두 초기화될 수 있음
로직 함수
BeginPlay(): 게임 시작 시 StartLevel() 호출
StartLevel():
- 코인 개수들 초기화 (SpawnedCoinCount=0, CollectedCoinCount=0)
- 스폰 볼륨들을 찾아서 40개 아이템 스폰(반복)
- 만약 SpawnRandomItem()이 ACoinItem을 반환하면 SpawnedCoinCount++
- 30초 타이머 설정 (OnLevelTimeUp 호출)
- OnLevelTimeUp(): 30초가 만료되면 레벨 종료(EndLevel())
- OnCoinCollected(): 코인 아이템을 먹을 때마다 호출
- CollectedCoinCount++
- CollectedCoinCount >= SpawnedCoinCount 이면, 즉시 EndLevel()
- EndLevel()
- 현재 레벨 타이머 정리
- CurrentLevelIndex++
- 만약 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를 활용한 데이터 유지하기
레벨 전환 시 맵 내에서 생성된 대부분의 객체가 처음부터 다시 생성이 됨
이전 레벨에서 획득한 점수, 플레이어 상태 등을 모든 레벨에 걸쳐 유지하고 싶을 때 쓰는 방법
- Game Instance
- 프로젝트가 시작될 때부터 애플리케이션이 완전히 종료될 때까지 유일하게 계속 살아있는 객체
- 맵이 전환되어도 파괴 X, 여기서 전역 데이터를 유지 가능
- Seamless Travel
- 멀티플레이 환경에서 주로 사용되는 레벨 전환 방식
- GameState, PlayerController 등을 파괴하지 않고 그대로 다음 맵으로 넘어가는 기능
- 대부분의 객체를 유지할 수 있지만, 설정과 로직이 조금 더 복잡함
- 싱글 플레이 전용 간단 프로젝트라면 GameInstance 를 사용하기 쉬움
Game Instance 생성 및 변수 선언
C++ Class → Game Instance 생성


인스턴스 값을 기존에 스테이트에 추가
// 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 선택
전체 게임 루프 요약
- 게임 실행
- GameInstance 생성, GameMode/GameState 생성, 첫 레벨 로드
- BeginPlay()
- ASpartaGameState::BeginPlay() → StartLevel()
- 스폰 볼륨(SpawnVolume)에서 40개 아이템 스폰
- 코인 개수 추적(SpawnedCoinCount)
- 30초 타이머 시작
- 플레이어가 코인 획득
- CoinItem::ActivateItem()에서 GameState->AddScore(), OnCoinCollected()
- 모든 코인을 모으면 즉시 EndLevel()
- 레벨 종료
- EndLevel()에서 CurrentLevelIndex++
- 남은 레벨이 있으면 UGameplayStatics::OpenLevel(...)로 다음 맵 로드
- 더 이상 레벨이 없으면 OnGameOver()
- 다음 맵 로드 시
- 새로운 GameState가 생성 → 다시 BeginPlay() → StartLevel()
- 이전 레벨에서 유지하고 싶은 정보는 GameInstance나 “Seamless Travel” 등을 통해 별도로 관리해야 함
- Game Over
- 로그 출력 (추후 UI 표시로 전환)
'C++와 Unreal Engine으로 3D 게임 개발' 카테고리의 다른 글
| C++와 Unreal Engine으로 3D 게임 개발 4-2 (0) | 2026.06.23 |
|---|---|
| C++와 Unreal Engine으로 3D 게임 개발 4-1 (0) | 2026.06.23 |
| C++와 Unreal Engine으로 3D 게임 개발 3-4 (0) | 2026.06.21 |
| C++와 Unreal Engine으로 3D 게임 개발 3-3 (0) | 2026.06.20 |
| C++와 Unreal Engine으로 3D 게임 개발 3-2 (0) | 2026.06.19 |