완전한 게임 제품으로 발전하려면
게임의 시작, 게임의 종료, 미션 달성, 게임 데이터의 저장 및 로딩 등의 기능이 필요하다.
이를 구현하기 위해 알아두면 좋은 언리얼 엔진의 기능을 학습한다.
<1>. 게임 데이터의 저장과 로딩
여태 게임플레이의 기본적인 틀을 확장하여 플레이어의 데이터를 저장하고 이를 불러들이는 로직을 구현해 본다.
※ SaveGame이라는 언리얼 오브젝트를 상속받은 클래스를 설계하고, 이를 세이브게임 시스템에 넘겨주면 게임 데이터의 저장과 로딩을 간편하게 구현할 수 있다. (Saved 폴더에 있는 SaveGames라는 폴더에 데이터가 저장된다.)
※게임 세이브 기능에는 각 저장 파일에 접근할 수 있는 고유 이름인 '슬롯 이름'이 필요하다.
처음에는 세이브된 데이터가 없으니 '기본 세이브 데이터를 생성하는 로직'을 플레이어 스테이트의 InitPlayerData에 구현.
//ABPlayerState.cpp
void AABPlayerState::InitPlayerData()
{
auto ABSaveGame = Cast<UABSaveGame>(UGameplayStatics::LoadGameFromSlot(SaveSlotName, 0));
//SaveSlotName 이름으로 저장된 게임 데이터 로드
if (nullptr == ABSaveGame) //저장된 데이터가 없다면?
{
ABSaveGame = GetMutableDefault<UABSaveGame>(); //기본 객체를 반환
}
SetPlayerName(ABSaveGame->PlayerName);
SetCharacterLevel(ABSaveGame->Level);
GameScore = 0;
GameHighScore = ABSaveGame->HighScore;
Exp = ABSaveGame->Exp;
}
이제 플레이어 관련 데이터가 변경될 때마다 SavePlayerData()를 사용해 이를 저장한다.
이제 플레이어 스테이트의 하이스코어 값을 HUD UI에 연동시킨다.
//ABHUDWidget.cpp
void UABHUDWidget::UpdatePlayerState()
{
...
HighScore->SetText(FText::FromString(FString::FromInt(CurrentPlayerState->GetGameHighScore())));
}
<2>. 전투 시스템의 설계
게임 진행의 난이도를 점진적으로 높이고 전투에 관련된 부가 요소를 추가해 본다.
- 레벨업을 할 때 회복한다. - (이미 구현 완료)
- 무기를 들 때 더 긴 공격 범위를 가진다.
- 무기에는 공격력 증가치가 랜덤으로 부여되며, 저하될 수도 있다.
- 현재 게임 스코어가 높을수록 생성되는 NPC의 레벨도 증가한다.
- 2번 로직 구현
- 무기가 없으면 캐릭터의 AttackRange 속성을, 무기를 들면 무기의 AttackRange 속성을 사용.
- 무기 속성 키워드에 EditAnywhere, BlueprintReadWrite를 지정해 무기 블루프린트에서도 공격 범위값을 다르게 설정.
- 무기를 들고 있어도 무기를 변경할 수 있도록 CanSetWeapon의 값을 무조건 true로 설정. (기존 무기 없애고 새로 습득)
//ABCharacter.cpp
void AABCharacter::SetWeapon(AABWeapon* NewWeapon)
{
if(nullptr != CurrentWeapon)
{
CurrentWeapon->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
//기존 무기 분리하기
CurrentWeapon->Destroy();
CurrentWeapon = nullptr;
}
...
}
- 3번 로직 구현
- 기존 공격력을 증폭시킴.
- 무기에 랜덤 한 순수 공격력과 효과치 속성을 설정하고 최종 대미지를 산출할 때 이 데이터를 활용.
//ABWeapon.cpp
AABWeapon::AABWeapon()
{
...
AttackDamageMin = -2.5f;
AttackDamageMax = 10.0f;
AttackModifierMin = 0.85f;
AttackModifierMax = 1.25f;
}
void AABWeapon::BeginPlay()
{
Super::BeginPlay();
AttackDamage = FMath::RandRange(AttackDamageMin, AttackDamageMax); //공격력 랜덤
AttackModifier = FMath::RandRange(AttackModifierMin, AttackModifierMax); //효과 랜덤
}
//ABCharacter.cpp
float AABCharacter::GetFinalAttackDamage() const
{
float AttackDamage = (nullptr != CurrentWeapon) ?
(CharacterStat->GetAttack() + CurrentWeapon->GetAttackDamage()) : CharacterStat->GetAttack();
float AttackModifier = (nullptr != CurrentWeapon) ? CurrentWeapon->GetAttackModifier() : 1.0f;
return AttackDamage * AttackModifier;
}
- 4번 로직 구현
- NPC LOADING 스테이트에서 현재 게임 스코어를 게임 모드에게 질의하고, 이를 기반으로 캐릭터 레벨 값 설정.
//AABCharacter.cpp
...
#include "ABGameMode.h"
void AABCharacter::SetCharacterState(ECharacterState NewState)
{
CurrentState = NewState;
switch (CurrentState)
{
case ECharacterState::LOADING:
{
...
else //bIsPlayer == false 일 때
{
auto ABGameMode = Cast<AABGameMode>(GetWorld()->GetAuthGameMode());
//GetAuthGameMode : 게임 실행 중 게임 모드의 포인터를 가져올 때
int32 TargetLevel = FMath::CeilToInt((float)ABGameMode->GetScore() * 0.8f);
//CeilToInt : 소수 올림하여 정수로 변환.
int32 FinalLevel = FMath::Clamp<int32>(TargetLevel, 1, 20);
CharacterStat->SetNewLevel(FinalLevel);
}
...
}
<3>. 타이틀 화면의 제작
완전한 게임 제작을 위해 게임 타이틀 화면을 추가해 본다. UI는 예제의 'UI_Title.uasset' 파일을 사용한다.
'빈 레벨' 템플릿을 생성하여, 아무 기능 없이 UI화면만 띄우는 역할을 수행하는 레벨("Title")을 만든다.
→ 새 레벨을 만들었으니, 해당 레벨에서 사용할 '게임 모드'와 UI를 띄울 '플레이어 컨트롤러'를 제작한다.
※ 플레이어 컨트롤러 제작
- 이를 상속받은 블루프린트에서 앞으로 띄울 UI 클래스 값을 에디터에서 설정할 수 있도록 위젯 클래스 속성 추가하고 EditDefaultOnly 키워드를 지정.
- 게임을 시작하면 해당 클래스로부터 UI 인스턴스를 생성하고, 이를 뷰포트에 띄운 후에 입력은 UI에만 전달되도록 제작한다.
//ABUIPlayerController.h
...
protected:
virtual void BeginPlay() override;
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = UI)
TSubclassOf<class UUserWidget> UIWidgetClass;
UPROPERTY()
class UUserWidget* UIWidgetInstance;
//ABUIPlayerController.cpp
#include "Blueprint/UserWidget.h"
void AABUIPlayerController::BeginPlay()
{
Super::BeginPlay();
UIWidgetInstance = CreateWidget<UUserWidget>(this, UIWidgetClass); //UIWidgetClass 유형의 새 위젯 인스턴스를 생성함.
//UIWidgetClass는 UUserWidget의 서브클래스.
UIWidgetInstance->AddToViewport();
FInputModeUIOnly Mode;
Mode.SetWidgetToFocus(UIWidgetInstance->GetCachedWidget());
SetInputMode(Mode);
bShowMouseCursor = true; //UI와 상호작용할 때 필수적임.
}
ABUIPlayerController 클래스를 부모 클래스로 지정한 블루프린트 생성.
→ UIWidget Class 속성에 'UI_Title' 에셋 지정.
※ 플레이어 컨트롤러를 띄울 게임 모드 생성. (블루프린트 - GameModeBase)
→ Default Pawn Class = Pawn / Player Controller Class = 방금 생성한 블루프린트.
방금은 Title 레벨 화면을 만들었고, 이번엔 캐릭터를 고르는 Select 레벨 화면을 만들어보자.
- 버튼을 누르면 스켈레탈 메시가 변경되게 하기.
- 현재 월드에 있는 특정 타입을 상속받은 액터의 목록 : TActorIterator <액터 타입>을 사용해 가져올 수 있다.
여기서 선택한 캐릭터가 게임플레이에서 동일하게 나오도록 하려면, 선택한 캐릭터 정보를 저장하고 이를 로딩하는 기능을 만들어야 한다.
<4>. 게임의 중지와 결과화면
게임플레이 레벨에 게임을 잠시 중지하는 UI(Menu), 게임 결과를 띄우는 UI(Result)를 추가한다.
편의를 위해 Menu, Result 두 위젯이 사용하는 버튼이 기능별로 동일한 이름을 가지도록 설계한다.
- btnResume : 현재 진행 중인 게임으로 돌아간다.
- btnReturnToTitle : 타이틀 레벨로 돌아간다.
- btnRetryGame : 게임에 재도전한다.
※ 게임플레이 중지 단축키는 'M'로 설정한다. → 빙의한 폰에 관계없이 입력을 처리하므로, 플레이어 컨트롤러에서 구현하는 것이 적합하다.
※ UI가 공용으로 사용할 기본 클래스를 'ABGameplayWidget'이라는 이름으로 생성한다. (UserWidget을 부모로 한다.)
UI 위젯을 초기화하는 시점에 있는 NativeConstruct 함수에서 버튼을 찾고, 바인딩하는 로직을 구현한다.
※ 메뉴 UI가 나오면 고려할 사항들
- 게임플레이 중지 (SetPause 함수 사용)
- 버튼 클릭할 수 있도록 마우스 커서 보여주기 (bShowMouseCursor = true)
- 입력이 게임에 전달되지 않고 UI에만 전달되도록 (SetInputMode 함수에 FInputModeUIOnly 클래스를 인자로)
이제 메뉴의 버튼을 눌렀을 때 행동을 구현한다.
- RemoveFromParent : UI시스템의 함수로, 현재 뷰포트에 띄워진 자신을 제거할 수 있다.
- GetOwningPlayer : UI의 함수로, 현재 자신을 생성하고 관리하는 플레이어 컨트롤러의 정보를 가져올 수 있다.
※ 결과 UI가 나오면 고려할 사항들
임시로 게임의 미션을 2개 섹션을 COMPLETE 스테이트로 만드는 것으로 정해놓고, 미션 달성여부는 게임의 정보이므로 GameState에 bGameCleared라는 속성을 추가한다.
게임플레이가 종료되는 시점은 2가지이다.
1. 플레이어가 죽을 때 = 게임의 미션을 클리어 X → 컨트롤러의 ShowResult 함수를 호출해 결과 UI를 띄운다.
2. 목표를 달성했을 때 = 게임의 미션을 클리어 O → 모든 폰을 멈춘다. / bGameCleared = true로 설정
드디어 처음으로 언리얼 책 한 권을 끝냈다. 1개월이라는 시간이 걸렸고, 여기서 배운 내용들을 최대한 활용하여 간단한 토이 프로젝트를 만들어볼 예정이다. 기획부터 퍼블리싱까지, 개발일지 형태로 적어볼 예정이다.
'게임 개발 > 이득우의 언리얼 C++' 카테고리의 다른 글
[이득우의 언리얼 C++ 게임개발의 정석] #14. 게임플레이의 제작 (0) | 2024.07.03 |
---|---|
[이득우의 언리얼 C++ 게임개발의 정석] #13. 프로젝트의 설정과 무한 맵의 제작 (0) | 2024.07.02 |
[이득우의 언리얼 C++ 게임개발의 정석] #12. AI 컨트롤러와 비헤이비어 트리 (0) | 2024.06.28 |
[이득우의 언리얼 C++ 게임개발의 정석] #11. 게임 데이터와 UI 위젯 (0) | 2024.06.26 |
[이득우의 언리얼 C++ 게임개발의 정석] #10. 아이템 상자와 무기 제작 (0) | 2024.06.25 |