1. 플레이어 데이터 저장

(1) LoadScreenSaveGame 커스텀 저장 게임 오브젝트 클래스

	/* Player */

	UPROPERTY()
	int32 PlayerLevel = 0;

	UPROPERTY()
	int32 XP = 0;

	UPROPERTY()
	int32 SpellPoints = 0;

	UPROPERTY()
	int32 AttributePoints = 0;

	// 1차 속성
	UPROPERTY()
	float Strength = 0.f;

	UPROPERTY()
	float Intelligence = 0.f;

	UPROPERTY()
	float Resilience = 0.f;

	UPROPERTY()
	float Vigor = 0.f;

};

이 클래스에 저장하고자 하는 기본 요소와 1차 속성을 정의한다.

 

 

(2) AuraCharacter의 SaveProgress_Implementation 함수

void AAuraCharacter::SaveProgress_Implementation(const FName& CheckpointTag)
{
    // 게임 모드에 접근
    AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(this));
    
    if (AuraGameMode)
    {
        // 저장 슬롯 찾기
        ULoadScreenSaveGame* SaveData = AuraGameMode->RetrieveInGameSaveData();
        if (SaveData == nullptr)
            return;

        // 데이터 저장
        SaveData->PlayerStartTag = CheckpointTag;

        if (AAuraPlayerState* AuraPlayerState = Cast<AAuraPlayerState>(GetPlayerState()))
        {
            SaveData->PlayerLevel = AuraPlayerState->GetCharacterLevel();
            SaveData->XP = AuraPlayerState->GetXP();
            SaveData->AttributePoints = AuraPlayerState->GetAttributePoints();
            SaveData->SpellPoints = AuraPlayerState->GetSpellPoints();
        }

        // 1차 속성
        SaveData->Strength = UAuraAttributeSet::GetStrengthAttribute().GetNumericValue(GetAttributeSet());
        SaveData->Intelligence = UAuraAttributeSet::GetIntelligenceAttribute().GetNumericValue(GetAttributeSet());
        SaveData->Resilience = UAuraAttributeSet::GetResilienceAttribute().GetNumericValue(GetAttributeSet());
        SaveData->Vigor = UAuraAttributeSet::GetVigorAttribute().GetNumericValue(GetAttributeSet());

        AuraGameMode->SaveInGameProgressData(SaveData);
    }
}

레벨, 경험치, 속성 포인트, 스펠 포인트는 AuraPlayerState, 각종 속성은 AuraAttributeSet에 위치한다.

 

 

2. 플레이어 데이터 불러오기

(1) AuraCharacter에서 PossessedBy 함수 수정

컨트롤러가 캐릭터를 소유하게 되면 호출되는 함수이다.

함수가 호출될 때, 저장 데이터에서 불러온 정보를 적용할 것이다.

void AAuraCharacter::InitAbilityActorInfo()
{
    AAuraPlayerState* AuraPlayerState = GetPlayerState<AAuraPlayerState>();
    check(AuraPlayerState);
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    InitializeDefaultAttributes();
}

이제 저장된 데이터를 불러오게 될 것이므로 아래의 코드 조각을 제거한다.

InitializeDefaultAttributes();

 

 

(2) CharacterClassInfo 수정하여 Set by Caller 버전의 1차 속성을 가지게 하기

1차 속성을 캐릭터에 적용시킬 때, 게임이 시작되는 순간에 게임플레이 이펙트를 사용하여 적용하고 있다.

게임 데이터에서 1차 속성 값을 가져와 캐릭터에 적용하기 위해 Set by Caller Magnitude를 활용한다.


- Set by Caller

게임플레이 이펙트의 속성에 대해 규모(Magnitude)를 Set by Caller로 지정하면, 이펙트 컨텍스트를 만들어 캐릭터에 적용하기 전에 Set by Caller로 설정한 속성의 값을 코드로 직접 적용할 수 있게 된다.


	// 1차 속성 설정을 위한 Set By Caller
	UPROPERTY(EditDefaultsOnly, Category = "Common Class Defaults")
	TSubclassOf<UGameplayEffect> PrimaryAttributes_SetByCaller;

DA_CharacterClassInfo가 캐릭터의 1차 속성의 값을 가지고 있다가 컨트롤러가 캐릭터를 소유하는 순간 이 값을 이용하여 저장 데이터에서 1차 속성의 값을 가져와 설정할 것이다.

 

 

(2-1) GE_AuraPrimaryAttributes를 복제한 GE_PrimaryAttributes_SetbyCaller 수정

4개 속성의 계산 타입을 Set by Caller로 설정하고, 각 속성에 맞는 데이터 태그를 설정한다.

 

 

(2-2) LoadScreenSaveGame 저장 오브젝트에 불리언 선언

	UPROPERTY()
	bool bFirstTimeLoading = true;

첫 로딩일 때에만 Set by Caller에 의해 1차 속성의 기본 값을 적용하고, 아닌 경우 저장 데이터에 접근하여 1차 속성을 Set by Caller로 설정할 것이다.

 

(2-3) AuraCharacter의 진행상황 저장 함수 SaveProgress_Implementation에 불리언 플래그 전환

void AAuraCharacter::SaveProgress_Implementation(const FName& CheckpointTag)
{
    // 게임 모드에 접근
    AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(this));
    
    if (AuraGameMode)
    {
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // 1차 속성
        SaveData->Strength = UAuraAttributeSet::GetStrengthAttribute().GetNumericValue(GetAttributeSet());
        SaveData->Intelligence = UAuraAttributeSet::GetIntelligenceAttribute().GetNumericValue(GetAttributeSet());
        SaveData->Resilience = UAuraAttributeSet::GetResilienceAttribute().GetNumericValue(GetAttributeSet());
        SaveData->Vigor = UAuraAttributeSet::GetVigorAttribute().GetNumericValue(GetAttributeSet());

        SaveData->bFirstTimeLoading = false;
        AuraGameMode->SaveInGameProgressData(SaveData);
    }
}

 

 

(2-4) AuraAbilitySystemLibrary에서 Set by Caller 이펙트를 가져와 1차 속성에 덮어 씌우기

	// 세이브 된 기본 속성 적용 게임플레이 이펙트
	UFUNCTION(BlueprintCallable, category="AuraAbilitySystemLibrary|CharacterClassDefaults")
	static void InitializeDefaultAttributesFromSaveData(const UObject* WorldContextObject, UAbilitySystemComponent* ASC, ULoadScreenSaveGame* SaveGame);
void UAuraAbilitySystemLibrary::InitializeDefaultAttributesFromSaveData(const UObject* WorldContextObject,
	UAbilitySystemComponent* ASC, ULoadScreenSaveGame* SaveGame)
{
	// 액터의 클래스 정보 가져오기
	UCharacterClassInfo* CharacterClassInfo = GetCharacterClassInfo(WorldContextObject);
	if (CharacterClassInfo == nullptr)
		return;

	// Set by Caller 가져오기
	const FAuraGameplayTags& GameplayTags = FAuraGameplayTags::Get();

	const AActor* SourceActor = ASC->GetAvatarActor();

	// 이펙트 컨텍스트 핸들 생성
	FGameplayEffectContextHandle EffectContextHandle = ASC->MakeEffectContext();
	EffectContextHandle.AddSourceObject(SourceActor);

	// 이펙트 적용을 위한 이펙트 스펙 핸들 생성
	const FGameplayEffectSpecHandle SpecHandle = ASC->MakeOutgoingSpec(CharacterClassInfo->PrimaryAttributes_SetByCaller, 1.f, EffectContextHandle);

	// Set By Caller Magnitude 설정
	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Attributes_Primary_Strength, SaveGame->Strength);
	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Attributes_Primary_Intelligence, SaveGame->Intelligence);
	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Attributes_Primary_Resilience, SaveGame->Resilience);
	UAbilitySystemBlueprintLibrary::AssignTagSetByCallerMagnitude(SpecHandle, GameplayTags.Attributes_Primary_Vigor, SaveGame->Vigor);

	// 게임플레이 이펙트 적용
	ASC->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
}

저장된 데이터를 불러오게 된 경우, 저장 오브젝트 내의 1차 속성을 가져와 Set by Caller로 지정된 1차 속성에 덮어씌우고, 게임플레이 이펙트 스펙을 캐릭터에게 적용한다.

 

 

(3) CharacterClassInfo에서 Aura 캐릭터에 적용할 2차 속성 게임플레이 이펙트

	// 2차 속성, Aura에게 적용, Type : Infinite
	UPROPERTY(EditDefaultsOnly, Category = "Common Class Defaults")
	TSubclassOf<UGameplayEffect> SecondaryAttributes_Infinite;

기존의 2차 속성 게임플레이 이펙트는 Infinite 타입이 아니어서, 몬스터가 생성되면 한 번만 적용되는 이펙트였다.

Aura 캐릭터를 위해 영구적으로 적용되는 게임플레이 이펙트 클래스를 생성한다.

 

 

(3-1) 세이브 데이터의 속성 값을 적용하는 함수 수정

void UAuraAbilitySystemLibrary::InitializeDefaultAttributesFromSaveData(const UObject* WorldContextObject,
	UAbilitySystemComponent* ASC, ULoadScreenSaveGame* SaveGame)
{
	// 게임플레이 이펙트 적용
	ASC->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());

	// 2차 속성, 바이탈 속성 적용
	FGameplayEffectContextHandle SecondaryAttributesContextHandle = ASC->MakeEffectContext();
	SecondaryAttributesContextHandle.AddSourceObject(SourceActor);

	FGameplayEffectContextHandle VitalAttributesContextHandle = ASC->MakeEffectContext();
	VitalAttributesContextHandle.AddSourceObject(SourceActor);
	
	const FGameplayEffectSpecHandle SecondaryAttributesSpecHandle = ASC->MakeOutgoingSpec(CharacterClassInfo->SecondaryAttributes_Infinite, 1.f, SecondaryAttributesContextHandle);
	ASC->ApplyGameplayEffectSpecToSelf(*SecondaryAttributesSpecHandle.Data.Get());

	const FGameplayEffectSpecHandle VitalAttributesSpecHandle = ASC->MakeOutgoingSpec(CharacterClassInfo->VitalAttributes, 1.f, VitalAttributesContextHandle);
	ASC->ApplyGameplayEffectSpecToSelf(*VitalAttributesSpecHandle.Data.Get());
}

 

 

(4) AuraCharacter의 진행 상황 불러오기 함수 수정

void AAuraCharacter::LoadProgress()
{
        // 첫 로딩일 때
        if (SaveData->bFirstTimeLoading)
        {
            // 기본 1차 속성 적용
            InitializeDefaultAttributes();
            AddCharacterAbilites();
        }
        else
        {
            // 1차 속성, 2차 속성 적용
            UAuraAbilitySystemLibrary::InitializeDefaultAttributesFromSaveData(this, AbilitySystemComponent, SaveData);
        }

게임을 처음 시작한 상황이 아니라면 세이브 데이터에서 속성 값을 가져와 적용시키도록 한다.

 

 

(5) DA_CharacterClassInfo에서 Aura 캐릭터의 2차 속성 게임플레이 이펙트 할당

 

 

2. 디버그

(1) 인게임에서 레벨이 올바르게 표기되지 않을 때

void AAuraCharacter::LoadProgress()
{
    // 게임 모드에 접근
    AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(this));

    if (AuraGameMode)
    {
        // 저장 슬롯 찾기
        ULoadScreenSaveGame* SaveData = AuraGameMode->RetrieveInGameSaveData();
        if (SaveData == nullptr)
            return;

        // 첫 로딩일 때
        if (SaveData->bFirstTimeLoading)
        {
            // 기본 1차 속성 적용
            InitializeDefaultAttributes();
            AddCharacterAbilites();
        }
        else
        {
            // 저장된 데이터 불러오기
            if (AAuraPlayerState* AuraPlayerState = Cast<AAuraPlayerState>(GetPlayerState()))
            {
                AuraPlayerState->SetLevel(SaveData->PlayerLevel);
                AuraPlayerState->SetXP(SaveData->XP);
                AuraPlayerState->SetAttributePoints(SaveData->AttributePoints);
                AuraPlayerState->SetSpellPoints(SaveData->SpellPoints);
            }
            
            // 1차 속성, 2차 속성 적용
            UAuraAbilitySystemLibrary::InitializeDefaultAttributesFromSaveData(this, AbilitySystemComponent, SaveData);
        }
    }
}

레벨, 경험치, 포인트는 저장된 데이터를 불러올 때 세이브 데이터에서 가져오면 되므로 위치를 변경한다.

 

 

(2) 저장 데이터 레벨 표기

 

(2-1) MVVM_LoadSlot에서 레벨을 필드 노티파이로 선언

private:
	/*필드 노티파이*/
	UPROPERTY(EditAnywhere, BlueprintReadWrite, FieldNotify, Setter, Getter, meta =(AllowPrivateAccess="true"))
	FString PlayerName;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, FieldNotify, Setter, Getter, meta =(AllowPrivateAccess="true"))
	int32 Level;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, FieldNotify, Setter, Getter, meta = (AllowPrivateAccess = "true"))
	FString MapName;
};

 

(2-2) 필드 노티파이 변수에 대한 Getter, Setter 선언

public:
	/* Getter, Setter */
	void SetPlayerName(FString InPlayerName);
	void SetLevel(int32 InLevel);
	void SetMapName(FString InMapName);

	FString GetPlayerName() const { return PlayerName; }
	int32 GetLevel() const { return Level;}
	FString GetMapName() const { return MapName; }
void UMVVM_LoadSlot::SetLevel(int32 InLevel)
{
	UE_MVVM_SET_PROPERTY_VALUE(Level, InLevel);
}

 

(2-3) MVVM_LoadScreen의 새로운 슬롯 생성 함수에서 플레이어 레벨 1로 지정

void UMVVM_LoadScreen::NewSlotButtonPressed(int32 Slot, const FString& EnteredName)
{
	AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(this));

	// 새로운 슬롯 생성
	LoadSlots[Slot]->SetMapName(AuraGameMode->DefaultMapName);
	LoadSlots[Slot]->SetLevel(1);

 

 

 

(2-4) MVVM_LoadScreen의 데이터 불러오기 함수에서 플레이어 레벨 가져오기

void UMVVM_LoadScreen::LoadData()
{
	AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(this));

	for (const TPair<int32, UMVVM_LoadSlot*> LoadSlot : LoadSlots)
	{
		// 저장된 게임 찾아오기
		ULoadScreenSaveGame* SaveObject = AuraGameMode->GetSaveSlotData(LoadSlot.Value->LoadSlotName, LoadSlot.Key);

		// 플레이어 이름 가져오기
		const FString PlayerName = SaveObject->PlayerName;

		TEnumAsByte<ESaveSlotStatus> SaveSlotStatus = SaveObject->SaveSlotStatus;

		LoadSlot.Value->SlotStatus = SaveSlotStatus;
		LoadSlot.Value->SetPlayerName(PlayerName);
		LoadSlot.Value->SetLevel(SaveObject->PlayerLevel);
		LoadSlot.Value->SetMapName(SaveObject->MapName);

 

 

 

(2-5) WBP_LoadSlot_Taken에서 뷰 바인딩

정수를 텍스트로 변환하도록 하고 값에 LoadSlot 뷰 모델의 Level 변수로 지정한다.

 

이제 로드 메뉴에서 레벨이 올바르게 표시된다.

 

 

 

(3) 저장 데이터 로드 후 레벨업 효과 발생

AuraCharacter에서 SaveData의 bFirstTimeLoading 변수를 체크하여, 첫 로딩이 아니라면 레벨업 효과를 발생하지 않도록 델리게이트를 수정한다.

 

(3-1) AuraPlayerState에 레벨업 델리게이트 추가

DECLARE_MULTICAST_DELEGATE_TwoParams(FOnLevelChanged, int32/*Level*/, bool /*bLevelUp*/);
	// 값 변화 델리게이트
	FOnPlayerStatChanged OnXPChangedDelegate;
	FOnLevelChanged OnLevelChangedDelegate;
	FOnPlayerStatChanged OnAttributePointChangedDelegate;
	FOnPlayerStatChanged OnSpellPointChangedDelegate;

기존의 OnPlayerStatChanged 시그니쳐를 사용하지 않고 새로 2개의 파라미터를 받는 멀티캐스트 델리게이트를 만든다.

 

 

(3-2) 레벨업에 사용되는 함수

void AAuraPlayerState::AddToLevel(int32 InLevel)
{
    Level += InLevel;
    OnLevelChangedDelegate.Broadcast(Level, true);
}

레벨업 델리게이트를 발동하기 위해  호출되는 함수이다.

이 함수에 true를 넘겨준다.

void AAuraPlayerState::SetLevel(int32 InLevel)
{
    Level = InLevel;
    OnLevelChangedDelegate.Broadcast(Level, false);
}

SetLevel 함수는 데이터를 불러올 때만 사용하게 할 것이다.

 

 

(3-3) Overlay 위젯 컨트롤러에서 델리게이트 추가

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnLevelChangedSignature, int32, NewValue, bool, bLevelUp);
	UPROPERTY(BlueprintAssignable, Category = "GAS|Level")
	FOnLevelChangedSignature OnPlayerLevelChangedDelegate;

레벨업을 감지하기 위해 두개의 파라미터를 가지는 델리게이트를 추가한다.

 

 

(3-4) Overlay 위젯 컨트롤러에서 콜백 수정

void UOverlayWidgetController::BindCallbacksToDependencies()
{
    GetAuraPS()->OnXPChangedDelegate.AddUObject(this, &UOverlayWidgetController::OnXPChanged);
    GetAuraPS()->OnLevelChangedDelegate.AddLambda([this](int32 NewLevel, bool bLevelUp)
        {
            OnPlayerLevelChangedDelegate.Broadcast(NewLevel, bLevelUp);
        });

레벨업 불리언 여부에 따라 이펙트를 발동시키기 위해 불리언을 넘긴다.

 

(3-5) 레벨업 메시지 발동에 불리언 조건 추가