게임 루프 설계를 통한 게임 흐름 제어하기
GameState를 이용한 게임 루프 구현하기
게임루프: 보통 게임의 핵심적인 흐름을 얘기합니다. 즉 게임이 시작할 때부터 종료까지 수행하는 단계들
게임플로우: 게임을 시작해서 진행하고 끝내는 과정에서 발생하는 모든 상호작용들
1️⃣ GameState와 GameMode
언리얼 엔진에서 게임 루프나 전역 상태를 관리할 때 대표적으로 고려되는 클래스는 GameState와 GameMode가 있습니다.
- GameMode를 쓰는 이유는?
- 서버 전용 로직을 담는 곳이며, 게임 규칙 (팀 배정, 승패 조건, 플레이어 스폰 등)을 서버에서 제어하는 데 사용됩니다.
- 클라이언트는 GameMode에 직접 접근할 수 없습니다. 따라서 클라이언트도 알아야 하는 정보(예: 남은 시간, 현재 점수 등)를 GameMode에만 두면 복잡해집니다.
- 보통 멀티플레이를 고려한다면, “중요 규칙 로직”만 GameMode에 두고, “서버-클라이언트가 공통으로 알아야 하는 상태”는 GameState에 두는 방식을 많이 사용합니다.
- GameState를 쓰는 이유는?
- 게임 전반에 걸쳐 모든 플레이어가 공유해야 하는 상태를 담는 클래스입니다. 보통 전역 상태가 필요할 경우 GameState를 활용합니다.
- GameState 객체는 게임이 시작될 때 서버에서 생성되고, 클라이언트는 이를 복제받아서 똑같은 정보를 읽을 수 있습니다. 즉, “서버와 클라이언트 모두” 동일한 정보를 가지게 됩니다.
- 이번 강의에서는 레벨마다 40개의 아이템을 스폰하고, 시간 제한 30초 내에 플레이어가 모든 코인을 주우면 즉시 레벨이 끝나고 다음 레벨로 넘어가는 구조를 만들 것입니다. 이처럼 멀티플레이를 고려했을 때도 클라이언트가 알아야 할 상태가 많아지므로, GameMode 대신 GameState를 선택하여 전역 상태를 관리하는 방식으로 구현합니다.
2️⃣ SpawnVolume 클래스 스폰 데이터 반환 수정
- 기존에는 스폰 함수가 void를 반환했는데, 스폰된 아이템의 정보 (코인이 맞는지, 혹은 다른 아이템인지)를 추후 GameState에서 카운팅 하려면, 스폰 함수가 스폰된 AActor*를 반환해야 합니다.
- 아래 코드에서는 SpawnRandomItem()이 스폰된 액터 포인터를 반환하도록 수정하고, 그 액터가 코인인지 확인할 수 있게 만듭니다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemSpawnRow.h" // 우리가 정의한 구조체
#include "SpawnVolume.generated.h"
class UBoxComponent;
UCLASS()
class BC_CH3_ASSIGNMENT_5_API ASpawnVolume : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ASpawnVolume();
UFUNCTION(BlueprintCallable, Category = "Spawning")
AActor* SpawnRandomItem(); // 리턴 형식을 AActor* 로 변경
protected:
//
FItemSpawnRow* GetRandomItem() const;
// === Components
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Spawning")
USceneComponent* Scene;
// 스폰 영역을 담당할 박스 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Spawning")
UBoxComponent* SpawningBox;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawning")
UDataTable* ItemDataTable;
// === Functions
// 특정 아이템 클래스를 스폰하는 함수
UFUNCTION(BlueprintCallable, Category="Spawning")
AActor* SpawnItem(TSubclassOf<AActor> ItemClass); // 리턴 형식을 AActor* 로 변경
// 스폰 볼륨 내부에서 무작위 좌표를 얻어오는 함수
UFUNCTION(BlueprintCallable, Category="Spawning")
FVector GetRandomPointInVolume() const;
};
#include "SpawnVolume.h"
#include "Components/BoxComponent.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
ASpawnVolume::ASpawnVolume()
{
PrimaryActorTick.bCanEverTick = false;
// 박스 컴포넌트를 생성하고, 이 액터의 루트로 설정
Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
SetRootComponent(Scene);
SpawningBox = CreateDefaultSubobject<UBoxComponent>(TEXT("SpawningBox"));
SpawningBox->SetupAttachment(Scene);
}
AActor* ASpawnVolume::SpawnRandomItem()
{
if (FItemSpawnRow* SelectedRow = GetRandomItem())
{
if (UClass* ActualClass = SelectedRow->ItemClass.Get())
{
return SpawnItem(ActualClass);
}
}
return nullptr;
}
FVector ASpawnVolume::GetRandomPointInVolume() const
{
// 1) 박스 컴포넌트의 스케일된 Extent, 즉 x/y/z 방향으로 반지름(절반 길이)을 구함
FVector BoxExtent = SpawningBox->GetScaledBoxExtent();
// 2) 박스 중심 위치
FVector BoxOrigin = SpawningBox->GetComponentLocation();
// 3) 각 축별로 -Extent ~ +Extent 범위의 무작위 값 생성
return BoxOrigin + FVector(
FMath::FRandRange(-BoxExtent.X, BoxExtent.X),
FMath::FRandRange(-BoxExtent.Y, BoxExtent.Y),
FMath::FRandRange(-BoxExtent.Z, BoxExtent.Z)
);
}
FItemSpawnRow* ASpawnVolume::GetRandomItem() const
{
if (!ItemDataTable)
return nullptr;
// 1) 모든 Row(행) 가져오기
TArray<FItemSpawnRow*> AllRows;
static const FString ContextString(TEXT("ItemSpawnContext")); // 디버깅 용도
ItemDataTable->GetAllRows(ContextString, AllRows);
if (AllRows.IsEmpty()) return nullptr;
// 2) 전체 확률 합 구하기
float TotalChance = 0.0f; // 초기화
for (const FItemSpawnRow* Row : AllRows) // AllRows 배열의 각 Row를 순회
{
if (Row) // Row가 유효한지 확인
{
TotalChance += Row->SpawnChance; // SpawnChance 값을 TotalChance에 더하기
}
}
// 3) 0 ~ TotalChance 사이 랜덤 값
const float RandValue = FMath::FRandRange(0.0f, TotalChance);
float AccumulateChance = 0.0f; // 랜덤 값 보정
//Algo::Accumulate( AllRows, AccumulateChance)
// 4) 누적 확률로 아이템 선택
for (FItemSpawnRow* Row : AllRows)
{
AccumulateChance += Row->SpawnChance;
if (RandValue <= AccumulateChance)
{
return Row;
}
}
return nullptr;
}
AActor* ASpawnVolume::SpawnItem(TSubclassOf<AActor> ItemClass)
{
if (!ItemClass) return nullptr;
// SpawnActor가 성공하면 스폰된 액터의 포인터가 반환됨
AActor* SpawnedActor = GetWorld()->SpawnActor<AActor>(
ItemClass,
GetRandomPointInVolume(),
FRotator::ZeroRotator
);
return SpawnedActor;
}
3️⃣ GameState 기반의 게임 루프 구현
- 게임 흐름 (3레벨, 각 레벨당 30초, 코인 모두 획득 시 즉시 다음 레벨로 이동, 3 레벨이 끝나면 Game Over)의 로직을 관리하도록 변경합니다.
- 레벨 시작 시 40개 아이템을 소환하며, 그중 코인 아이템이 몇 개 생성되었는지 추적(SpawnedCoinCount)하고, 플레이어가 먹은 코인 개수를 추적(CollectedCoinCount)하여 “모두 먹었다면” 즉시 레벨 종료합니다.
- 맵 전환 (OpenLevel) 시 주의
- UGameplayStatics::OpenLevel을 호출하면 지금 월드가 언로드 (제거) 되고, 새로운 맵이 로드되면서 BeginPlay()가 다시 실행됩니다. 이때 GameState도 새로 생성되기 때문에, 이전 레벨에서 유지하던 변수가 모두 초기화될 수 있습니다.
- 로직 함수
- BeginPlay(): 게임 시작 시 StartLevel() 호출
- StartLevel():
- 코인 개수들 초기화(SpawnedCoinCount=0, CollectedCoinCount=0)
- 스폰 볼륨들을 찾아서 40개 아이템 스폰(반복).
- 만약 SpawnRandomItem()이 ACoinItem을 반환하면 SpawnedCoinCount++
- 30초 타이머 설정 (OnLevelTimeUp 호출)
- UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASpawnVolume::StaticClass(), FoundVolumes);
- GetAllActorsOfClass(): 현재 월드에서 이 액터에 해당되는 것들을 모두 가져오는 함수
- #include "Kismet/GameplayStatics.h" 추가 해줘야합니다
- TArray<AActor*> FoundVolumes;
- IsA() : 해당 포인터가 찾는 게 맞는지 확인. (하위 클래스도 같이 확인해 줍니다)
- OnLevelTimeUp(): 30초가 만료되면 레벨 종료(EndLevel())
- OnCoinCollected(): 코인 아이템을 먹을 때마다 호출.
- CollectedCoinCount++
- CollectedCoinCount >= SpawnedCoinCount 이면, 즉시 EndLevel()
- GetWorldTimerManager().ClearTimer: 타이머 초기화
- EndLevel()
- 현재 레벨 타이머 정리
- CurrentLevelIndex++
- 만약 CurrentLevelIndex >= MaxLevels 이면 OnGameOver()
- 아니면 다음 레벨 StartLevel()
- OnGameOver()
- GameOver 로그 출력 (혹은 UI 호출 등)
- 추후 RestartGame 등 처리(블루프린트나 GameMode에서 구현)
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameState.h"
#include "MainGameState.generated.h"
UCLASS()
class BC_CH3_ASSIGNMENT_5_API AMainGameState : public AGameState
{
GENERATED_BODY()
public:
AMainGameState();
virtual void BeginPlay() override;
// === Variables ===
// 전역 점수를 저장하는 변수
UPROPERTY(VisibleAnywhere, 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;
// === Func ===
// 현재 점수를 읽는 함수
UFUNCTION(BlueprintPure, Category="Score")
int32 GetScore() const;
// 점수를 추가해주는 함수
UFUNCTION(BlueprintCallable, Category="Score")
void AddScore(int32 Amount);
UFUNCTION(BlueprintCallable, Category = "Level")
void OnGameOver();
// 레벨을 시작할 때, 아이템 스폰 및 타이머 설정
void StartLevel();
// 레벨 제한 시간이 만료되었을 때 호출
void OnLevelTimeUp();
// 코인을 주웠을 때 호출
void OnCoinCollected();
// 레벨을 강제 종료하고 다음 레벨로 이동
void EndLevel();
};
#include "MainGameState.h"
#include "Kismet/GameplayStatics.h"
#include "SpawnVolume.h"
#include "CoinItem.h"
AMainGameState::AMainGameState()
{
Score = 0;
SpawnedCoinCount = 0;
CollectedCoinCount = 0;
LevelDuration = 30.0f; // 한 레벨당 30초
CurrentLevelIndex = 0;
MaxLevels = 3;
}
void AMainGameState::BeginPlay()
{
Super::BeginPlay();
// 게임 시작 시 첫 레벨부터 진행
StartLevel();
}
int32 AMainGameState::GetScore() const
{
return Score;
}
void AMainGameState::AddScore(int32 Amount)
{
Score += Amount;
}
void AMainGameState::StartLevel()
{
// 레벨 시작 시, 코인 개수 초기화
SpawnedCoinCount = 0;
CollectedCoinCount = 0;
// 현재 맵에 배치된 모든 SpawnVolume을 찾아 아이템 40개를 스폰
TArray<AActor*> FoundVolumes;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASpawnVolume::StaticClass(), FoundVolumes);
const int32 ItemToSpawn = 40;
for (int32 i = 0; i < ItemToSpawn; i++)
{
if (FoundVolumes.Num() > 0)
{
ASpawnVolume* SpawnVolume = Cast<ASpawnVolume>(FoundVolumes[0]);
if (SpawnVolume)
{
AActor* SpawnedActor = SpawnVolume->SpawnRandomItem();
// 만약 스폰된 액터가 코인 타입이라면 SpawnedCoinCount 증가
if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass()))
{
SpawnedCoinCount++;
}
}
}
}
// 30초 후에 OnLevelTimeUp()가 호출되도록 타이머 설정
GetWorldTimerManager().SetTimer(
LevelTimerHandle,
this,
&AMainGameState::OnLevelTimeUp,
LevelDuration,
false
);
UE_LOG(LogTemp, Warning, TEXT("Level %d Start!, Spawned %d coin"),
CurrentLevelIndex + 1,
SpawnedCoinCount);
}
void AMainGameState::OnLevelTimeUp()
{
// 시간이 다 되면 레벨을 종료
EndLevel();
}
void AMainGameState::OnCoinCollected()
{
CollectedCoinCount++;
UE_LOG(LogTemp, Warning, TEXT("Coin Collected: %d / %d"),
CollectedCoinCount,
SpawnedCoinCount)
// 현재 레벨에서 스폰된 코인을 전부 주웠다면 즉시 레벨 종료
if (SpawnedCoinCount > 0 && CollectedCoinCount >= SpawnedCoinCount)
{
EndLevel();
}
}
void AMainGameState::EndLevel()
{
// 타이머 해제
GetWorldTimerManager().ClearTimer(LevelTimerHandle);
// 다음 레벨 인덱스로
CurrentLevelIndex++;
// 모든 레벨을 다 돌았다면 게임 오버 처리
if (CurrentLevelIndex >= MaxLevels)
{
OnGameOver();
return;
}
// 레벨 맵 이름이 있다면 해당 맵 불러오기
if (LevelMapNames.IsValidIndex(CurrentLevelIndex))
{
UGameplayStatics::OpenLevel(GetWorld(), LevelMapNames[CurrentLevelIndex]);
}
else
{
// 맵 이름이 없으면 게임오버
OnGameOver();
}
}
void AMainGameState::OnGameOver()
{
UE_LOG(LogTemp, Warning, TEXT("Game Over!!"));
// 여기서 UI를 띄운다거나, 재시작 기능을 넣을 수도 있음
}
4️⃣ 코인 아이템 점수 획득 로직 수정
- CoinItem은 플레이어가 닿았을 때 (ActivateItem) 점수를 획득하고 자기 자신을 제거하는 구조입니다. 여기서 추가로 “코인을 하나 더 먹었다”라고 GameState에게 알려야 합니다
#include "CoinItem.h"
#include "Engine/World.h"
#include "MainGameState.h"
ACoinItem::ACoinItem()
{
// 부모클래스라 안해도 되지만
// 점수 기본값을 0으로 설정
PointValue = 0;
ItemType = "DefaultCoin";
}
void ACoinItem::ActivateItem(AActor* Activator)
{
// 플레이어 태그 확인
if (Activator && Activator->ActorHasTag("Player"))
{
// 점수 획득 디버그 메시지
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green,
FString::Printf(TEXT("Player Gained %i Points!"), PointValue));
if (UWorld* World = GetWorld())
{
if (AMainGameState* GameState = World->GetGameState<AMainGameState>())
{
GameState->AddScore(PointValue);
GameState->OnCoinCollected();
}
}
// 부모 클래스 (BaseItem)에 정의된 아이템 파괴 함수 호출
DestroyItem();
}
}
Game Instance를 활용한 데이터 유지하기
BP_MainGameState의 Level Map Names Setting.

현재 만들어둔 레벨들의 이름을 넣습니다.

Proejct -Maps & Modes 세팅에서 Game State Class를 BP_MainGameState 로 설정해 줍니다.

현재 게임은 Level이 전환될 때마다 GameState 가 새로 만들어져서 Variable 들의 정보가 다 초기화됩니다 (Score, CurrentLevelIndex 등등) 그래서 유지해야 하는 데이터들을 저장하기 위한 작업을 해보겠습니다.
1️⃣ Game Instance란?
- 레벨 전환 시에는 GameState, GameMode 같은 기본 클래스를 비롯해, 맵 내에서 생성된 대부분의 객체가 처음부터 다시 생성됩니다.
- 하지만 어떤 경우에는 “이전 레벨에서 획득한 점수나 플레이어 상태” 등을 모든 레벨에 걸쳐 유지하고 싶을 수 있습니다. 이를 위해서는 보통 두 가지 방법을 활용합니다.
- Game Instance
- 프로젝트가 시작될 때 (에디터에서 게임 실행을 누른 시점)부터 애플리케이션이 완전히 종료될 때까지 유일하게 계속 살아있는 객체입니다.
- 마치 Singleton과 비슷하다.
- 맵이 전환되어도 파괴되지 않으므로, 여기서 전역 데이터를 유지할 수 있습니다.
- Seamless Travel
- 멀티플레이 환경에서 주로 사용되는 레벨 전환 방식으로, GameState/PlayerController 등을 파괴하지 않고 그대로 다음 맵으로 넘어가는 기능입니다.
- Seamless Travel을 사용하면 대부분의 객체를 유지할 수 있지만, 설정과 로직이 조금 더 복잡하므로, 싱글 플레이 전용 간단 프로젝트라면 GameInstance를 사용하기가 쉽습니다.
Game Instance Subsystem 이란 것 또한 많이 사용하는데 이것은 추후에 더 알아보도록 하겠습니다.


2️⃣ Game Instance 생성 및 변수 선언
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MainGameInstance.generated.h"
UCLASS()
class BC_CH3_ASSIGNMENT_5_API UMainGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
UMainGameInstance();
// 게임 전체 누적 점수
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "GameData")
int32 TotalScore;
// 현재 레벨 인덱스 (GameState에서도 관리할 수 있지만, 맵 전환 후에도 살리고 싶다면 GameInstance에 복제할 수 있음)
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "GameData")
int32 CurrentLevelIndex;
UFUNCTION(BlueprintCallable, Category = "GameData")
void AddToScore(int32 Amount);
};
#include "MainGameInstance.h"
UMainGameInstance::UMainGameInstance()
{
TotalScore = 0;
CurrentLevelIndex = 0;
}
void UMainGameInstance::AddToScore(int32 Amount)
{
TotalScore += Amount;
UE_LOG(LogTemp, Warning, TEXT("Total Score Updated: %d"), TotalScore);
}
Update 된 MainGameState.cpp
#include "MainGameState.h"
#include "MainGameInstance.h"
#include "Kismet/GameplayStatics.h"
#include "SpawnVolume.h"
#include "CoinItem.h"
AMainGameState::AMainGameState()
{
Score = 0;
SpawnedCoinCount = 0;
CollectedCoinCount = 0;
LevelDuration = 30.0f; // 한 레벨당 30초
CurrentLevelIndex = 0;
MaxLevels = 3;
}
void AMainGameState::BeginPlay()
{
Super::BeginPlay();
// 게임 시작 시 첫 레벨부터 진행
StartLevel();
}
int32 AMainGameState::GetScore() const
{
return Score;
}
void AMainGameState::AddScore(int32 Amount)
{
if (UGameInstance* GameInstance = GetGameInstance())
{
UMainGameInstance* MainGameInstance = Cast<UMainGameInstance>(GameInstance);
if (MainGameInstance)
{
MainGameInstance->AddToScore(Amount);
}
}
}
void AMainGameState::StartLevel()
{
if (UGameInstance* GameInstance = GetGameInstance())
{
UMainGameInstance* MainGameInstance = Cast<UMainGameInstance>(GameInstance);
if (MainGameInstance)
{
CurrentLevelIndex = MainGameInstance->CurrentLevelIndex;
}
}
// 레벨 시작 시, 코인 개수 초기화
SpawnedCoinCount = 0;
CollectedCoinCount = 0;
// 현재 맵에 배치된 모든 SpawnVolume을 찾아 아이템 40개를 스폰
TArray<AActor*> FoundVolumes;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASpawnVolume::StaticClass(), FoundVolumes);
const int32 ItemToSpawn = 40;
for (int32 i = 0; i < ItemToSpawn; i++)
{
if (FoundVolumes.Num() > 0)
{
ASpawnVolume* SpawnVolume = Cast<ASpawnVolume>(FoundVolumes[0]);
if (SpawnVolume)
{
AActor* SpawnedActor = SpawnVolume->SpawnRandomItem();
// 만약 스폰된 액터가 코인 타입이라면 SpawnedCoinCount 증가
if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass()))
{
SpawnedCoinCount++;
}
}
}
}
// 30초 후에 OnLevelTimeUp()가 호출되도록 타이머 설정
GetWorldTimerManager().SetTimer(
LevelTimerHandle,
this,
&AMainGameState::OnLevelTimeUp,
LevelDuration,
false
);
UE_LOG(LogTemp, Warning, TEXT("Level %d Start!, Spawned %d coin"),
CurrentLevelIndex + 1,
SpawnedCoinCount);
}
void AMainGameState::OnLevelTimeUp()
{
// 시간이 다 되면 레벨을 종료
EndLevel();
}
void AMainGameState::OnCoinCollected()
{
CollectedCoinCount++;
UE_LOG(LogTemp, Warning, TEXT("Coin Collected: %d / %d"),
CollectedCoinCount,
SpawnedCoinCount)
// 현재 레벨에서 스폰된 코인을 전부 주웠다면 즉시 레벨 종료
if (SpawnedCoinCount > 0 && CollectedCoinCount >= SpawnedCoinCount)
{
EndLevel();
}
}
void AMainGameState::EndLevel()
{
// 타이머 해제
GetWorldTimerManager().ClearTimer(LevelTimerHandle);
// 다음 레벨 인덱스로
//CurrentLevelIndex++;
if (UGameInstance* GameInstance = GetGameInstance())
{
UMainGameInstance* MainGameInstance = Cast<UMainGameInstance>(GameInstance);
if (MainGameInstance)
{
AddScore(Score);
CurrentLevelIndex++;
MainGameInstance->CurrentLevelIndex = CurrentLevelIndex;
}
}
// 모든 레벨을 다 돌았다면 게임 오버 처리
if (CurrentLevelIndex >= MaxLevels)
{
OnGameOver();
return;
}
// 레벨 맵 이름이 있다면 해당 맵 불러오기
if (LevelMapNames.IsValidIndex(CurrentLevelIndex))
{
UGameplayStatics::OpenLevel(GetWorld(), LevelMapNames[CurrentLevelIndex]);
}
else
{
// 맵 이름이 없으면 게임오버
OnGameOver();
}
}
void AMainGameState::OnGameOver()
{
UE_LOG(LogTemp, Warning, TEXT("Game Over!!"));
// 여기서 UI를 띄운다거나, 재시작 기능을 넣을 수도 있음
}
프로젝트 세팅 안에 Game Instance 도 변경해줘야 합니다. 그전에 BP_MainGmaeInstace 도 만들고 설정하겠습니다.


3️⃣ 전체 게임 루프 요약
- 게임 실행
- 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 표시로 전환)
추천
[Unreal Engine/UE 기초] - 인터페이스 기반 아이템 클래스 설계하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
인터페이스 기반 아이템 클래스 설계하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
인터페이스 기반 아이템 클래스 설계하기 인터페이스 이해하기1️⃣ 인터페이스란?인터페이스 (Interface)란 클래스 (또는 오브젝트)가 반드시 구현해야 할 함수 목록만을 미리 정의해 두고, 실제
devcol.tistory.com
[Unreal Engine/UE 기초] - 충돌 이벤트로 획득되는 아이템 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
충돌 이벤트로 획득되는 아이템 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
충돌 이벤트로 획득되는 아이템 구현하기 이전 포스팅에서는 클래스 구조만 짜고 로직은 비여있던 상태.이전 포스팅: [Unreal Engine/UE 기초] - 인터페이스 기반 아이템 클래스 설계하기 | [언리얼
devcol.tistory.com
[Unreal Engine/UE 기초] - 아이템 스폰 및 레벨 데이터 관리하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
아이템 스폰 및 레벨 데이터 관리하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
아이템 스폰 및 레벨 데이터 관리하기 랜덤 위치에 아이템 스폰하기 1️⃣ 레벨 셋팅하기Resources 폴더에 Maps 폴더에는 3개의 레벨이 이미 존재합니다. 각각 난이도에 따라서 크기가 다른 맵이고,
devcol.tistory.com
[Unreal Engine/UE 기초] - 캐릭터 체력 및 점수 관리 시스템 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
캐릭터 체력 및 점수 관리 시스템 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
캐릭터 체력 및 점수 관리 시스템 구현하기 캐릭터 체력 시스템 구현하기 1️⃣ 캐릭터 클래스에 체력 변수 및 함수 선언PlayerState를 쓰지 않는 이유PlayerState: 각 플레이어마다의 어떤 정보를 관
devcol.tistory.com