10 minute read

저장과 로드(일부)

유튜브 보고 하다가 버리고, 유데미 선생님과 함께 진행했다.

먼저 새로운 C++ 클래스 SaveGame 클래스를 만든다.

YaroSaveGame.h

#include "CoreMinimal.h"
#include "GameFramework/SaveGame.h"
#include "YaroSaveGame.generated.h"

USTRUCT(BlueprintType)
struct FCharacterStats
{
	GENERATED_BODY()

	UPROPERTY(VisibleAnywhere, Category = "SaveGameData")
	float HP;
	UPROPERTY(VisibleAnywhere, Category = "SaveGameData")
	float MaxHP;

	UPROPERTY(VisibleAnywhere, Category = "SaveGameData")
	float MP;
	UPROPERTY(VisibleAnywhere, Category = "SaveGameData")
	float MaxMP;

	UPROPERTY(VisibleAnywhere, Category = "SaveGameData")
	float SP;
	UPROPERTY(VisibleAnywhere, Category = "SaveGameData")
	float MaxSP;

	UPROPERTY(VisibleAnywhere, Category = "SaveGameData")
	FVector Location;

	UPROPERTY(VisibleAnywhere, Category = "SaveGameData")
	FRotator Rotation;

	UPROPERTY(VisibleAnywhere, Category = "SaveGameData")
	FString WeaponName;
};

USTRUCT(BlueprintType)
struct FEnemyInfo
{
	GENERATED_BODY()

	UPROPERTY(VisibleAnywhere, Category = "SaveGameData")
	FVector Location;

	UPROPERTY(VisibleAnywhere, Category = "SaveGameData")
	FRotator Rotation;

	/*UPROPERTY(VisibleAnywhere, Category = "SaveGameData")
	FString EnemyName;*/

	UPROPERTY(VisibleAnywhere, Category = "SaveGameData")
	int EnemyIndex;
};

/**
 * 
 */
UCLASS()
class YARO_API UYaroSaveGame : public USaveGame
{
	GENERATED_BODY()

public:
	UYaroSaveGame();

	UPROPERTY(VisibleAnywhere, Category = Basic)
	FString SaveName;

	UPROPERTY(VisibleAnywhere, Category = Basic)
	uint32 UserIndex;

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Basic)
	int32 PlayerGender = 0;

	UPROPERTY(VisibleAnywhere, Category = Basic)
	FCharacterStats CharacterStats;

	UPROPERTY(VisibleAnywhere, Category = Basic)
	TArray<FEnemyInfo> EnemyInfoArray;

	UPROPERTY(EditAnywhere)
	FEnemyInfo EnemyInfo;
};

오랜만에 클래스를 처음부터 설명하려니 살짝 막막하다. 막 구조체도 있고 그래서 마냥 쉽지 않아가지고..

일단 위의 FCharacterStats와 FEnemyInfo는 구조체다. FCharacterStats은 뭐 보면 알 수 있듯 스탯 정보와 위치/회전값 정보, 무기 이름 등이 들어가있다.

FEnemyInfo는 적 관련 정보들을 저장하려고 한 것인데 아직 미완성이다.. 해보려했는데 아직 제대로 성공하지 못했다. 매우 쉽지 않다..

SaveName과 UserIndex는 세이브 슬롯과 관련한 것들인데, 참고로 우리는 슬롯을 단 하나만 쓸 것이다.

PlayerGender는 플레이어 성별 정보이고, 남자가 1, 여자가 2다.

그 밑은 플레이어 스탯 구조체 변수와 적 정보 관련 배열, 구조체 변수인데 적 관련한 건 더 이상 언급하지 않겠음.


YaroSaveGame.cpp

#include "YaroSaveGame.h"

UYaroSaveGame::UYaroSaveGame()
{
    SaveName = TEXT("Default");
    UserIndex = 0;

    CharacterStats.WeaponName = TEXT("");

    //for (int i = 0; i < EnemyInfo.Num(); i++)
    //{
    //    EnemyInfo[i].EnemyName = TEXT("");
    //}

}

cpp에서 SaveName, UserIndex를 초기화해주고, 무기 이름도 초기화해준다.


저장, 로드 메서드 둘 다 Main 클래스 안에 있다.

in Man.cpp

void AMain::SaveGame()
{
    UYaroSaveGame* SaveGameInstance = Cast<UYaroSaveGame>(UGameplayStatics::CreateSaveGameObject(UYaroSaveGame::StaticClass()));

    SaveGameInstance->PlayerGender = Gender;
    SaveGameInstance->CharacterStats.HP = HP;
    SaveGameInstance->CharacterStats.MaxHP = MaxHP;
    SaveGameInstance->CharacterStats.MP = MP;
    SaveGameInstance->CharacterStats.MaxMP = MaxMP;
    SaveGameInstance->CharacterStats.SP = SP;
    SaveGameInstance->CharacterStats.MaxSP = MaxSP;

    SaveGameInstance->CharacterStats.Location = GetActorLocation();
    SaveGameInstance->CharacterStats.Rotation = GetActorRotation();

    if (EquippedWeapon) SaveGameInstance->CharacterStats.WeaponName = EquippedWeapon->Name;

    UGameplayStatics::SaveGameToSlot(SaveGameInstance, SaveGameInstance->SaveName, SaveGameInstance->UserIndex);

}

SaveGameInstance라는 변수에 YaroSaveGame클래스를 할당하는 건데 어엄.. 어렵다.

현재 플레이어의 성별, 스탯, 위치/회전값, 장비한 무기 이름을 SaveGameInstance에 저장해준다.

그리고 슬롯에 SaveGameInstance의 내용을 저장한다. 매개변수인 슬롯 이름으로 SaveGameInstance->SaveName을 넣어주며 UserIndex도 마찬가지.

UserIndex는 저장을 수행하는 사용자를 식별하는 데에 쓴다고 한다.


이제 로드할 차례인데.. 무기 로드 덕분에 새로운 c++ 클래스를 하나 더 만들어야 한다.

액터 클래스를 상속받는 ItemStorage라는 클래스를 만든다.

ItemStorage.h

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemStorage.generated.h"

UCLASS()
class YARO_API AItemStorage : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AItemStorage();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	
	UPROPERTY(EditDefaultsOnly, Category = "SaveData")
	TMap<FString, TSubclassOf<class AWeapon>> WeaponMap;

	UPROPERTY(EditAnywhere, Category = "SaveData")
	TMap<int32, TSubclassOf<class AEnemy>> EnemyMap;
};

이게 음 맵이라는 새로운 데이터 유형을 사용하는데, key와 value가 한 세트인 그 약간 딕셔너리 같은? 그런 거 같다.

아무튼 키는 문자열, value는 무기 클래스인 맵 변수를 선언한다.

ItemStorage.cpp

#include "ItemStorage.h"

// Sets default values
AItemStorage::AItemStorage()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = false;  // Default value is true

}

// Called when the game starts or when spawned
void AItemStorage::BeginPlay()
{
	Super::BeginPlay();
	
}

참고로 cpp에서 하는 일은 거의 없다. 그냥 틱 안 쓴다는 것 정도.

얘는 블루프린트에서 일을 해줘야 한다.


이미지

블루프린트 클래스 디폴트의 디테일 패널을 보면 SaveData 카테고리에 WeaponMap이 보인다.

여기에서 +를 클릭해서 요소를 추가하고 키값(문자열)으로 PlayerWand, 그리고 value로는 플레이어 무기 블루프린트 클래스를 줬다.


이제 로드 메서드를 보자.

in Man.cpp

void AMain::LoadGame()
{
	UYaroSaveGame* LoadGameInstance = Cast<UYaroSaveGame>(UGameplayStatics::CreateSaveGameObject(UYaroSaveGame::StaticClass()));

	LoadGameInstance = Cast<UYaroSaveGame>(UGameplayStatics::LoadGameFromSlot(LoadGameInstance->SaveName, LoadGameInstance->UserIndex));

	MainPlayerController = Cast<AMainPlayerController>(GetController());

	HP = LoadGameInstance->CharacterStats.HP;
	MaxHP = LoadGameInstance->CharacterStats.MaxHP;
	MP = LoadGameInstance->CharacterStats.MP;
	MaxMP = LoadGameInstance->CharacterStats.MaxMP;
	SP = LoadGameInstance->CharacterStats.SP;
	MaxSP = LoadGameInstance->CharacterStats.MaxSP;

	SetActorLocation(LoadGameInstance->CharacterStats.Location);
	SetActorRotation(LoadGameInstance->CharacterStats.Rotation);

	if (ObjectStorage)
	{
		if (Storage)
		{
			FString WeaponName = LoadGameInstance->CharacterStats.WeaponName;

			if (Storage->WeaponMap.Contains(WeaponName))
			{
				AWeapon* WeaponToEquip = GetWorld()->SpawnActor<AWeapon>(Storage->WeaponMap[WeaponName]);
				WeaponToEquip->Equip(this);
			}
		}
	}
}

여기도 비슷하게 LoadGameInstance라는 변수에 YaroSaveGame클래스를 할당한다.

그리고 LoadGameFromSlot라는 함수를 이용해서 매개변수로 슬롯이름이랑 유저 인덱스 주고 저장된 게임을 불러온다. 불러와서는 뭐 저장된 정보로 스탯 초기화 해주고 플레이어 위치랑 회전 설정해준다.

그 다음은 오브젝트 스토리지가 null이 아니어야 하는데 이것은


in Main.h

UPROPERTY(EditDefaultsOnly, Category = "SavedData")
TSubclassOf<class AItemStorage> ObjectStorage;

class AItemStorage* Storage;

이미지

플레이어 블루프린트 클래스 디폴트에서 아까 만든 아이템 스토리지 블루프린트를 넣어준다.

(오른쪽의 SaveData카테고리 아래)


그리고 BeginPlay()에서 이것을 월드에 스폰하여 스토리지 변수에 넣어준다.

Storage = GetWorld()->SpawnActor<AItemStorage>(ObjectStorage);


다시 로드 메서드로 돌아와서 보면

if (ObjectStorage)
	{
		if (Storage)
		{
			FString WeaponName = LoadGameInstance->CharacterStats.WeaponName;

			if (Storage->WeaponMap.Contains(WeaponName))
			{
				AWeapon* WeaponToEquip = GetWorld()->SpawnActor<AWeapon>(Storage->WeaponMap[WeaponName]);
				WeaponToEquip->Equip(this);
			}
		}
	}

오브젝트 스토리지와 스토리지가 null이 아니면 WeaponName 변수를 저장된 무기 이름으로 초기화한다.

그리고 스토리지의 무기 맵에서 이 WeaponName을 가지고 있으면(키값이 존재하면)

해당 키값에 맞는 value(무기)를 월드에 스폰하고 WeaponToEquip 변수에 넣는다.

그 다음, WeaponToEquip의 Equip메서드를 호출하고 매개변수에 플레이어 본인을 주어서 플레이어가 무기를 장착하게 한다.



적 사운드

사운드큐로 줬다.

in Enemy.h

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sounds")
class USoundCue* AgroSound;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sounds")
class USoundCue* DeathSound;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Sounds")
class USoundCue* SkillSound;

근데 몹마다 다르다. 어울리는 사운드가 없어서 안 넣어준 몹도 많다.

그리고 스킬 사운드는 일단 골렘 3번 스킬에만 해당.


사운드 큐 쓰려면 Enemy.cpp 위에 아래 코드 추가

#include "Sound/SoundCue.h"


in AgroSphereOnOverlapBegin() of Enemy.cpp

if (AgroSound && AgroTargets.Num() == 0) UGameplayStatics::PlaySound2D(this, AgroSound);


in Die() of Enemy.cpp

if (DeathSound) UGameplayStatics::PlaySound2D(this, DeathSound);


골렘 3번 스킬 땅치기

void AEnemy::HitGround() //Golem's third skill
{
	if (SkillSound) UGameplayStatics::PlaySound2D(this, SkillSound);

	if (CombatTarget)
	{
		UGameplayStatics::ApplyDamage(CombatTarget, Damage, AIController, this, DamageTypeClass);
	}
}


이미지

사운드는 블루프린트에서 넣어준다.


NPC 마법 세팅~

in YaroCharacter.cpp

void AYaroCharacter::Attack()
{
	if ((!bAttacking) && (Player->MovementStatus != EMovementStatus::EMS_Dead) && (CombatTarget) && (CombatTarget->EnemyMovementStatus != EEnemyMovementStatus::EMS_Dead))
	{

		SkillNum = FMath::RandRange(1, 3);
		UBlueprintGeneratedClass* LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/GreenStormAttack.GreenStormAttack_C")); //초기화 안 하면 ToSpawn에 초기화되지 않은 변수 넣었다고 오류남
		if (this->GetName().Contains("Luko"))
		{
			switch (SkillNum)
			{
				case 1:
					LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/GreenStormAttack.GreenStormAttack_C"));
					break;
				case 2:
					LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/DarkAttack.DarkAttack_C"));
					break;
				case 3:
					LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/LightAttack.LightAttack_C"));
					break;
				default:
					break;
			}
		}
		if (this->GetName().Contains("Momo"))
		{
			switch (SkillNum)
			{
				case 1:
					LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/RedStormAttack.RedStormAttack_C"));
					break;
				case 2:
					LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/Fireball_Hit_Attack.Fireball_Hit_Attack_C"));
					break;
				case 3:
					LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/FireAttack.FireAttack_C"));
					break;
				default:
					break;
			}
		}
		if (this->GetName().Contains("Vovo"))
		{
			switch (SkillNum)
			{
			case 1:
				LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/YellowStormAttack.YellowStormAttack_C"));
				break;
			case 2:
				LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/Waterball_Hit_Attack.Waterball_Hit_Attack_C"));
				break;
			case 3:
				LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/AquaAttack.AquaAttack_C"));
				break;
			default:
				break;
			}
		}
		if (this->GetName().Contains("Vivi"))
		{
			switch (SkillNum)
			{
			case 1:
				LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/BlueStormAttack.BlueStormAttack_C"));
				break;
			case 2:
				LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/Ice_Hit_Attack.Ice_Hit_Attack_C"));
				break;
			case 3:
				LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/IceAttack.IceAttack_C"));
				break;
			default:
				break;
			}
		}
		if (this->GetName().Contains("Zizi"))
		{
			switch (SkillNum)
			{
			case 1:
				LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/PurpleStormAttack.PurpleStormAttack_C"));
				break;
			case 2:
				LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/Thunderball_Hit_Attack.Thunderball_Hit_Attack_C"));
				break;
			case 3:
				LoadedBP = LoadObject<UBlueprintGeneratedClass>(GetWorld(), TEXT("/Game/Blueprint/MagicAttacks/LightningAttack.LightningAttack_C"));
				break;
			default:
				break;
			}
		}
		ToSpawn = Cast<UClass>(LoadedBP);

		bAttacking = true;
		SetInterpToEnemy(true);

		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
		if (AnimInstance && CombatMontage)
		{
			AnimInstance->Montage_Play(CombatMontage);
			AnimInstance->Montage_JumpToSection(FName("Attack"), CombatMontage);

			Spawn();
		}
	}
}


in Spawn() of YaroCharacter.cpp

if (this->GetName().Contains("Luko"))
{
    if (SkillNum != 1) //루코의 경우 2,3번 스킬은 적 위치에서 스폰
    {
        spawnLocation = CombatTarget->GetActorLocation();
    }
}
else
{
    if (SkillNum == 3) //루코 제외 3번 스킬만 적 위치에서 스폰
    {
        spawnLocation = CombatTarget->GetActorLocation();
    }
}



스탯 회복 타이머

in Main.cpp

void AMain::RecoveryHP()
{
	HP += 5.f;
	if (HP >= MaxHP)
	{
		HP = MaxHP;
		GetWorldTimerManager().ClearTimer(HPTimer);
	}
}

void AMain::RecoveryMP()
{
	MP += 5.f;
	if (MP >= MaxMP)
	{
		MP = MaxMP;
		GetWorldTimerManager().ClearTimer(MPTimer);
	}
}

void AMain::RecoverySP()
{
	SP += 1.f;
	if (SP >= MaxSP)
	{
		SP = MaxSP;
		GetWorldTimerManager().ClearTimer(SPTimer);
		recoverySP = false;
	}
}

스탯마다 회복 속도가 다르다.

마법 공격 스폰할 때 마나가 소모되므로 MP 회복 타이머는 이 때 작동한다.

in Spawn() in Main.cpp

GetWorldTimerManager().SetTimer(MPTimer, this, &AMain::RecoveryMP, MPDelay, true);


HP 회복 타이머는 공격 받았을 때 작동.

in TakeDamage() in Main.cpp

GetWorldTimerManager().SetTimer(HPTimer, this, &AMain::RecoveryHP, HPDelay, true);	


SP 회복 타이머는 달릴 때 작동하는데 이동 메서드들에서 코드를 살짝 추가했다.

in Main.cpp

void AMain::MoveForward(float Value)
{
	if ((Controller != nullptr) && (Value != 0.0f) && (!bAttacking) && (MovementStatus != EMovementStatus::EMS_Dead))
	{
		// find out which way is forward
		const FRotator Rotation = Controller->GetControlRotation(); // 회전자 반환 함수
		const FRotator YawRotation(0.f, Rotation.Yaw, 0.f);

		const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
		AddMovementInput(Direction, Value);

		if (bRunning && SP >= 0.f)// 달리고 있는 상태 + 스태미나가 0이상일 때 스태미나 감소
		{
			SP -= 1.f;
		}
	}

}

void AMain::MoveRight(float Value)
{
	if ((Controller != nullptr) && (Value != 0.0f) && (!bAttacking) && (MovementStatus != EMovementStatus::EMS_Dead))
	{
		// find out which way is forward
		const FRotator Rotation = Controller->GetControlRotation(); // 회전자 반환 함수
		const FRotator YawRotation(0.f, Rotation.Yaw, 0.f);

		const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
		AddMovementInput(Direction, Value);

		if (bRunning && SP >= 0.f)// 달리고 있는 상태 + 스태미나가 0이상일 때 스태미나 감소
		{
			SP -= 1.f;
		}
	}
}

void AMain::Run(float Value)
{
	if (!Value || SP < 0.f) //쉬프트키 안 눌려 있거나 스태미나가 0 이하일 때
	{
		bRunning = false;
		GetCharacterMovement()->MaxWalkSpeed = 350.f; //속도 하향

		if (SP < MaxSP && !recoverySP)
		{
			recoverySP = true;
			GetWorldTimerManager().SetTimer(SPTimer, this, &AMain::RecoverySP, SPDelay, true);
		}
	}
	else if(!bRunning && SP >= 1.f) //쉬프트키가 눌려있고 달리는 상태가 아니면
	{
		bRunning = true;
		GetCharacterMovement()->MaxWalkSpeed = 600.f; //속도 상향		
	}
}

실제로 쉬프트키를 누르고 이동을 해야만 SP가 감소하게끔 했다.



Pause Menu

새로운 HUD

이미지

이미지

둘 중 어떤 옵션을 선택해도 자동으로 저장 메서드가 호출된다.


이것 또한 타겟 화살표, 적 생명력바와 같이 MainPlayerController에서 코드 작업해준다.

그냥 뭐 거의 똑같다.

in MainPlayerController.h

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Widgets")
TSubclassOf<UUserWidget> WPauseMenu;

UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Widgets")
UUserWidget* PauseMenu;

bool bPauseMenuVisible;

void DisplayPauseMenu();
void RemovePauseMenu();
void TogglePauseMenu();


in BeginPlay() of MainPlayerController.cpp

if (WPauseMenu)
{
    PauseMenu = CreateWidget<UUserWidget>(this, WPauseMenu);
    if (PauseMenu)
    {
        PauseMenu->AddToViewport();
        PauseMenu->SetVisibility(ESlateVisibility::Hidden);
    }
}


in MainPlayerController.cpp

void AMainPlayerController::DisplayPauseMenu()
{
    if (PauseMenu)
    {
        bPauseMenuVisible = true;
        PauseMenu->SetVisibility(ESlateVisibility::Visible);

        FInputModeGameAndUI InputMode;
        SetInputMode(InputMode);
        bShowMouseCursor = true;
    }
}

void AMainPlayerController::RemovePauseMenu()
{
    if (PauseMenu)
    {   
        bPauseMenuVisible = false;
        PauseMenu->SetVisibility(ESlateVisibility::Hidden);

        FInputModeGameOnly InputModeGameOnly;
        SetInputMode(InputModeGameOnly);
        bShowMouseCursor = false;
    }
}

void AMainPlayerController::TogglePauseMenu()
{
    if (bPauseMenuVisible)
    {
        RemovePauseMenu();
    }
    else
    {
        DisplayPauseMenu();
    }
}

하지만 얘만의 특성이 있었으니..!

바로 마우스 커서! 메뉴의 옵션들을 클릭하려면 당연히 마우스 커서가 필요하다.

InputMode라는 것이 있는데 FInputModeGameAndUI는 게임 내의 조작과 UI 조작 둘 다 가능한 모드고, FInputModeGameOnly는 말 그대로 게임 내 조작만 가능하다.

그리고 토글 메서드가 있음! (필요없을 수도 있지만 아무튼 유데미쌤이 만듬.)

파우스 메뉴는 원래는 esc키로 나오는 게 맞는데, 테스트할 때 esc 누르면 테스트가 바로 꺼지기 때문에 Q키를 눌러도 나오게끔 했다.

이미지

한 번만 누르면 되므로 액션 매핑에 추가

in SetupPlayerInputComponent() of Main.cpp

PlayerInputComponent->BindAction("ESC", IE_Pressed, this, &AMain::ESCDown);
PlayerInputComponent->BindAction("ESC", IE_Released, this, &AMain::ESCUp);


in Main.cpp

void AMain::ESCDown()
{
	bESCDown = true;

	if (MainPlayerController)
	{
		MainPlayerController->TogglePauseMenu();
	}
}

void AMain::ESCUp()
{
	bESCDown = false;
}



마법 공격별, 몬스터별 다른 데미지 설정 완료

몬스터 각각 체력도 다르게 했음.



타이틀화면에서 이어하기 가능

이미지

블루프린트가 좀 길어서 두 개로 나눔. 일단 게임 종료는 설명 안 하고.

새로 시작은 기존 세이브 파일이 있으면 세이브 삭제한 뒤 성별 선택으로 넘어가고 없으면 바로 넘어감.

이어하기는 세이브 파일이 없으면 세이브 파일 없다는 알림창 나오는데 이건 밑에서 얘기하겠음.

세이브 파일이 있으면 로드해서 플레이어 성별 정보에 따라 나뉘는데

이미지

값이 1이면 남캐, 2면 여캐 클래스 정보 가져와서 게임인스턴스의 character변수에 넣어주고 던전맵 열기.


이미지

초반에 했던 내용일 텐데 character변수에 담긴 애를 월드에 스폰하기 때문.

이미지

그 뒤는 그 스탯 HUD 뷰포트에 추가한 뒤 세이브 파일이 있는지 확인.

없으면 암것도 안 해도 되고 있으면 로드 메서드 호출



캐릭터 조명에 관한 부분..

밝기를 조금만 낮추는 방법까지만 알아냈다..ㅠ

그 vrm 플러그인 콘텐츠 보기하면은 어쩌고 Util폴더에 액터 폴더였나 아무튼 들어가서 MToonMaterialSystem을 월드에 끌고 와서 월드아웃라이너에서 선택해서 디테일 패널 보면 Shader Param 카테고리에 Light Scale Post라고 있다. 디폴트가 1인데 0.7로 줄여줌.

이미지

이 던전이 안 좋은 게 완전 그늘진 곳도 있고 햇살 짱짱한 곳도 있어서 캐릭터 밝기가 유동적이어야 하는데 Light Scale Post는 고정되는 밝기라서..

어쩔 수 없다. vroid 너무 빡세다..ㅠvㅠ



그리고 이 던전은 두 팀으로 나눠서 진행하기로 했다.

좁기도 하고 여섯이서 마법 막 쓰면 정말 정신없을 것이기 때문에..

  • 모모/루코/플레이어
  • 보보/비비/지지

루코와 모모는 기존처럼 플레이어를 따라가게 하고, 보보/비비/지지는 건너편에서 전투 진행해서 가운데 골렘쪽에서 만나는 걸로 했다.

in BeginPlay() of YaroCharacter.cpp

if (this->GetName().Contains("Momo") || this->GetName().Contains("Luko")) MoveToPlayer();

단, MoveToPlayer()의 조건에 추가사항이 생겼다.

in MoveToPlayer() of YaroCharacter.cpp

if (Player && Player->NpcGo && !CombatTarget && !bAttacking && !bOverlappingCombatSphere)

플레이어의 NpcGo라는 불 변수가 true여야 작동한다.

사실 이건 테스트하기 편하려고 넣은 것임. G키 누르면 true되도록 해놓음.

나중에 없앨 수도 있음..

in YaroCharacter.cpp

void AYaroCharacter::Tick(float DeltaTime)
{
	if (bInterpToEnemy && CombatTarget)
	{
		FRotator LookAtYaw = GetLookAtRotationYaw(CombatTarget->GetActorLocation());
		FRotator InterpRotation = FMath::RInterpTo(GetActorRotation(), LookAtYaw, DeltaTime, InterpSpeed); //smooth transition

		SetActorRotation(InterpRotation);
	}

	if (this->GetName().Contains("Vovo") || this->GetName().Contains("Vivi") || this->GetName().Contains("Zizi"))
	{
		if (!Player)
		{
			ACharacter* p = UGameplayStatics::GetPlayerCharacter(this, 0);
			Player = Cast<AMain>(p);
		}
		if (!canGo && Player && Player->NpcGo)
		{
			canGo = true;
			MoveToLocation();
		}
	}
}

틱 함수인데, 저 첫번째 조건은 보간 관련.

지금 봐야할 건 두번째 조건. 보보/비비/지지일 경우 조건에 맞으면 MoveToPlayer()가 아닌 MoveToLocation() 호출함.

이 메서드는 말 그대로 정해진 위치로 이동하게 하는 메서드임.

void AYaroCharacter::MoveToLocation()
{	
	if (AIController)
	{
		AIController->MoveToLocation(Pos[index]);
	}

	GetWorld()->GetTimerManager().SetTimer(TeamMoveTimer, FTimerDelegate::CreateLambda([&]() {
		if (!CombatTarget && !bOverlappingCombatSphere)
		{
			if (index <= 4)
			{
				float distance = (GetActorLocation() - Pos[index]).Size();
	
				if (AIController && distance <= 70.f)
				{
                      index++;
					AIController->MoveToLocation(Pos[index]);
				}
				else
				{
					AIController->MoveToLocation(Pos[index]);
				}
			}
		}
	}), 1.f, true);
}

위치 배열이 있음. 이 위치 배열은 생성자에서 추가해뒀음.

in 생성자 of YaroCharacter.cpp

Pos.Add(FVector(2517.f, 5585.f, 3351.f));
Pos.Add(FVector(2345.f, 4223.f, 2833.f));
Pos.Add(FVector(2080.f, 283.f, 2838.f));
Pos.Add(FVector(1550.f, -1761.f, 2843.f));
Pos.Add(FVector(1026.f, -1791.f, 2576.f));

대충 중간에 어디 끼거나 할 일 없이 몬스터랑 전투하고 골렘 있는 곳까지 무사히 오도록 내가 직접 위치 알아보고 넣어줌.

다시 위의 메서드 내용 보면 참고로 전투타겟이 있고 전투 범위에 적이 들어온 경우엔 이동 안 함.

전투 중이 아닐 때만 이동.

거리 계산해서 대충 도달했다 싶으면 인덱스 증가시켜서 다음 위치로 이동함.



드디어 영상으로 확인하시겠습니다~ 대충 여기까지가 졸작 수업 기말이었음.



사실 2번째 영상 골렘 죽이고 보보/비비/지지 위치 배열 인덱스 오류 나서 강종됐는데 그 뒤에 바로 고쳤음.


아무튼…

드디어 밀린 포스팅 끝…!!!!!!!!!!!!!!!

Updated: