게임 저장 기능과 빌드

sssukh·2024년 4월 18일

마지막으로 게임을 완성하기 위해 클리어 조건을 추가하고 저장하는 기능과 최종 빌드까지 진행하였다.

게임클리어 조건 설정

	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Game)
	int32 ClearScore;

	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Game)
	int32 CurrentScore;

	UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Game)
	uint8 bIsCleared : 1;
    
    AABGameMode::AABGameMode()
{
	//...

	ClearScore = 3;
	CurrentScore = 0;
	bIsCleared = false;
}

게임을 클리어할 조건을 설정한다. 이를 위해서 게임의 심판 역할을 하는 GameMode에 목표점수, 현재 점수, 클리어 여부를 담을 변수들을 추가해주고 생성자에서 초기화해준다.

스테이지 기믹의 OnOpponentDistroyed에서 점수를 추가해서 점수를 일정 이상 얻으면 보상 대신 게임 종료를 하도록 한다.
이를 위해 게임모드 가져오는데 직접 참조하는 것이 아닌 인터페이스를 통해서 가져오도록 한다.

public:
	virtual void OnPlayerScoreChanged(int32 NewPlayerScore) = 0;
	virtual void OnPlayerDead() = 0;
	virtual bool IsGameCleared() = 0;

인터페이스를 새로 생성하고 GameMode에서 상속받도록 한다.

각각 점수를 변경시킬 때, 플레이어가 죽었을 때, 게임이 클리어되었을 때 호출하는 함수들을 선언했다.

void AABGameMode::OnPlayerScoreChanged(int32 NewPlayerScore)
{
	CurrentScore = NewPlayerScore;

	if (CurrentScore >= ClearScore)
	{
		bIsCleared = true;
	}
}

bool AABGameMode::IsGameCleared()
{
	return bIsCleared;
}

void AABStageGimmick::OnOpponentDestroyed(AActor* DestroyedActor)
{
	IABGameInterface* ABGameMode = Cast<IABGameInterface>(GetWorld()->GetAuthGameMode());
	if (ABGameMode)
	{
		ABGameMode->OnPlayerScoreChanged(CurrentStageNum);
		if (ABGameMode->IsGameCleared())
		{
			return;
		}
	}

	SetState(EStageState::REWARD);
}

내용을 정의해주고 스테이지 기믹의 OnOpponentDistroyed에서 점수가 클리어조건을 달성하면 상자가 나오지 않고 멈추게 될 것이다.

void AABCharacterPlayer::SetDead()
{
	Super::SetDead();

	APlayerController* PlayerController = Cast<APlayerController>(GetController());
	if (PlayerController)
	{
		DisableInput(PlayerController);

		IABGameInterface* ABGameMode = Cast<IABGameInterface>(GetWorld()->GetAuthGameMode());
		if (ABGameMode)
		{
			ABGameMode->OnPlayerDead();
		}
	}
}

아까 게임모드에 정의한 OnPlayerDead()를 호출해주는데 게임모드를 불러올 때 사용한GetAuthGameMode는 모든 플레이어를 통틀어서 게임모드는 단 하나만 존재하는데 이 때 방장의 게임모드를 가져온다. 여기서는 플레이어가 하나밖에 없기 때문에 우리가 사용하는 게임모드를 가져온다.

어느 이벤트가 일어날 때 플레이어와 소통하는 방법중에 플레이어에게 UI를 띄우는 방법이 있는데 게임모드가 직접 UI를 띄우는 것이 아니라 띄우도록 호출해준다. 그래서 플레이어 컨트롤러에서 UI를 띄우도록 하는 기능을 추가해보겠다.

플레이어 컨트롤러에서 UI 생성

void GameScoreChanged(int32 NewScore);
void GameClear();
void GameOver();

void AABGameMode::OnPlayerScoreChanged(int32 NewPlayerScore)
{
	CurrentScore = NewPlayerScore;

	AABPlayerController* ABPlayerController = Cast<AABPlayerController>(GetWorld()->GetFirstPlayerController());
	if (ABPlayerController)
	{
		ABPlayerController->GameScoreChanged(CurrentScore);
	}

	if (CurrentScore >= ClearScore)
	{
		bIsCleared = true;

		if (ABPlayerController)
		{
			ABPlayerController->GameClear();
		}
	}
}

void AABGameMode::OnPlayerDead()
{
	AABPlayerController* ABPlayerController = Cast<AABPlayerController>(GetWorld()->GetFirstPlayerController());
	if (ABPlayerController)
	{
		ABPlayerController->GameOver();
	}
}

게임이 진행과정에서 플레이어 컨트롤러에 접근하여 우리가 정의한 함수들을 호출하게 된다.
이 함수들은 알맞은 UI를 띄워줘야하는데 생산성을 위해 블루프린트로 동작하도록 진행됐다.

	UFUNCTION(BlueprintImplementableEvent, Category = Game, Meta = (DisplayName = "OnScoreChangedCpp"))
	void K2_OnScoreChanged(int32 NewScore);
	UFUNCTION(BlueprintImplementableEvent, Category = Game, Meta = (DisplayName = "OnGameClearCpp"))
	void K2_OnGameClear();
	UFUNCTION(BlueprintImplementableEvent, Category = Game, Meta = (DisplayName = "OnGameOverCpp"))
	void K2_OnGameOver();
    
    
void AABPlayerController::GameScoreChanged(int32 NewScore)
{
	K2_OnScoreChanged(NewScore);
}

void AABPlayerController::GameClear()
{
	K2_OnGameClear();
}

void AABPlayerController::GameOver()
{
	K2_OnGameOver();
}

블루프린트에서 사용할 함수들을 PlayerController에 선언해준다. 이름 앞에 K2가 붙는 이유는 과거에 사용했던 kismet의 잔재이고 코드에서 동작하는 함수이름과 달라야하기 때문이라고 한다.

이 함수들이 블루프린트에서 이벤트를 발동시키는 것처럼 동작하려면 BlueprintImplementableEvent라는 키워드를 UFUNCTION내부에 넣어줘야한다.

그리고 Meta=(DisplayName="")에 블루프린트에서 보일 함수 이름을 지정할 수 있다.

이 경우엔 함수를 구현하지 않아도 언리얼엔진이 UFUNCTION매크로를 통해서 자동으로 이벤트임을 감지해서 본문을 만들어주기 때문에 따로 만들어주지 않는다.

아까 만들었던 함수 내부에서 K2함수들을 호출하도록 한다.

기존에 사용하던 C++ 게임모드 대신 블루프린트 게임모드로 대체한다.

컨트롤러역시 기존의 ABPlayerController를 상속받은 블루프린트로 대체하는데 내부의 이벤트그래프에 다음과 같이 노드를 놓는다.

게임오버되는 경우 IsSuccess를 false처리하고 위젯을 띄우고 게임을 클리어하는 경우 IsSuccess를 true로 처리하고 위젯을 띄운후에 Congratulation이라는 액터를 스폰한다.

코드에서 정의한 K2가 붙은 함수들도 연결해준다.


기존 HUD에 새로운 UI를 추가하는데 수업에서 제공받은 플레이어의 점수를 나타내는 UI를 우측 상단에 앵커를 두도록 해서 추가한다.


BeginPlay()가 호출될 때 우리가 만든 ABHUDWidget을 Create하고 AddViewport를 호출하여 화면에 띄운다.

ScoreWidget이라는 변수에 보관하여 변수가 변경될 때 스코어를 새로 세팅하도록 한다.

화면 우측 상단에 잘 나타난다.


이어서 ClearScore값을 세팅하기 위해 게임모드의 SetClearScore를 호출시킨다.

변수 ClearScore에 2를 넣어도 잘 업데이트가 되는 것을 볼 수 있다.


게임을 클리어했을 때와 게임오버가 되었을 때의 화면이다.

게임 저장

우리가 이 게임을 클리어하기 위해 몇번을 시도했는지 정보를 저장하고 불러들이는 기능을 추가하였다.

USaveGame을 상속받는 C++클래스를 생성해준다.

	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Game)
	int32 RetryCount;

시도 횟수를 저장할 변수 RetryCount 만 추가해주고 생성자에서 0으로 초기화시킨다.


// Save Game Section
protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = SaveGame)
	TObjectPtr<class UABSaveGame> SaveGameInstance;

게임이 실행되는 동안 SaveGame객체가 관리되도록 PlayerController에 변수를 추가해준다.

void AABPlayerController::BeginPlay()
{
	Super::BeginPlay();

	FInputModeGameOnly GameOnlyInputMode;
	SetInputMode(GameOnlyInputMode);

	SaveGameInstance = Cast<UABSaveGame>(UGameplayStatics::LoadGameFromSlot(TEXT("Player0"), 0));
	if (SaveGameInstance)
	{
		SaveGameInstance->RetryCount++;
	}
	else
	{
		SaveGameInstance = NewObject<UABSaveGame>();
		SaveGameInstance->RetryCount = 0;
	}

	K2_OnGameRetryCount(SaveGameInstance->RetryCount);
}

게임이 시작되면 저장된 게임이 있는지 확인하고 가져오도록 한다.

이때 UGameplayStatics::LoadGameFromSlot() 함수를 사용하면 편리하게 게임을 저장할 수 있다.

인자로 파일의 이름과 플레이어의 아이디를 넣는데 싱글플레이어의 경우 아이디가 항상 0번이 할당된다.

void AABPlayerController::GameOver()
{
	K2_OnGameOver();

	if (!UGameplayStatics::SaveGameToSlot(SaveGameInstance, TEXT("Player0"), 0))
	{
		UE_LOG(LogABPlayerController, Error, TEXT("Save Game Error!"));
	}

	K2_OnGameRetryCount(SaveGameInstance->RetryCount);
}

게임오버되었을 땐 UGameplayStatics::SaveGameToSlot()을 이용해서 반대로 저장해주면 된다.

	UFUNCTION(BlueprintImplementableEvent, Category = Game, Meta = (DisplayName = "OnGameRetryCountCpp"))
	void K2_OnGameRetryCount(int32 NewRetryCount);

게임데이터를 불러들였을 때 UI에 표시하기 위한 이벤트이다.

이벤트가 호출되었을 때 위젯의 RetryCount가 업데이트되도록 한다.

Retry를 하게되면 화면의 RetryCount가 증가할 것이고 우리가 저장한 파일이 있기 때문에 게임을 다시 플레이해도 그 횟수는 사라지지 않는다.

해당 파일은 우리가 지정한 이름으로 ProjectFolder/Saved/SaveGames에 저장된다.
해당 파일을 지우면 RetryCount가 0에서 시작하는 것을 볼 수 있다.

게임 빌드하기


shipping으로 빌드하게 되면 가장 빠르면서 용량이 작은 최종 결과물을 만들어낼 수 있다.

ProjectSetting-Packaging-Advanced에 가면 Additional Asset Directories to Cook 이라는 메뉴가 있는데 Cook은 우리가 사용할 애셋들을 플랫폼에 맞춰서 변환하는 작업이다.

우리가 만든 아이템 애셋들이 강한 참조를 걸고있지 않지만 코드를 통해서 불러들이도록 설정했다.
패키징을 하면 맵에 관련된 애셋들만 뽑아서 저장이 되는데 아이템에 관련된 정보는 빠지게 된다. 그래서 해당 폴더를 수동으로 추가해줘야 한다.

설정을 마치고 패키지를 담을 폴더를 설정하면 패키징이 시작된다.

애셋을 쿠킹하는 과정에서 오랜 시간이 걸린다.

패키징이 완료된 게임을 실행하면 Shipping 빌드이기 때문에 우리가 설정했던 DebugDraw같은 기능들은 다 사라져있다.

...
...
...

이 강의를 들으면서 게임의 전반적인 흐름을 약간이나마 이해할 수 있었던 것 같고 내가 배운것들을 이용해 직접 만들면서 잘 익힐 수 있도록 해야겠다.

profile
한번 해보자

0개의 댓글