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

김여울·2025년 7월 20일
0

내일배움캠프

목록 보기
46/114

📍 3주차 5강

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

게임 루프 게임 시작 ~ 종료까지 수행하는 단계들
게임 플로우 플레이어가 게임을 시작, 진행, 끝내는 과정에서 발생하는 모든 상호작용들 (레벨 전환 등)

9.1 GameState를 이용한 게임 루프 구현하기

a. GameMode vs GameState

  • GameMode

    • 서버 전용 로직 처리 (클라이언트는 접근 불가)
    • 역할: 팀 배정, 승패 조건, 스폰 등 게임 규칙 관리
    • 멀티플레이에서는 서버에서만 판단해야 할 규칙만 GameMode에 둠
  • GameState

    • 모든 플레이어가 공유하는 정보 저장
    • 역할: 남은 시간, 점수, 아이템 개수 등 전역 상태 관리
    • 서버가 만든 GameState를 클라이언트가 복제받아 똑같이 읽음
  • 정리
    • GameMode: 규칙, 서버 전용
      → 서버 판단
    • GameState: 상태, 서버+클라이언트 공유
      → 멀티플레이에서는 공통 정보

30초 제한 안에 40개 아이템을 모두 먹으면 다음 레벨 넘어감
→ 이런 게임 상태 공유가 중요하므로 GameState 사용!

그리고 GameState가 GameStateBase보다 GameMode와 더 긴밀히 연결되어 있으므로
9.1.c에서 GameState로 변경하기

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

스폰된 아이템의 정보(coin이 맞음?)를 GameState에서 카운팅 하기 위해 spawn 함수를 void → 스폰된 AActor* 로 반환하기

SpawnVolume.h

UFUNCTION(BlueprintCallable, Category = "Spawning")
AActor* SpawnRandomItem();

FVector GetRandomPointInVolume() const;	
FItemSpawnRow* GetRandomItem() const;
AActor* SpawnItem(TSubclassOf<AActor> ItemClass);

SpawnVolume.cpp

// 아이템 랜덤으로 갖고 오는 함수
AActor* ASpawnVolume :: SpawnRandomItem()
{
	if (FItemSpawnRow* SelectedRow = GetRandomItem())
	{
		if (UClass* ActualClass = SelectedRow->ItemClass.Get())
		{
			// // 여기서 SpawnItem()을 호출하고, 스폰된 AActor 포인터를 리턴
			return SpawnItem(ActualClass);
		}
	}

	return nullptr;	// 실패 시
}

// ...

// 실질적으로 Spawn해주는 함수
AActor* ASpawnVolume::SpawnItem(TSubclassOf<AActor> ItemClass)
{
	// 안전 코드
	if (!ItemClass) return nullptr;

	// SpawnActor가 성공하면 스폰된 액터의 포인터가 반환됨
	AActor* SpawnedActor = GetWorld()->SpawnActor<AActor>(
		ItemClass,
		GetRandomPointInVolume(),	// 위치 (랜덤 값 갖고 옴)
		FRotator::ZeroRotator	// 회전은 안하게
	);

	return SpawnedActor;
}

c. GameStateBase vs GameState

GameStateBase vs GameState

항목GameStateBaseGameState
부모/자식부모 클래스자식 클래스 (GameStateBase 상속)
기능 수준최소한의 상태 기능 제공상태 관련 기능이 더 많음
추가된 기능없음시간 관리, 경기 흐름 동기화 등
주로 쓰는 상황커스텀 상태만 간단히 관리할 때일반적인 게임 진행 상황 관리할 때
멀티플레이지원멀티플레이에 최적화

GameState가 GameMode와 더 밀접한 이유

  • 자동 연결 구조

    • GameMode가 생성되면 자동으로 GameState도 같이 생성
    • GameMode::GameStateClass 변수에 어떤 GameState를 쓸지 지정할 수 있음
  • 직접 참조 가능

    • GameMode 안에서 현재 GameState를 쉽게 가져올 수 있음
    • 반대로 GameState에서도 GameMode 참조 가능
      (주의: 서버에서만!)
  • 역할 분담이 명확

    클래스역할
    GameMode판단: 규칙 처리 (언제 끝나는지, 승리 조건 등)
    GameState기록: 상태 저장 (시간, 점수, 현재 라운드 등)

GameState로 변경하기

1️⃣ GameState로 변경 (UE, VS)

  • GameStateBase 복사 붙여넣기
  • GameStateBase 쓰인 곳 다 GameState로 변경하기

2️⃣ GameStateBase 삭제 (VS, 프로젝트 폴더 Source)

3️⃣ BP_GameState 만들기

4️⃣ 레벨 별로 World Settings에서 GameMode 지정하기

5️⃣ Project Settings에서 다 BP 형태로 변경하기

d. GameState 기반의 게임 루프 구현

[UI] - 게임 시작 버튼 (메뉴창)

1️⃣ 게임시작 → BeginPlay() → StartLevel() → Basic Map

// SpartaGameState.h
public:
	ASpartaGameState();	// 생성자
    
    void StartLevel();

// SpartaGameState.cpp
void ASpartaGameState::BeginPlay()
{
	Super::BeginPlay();

	StartLevel();
}

void ASpartaGameState::StartLevel()
{ 
}

2️⃣ 레벨마다 40개의 아이템이 소환됨

// SpartaGameState.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Coin")
int32 SpawnedCoinCount;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Coin")
int32 CollectedCoinCount;

// SpartaGameState.cpp
#include "Kismet/GamePlayStatics.h"
#include "SpawnVolume.h"
#include "CoinItem.h"

ASpartaGameState::ASpartaGameState()
{
	Score = 0;
	// 전체 초기화
	// coin 관련 카운트 초기화 하는 변수
	SpawnedCoinCount = 0;
	CollectedCoinCount = 0;
}

void ASpartaGameState::StartLevel()
{ 
	// 레벨을 불러올 때마다 초기화
	// coin 관련 카운트 초기화 하는 변수
	SpawnedCoinCount = 0;
	CollectedCoinCount = 0;

	// World에 SpawnVolume이 몇 개 있는지 세고 거기에 아이템 40개 스폰함
	// 찾은 Volume을 배열 형태로 저장
	TArray<AActor*> FoundVolumes;
	// 현재 월드에서 Actor에 해당되는 모든 걸 가져옴 -> 찾은 것들을 FoundVolumes에 넣음
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASpawnVolume::StaticClass(), FoundVolumes);

	// 찾은 볼륨들에 몇 개를 스폰할지 지정
	const int32 ItemToSpawn = 40;

	for (int32 i = 0; i < ItemToSpawn; i++)
	{
		if (FoundVolumes.Num() > 0)	// 0개 이상인지 확인
		{
			ASpawnVolume* SpawnVolume = Cast<ASpawnVolume>(FoundVolumes[0]);
			if (SpawnVolume)
			{
				AActor* SpawnedActor = SpawnVolume->SpawnRandomItem();	// 9.1.b에서 void 바꿔놨음
				// IsA: Coin 맞음??, 하위클래스까지 인식
				if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass()))	
				{
					SpawnedCoinCount++;

				}
			}
		}

	}
}

3️⃣ 한 레벨 당 30초. 타임오버가 돼도 바로 다음 레벨로 이동

// SpartaGameState.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Level")
float LevelDuration;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Level")
int32 CurrentLevelIndex;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Level")
int32 MaxLevels;

FTimerHandle LevelTimerHandle;

UFUNCTION(BlueprintCallable, Category = "Level")
void OnGameOver();

void StartLevel();
void OnLevelTimeUp();
void EndLevel();

// SpartaGameState.cpp
ASpartaGameState::ASpartaGameState()
{
	Score = 0;
	// 전체 초기화
	// coin 관련 카운트 초기화 하는 변수
	SpawnedCoinCount = 0;
	CollectedCoinCount = 0;
	LevelDuration = 30.0f;
	CurrentLevelIndex = 0;
    MaxLevels = 3;
}

// 2번의 for문 안에 추가
// 30초 후에 OnLevelTimeUp()가 호출되도록 타이머 설정
GetWorldTimerManager().SetTimer(
	LevelTimerHandle,
	this,
	&ASpartaGameState::OnLevelTimeUp,
	LevelDuration,
	false
);

UE_LOG(LogTemp, Warning, TEXT("Level %d Start!, Spawned %d coin"),
	CurrentLevelIndex + 1,
	SpawnedCoinCount);
    
void ASpartaGameState::OnLevelTimeUp()
{
	// 시간이 다 되면 레벨을 종료
	EndLevel();
}

void ASpartaGameState::EndLevel()
{
	// 타이머 해제
	GetWorldTimerManager().ClearTimer(LevelTimerHandle);
	// 다음 레벨 인덱스로
	CurrentLevelIndex++;

	// 모든 레벨을 다 돌았다면 게임 오버 처리
	if (CurrentLevelIndex > MaxLevels)
	{
		OnGameOver();
	}
	else
	{
		StartLevel();
	}
}

void ASpartaGameState::OnGameOver()
{
	UE_LOG(LogTemp, Warning, TEXT("Game Over!!"))
	// 여기서 UI를 띄운다거나, 재시작 기능을 넣을 수도 있음 -> 나중에 추가
}

4️⃣ 플레이어가 먹은 CoinItem 세기

// SpartaGameState.h
// 코인을 주웠을 때 호출
void OnCoinCollected();
        
// SpartaGameState.cpp
void ASpartaGameState::OnCoinCollected()
{
		CollectedCoinCount++;
	
		UE_LOG(LogTemp, Warning, TEXT("Coin Collected: %d / %d"), 
			CollectedCoinCount,
			SpawnedCoinCount)
	
		// 현재 레벨에서 스폰된 코인을 전부 주웠다면 즉시 레벨 종료
		if (SpawnedCoinCount > 0 && CollectedCoinCount >= SpawnedCoinCount)
		{
				EndLevel();
		}
}

5️⃣ LevelMapNames - 레벨 이름 담는 배열

// SpartaGameState.h
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Level")
TArray<FName> LevelMapNames;


// SpartaGameState.cpp
void ASpartaGameState::EndLevel()
{
	// 타이머 해제
	GetWorldTimerManager().ClearTimer(LevelTimerHandle);
	// 다음 레벨 인덱스로
	CurrentLevelIndex++;

	// 모든 레벨을 다 돌았다면 게임 오버 처리
	if (CurrentLevelIndex >= MaxLevels)
	{
		OnGameOver();
		return;
	}
	if (LevelMapNames.IsValidIndex(CurrentLevelIndex))
	{
		UGameplayStatics::OpenLevel(GetWorld(), LevelMapNames[CurrentLevelIndex]);
	}
	else
	{
		OnGameOver();
	}
}

▶ BP에서 레벨 이름 정해주기

➡ BP_SpawnVolume 가서 전에 만들었던 이벤트 그래프 지우기
➡ Project Settings에서 BP_GameState 설정하기

[UI] - 게임 종료 이후 재시작 버튼 (메뉴창)

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

// CoinItem.cpp
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();
	}
}

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

a. Game Instance

레벨 전환 시 상태 유지 방법 (with 싱글톤)
레벨 전환 시 GameMode, GameState 등은 초기화됨
→ 점수나 진행 상태 같은 데이터는 따로 보존해야함

  • GameInstance

    • 게임 전체에서 지속적으로 유지되는 싱글톤 객체
    • 레벨이 바뀌어도 파괴되지 않음
    • (예시) Score, CurrentLevelIndex 같은 값 저장
    • 싱글플레이 프로젝트에 최적
  • Seamless Travel

    • 멀티플레이용 레벨 전환 방식
    • GameState, PlayerController 등 중요 객체 유지
    • 설정이 다소 복잡함
  • 정리

    • GameInstance: 싱글톤, 레벨 전환 간 상태 유지에 편리 (점수, 진행도 등)
    • Seamless Travel: 객체 유지하면서 매끄럽게 맵 이동 (멀티용)

b. Game Instance 생성 및 변수 선언

생성

변수 선언

// SpartaGameInstance.h
UCLASS()
class SPARTAPROJECT_API USpartaGameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:

	USpartaGameInstance();	// 생성자

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "GameData")
	int TotalScore;
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "GameData")
	int32 CurrentLevelIndex;

	// 총점수 함수
	UFUNCTION(BlueprintCallable, Category = "GameData")
	void AddToScore(int32 Amount)
};

// SpartaGameInstance.cpp
#include "SpartaGameInstance.h"

// 생성자
USpartaGameInstance::USpartaGameInstance()
{
	TotalScore = 0;
	CurrentLevelIndex = 0;
}

void USpartaGameInstance::AddToScore(int32 Amount)
{
	TotalScore += Amount;
	UE_LOG(LogTemp, Warning, TEXT("Total Score Updated: %d"), TotalScore);
}

이 게임인스턴스를 프로젝트의 기본으로 설정해야 레벨이 바뀌어도 유지됨
GameState에서 활용하기

// GameState.cpp
void ASpartaGameState::AddScore(int32 Amount)
{
	// GameInstance 호출
	if (UGameInstance* GameInstance = GetGameInstance())
	{
		USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GameInstance);
		if (SpartaGameInstance)
		{
			SpartaGameInstance->AddToScore(Amount);	// 전체 총점에 점수 추가
		}
			
	}
}

void ASpartaGameState::StartLevel()
{ 
	if (UGameInstance* GameInstance = GetGameInstance())
	{
		USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GameInstance);
		if (SpartaGameInstance)
		{
			CurrentLevelIndex = SpartaGameInstance->CurrentLevelIndex;	// 임시 저장
		}

	}
    //...
}

void ASpartaGameState::EndLevel()
{
	// 타이머 해제
	GetWorldTimerManager().ClearTimer(LevelTimerHandle);
	// 다음 레벨 인덱스로
	CurrentLevelIndex++;

	if (UGameInstance* GameInstance = GetGameInstance())
	{
    	AddScore(Score);	// 레벨이 끝날 때마다 점수 더해줌
		USpartaGameInstance* SpartaGameInstance = Cast<USpartaGameInstance>(GameInstance);
		if (SpartaGameInstance)
		{
			SpartaGameInstance->CurrentLevelIndex = CurrentLevelIndex;
		}
	}
	///...
}

c. 에디터에 GameInstance 적용하기

GameInstance 블루프린트로 만들어 적용하기

짠~ 완료

0개의 댓글