본문 바로가기

게임 개발/이득우의 언리얼 C++

[이득우의 언리얼 C++ 게임개발의 정석] #15. 게임의 완성

반응형

완전한 게임 제품으로 발전하려면
게임의 시작, 게임의 종료, 미션 달성, 게임 데이터의 저장 및 로딩 등의 기능이 필요하다.

이를 구현하기 위해 알아두면 좋은 언리얼 엔진의 기능을 학습한다.

<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>. 전투 시스템의 설계

게임 진행의 난이도를 점진적으로 높이고 전투에 관련된 부가 요소를 추가해 본다.

  1. 레벨업을 할 때 회복한다. - (이미 구현 완료)
  2. 무기를 들 때 더 긴 공격 범위를 가진다.
  3. 무기에는 공격력 증가치가 랜덤으로 부여되며, 저하될 수도 있다.
  4. 현재 게임 스코어가 높을수록 생성되는 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 = 방금 생성한 블루프린트.

 

타이틀-UI

 

 

방금은 Title 레벨 화면을 만들었고, 이번엔 캐릭터를 고르는 Select 레벨 화면을 만들어보자.

- 버튼을 누르면 스켈레탈 메시가 변경되게 하기.

- 현재 월드에 있는 특정 타입을 상속받은 액터의 목록 : TActorIterator <액터 타입>을 사용해 가져올 수 있다.

Select-UI

 

여기서 선택한 캐릭터가 게임플레이에서 동일하게 나오도록 하려면, 선택한 캐릭터 정보를 저장하고 이를 로딩하는 기능을 만들어야 한다.

 

<4>. 게임의 중지와 결과화면

게임플레이 레벨에 게임을 잠시 중지하는 UI(Menu), 게임 결과를 띄우는 UI(Result)를 추가한다.

 

편의를 위해 Menu, Result 두 위젯이 사용하는 버튼이 기능별로 동일한 이름을 가지도록 설계한다.

  • btnResume : 현재 진행 중인 게임으로 돌아간다.
  • btnReturnToTitle : 타이틀 레벨로 돌아간다.
  • btnRetryGame : 게임에 재도전한다.

※ 게임플레이 중지 단축키는 'M'로 설정한다. → 빙의한 폰에 관계없이 입력을 처리하므로, 플레이어 컨트롤러에서 구현하는 것이 적합하다.

 

※ UI가 공용으로 사용할 기본 클래스를 'ABGameplayWidget'이라는 이름으로 생성한다. (UserWidget을 부모로 한다.)

   UI 위젯을 초기화하는 시점에 있는 NativeConstruct 함수에서 버튼을 찾고, 바인딩하는 로직을 구현한다.

 

※ 메뉴 UI가 나오면 고려할 사항들

  • 게임플레이 중지 (SetPause 함수 사용)
  • 버튼 클릭할 수 있도록 마우스 커서 보여주기 (bShowMouseCursor = true)
  • 입력이 게임에 전달되지 않고 UI에만 전달되도록 (SetInputMode 함수에 FInputModeUIOnly 클래스를 인자로)

Menu-화면

 

이제 메뉴의 버튼을 눌렀을 때 행동을 구현한다.

  • RemoveFromParent : UI시스템의 함수로, 현재 뷰포트에 띄워진 자신을 제거할 수 있다.
  • GetOwningPlayer : UI의 함수로, 현재 자신을 생성하고 관리하는 플레이어 컨트롤러의 정보를 가져올 수 있다.

※ 결과 UI가 나오면 고려할 사항들

 

임시로 게임의 미션을 2개 섹션을 COMPLETE 스테이트로 만드는 것으로 정해놓고, 미션 달성여부는 게임의 정보이므로 GameState에 bGameCleared라는 속성을 추가한다.

 

게임플레이가 종료되는 시점은 2가지이다.

1. 플레이어가 죽을 때 = 게임의 미션을 클리어 X → 컨트롤러의 ShowResult 함수를 호출해 결과 UI를 띄운다.

2. 목표를 달성했을 때 = 게임의 미션을 클리어 O → 모든 폰을 멈춘다. /    bGameCleared = true로 설정

  

Result-화면

 

 

드디어 처음으로 언리얼 책 한 권을 끝냈다. 1개월이라는 시간이 걸렸고, 여기서 배운 내용들을 최대한 활용하여 간단한 토이 프로젝트를 만들어볼 예정이다. 기획부터 퍼블리싱까지, 개발일지 형태로 적어볼 예정이다.

반응형