* 타이틀 제작을 위해서 빈공백의 레벨을 새로 만든 후 Title 로 저장한다.

* 해당 레벨은 아무 기능 없이 UI 화면만 띄우는 역활을 수행할 것이다.

* 사용할 게임 모드와 UI를 띄울 플레이어 컨트롤러를 제작한다.

* PlayerController 상속받아 ABUIPlayerController 클래스 생성


* 새로운 플레이어 컨트롤러 클래스를 생성하면, 이를 상속받은 블루프린트에서 앞으로 띄울UI 클래스 값을 에디터에서 설정할 수 있도록 위젯 클래스 속성을 추가하고 EditDefaultOnly 키워드를 지정한다.

* 플레이어 컨트롤러의 로직은 게임을 시작하면 해당 클래스로부터 UI 인스턴스를 생성하고, 이를 뷰포트에 띄운 후에 입력은 UI에만 전달되도록 제작한다.

//ABUIPlayerController.h
#include "ArenaBattle.h"
#include "GameFramework/PlayerController.h"
#include "ABUIPlayerController.generated.h"

UCLASS()
class ARENABATTLE_API AABUIPlayerController : public APlayerController
{
	GENERATED_BODY()
	
protected:
	virtual void BeginPlay() override;

	UPROPERTY(EditDefaultsOnly, BlueprintReadwrite, Category = UI)
	TSubclassOf<class UUserWidget> UIWidgetClass;

	UPROPERTY()
	class UUSerWidget* UIWidgetInstance;
};

//ABUIPlayerController.cpp
#include "ABUIPlayerController.h"
#include "Blueprint/UserWidget.h"

void AABUIPlayerController::BeginPlay()
{
	Super::BeginPlay();

	ABCHECK(nullptr != UIWidgetClass);

	UIWidgetInstance = CreateWidget<UUserWidget>(this, UIWidgetClass);
	ABCHECK(nullptr != UIWidgetInstance);

	UIWidgetInstance->AddToViewport();

	FInputModeUIOnly Mode;
	Mode.SetWidgetToFocus(UIWidgetInstance->GetCachedWidget());

	SetInputMode(Mode);
	bShowMouseCursor = true;
}

* C++ 플레이어 컨트롤러의 기본 로직을 완성하면 이를 기반으로 한 블루프린트 클래스를 생성한다. 

* 새롭게 생성된 블루 프린트 애셋에는 BP_TitleUIPlayerController 로 정함

* 생성된 플레이어 컨트롤러 블루 프린트를 더블 클릭한 후 오른쪽 상단의 UIWidget Class 속성을 클릭해 복사해 넣은 UI 애셋인 UI_Title을 지정한다.

.

* 컨트롤러 세팅이 끝나면, BP를 이용해 게임모드를 생성해 본다. Game Mode Base를 상속한 BP_TitleGameMode 생성

* Default Pawn Class의 정보를 아무 기능이 없는 Pawn으로 지정하고
* Player Controller class의 정보를 방금 생성한 BP_TitleGameMode로 설정한다

 

* 이젠 월드세팅에서 게임모드를 쉽게 변경 할 수 있다.


타이틀 UI가 뜬 결과 화면


* UI 에서 새로시작하기 버튼을 누르면 캐릭터 선택을 위한 Select 레벨로 이동하고
이어하기 버튼을 누르면 GamePlay 레벨로 이동한다. 또한 Player1이라는 슬롯의 게임 데이터가 없으면, 이어하기 버튼은 누를 수 없게 비활성화 된다.
* 로직은 다음과 같음

* 아직 Select 라는 레벨이 없어서 새로 시작하기 버튼을 누르면 오류가 발생한다.

* 그래서 캐릭터 선택을 담당하는 레벨인 Select 레벨을 제작해본다.
* 위와 동일한 방법으로 ABUIPlayerController을 상속받은 블루프린트 BP_SelectUIPlayerController 을 생성한다
* 그리고 BP_SelectGameMode라는 게임 모드를 생성하고, PlayerController 클래스와 DefaultPawnClass를 각각 BP_SelectUIPlayerContoller와 Pawn으로 지정한다

 

* 이제 월드세팅에서 BP_SelectGame를 기본 게임모드로 지정하면 캐릭터 양옆으로 버튼이 보인다.

 


* 이제 C++로 각 버튼에 대한 로직을 구현해본다
* UserWidget을 기반으로 하는 ABCharacterSelectWidget 클래스를 생성한다
* 현재 레벨에 있는 스켈레톤 메시 액터의 목록을 가져오고, 버튼을 누를 떄마다 스켈레톤 메시 컴포넌트에 지정한 스켈레톤 메시를 변경한 ㄴ기능을 구현한다.
* 현재월드에 있는 특정 타입을 상속 받은 액터의 목록은 TActorIterator<액터타입> 구문을 사용해 가져올 수 있다

//ABCharacterSelectWidget.h
#pragma once

#include "ArenaBattle.h"
#include "Blueprint/UserWidget.h"
#include "ABCharacterSelectWidget.generated.h"

UCLASS()
class ARENABATTLE_API UABCharacterSelectWidget : public UUserWidget
{
	GENERATED_BODY()
	

protected:
	UFUNCTION(BlueprintCallable)
		void NextCharacter(bool bForward = true);

	virtual void NativeConstruct() override;
protected:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Character)
	int32 CurrentIndex;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Character)
	int32 MaxIndex;

	TWeakObjectPtr<USkeletalMeshComponent> TargetComponent;
};

//ABCharacterSelectWidget.cpp
#include "ABCharacterSelectWidget.h"
#include "ABCharacterSetting.h"
#include "ABGameInstance.h"
#include "EngineUtils.h"
#include "Animation/SkeletalMeshActor.h"


void UABCharacterSelectWidget::NextCharacter(bool bForward)
{
	bForward ? CurrentIndex++ : CurrentIndex--;

	if (CurrentIndex == -1) CurrentIndex = MaxIndex = 1;
	if (CurrentIndex == MaxIndex) CurrentIndex = 0;

	auto CharacterSetting = GetDefault<UABCharacterSetting>();
	auto AssetRef = CharacterSetting->CharacterAssets[CurrentIndex];

	auto ABGameInstance = GetWorld()->GetGameInstance<UABGameInstance>();
	ABCHECK(nullptr != ABGameInstance);
	ABCHECK(TargetComponent.IsValid());

	USkeletalMesh* Asset = ABGameInstance->StreamableManager.LoadSynchronous<USkeletalMesh>(AssetRef);
	if (nullptr != Asset)
	{
		TargetComponent->SetSkeletalMesh(Asset);
	}

}

void UABCharacterSelectWidget::NativeConstruct()
{
	Super::NativeConstruct();

	CurrentIndex = 0;
	auto CharacterSetting = GetDefault<UABCharacterSetting>();
	MaxIndex = CharacterSetting->CharacterAssets.Num();

	for (TActorIterator<ASkeletalMeshActor> It(GetWorld()); It; ++It)
	{
		TargetComponent = It->GetSkeletalMeshComponent();
		break;
	}
}

* NextCharacter 함수는 블루프린트에서 사용할 수 있도록 함수에 UFUNCTION 매크로를 추가하고 BlueprintCallable 키워드를 지어했다. 

* 이번 C++코드에서 버튼 컨트롤과 NextCharacter 함수를 직접바인딩 했지만, 해당 함수는 블루프린트 로직에서도 호출할 수 있다.

//ABCharacterSelectWidget.h
{
protected:
...
	UPROPERTY()
		class UButton* PrevButton;

	UPROPERTY()
		class UButton* NextButton;

	UPROPERTY()
		class UEditableTextBox* TextBox;

	UPROPERTY()
		class UButton* ConfirmButton;

private:
	UFUNCTION()
		void OnPrevClicked();

	UFUNCTION()
		void OnNextClicked();

	UFUNCTION()
		void OnConfirmClicked();
}        

//ABCharacterSelectWidget.cpp
#include "Components/Button.h"
#include "Components/EditableTextBox.h"
#include "ABSaveGame.h"
#include "ABPlayerState.h"

void UABCharacterSelectWidget::NativeConstruct()
{
.....
	PrevButton = Cast<UButton>(GetWidgetFromName(TEXT("btnPrev")));
	ABCHECK(nullptr != PrevButton);

	NextButton = Cast<UButton>(GetWidgetFromName(TEXT("btnNext")));
	ABCHECK(nullptr != NextButton);

	TextBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("edtPlayerName")));
	ABCHECK(nullptr != TextBox);

	ConfirmButton = Cast<UButton>(GetWidgetFromName(TEXT("btnConfirm")));
	ABCHECK(nullptr != ConfirmButton);

	PrevButton->OnClicked.AddDynamic(this, &UABCharacterSelectWidget::OnPrevClicked);
	NextButton->OnClicked.AddDynamic(this, &UABCharacterSelectWidget::OnNextClicked);
	ConfirmButton->OnClicked.AddDynamic(this, &UABCharacterSelectWidget::OnConfirmClicked);
}


void UABCharacterSelectWidget::OnPrevClicked()
{
	NextCharacter(false);
}

void UABCharacterSelectWidget::OnNextClicked()
{
	NextCharacter(true);
}

void UABCharacterSelectWidget::OnConfirmClicked()
{
	FString CharacterName = TextBox->GetText().ToString();
	if (CharacterName.Len() <= 0 || CharacterName.Len() > 10) return;

	UABSaveGame* NewPlayerData = NewObject<UABSaveGame>();
	NewPlayerData->PlayerName = CharacterName;
	NewPlayerData->Level = 1;
	NewPlayerData->Exp = 0;
	NewPlayerData->HighScore = 0;

	auto ABPlayerState = GetDefault<AABPlayerState>();
	if (UGameplayStatics::SaveGameToSlot(NewPlayerData, ABPlayerState->SaveSlotName, 0))
	{
		UGameplayStatics::OpenLevel(GetWorld(), TEXT("GamePlay"));
	}
	else
	{
		ABLOG(Error, TEXT("SaveGame Error!"));
	}
}

 


* 컴파일에 문제가 없다면 UI_Select 애셋의 부모클래스를 ABCharacterSelectWidget으로 변경한다

 

* 이제 진행을 하면 버튼을 누르 때 마다 캐릭터 애셋이 변경이 된다.

 

* 그리고 캐릭터를 선택한 수 하단의 텍스트 박스에 캐릭터가 사용할 이름을 입력하고 캐릭터 생성 버튼을 누른다. 그러면 입력한 플레이어 이름으로 게임이 시작되지만, 선택한 캐릭터와 다른 기본 캐릭터가 나온다.

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

* 캐릭터 생성 버튼을 누르면 현재 선택한 캐릭터 정보를 세이브데이터에 저장하고 이를 읽어들이는 기능을 구현한다

 

//ABSaveGame.h
{
public:
...
	UPROPERTY()
		int32 CharaterIndex;
};


//ABSaveGame.cpp
UABSaveGame::UABSaveGame()
{
...
	CharaterIndex = 0;
}
//ABCharacterSelectWidget.cpp
void UABCharacterSelectWidget::OnConfirmClicked()
{
	FString CharacterName = TextBox->GetText().ToString();
	if (CharacterName.Len() <= 0 || CharacterName.Len() > 10) return;

	UABSaveGame* NewPlayerData = NewObject<UABSaveGame>();
	NewPlayerData->PlayerName = CharacterName;
	NewPlayerData->Level = 1;
	NewPlayerData->Exp = 0;
	NewPlayerData->HighScore = 0;
    //추가
	NewPlayerData->CharaterIndex = CurrentIndex;

.......
}
//ABPlayerState.h
{
public:
	int32 GetCharacterInex() const;

protected:
	UPROPERTY(Transient)
	int32 CharacterIndex;
}

//ABPlayerState.cpp
AABPlayerState::AABPlayerState()
{
...
	CharacterIndex = 0;
}

int32 AABPlayerState::GetCharacterInex() const
{
	return CharacterIndex;
}

void AABPlayerState::InitPlayerData()
{
	auto ABSaveGame = Cast<UABSaveGame>(UGameplayStatics::LoadGameFromSlot(SaveSlotName, 0));
	if (nullptr == ABSaveGame)
	{
		ABSaveGame = GetMutableDefault<UABSaveGame>();
	}

	SetPlayerName(ABSaveGame->PlayerName);
	SetCharacterLevel(ABSaveGame->Level);
	GameScore = 0;
	GameHighScore = ABSaveGame->HighScore;
	Exp = ABSaveGame->Exp;
	CharacterIndex = ABSaveGame->CharaterIndex; //추가
	SavePlayerData();
}

void AABPlayerState::SavePlayerData()
{
	UABSaveGame* NewPlayerData = NewObject<UABSaveGame>();
	NewPlayerData->PlayerName = GetPlayerName();
	NewPlayerData->Level = CharacterLevel;
	NewPlayerData->Exp = Exp;
	NewPlayerData->HighScore = GameHighScore;
	NewPlayerData->CharaterIndex = CharacterIndex; //추가

	if (!UGameplayStatics::SaveGameToSlot(NewPlayerData, SaveSlotName, 0))
	{
		ABLOG(Error, TEXT("SaveGame Error!"));
	}

}
//ABCharacter.cpp

void AABCharacter::BeginPlay()
{
.....

	auto DefaultSetting = GetDefault<UABCharacterSetting>();

	if (bIsPlayer)
	{
		//AssetIndex = 4;
        //추가함
		auto ABPlayerState = Cast<AABPlayerState>(GetPlayerState());
		ABCHECK(nullptr != ABPlayerState);
		AssetIndex = ABPlayerState->GetCharacterIndex();
	}
.....
}

 

※ GetCharacterInex() -> GetCharacterIndex() 수정

* 이제 시작 UI 기능을 모두 완성했으므로 프로젝트 세팅으로 가서  Title 레벨을 기본 레벨로 설정한다.

+ Recent posts