언리얼 엔진이 제공하는 게임 관리 기능에 대해서 알아보자.
게임 모드
, 게임 상태
, 플레이어 상태
, 데이터 저장
에 대해서 집중적으로 알아볼 것.
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에 선언한다.
GetWorld() -> GetAuthGameMode()
GameMode를 따로 import 하지 않아도 전역변수인 GetWorld로부터 가져올 수 있다.
여기서 GetAuth라는 것을 뭐를 의미할까?
// 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를 만든다.
우리는 다음과 같은 상황들에서 HUD를 추가로 구현할 것이다.
과거 강의에서 Widget을 띄울 수 있는 객체는 Player Controller임을 배웠다.
GameMode에서 Controller를 가져오고 Controller에서 UI Widget을 불러와보자.
// GameMode.cpp
GetWorld() -> GetFirstPlayerController()
우리는 멀티플레이 게임이 아니라 싱글 게임이기 때문에 첫번째 Controller를 GetWorld를 통해 가져오면 된다. 조종하고 싶은 Pawn에 player0번 controller로 posses 했던 것을 기억해보자.
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가 성공적으로 업데이트 될 수 있는 것이다.
Controller에서 CreateWidget
과 AddToViewport
를 통해 UI Widget을 생성했다.
이를 주석처리하고 로직을 BP로 옮겨보자
// Player Controller.cpp
//RyanHUDWidget = CreateWidget<URyanHUDWidget>(this, RyanHUDWidgetClass);
//if (RyanHUDWidget)
//{
// RyanHUDWidget->AddToViewport();
//}
BP에 CreateWidget
과 AddToViewport
에 해당하는 노드들이 존재한다. 이 부분을 주석 처리하고 우리가 World Setting에서 설정한 BP_Controller에 다음과 같은 BP 로직을 구현한다.
우리가 과거 강의에서 만들었던 Main HUD에 점수 UI Widget을 만들어서 추가해볼 것이다. Retry Count
, Current Score
, Clear Score
총 3개의 데이터 값을 매핑해야 한다. Retry Count
는 밑에서 SaveGame
관련 부분에서 다루고 지금은 나머지 2개의 값을 매핑해보자.
Player Score Widget을 만들고 Current Score
, Clear Score
의 이름을 각각 Txt Score
, Txt Clear Score
라고 붙이자. BP에서 이 이름들을 통해 관련 Text 값들을 바꿀 것이다.
Txt Clear Score
는 게임의 승리를 위해 몇 명의 NPC를 처리해야 하는지에 대한 지표이다.
Txt Score
는 점수 추가에 색깔까지 초록색으로 바뀌는 로직을 구현한다.
먼저, 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 변수를 저장한다.
// PlayerController 부분
SaveGameInstance = Cast<URyanSaveGame>(UGameplayStatics::LoadGameFromSlot(TEXT("Player0"),0));
if (SaveGameInstance)
{
SaveGameInstance->RetryCount++;
}
else
{
SaveGameInstance = NewObject<URyanSaveGame>();
}
SaveGame
을 통해 게임 데이터를 관리하면 결과값들이 파일로 저장되기 때문에
언제든지 게임을 플레이해도 이전 데이터 값들이 불러와진다. UGameplayStatics
의 LoadGameFromSlot
키워드를 통해 저장.
SaveGame
인스턴스의 저장 경로는
Project 폴더
-> Saved
-> SaveGames
에서 .sav
형태로 저장된 것을 찾을 수 있다.
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의 목적도 이와 비슷하다.
클래스의 계층구조에서 아래 layer에 있는 클래스들이 윗 layer에 있는 클래스들을 참조할때도 interface를 관습적으로 사용한다. 이에 대한 이유는 아직 명확하지 않지만, 참조의 방향이 위에서 아래냐, 아래에서 위냐에 대한 구분을 두기 위함이라고 생각한다.
No C++ Implementation: Functions marked with BlueprintImplementableEvent should not have a C++ implementation. They are meant to be implemented in Blueprints only.
블루프린트 레퍼런스 뷰어 스샷