언리얼 - 엔진 20 : 게임 루프

김정환·2025년 4월 14일
0

Unreal Engine

목록 보기
21/24

게임 루프

  • 게임 시작 ~ 종료까지 단계
    • 레벨 로드 - 플레이어 진행 - 게임 종료 => 다음 레벨 or 레벨 실패
  • 게임 플로우, 웨이브라고도 함

게임 루프 구현

  • 게임 흐름에 대한 구현은 프로젝트 구조와도 관련됨.
    • 이는 개개인의 스타일, 장르에 따라 달라질 수 있음.
    • 이번엔 싱글 플레이 게임이기 때문에 GameState에 구현해볼 것.

GameState와 GameMode

  • UE에서 게임 루프나 전역 상태를 관리할 때 대표적으로 고려되는 클래스는
    GameStateGameMode가 있음.

GameMode를 쓰는 이유

  • 여기는 대체로 서버 전용 로직을 담는 곳이며 게임 규칙 등을 서버에서 제어하는데 사용.
  • 클라이언트는 GameMode에 직접 접근할 수 없기 때문에 클라이언트도 알아야하는 정보를 GameMode에 두면 복잡해짐.
  • 보통 멀티플레이를 고려한다면 중요 규칙 로직GameMode에 두고,
    서버-클라이언트도 공통으로 알아야 하는 상태(정보, 데이터)는 GameState에 두는 방식을 많이 사용.

GameState를 쓰는 이유

  • GameState : 게임의 전역 데이터를 관리
    • 게임 전반에 걸쳐 모든 플레이어가 공유해야 하는 상태를 담는 클래스
  • GameState 객체를 게임이 시작될 때 서버에서 생성되고,
    클라이언트는 이를 복제 받아서 똑같은 정보를 읽을 수 있음.
    • 서버와 클라이언트 모두 동일한 정보를 가질 수 있는 것.

게임 루프 구성

  • UI - 게임 시작 버튼 (메뉴창)
  1. 게임 시작 - BeginPlay() - Basic Level 열어주기
  2. 시작 시, 레벨에서 40개의 아이템이 소환
  3. 코인 아이템을 다 먹었으면 바로 다음 레벨로 넘어감
  4. 한 레벨 당 30초의 시간이 주어짐. 타임 오버가 돼도 다음 레벨로
  5. 캐릭터 체력이 0이 되면, 바로 게임 오버
  6. 게임 종료 이후, 재시작
  • UI - 게임 종료 이후 -> 재시작 버튼 (메뉴창)

게임 루프에서 필요한 데이터

  • 아이템을 40개까지 생성
    • 아이템 생성 시, 반환형이 void이었던 함수를 생성한 아이템의 AActor*를 반환하도록 변경.
  • 아이템 중 코인 아이템의 갯수 세기
  • 30초 타이머
  • 캐릭터 체력 관리

GameState 기반의 게임 루프 구현

  • 이전에는 GameMode 버전과의 일관성을 위해서 GameStateBase를 상속했던 것을 GameState를 상속하도록 변경.
  • 게임 루프의 로직을 관리하도록 변경.

1. 코인 관리

  • 아이템을 40개 생성하고, 그중 코인 아이템이 몇개 생성되었는지 추적.
  • 플레이어가 먹은 코인 갯수를 추적해서 모두 먹었다면 종료.
//.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameState.h"
#include "Cp2GameState.generated.h"

UCLASS()
class CHAPTER2_API ACp2GameState : public AGameState
{
	GENERATED_BODY()

public:
	ACp2GameState();

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Score")
	int32 Score;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Coin")
	int32 SpawnedCoinCount;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Coin")
	int32 CollectedCoinCount;
	
	virtual void BeginPlay() override;

	UFUNCTION(BlueprintPure, Category = "Score")
	int32 GetScore() const { return Score; }
	UFUNCTION(BlueprintCallable, Category = "Score")
	void AddScore(int32 amount);

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

	void StartLevel();
	void EndLevel();
	void OnCoinCollected();
}
  • 코인 관리에 필요한 변수와 게임오버 함수들을 작성.

월드 내 객체 찾기

  • 레벨이 시작할 때, 레벨에 배치했던 SpawnVolume BP 액터를 찾아서 아이템을 생성해줄 것.
  • 40개의 아이템을 생성하고 그 중에서 코인 아이템을 셀 것.
// .cpp
~~
#include "Kismet/GameplayStatics.h"
~~

void ACp2GameState::StartLevel()
{
	SpawnedCoinCount = 0;
	CollectedCoinCount = 0;

	// 레벨에 있는 스폰 볼륨 찾기
	TArray<AActor*> FoundVolume;
    
	// 현재 레벨에서 원하는 클래스를 찾아내는 함수
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASpawnVolume::StaticClass(), FoundVolume);

	const int32 ItemToSpawn = 40;

	for(int32 i = 0; i < ItemToSpawn; ++i)
	{
		if (FoundVolume.Num() > 0)
		{
			ASpawnVolume* SpawnVolume = Cast<ASpawnVolume>(FoundVolume[0]);
			if(SpawnVolume)
			{
				AActor* SpawnedActor = SpawnVolume->SpawnRandomItem();

				// 해당 클래스가 코인 아이템인지
				if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass()))
					++SpawnedCoinCount;
			}
		}
	}
}
~~
  • 레벨 내 액터 찾기

    // 현재 맵에 배치된 모든 SpawnVolume을 찾아 아이템 40개를 스폰
    TArray<AActor*> FoundVolumes;
    
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASpawnVolume::StaticClass(), FoundVolumes);
    • UGameplayStatics::GetAllActorsOfClass(...)
      • #include "Kismet/GameplayStatics.h" 포함 필요
      • 레벨에 있는 특정 클래스의 액터를 찾아내는 함수인데, 레벨에 액터가 많을 수록 느리다.
        • 아마도 레벨 내 액터들을 순회하면서 찾는 방식으로 보인다.
        • 성능에 좋지 않으므로 자주 쓰지 않는 것이 좋을 것 같음.
    • 찾아낸 액터들을 반환하므로 배열 형태의 변수를 요구함.
  • 아이템 생성 후 코인 세기

    • 아이템을 레벨마다 할당했던 테이블의 확률에 따라 랜덤으로 생성.
    • 그 중에서 코인 아이템인지 확인함.
    // 해당 클래스가 코인 아이템인지
    if (SpawnedActor && SpawnedActor->IsA(ACoinItem::StaticClass()))
        ++SpawnedCoinCount;
    • IsA(Type)
      • 검사하는 액터가 Type 클래스거나 상속받는 하위 클래스인지 확인 검사함.
      • 반환형은 boolean

코인 획득 시 처리

void ACp2GameState::OnCoinCollected()
{
	++CollectedCoinCount;

	if (SpawnedCoinCount > 0 && CollectedCoinCount >= SpawnedCoinCount)
		EndLevel();
}

void ACp2GameState::EndLevel()
{
	// 타이머 초기화
	GetWorldTimerManager().ClearTimer(LevelTimerHandle);

	// 변경 사항 저장
	if (UGameInstance* gameInst = GetGameInstance())
	{
		UCp2GameInstance* cp2GameInst = Cast<UCp2GameInstance>(gameInst);

		if (cp2GameInst)
		{
			AddScore(Score);
			++CurrentLevelIndex;
			cp2GameInst->CurrenctLevelIndex = CurrentLevelIndex;
		}
	}

	if(CurrentLevelIndex >= MaxLevel)
	{
		OnGameOver();
		return;
	}

	if (LevelNames.IsValidIndex(CurrentLevelIndex))
		UGameplayStatics::OpenLevel(GetWorld(), LevelNames[CurrentLevelIndex]);
	else
		OnGameOver();

}
  • UGamePlayStatics::OpenLevel(...) 맵 전환. 레벨 이동 함수
    • 맵 전환 시, 현재 레벨은 언로드(제거)되고 새로운 맵이 로드되며 BeginPlay()가 호출됨.
    • 이때 해당 레벨의 GameState도 새로 생성되니 이전 레벨에서 유지하던 변수가 모두 초기화됨.
    • 데이터를 전역적으로 유지할 필요가 있음.
      • 현재 레벨의 인덱스, 누적 점수 등등

게임 인스턴스 Game Instance

  • 현재 레벨 인덱스와 같이 유지가 필요한 경우가 있음.
  • 이럴 때 사용하는 것이 게임 인스턴스
    • 일종의 싱글턴 객체
#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "Cp2GameInstance.generated.h"

UCLASS()
class CHAPTER2_API UCp2GameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:
	UCp2GameInstance();

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

	UFUNCTION(BlueprintCallable, Category = "GameData")
	void AddToScore(int32 scoreToAdd);

};

게임 인스턴스 반영

  • Edit > Project Setting > Maps & Modes > Game Instance Class 변경

호출 예시

  • GameState에서 점수 획득 시 호출하는 함수 수정
    • 점수를 획득하면 GameState를 통해서 AddScore() 해줄 것.
    • 이때 실제 점수의 저장은 GameInstance에 반영해줌.
void ACp2GameState::AddScore(int32 amount)
{
	if(UGameInstance* gameInst = GetGameInstance())
	{
		UCp2GameInstance* cp2GameInst = Cast<UCp2GameInstance>(gameInst);
        
		if (cp2GameInst)
			cp2GameInst->AddToScore(amount);
	}
}

2. 타이머

  • UE에선 타이머를 관리하는 기능을 지원하고 있음.

  • 이번 프로젝트에서 레벨 당 30초의 타이머를 설정해서 제한시간을 줄 것.

    • GameState에 타이머 관련 변수
    #pragma once
    
    #include "CoreMinimal.h"
    #include "GameFramework/GameState.h"
    #include "Cp2GameState.generated.h"
    
    UCLASS()
    class CHAPTER2_API ACp2GameState : public AGameState
    {
        GENERATED_BODY()
    
    public:
        ACp2GameState();
        ~~
        FTimerHandle LevelTimerHandle;
    
        void OnLevelTimeUp();
        ~~
    };
void ACp2GameState::StartLevel()
{
	~~
	// 타이머
	GetWorldTimerManager().SetTimer(
		LevelTimerHandle,
		this,
		&ACp2GameState::OnLevelTimeUp,
		LevelDuration,
		false
	);
}

void ACp2GameState::OnLevelTimeUp()
{
	EndLevel();
}
  • GetWorldTimerManager().SetTimer(...) : 타이머 관리자에서 타이머를 설정해줌.
    • FTimerHandle& InOutHandle : 설정해줄 타이머 구조체 참조
    • GameState* InObj : 현재 월드의 GameState 객체.
      • GameState에서 호출하므로 this 넣어줌
    • void GameState::*InTimerMethod() : 타이머가 완료되면 호출할 함수 포인터
    • float InRate : 타이머 설정 시간.
    • bool InbLoop : 반복 여부
    • float InFirstDelay : 타이머 딜레이를 주는 시간. (선택 옵션)
profile
만성피로 개발자

0개의 댓글