2-15강 게임의 완성

Ryan Ham·2024년 7월 18일
0

이득우 Unreal

목록 보기
22/23
post-thumbnail

강의 목표

  • GameMode를 활용한 게임의 승패조건을 설정하는 과정의 이해
  • C++에서 Blueprint로 확장하는 방법의 학습
  • SaveGame 객체를 활용한 간편한 게임 데이터 저장과 불러오기
  • 최종 게임의 패키징

언리얼 엔진이 제공하는 게임 관리 기능에 대해서 알아보자.
게임 모드, 게임 상태, 플레이어 상태, 데이터 저장에 대해서 집중적으로 알아볼 것.


게임의 승패조건 관리

게임 모드

  • 멀티 플레이를 포함해 게임에서 유일하게 존재하는 게임의 심판 오브젝트
  • 최상단에서 게임의 진행을 관리하며, 게임 판정에 관련된 중요한 행동을 주관하는데 적합함.
  • 다양한 게임 규칙을 적용할 수 있도록 핵심 기능과 분리해 설계하는 것이 바람직함.
  • 게임의 상태와 플레이어의 상태를 별도로 저장할 수 있는 프레임웍을 제공함.

GameMode를 멀티플레이로 더 확장하게 된다면, 게임의 상태를 관리하는 GameState Actor, 플레이어의 정보를 관리하는 PlayerState Actor와 같은 다양한 Actor framework들 또한 제공하고 있다. 하지만 싱글플레이 게임에서는 GameMode 하나로 작업할 예정.

// GameMode.cpp
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Game)
int32 ClearScore;

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

UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Game)
uint8 bIsCleared : 1;

Game의 승패와 관련된 중요한 변수들은 GameMode에서 관리해주는 것이 좋다. 게임의 우승 조건인 ClearScore와, 게임의 현재 점수를 관리할 CurrentScore 변수를 GameMode에 선언한다.


GameMode 가져오기

GetWorld() -> GetAuthGameMode()

GameMode를 따로 import 하지 않아도 전역변수인 GetWorld로부터 가져올 수 있다.

여기서 GetAuth라는 것을 뭐를 의미할까?

  • Auth는 Authorized의 약자이다. 우리가 MultiPlayer까지 게임의 범위를 확장했을때도 이 GameMode는 모든 플레이어를 통틀어서 단 하나만 존재한다. 따라서 AuthGameMode는 방장이 소유한 (인증된) GameMode이다.
// StageGimmick.cpp
// 죽었을때 바로 REWARD state로 넘어가는 것이 아니라, GameMode의 달성 조건을 체크하고 넘어간다. 
void ARyanStageGimmick::OnOpponentDestroyed(AActor* DestroyedActor)
{
	IRyanGameInterface* RyanGameMode = Cast<IRyanGameInterface>(GetWorld()->GetAuthGameMode());
	
	if (RyanGameMode)
	{
		RyanGameMode->OnPlayerScoreChanged(CurrentStageNum);
		
		if (RyanGameMode->IsGameCleared())
		{
			return;
		}
	}
	
	SetState(EStageState::REWARD);
}

NPC를 처치할때 OnOpponentDestroyed라는 함수가 불리게 설정해두었다. 이 함수 안에서 GameMode에 접근해 몬스터가 죽었을때 관련 점수를 업데이트 해보자.

// GameInterface.h
virtual void OnPlayerScoreChanged(int32 NewPlayerScore) = 0;
virtual void OnPlayerDead() = 0;
virtual void IsGameCleared() = 0;

GameMode에서 꼭 구현해야 할 함수들을 만드는 작업의 일환으로 GameInterface를 만든다.


UI 보여주기

우리는 다음과 같은 상황들에서 HUD를 추가로 구현할 것이다.

  • 플레이어가 게임을 clear 했을때
  • 플레이어가 죽었을 때

과거 강의에서 Widget을 띄울 수 있는 객체는 Player Controller임을 배웠다.
GameMode에서 Controller를 가져오고 Controller에서 UI Widget을 불러와보자.

// GameMode.cpp
GetWorld() -> GetFirstPlayerController()

우리는 멀티플레이 게임이 아니라 싱글 게임이기 때문에 첫번째 Controller를 GetWorld를 통해 가져오면 된다. 조종하고 싶은 Pawn에 player0번 controller로 posses 했던 것을 기억해보자.

UI widget을 부르는 event 만들기

Event를 통해서 Controller가 관련 UI들을 띄워주어야 하는데, 이를 C++로 모두 구현하는 것은 생산성이 좋지 않다. 따라서 이를 블루프린트로 가지고 오자.

Step 1 : Controller.h에 event trigger하는 함수를 선언.

// PlayerController.h
// Blueprint와 호환하기 위해 UFUNCTION 매크로를 달아주고, 
// BP의 전신인 Kistmet을 표현해주기 위해서 접두사 K2를 붙인다. 접두사 뒤에 붙은 함수를 실행해 줄것이라는 뜻으로 이렇게 관습적으로 작명
// C++함수 실행 -> Kismet 접두사 붙은 블루프린트 함수 실행
UFUNCTION(BlueprintImplementableEvent, Category = Game, Meta = (DisplayName = "OnScoreChangedCpp"))
void K2_OnScoreChanged(int32 NewScore);

// BP의 전신인 Kistmet을 표현해주기 위해서 접두사 K2를 붙인다.
UFUNCTION(BlueprintImplementableEvent, Category = Game, Meta = (DisplayName = "OnGameClearCpp"))
void K2_OnGameClear();

// BP의 전신인 Kistmet을 표현해주기 위해서 접두사 K2를 붙인다.
UFUNCTION(BlueprintImplementableEvent, Category = Game, Meta = (DisplayName = "OnGameOverCpp"))
void K2_OnGameOver();

UFUNCTION 매크로 안에 들어있는 BlueprintImplementableEvent meta keyword는 선언된 함수의 구현부를 Blueprint에서 작성하겠다는 의미이다. 따라서, C++에서 작성하지 않아도 컴파일 에러가 나지 않는다.

Step 2 : Event trigger하기

// Player Controller.cpp
void ARyanPlayerController::GameScoreChanged(int32 NewScore)
{
	K2_OnScoreChanged(NewScore);
}

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

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

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

Event를 trigger하는 방법은 다음과 같다. 우선 C++에서 함수를 하나 만들고 이 함수에서는 Step 1에 선언한 함수를 부른다. Kismet 접두사가 붙은 함수들이 불리면 아래 BP에서 이벤트(빨간색 화살표)가 실행되고 UI가 성공적으로 업데이트 될 수 있는 것이다.

UI 생성 로직 C++에서 BP로 옮기기

Controller에서 CreateWidgetAddToViewport를 통해 UI Widget을 생성했다.
이를 주석처리하고 로직을 BP로 옮겨보자

// Player Controller.cpp

//RyanHUDWidget = CreateWidget<URyanHUDWidget>(this, RyanHUDWidgetClass);	
//if (RyanHUDWidget)
//{
//	RyanHUDWidget->AddToViewport();
//}

BP에 CreateWidgetAddToViewport에 해당하는 노드들이 존재한다. 이 부분을 주석 처리하고 우리가 World Setting에서 설정한 BP_Controller에 다음과 같은 BP 로직을 구현한다.


점수판 UI Widget

우리가 과거 강의에서 만들었던 Main HUD에 점수 UI Widget을 만들어서 추가해볼 것이다. Retry Count, Current Score, Clear Score 총 3개의 데이터 값을 매핑해야 한다. Retry Count는 밑에서 SaveGame 관련 부분에서 다루고 지금은 나머지 2개의 값을 매핑해보자.

Player Score BP

Player Score Widget을 만들고 Current Score, Clear Score의 이름을 각각 Txt Score, Txt Clear Score라고 붙이자. BP에서 이 이름들을 통해 관련 Text 값들을 바꿀 것이다.

Txt Clear Score는 게임의 승리를 위해 몇 명의 NPC를 처리해야 하는지에 대한 지표이다.

Txt Score는 점수 추가에 색깔까지 초록색으로 바뀌는 로직을 구현한다.

Controller BP

먼저, Controller.h에서 관련 변수들을 UPROPERTY 매크로를 붙여주어서 Controller BP에서도 사용 가능하게 만든다.

BP 내부에서 Object 변수하나를 설정하고 이 변수를 위에서 만들었던 Player Score Widget과 연동시킨다. BP 사진에서 Score Widget이 이 오브젝트 이름.

Score를 업데이트 하는 부분과 Retry score를 업데이트하는 BP 로직 부분은 비교적 간단하다. Score Widget에서 핀을 뽑으면 이 BP안에 있는 변수들과 함수들에 대한 setter를 접근할 수 있는데 event도 일종의 함수이므로 바로 Score와 Retry Count에 대한 set를 할 수 있다.

위 그림은 게임의 승패시에 활성화되는 event들이다. 성공시에는 성공 HUD, 실패시에는 실패 HUD를 각각 spawn한다.

Score가 승리 Score에 도달하게 되면 승리 HUD가 나타나게 된다.

캐릭터가 NPC에 의해 죽게 되면 죽음 HUD를 표시한다. Retry 버튼을 누르면 해당 게임이 재시작되게 되고 Retry Count에 +1을 업데이트 해준다.


게임의 저장 기능

언리얼 엔진에서 제공하는 SaveGame 클래스가 있다. 여기에 재시도 횟수인 Retry Count 변수를 저장한다.

SaveGame 로드하기

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

SaveGame을 통해 게임 데이터를 관리하면 결과값들이 파일로 저장되기 때문에
언제든지 게임을 플레이해도 이전 데이터 값들이 불러와진다. UGameplayStaticsLoadGameFromSlot 키워드를 통해 저장.

SaveGame 인스턴스의 저장 경로는

Project 폴더 -> Saved -> SaveGames에서 .sav 형태로 저장된 것을 찾을 수 있다.

.exe 파일로 게임 만들기

Windows에서 작동하는 실행파일 만들기

Step 1
Platforms -> Windows -> Shipping으로 옵션 선택

Step 2
Platforms -> Packaging Settings에서
Project -> Packaging -> Advanced -> Additional Asset Directories to Cook에서 추가로 Cooking할 에셋 등록

Cook은 우리가 사용할 에셋들을 플랫폼에 맞춰서 변환하는 작업인데, 만약 약참조를 걸고 있거나, 코드에서 에셋들을 불러들이는 부분들이 있다면 여기서 그러한 에셋들이 정보를 수동으로 추가해 주어야 한다.

여기에 추가로 우리가 빌드하고 싶은 Map만 넣을 수도 있다. 위에서는 게임에서 실행될 "Main" 맵만 추가시킨 모습.

Step 3
Platforms -> Windows -> Package project을 눌러서 .exe 파일을 최종적으로 뽑아내 보자.


끝!


기타

Interface의 사용 목적

다른 언어에서 추상 클래스를 만들면 이를 상속하는 클래스들은 추상 클래스에 있는 순수 가상함수들을 모두 다 구현해야 한다. Interface의 목적도 이와 비슷하다.

클래스의 계층구조에서 아래 layer에 있는 클래스들이 윗 layer에 있는 클래스들을 참조할때도 interface를 관습적으로 사용한다. 이에 대한 이유는 아직 명확하지 않지만, 참조의 방향이 위에서 아래냐, 아래에서 위냐에 대한 구분을 두기 위함이라고 생각한다.

BlueprintImplementableEvent 메타데이터는 무엇일까?

No C++ Implementation: Functions marked with BlueprintImplementableEvent should not have a C++ implementation. They are meant to be implemented in Blueprints only.

Blueprint Reference Viewer

블루프린트 레퍼런스 뷰어 스샷

profile
🏦KAIST EE | 🏦SNU AI(빅데이터 핀테크 전문가 과정) | 📙CryptoHipsters 저자

0개의 댓글