게임 개발 (언리얼 엔진)

UE4 생존게임 제작 - 17 (SaveGameObject를 사용한 게임 저장 및 각종 UI 제작)

언린이 2020. 11. 22. 18:31

1. 시작 UI 수정

 

 

SaveGameObject를 사용해서 게임 저장을 제작하기 전에, 사용자가 저장된 게임을 불러올 수 있도록 시작 UI에 Continue Play 버튼을 추가해주었습니다.

 

 

2. SaveGameObject 제작

 

 

SaveGame 클래스를 상속받은 C++ 클래스를 생성하였습니다.

 

 

// 플레이어의 능력치
	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 HP;

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 Moisture;

	UPROPERTY(VisibleAnywhere, Category = Basic)
	float MoistureMax;

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 ArrowNumMax;

	UPROPERTY(VisibleAnywhere, Category = Basic)
	float Armor;

	UPROPERTY(VisibleAnywhere, Category = Basic)
	float Attack;

	UPROPERTY(VisibleAnywhere, Category = Basic)
	FVector SurvivorLocation;

	// 인벤토리 아이템
	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 RawMeatCount; // 0

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 GrilledMeatCount; // 1

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 DearLeatherCount; // 2

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 ZebraBoneCount; // 3

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 BranchCount; // 4

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 LionLeatherCount; // 5

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 HornCount; // 6

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 ArrowCount; // 7

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 LeatherCount; // 8

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 HardLeatherCount; // 9

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 SunscreenCount; // 10

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 CloakCount; // 11

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 ArrowBagCount; // 12

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 ArmorUpCount; // 13

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 AttackUpCount; //14

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 EssenceCount; // 15

	// 슬롯 이름과 인덱스
	UPROPERTY(VisibleAnywhere, Category = Basic)
	FString SaveSlotName;

	UPROPERTY(VisibleAnywhere, Category = Basic)
	int32 UserIndex;

 

저장해야 할 데이터들, 슬롯 이름 그리고 인덱스를 선언해주었습니다.

 

 

USaveSurvivorState::USaveSurvivorState()
{
	SaveSlotName = TEXT("SurvivorSaveSlot");
	UserIndex = 0;
}

 

그리고 나서 생성자에서 슬롯 이름과 인덱스를 정의해주었습니다.

 

 

// Save 버튼 클릭시 실행될 함수
void UUISetting::SaveButtonCallback()
{
	APlayerSurvivor* pOwner = Cast<APlayerSurvivor>(GetWorld()->GetFirstPlayerController()->GetPawn());

	if (IsValid(pOwner))
		pOwner->SaveSurvivorState();
}

 

저번에 만들어 준 SaveButtonCallback 함수에서 플레이어의 SaveSurvivorState 함수를 호출해주었습니다.

해당 함수에서 플레이어의 데이터들을 저장할 것입니다.

 

 

// 플레이어의 데이터 저장
void APlayerSurvivor::SaveSurvivorState()
{
	USaveSurvivorState* SaveGameInstance = Cast<USaveSurvivorState>(UGameplayStatics::CreateSaveGameObject(USaveSurvivorState::StaticClass()));
	
	// 플레이어 상태 저장
	SaveGameInstance->HP = SurvivorState.iHP;
	SaveGameInstance->Moisture = SurvivorState.iMoisture;
	SaveGameInstance->MoistureMax = MoistureMax;
	SaveGameInstance->ArrowNumMax = SurvivorState.iArrowNumMax;
	SaveGameInstance->Armor = SurvivorState.fArmor;
	SaveGameInstance->Attack = SurvivorState.fAttack;
	SaveGameInstance->SurvivorLocation = GetActorLocation();

	// 인벤토리 아이템 저장
	SaveGameInstance->RawMeatCount = Cast<UInventoryItemData>(List->GetItemAt(0))->GetItemCount();
	SaveGameInstance->GrilledMeatCount = Cast<UInventoryItemData>(List->GetItemAt(1))->GetItemCount();
	SaveGameInstance->DearLeatherCount = Cast<UInventoryItemData>(List->GetItemAt(2))->GetItemCount();
	SaveGameInstance->ZebraBoneCount = Cast<UInventoryItemData>(List->GetItemAt(3))->GetItemCount();
	SaveGameInstance->BranchCount = Cast<UInventoryItemData>(List->GetItemAt(4))->GetItemCount();
	SaveGameInstance->LionLeatherCount = Cast<UInventoryItemData>(List->GetItemAt(5))->GetItemCount();
	SaveGameInstance->HornCount = Cast<UInventoryItemData>(List->GetItemAt(6))->GetItemCount();
	SaveGameInstance->ArrowCount = Cast<UInventoryItemData>(List->GetItemAt(7))->GetItemCount();
	SaveGameInstance->LeatherCount = Cast<UInventoryItemData>(List->GetItemAt(8))->GetItemCount();
	SaveGameInstance->HardLeatherCount = Cast<UInventoryItemData>(List->GetItemAt(9))->GetItemCount();
	SaveGameInstance->SunscreenCount = Cast<UInventoryItemData>(List->GetItemAt(10))->GetItemCount();
	SaveGameInstance->CloakCount = Cast<UInventoryItemData>(List->GetItemAt(11))->GetItemCount();
	SaveGameInstance->ArrowBagCount = Cast<UInventoryItemData>(List->GetItemAt(12))->GetItemCount();
	SaveGameInstance->ArmorUpCount = Cast<UInventoryItemData>(List->GetItemAt(13))->GetItemCount();
	SaveGameInstance->AttackUpCount = Cast<UInventoryItemData>(List->GetItemAt(14))->GetItemCount();
	SaveGameInstance->EssenceCount = Cast<UInventoryItemData>(List->GetItemAt(15))->GetItemCount();

	UGameplayStatics::SaveGameToSlot(SaveGameInstance, SaveGameInstance->SaveSlotName, SaveGameInstance->UserIndex);
}

 

SaveGame 인스턴스를 생성한 후, 해당 인스턴스에 플레이어의 상태와 인벤토리 아이템들을 저장해주었습니다.

 

 

 

그리고 나서 게임을 실행한 후, Save 버튼을 클릭하면 프로젝트 폴더에 sav 파일이 생성됩니다. 해당 파일에 저장된 데이터들이 들어있습니다.

 

 

	// 저장된 sav 파일을 가져온다
	USaveSurvivorState* LoadGameInstance = Cast<USaveSurvivorState>(UGameplayStatics::CreateSaveGameObject(USaveSurvivorState::StaticClass()));
	LoadGameInstance = Cast<USaveSurvivorState>(UGameplayStatics::LoadGameFromSlot(LoadGameInstance->SaveSlotName, LoadGameInstance->UserIndex));
	
	if (pSurvivorInfo)
	{
		SurvivorState.fAttack = LoadGameInstance->Attack;
		SurvivorState.fArmor = LoadGameInstance->Armor;
		SurvivorState.iHP = LoadGameInstance->HP;
		SurvivorState.iHPMax = pSurvivorInfo->HPMax;
		SurvivorState.iMoisture = LoadGameInstance->Moisture;
		SurvivorState.iMoistureMax = pSurvivorInfo->MoistureMax;
		MoistureMax = LoadGameInstance->MoistureMax;
		SurvivorState.iArrowNumMax = LoadGameInstance->ArrowNumMax;
	}
	
	// 저장된 인벤토리 항목들을 List에 재설정 해준다
	Cast<UInventoryItemData>(List->GetItemAt(0))->SetItemCount(LoadGameInstance->RawMeatCount);
	Cast<UInventoryItemData>(List->GetItemAt(1))->SetItemCount(LoadGameInstance->GrilledMeatCount);
	Cast<UInventoryItemData>(List->GetItemAt(2))->SetItemCount(LoadGameInstance->DearLeatherCount);
	Cast<UInventoryItemData>(List->GetItemAt(3))->SetItemCount(LoadGameInstance->ZebraBoneCount);
	Cast<UInventoryItemData>(List->GetItemAt(4))->SetItemCount(LoadGameInstance->BranchCount);
	Cast<UInventoryItemData>(List->GetItemAt(5))->SetItemCount(LoadGameInstance->LionLeatherCount);
	Cast<UInventoryItemData>(List->GetItemAt(6))->SetItemCount(LoadGameInstance->HornCount);
	Cast<UInventoryItemData>(List->GetItemAt(7))->SetItemCount(LoadGameInstance->ArrowCount);
	Cast<UInventoryItemData>(List->GetItemAt(8))->SetItemCount(LoadGameInstance->LeatherCount);
	Cast<UInventoryItemData>(List->GetItemAt(9))->SetItemCount(LoadGameInstance->HardLeatherCount);
	Cast<UInventoryItemData>(List->GetItemAt(10))->SetItemCount(LoadGameInstance->SunscreenCount);
	Cast<UInventoryItemData>(List->GetItemAt(11))->SetItemCount(LoadGameInstance->CloakCount);
	Cast<UInventoryItemData>(List->GetItemAt(12))->SetItemCount(LoadGameInstance->ArrowBagCount);
	Cast<UInventoryItemData>(List->GetItemAt(13))->SetItemCount(LoadGameInstance->ArmorUpCount);
	Cast<UInventoryItemData>(List->GetItemAt(14))->SetItemCount(LoadGameInstance->AttackUpCount);
	Cast<UInventoryItemData>(List->GetItemAt(15))->SetItemCount(LoadGameInstance->EssenceCount);

 

저장된 데이터들을 사용할 곳에서 인스턴스를 생성해준 후, 데이터들을 가져와서 사용해주면 됩니다.

저의 경우에는 사용자가 Continue Play 버튼을 눌렀을 경우 데이터들을 불러와 사용해주었습니다.

 

 

3. 확인 문구 UI 제작

 

저장된 게임이 존재하는데 사용자가 New Game 버튼을 실수로 눌렀을 때, 바로 새로운 게임이 시작된다면 사용자에게 큰 낭패일 것입니다. 그래서 New Game 버튼을 눌렀을 때, 만약 저장된 게임이 존재한다면 확인 문구를 띄워줄 것입니다. 또한, 저장된 게임이 없는데 사용자가 Continue Play 버튼을 눌렀을 때는 경고 문구를 띄워줄 것입니다.

 

 

 

먼저 위젯 블루프린트를 생성한 후, UI를 꾸며주었습니다.

 

 

// 위젯 클래스의 생성자
void UUICheckNewGame::NativeConstruct()
{
	Super::NativeConstruct();

	// 위젯 블루프린트의 버튼을 이름을 통해 가져온다
	YesButton = Cast<UButton>(GetWidgetFromName(TEXT("YesButton")));
	NoButton = Cast<UButton>(GetWidgetFromName(TEXT("NoButton")));

	// 버튼 클릭시 호출될 델리게이트에 함수를 등록한다
	YesButton->OnClicked.AddDynamic(this, &UUICheckNewGame::YesButtonCallback);
	NoButton->OnClicked.AddDynamic(this, &UUICheckNewGame::NoButtonCallback);
}

 

그리고 위젯 블루프린트의 부모 클래스로 지정해줄 클래스를 생성한 후,

버튼을 가져와서 버튼 클릭시 호출될 델리게이트에 함수를 등록해주었습니다.

 

 

// Yes 버튼 클릭시 실행될 함수
void UUICheckNewGame::YesButtonCallback()
{
	UMainGameInstance* GameInst = GetGameInstance<UMainGameInstance>();
	GameInst->SetContinueGame(false);

	// 레벨을 바꿔주는 함수
	UGameplayStatics::OpenLevel(GetWorld(), TEXT("Africa"));
}

// No 버튼 클릭시 실행될 함수
void UUICheckNewGame::NoButtonCallback()
{
	VisibleCheck(ESlateVisibility::Collapsed);
}

void UUICheckNewGame::VisibleCheck(ESlateVisibility eVisibility)
{
	this->SetVisibility(eVisibility);
}

 

MainGameInstance에 사용자가 저장된 정보를 사용할 것인지 아닌지에 대해 판단할 bool 변수를 만들어준 후,

사용자가 Yes 버튼을 클릭하면 해당 bool 변수를 false로 초기화해주었습니다.

그리고 사용자가 No 버튼을 클릭하면 확인 문구 UI를 안보이는 상태로 바꿔주었습니다.

 

 

// New Game 버튼 클릭시 실행될 함수
void UStartLevelWidget::NewGameButtonCallback()
{
	USaveSurvivorState* LoadGameInstance = Cast<USaveSurvivorState>(UGameplayStatics::CreateSaveGameObject(USaveSurvivorState::StaticClass()));
	LoadGameInstance = Cast<USaveSurvivorState>(UGameplayStatics::LoadGameFromSlot(LoadGameInstance->SaveSlotName, LoadGameInstance->UserIndex));

	// sav 파일이 있을 경우
	if (IsValid(LoadGameInstance))
	{
		// 확인 문구 UI를 띄워준다
		CheckNewGame->VisibleCheck(ESlateVisibility::Visible);
	}

	// sav 파일이 없을 경우 바로 시작
	else
	{
		UMainGameInstance* GameInst = GetGameInstance<UMainGameInstance>();
		GameInst->SetContinueGame(false);

		// 레벨을 바꿔주는 함수
		UGameplayStatics::OpenLevel(GetWorld(), TEXT("Africa"));
	}
}

 

시작 화면 UI 클래스의 New Game 버튼 클릭시에 호출될 함수에서 먼저 sav 파일을 불러왔습니다.

만약 sav 파일이 있을 경우 확인 문구를 띄워주었습니다.

그렇지 않은 경우는 MainGameInstance의 bool 변수에 false로 초기화해준 후 레벨을 넘겨주었습니다.

 

 

 

이 문구는 저장된 게임이 없는데 사용자가 Continue Play 버튼을 눌렀을 경우 화면에 띄워줄 경고 문구입니다.

 

 

// Continue 버튼 클릭시 실행될 함수
void UStartLevelWidget::ContinueButtonCallback()
{
	USaveSurvivorState* LoadGameInstance = Cast<USaveSurvivorState>(UGameplayStatics::CreateSaveGameObject(USaveSurvivorState::StaticClass()));
	LoadGameInstance = Cast<USaveSurvivorState>(UGameplayStatics::LoadGameFromSlot(LoadGameInstance->SaveSlotName, LoadGameInstance->UserIndex));

	// sav 파일이 없을 경우
	if (!IsValid(LoadGameInstance))
	{
		CheckContinueGame->VisibleCheck(ESlateVisibility::Visible);
	}

	// sav 파일이 있을 경우 바로 시작
	else
	{
		UMainGameInstance* GameInst = GetGameInstance<UMainGameInstance>();
		GameInst->SetContinueGame(true);

		// 레벨을 바꿔주는 함수
		UGameplayStatics::OpenLevel(GetWorld(), TEXT("Africa"));
	}
}

 

이 문구는 New Game 버튼의 확인 문구와는 반대로 sav 파일이 없는 경우 화면에 띄워주었습니다.

그리고 sav 파일이 있는 경우에는 MainGameInstance의 bool 변수를 true로 초기화해준 후 레벨을 넘겨주었습니다.

 

 

// 새로운 게임을 시작할 때
	if (!GameInst->GetContinueGame())
	{
		// 플레이어를 기본 정보로 설정
		if (pSurvivorInfo)
		{
			SurvivorState.fAttack = pSurvivorInfo->Attack;
			SurvivorState.fArmor = pSurvivorInfo->Armor;
			SurvivorState.iHP = pSurvivorInfo->HP;
			SurvivorState.iHPMax = pSurvivorInfo->HPMax;
			SurvivorState.iMoisture = pSurvivorInfo->Moisture;
			SurvivorState.iMoistureMax = pSurvivorInfo->MoistureMax;
			SurvivorState.iArrowNumMax = pSurvivorInfo->ArrowNumMax;
		}
	}

	// 저장된 게임을 시작할 때
	else
	{
		// 저장된 sav 파일을 가져온다
		USaveSurvivorState* LoadGameInstance = Cast<USaveSurvivorState>(UGameplayStatics::CreateSaveGameObject(USaveSurvivorState::StaticClass()));
		LoadGameInstance = Cast<USaveSurvivorState>(UGameplayStatics::LoadGameFromSlot(LoadGameInstance->SaveSlotName, LoadGameInstance->UserIndex));
		
		// 플레이어를 저장된 정보로 설정
		if (pSurvivorInfo)
		{
			SurvivorState.fAttack = LoadGameInstance->Attack;
			SurvivorState.fArmor = LoadGameInstance->Armor;
			SurvivorState.iHP = LoadGameInstance->HP;
			SurvivorState.iHPMax = pSurvivorInfo->HPMax;
			SurvivorState.iMoisture = LoadGameInstance->Moisture;
			SurvivorState.iMoistureMax = pSurvivorInfo->MoistureMax;
			MoistureMax = LoadGameInstance->MoistureMax;
			SurvivorState.iArrowNumMax = LoadGameInstance->ArrowNumMax;
		}
		
		// 저장된 인벤토리 항목들을 불러온다
		Cast<UInventoryItemData>(List->GetItemAt(0))->SetItemCount(LoadGameInstance->RawMeatCount);
		Cast<UInventoryItemData>(List->GetItemAt(1))->SetItemCount(LoadGameInstance->GrilledMeatCount);
		Cast<UInventoryItemData>(List->GetItemAt(2))->SetItemCount(LoadGameInstance->DearLeatherCount);
		Cast<UInventoryItemData>(List->GetItemAt(3))->SetItemCount(LoadGameInstance->ZebraBoneCount);
		Cast<UInventoryItemData>(List->GetItemAt(4))->SetItemCount(LoadGameInstance->BranchCount);
		Cast<UInventoryItemData>(List->GetItemAt(5))->SetItemCount(LoadGameInstance->LionLeatherCount);
		Cast<UInventoryItemData>(List->GetItemAt(6))->SetItemCount(LoadGameInstance->HornCount);
		Cast<UInventoryItemData>(List->GetItemAt(7))->SetItemCount(LoadGameInstance->ArrowCount);
		Cast<UInventoryItemData>(List->GetItemAt(8))->SetItemCount(LoadGameInstance->LeatherCount);
		Cast<UInventoryItemData>(List->GetItemAt(9))->SetItemCount(LoadGameInstance->HardLeatherCount);
		Cast<UInventoryItemData>(List->GetItemAt(10))->SetItemCount(LoadGameInstance->SunscreenCount);
		Cast<UInventoryItemData>(List->GetItemAt(11))->SetItemCount(LoadGameInstance->CloakCount);
		Cast<UInventoryItemData>(List->GetItemAt(12))->SetItemCount(LoadGameInstance->ArrowBagCount);
		Cast<UInventoryItemData>(List->GetItemAt(13))->SetItemCount(LoadGameInstance->ArmorUpCount);
		Cast<UInventoryItemData>(List->GetItemAt(14))->SetItemCount(LoadGameInstance->AttackUpCount);
		Cast<UInventoryItemData>(List->GetItemAt(15))->SetItemCount(LoadGameInstance->EssenceCount);
	}

 

마지막으로 플레이어의 BeginPlay 함수에서 MainGameInstance에 설정해두었던 bool 변수가 false일 경우, 사용자가 새로운 게임을 시작한 것이므로 플레이어를 기본 정보로 설정해주었습니다.

그리고 bool 변수가 true일 경우는 저장된 데이터가 존재하고 사용자가 저장된 게임을 불러온 것이므로 플레이어를 저장된 데이터들을 사용하여 설정해주었습니다.

 

 

다음에 해야할 일

 

Game Ending 시스템 제작