게임 루프 설계를 통한 게임 흐름 제어하기 | [언리얼 엔진 C++ (Unreal Engine C++)]

2026. 6. 11. 19:54·Unreal Engine/UE 기초

 

게임 루프 설계를 통한 게임 흐름 제어하기

 


 

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():
      1. 코인 개수들 초기화(SpawnedCoinCount=0, CollectedCoinCount=0)
      2. 스폰 볼륨들을 찾아서 40개 아이템 스폰(반복).
        • 만약 SpawnRandomItem()이 ACoinItem을 반환하면 SpawnedCoinCount++
      3. 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()
      1. 현재 레벨 타이머 정리
      2. CurrentLevelIndex++
      3. 만약 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 같은 기본 클래스를 비롯해, 맵 내에서 생성된 대부분의 객체가 처음부터 다시 생성됩니다.
  • 하지만 어떤 경우에는 “이전 레벨에서 획득한 점수나 플레이어 상태” 등을 모든 레벨에 걸쳐 유지하고 싶을 수 있습니다. 이를 위해서는 보통 두 가지 방법을 활용합니다.
  1. Game Instance
    • 프로젝트가 시작될 때 (에디터에서 게임 실행을 누른 시점)부터 애플리케이션이 완전히 종료될 때까지 유일하게 계속 살아있는 객체입니다.
    • 마치 Singleton과 비슷하다.
    • 맵이 전환되어도 파괴되지 않으므로, 여기서 전역 데이터를 유지할 수 있습니다.
  2. 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️⃣ 전체 게임 루프 요약

  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 표시로 전환)

 


 

 

추천

 

 

[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

 

 

[페이지] Unreal Engine | 언리얼 엔진

저작자표시 동일조건 (새창열림)
'Unreal Engine/UE 기초' 카테고리의 다른 글
  • 게임 흐름에 맞춘 메뉴 UI 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
  • UI 위젯 설계와 실시간 데이터 연동하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
  • 캐릭터 체력 및 점수 관리 시스템 구현하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
  • 아이템 스폰 및 레벨 데이터 관리하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
DevCol
DevCol
DevCol (Development Collaboration). 함께 개발 & 공부 & IT 정보 나눔장소
  • DevCol
    DevCol (Development Collaboration)
    DevCol
  • 블로그 메뉴

    • Unreal Engine
    • TIL
    • 게임국가기술자격검정 게임프로그래밍전문가 [한국콘텐츠진흥원]
    • 분류 전체보기 (73) N
      • Unreal Engine (31) N
        • Project (2) N
        • Dev Log (0)
        • Debugging (2) N
        • Blueprint (1)
        • UE 기초 (25) N
        • UE 심화 (0)
        • TA (1) N
      • Programming Language (0)
        • C++ (0)
        • C# (0)
      • Unity Engine (0)
      • 자격증 (3)
        • 게임국가기술자격검정 [한국콘텐츠진흥원] (3)
      • Coding Test | 코딩테스트 (0)
        • 프로그래머스 기초 (0)
        • 프로그래머스 입문 (0)
      • TIL (38) N
        • Boot Camp (32) N
      • Git & Github (1)
  • 링크

    • Youtube
    • GitHub
    • itch.io
    • Blog (En)
  • 공지사항

  • 인기 글

  • 태그

    프로그래밍
    til
    UE5
    코드 카타
    Game Dev
    Unreal engine
    cpp
    내일배움캠프
    게임 개발
    코드카타
    UE
    C++
    게임개발
    Boot Camp
    언리얼 엔진
    기초
    Programming
    Code Kata
    c
    Devlog
  • 최근 글

  • GitHub Youtube itch
  • hELLO · Designed By 정상우.v4.10.6
  • DevCol
    게임 루프 설계를 통한 게임 흐름 제어하기 | [언리얼 엔진 C++ (Unreal Engine C++)]
    상단으로

    티스토리툴바