본문 바로가기

공부/이득우의 언리얼 프로그래밍

[Study] Part 2 - 게임데이터 관리 (10/15)

728x90
반응형

 

 

  • 정리
    • 데이터 기반의 게임 시스템 구축
      1. 외부 데이터 파일로부터 게임 데이터를 관리하는 다양한 방법의 학습
      2. 데이터를 관리하는 싱글톤 클래스 생성 방법의 이해
      3. 데이터 테이블 기반의 캐릭터 스텟 시스템 구축
      4. 지연 생성을 활용한 액터 초기화 방법의 이해
  • 데이터 기반의 게임 시스템 구축
    • 이때까지 사용한 데이터를 데이터 기반으로 변경하기 (csv)
    • 엑셀 기반의 csv파일을 만들었다면, 언리얼 에디터 내에서 이것과 동일한 이름을 가진 속성의 구조체를 선언해줘야 한다.
  • 엑셀 데이터의 임포트
    • csv파일을 언리얼 엔진은 임포트를 할 수 있도록 기본적으로 기능을 제공하고 있다.
    • DataAsset과 유사하게 FTableRowBase를 상속받은 구조체를 선언
    • 엑셀 데이터 테이블의 컬럼에는 Name이라는 컬럼이 들어가야 한다. (키값이 됨)
    • 엑셀의 Name 컬럼을 제외한 컬럼과 동일하게 UPROPERTY 속성을 선언
    • 엑셀 데이터를 csv로 익스포트한 후 언리얼 엔진에 임포트

 

 

엑셀 데이터의 임포트

  • Name 칼럼에는 고유한 식별자를 부여
  • 나머지에는 데이터를 집어 넣고, 구조체에는 이 다섯 개의 속성만 선언해서 지정해주면 된다.

 

 

  • 텍스트 파일로 헤더파일을 만들어주고 csv 파일과 함께 형식을 맞춘 멤버 변수들을 선언하고 컴파일을 해준다.
#pragma once

#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "ABCharacterStat.generated.h"

USTRUCT(BlueprintType)
struct FABCharacterStat : public FTableRowBase
{
	GENERATED_BODY()
	
public:
	FABCharacterStat() : MaxHp(0.0f), Attack(0.0f), AttackRange(0.0f), AttackSpeed(0.0f) {}

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
	float MaxHp;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
	float Attack;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
	float AttackRange;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
	float AttackSpeed;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stat)
	float MovementSpeed;

	// 연산자 재정의
	// 모든 멤버변수가 float이라고 가정이 되면, 구조체의 데이터는 float의 집합으로 구성됨
	// 이 크기만큼 개수를 확인하고 더해주면 새로운 연산자를 추가해도 덧셈 연산을 고칠 필요가 없다.
	FABCharacterStat operator+(const FABCharacterStat& Other) const
	{
		const float* const ThisPtr = reinterpret_cast<const float* const>(this);
		const float* const OtherPtr = reinterpret_cast<const float* const>(&Other);

		FABCharacterStat Result;
		float* ResultPtr = reinterpret_cast<float*>(&Result);
		int32 StatNum = sizeof(FABCharacterStat) / sizeof(float);
		for (int32 i = 0; i < StatNum; i++)
		{
			ResultPtr[i] = ThisPtr[i] + OtherPtr[i];
		}

		return Result;
	}
};
  • ABCharacterStat.h

Miscellaneous>Data Table
Miscellaneous>Data Table>Pick Row Structure

  • 똑같은 폴더명의 폴더를 만들고, Miscellaneous>Data Table을 생성해준다.
    • 정상적으로 헤더파일이 컴파일 되었다면 방금 만든 ABCharacterStat라는 구조체를 선택할 수 있게 나온다.
    • 생성한 Data Table에서 Reimport를 하여 반영할 csv파일을 넣어준다.
  • 데이터를 관리할 싱글톤 클래스의 설정
    • 언리얼 엔진에서 제공하는 싱글톤 클래스
      • 게임 인스턴스
      • 에셋 매니저
      • 게임 플레이 관련 액터 (게임 모드, 게임 스테이트)
      • 프로젝트에 싱글톤으로 등록한 언리얼 오브젝트
    • 언리얼 오브젝트 생성자에서 사용하지 않도록 주의

 

 

Edit>Project Settings>Engine>General Settings> Defalut Classes

 

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "ABCharacterStat.h"				// 캐릭터 스탯들에 대한 테이블을 가지고 오기 위함
#include "ABGameSingleton.generated.h"

DECLARE_LOG_CATEGORY_EXTERN(LogABGameSingleton, Error, All);

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABGameSingleton : public UObject
{
	GENERATED_BODY()
	
public:
	UABGameSingleton();
	static UABGameSingleton& Get();

// Character Stat Data Section
public:
	// Get함수, 값이 유효하면 전자, 아니면 그냥 기본 생성자를 만들어서 넘겨주도록 선언
	FORCEINLINE FABCharacterStat GetCharacterStat(int32 InLevel) const { return CharacterStatTable.IsValidIndex(InLevel) ? CharacterStatTable[InLevel - 1] : FABCharacterStat(); }

	UPROPERTY()
	// 총 몇개의 레벨?
	int32 CharacterMaxLevel;

private:
	TArray<FABCharacterStat> CharacterStatTable;		// 캐릭터 스탯들에 대한 테이블들을 내부적으로 보관해서 필요한 게임 객체들에게 제공하기 위한 Talbe Array
};
  • ABGameSingleton.h

 

  • 해당 ABGameSingleton 클래스를 통해서 스탯들에 대한 테이블들을 필요한 게임 객체들에게 제공할수있도록 Table Array를 설정한다.
  • Get함수를 통해 Table Array의 값이 유효하면 해당 값을, 아니면 기본 생성자를 만들어서 넘겨준다.
#include "GameData/ABGameSingleton.h"

DEFINE_LOG_CATEGORY(LogABGameSingleton);

// 엑셀 테이블에서 만든 로우 테이블 에셋을 접근해서 TArray로 변환, 저장시키기
UABGameSingleton::UABGameSingleton()
{
	// 만든 DataTable 주소를 가져온다.
	// DataTable은 맵 형태로 Key,Value 값으로 들어온다.
	static ConstructorHelpers::FObjectFinder<UDataTable> DataTableRef(TEXT("/Script/Engine.DataTable'/Game/ArenaBattle/GameData/ABCharacterStatTable.ABCharacterStatTable'"));
	if (nullptr != DataTableRef.Object)
	{
		// 갯수가 맞는지 체크
		const UDataTable* DataTable = DataTableRef.Object;
		check(DataTable->GetRowMap().Num() > 0);

		// Key값은 순차적으로 오기 때문에 Key값은 필요없고, Value값만 Array에 저장
		TArray<uint8*> ValueArray;
		DataTable->GetRowMap().GenerateValueArray(ValueArray);
		Algo::Transform(ValueArray, CharacterStatTable,
			[](uint8* Value)
			{
				return *reinterpret_cast<FABCharacterStat*>(Value);
			}
		);
	}

	// 배열의 갯수가 0보다 큰지 확인
	CharacterMaxLevel = CharacterStatTable.Num();
	ensure(CharacterMaxLevel > 0);
}

// Singleton Get() 함수
UABGameSingleton& UABGameSingleton::Get()
{
	// CastChecked로 강력하게 검사, GEngine에 있는 아까 적용한 GameSingleton을 가져오기
	UABGameSingleton* Singleton = CastChecked<UABGameSingleton>(GEngine->GameSingleton);
	if (Singleton)
	{
		return *Singleton;
	}
	// 혹시 몰라서 만약 Singleton이 null 값이라면 에러를 띄우도록 함
	// 코드의 흐름을 위해서 return값의 인스턴스를 생성하고 리턴함
	UE_LOG(LogABGameSingleton, Error, TEXT("Invalide Game Singleton"));
	return *NewObject<UABGameSingleton>();
}
  • ABGameSingleton.cpp

 

  • 생성자에서는 DataTable을 초기화시켜주고, Key값은 순차적으로 정렬되어 있기 때문에 따로 필요 없이 Value값만 Array를 통해 저장시켜준다.
  • Get()함수에서는 Project Settings에서 GameSingleton으로 설정한 싱글톤 클래스를 가지고 오고 리턴하도록 구현해준다.

 

  • 프로젝트의 주요 레이어
    • 게임 레이어 : 기믹과 NPC
    • 미들웨어 레이어 : 캐릭터 스탯 컴포넌트, 아이템 박스
    • 데이터 레이어 : 스탯 데이터, 데이터 관리를 위한 싱글톤 클래스
    • 위에서 아래로만 참조할 수 있고, 아래에서 위로 참조하지 못하게 구조를 구성

프로젝트의 주요 레이어
캐릭터 스탯 시스템

  • 캐릭터 스탯 시스템은 다음과 같이 구성

 

 

class ARENABATTLE_API UABCharacterStatComponent : public UActorComponent
{
	// ...

	// Stat Section
	// 레벨을 설정해주는 함수
	void SetLevelStat(int32 InNewLevel);
	// 레벨 Getter 함수
	FORCEINLINE float GetCurrentLevel() const { return CurrentLevel; }
	// 무기 .. 등을 획득했을때 ModifierStat을 변경해줄 수 있는 Setter 함수
	FORCEINLINE void SetModifierStat(const FABCharacterStat& InModifierStat) { ModifierStat = InModifierStat; }
	// 캐릭터의 전체 스텟값을 받아올 수 있도록
	FORCEINLINE FABCharacterStat GetTotalStat() const { return BaseStat + ModifierStat; }
	
	// 해당 네 가지 값들은 캐릭터가 초기화될 때마다 언제든지 바뀔 수 있다. 그렇기 때문에 언리얼 엔진을 저장할 때는 해당 정보가 저장되지 않도록 Transient 키워드를 추가하고, 에디터에서는 읽기 전용으로 설정
	// 디스크에 저장할 필요가 없는 휘발성 데이터의 경우 Transient 라는 키워드를 추가하여 디스크에 저장할때 불필요한 공간이 낭비되지 않도록 지정할 수 있음
	UPROPERTY(VisibleInstanceOnly, Category = Stat)
	float CurrentHp;

	// 캐릭터 스텟은 현재 레벨의 정보를 기반으로 해서 게임 싱글톤으로부터 스텟 정보를 제공받게 된다.
	UPROPERTY(Transient, VisibleInstanceOnly,Category = Stat)
	float CurrentLevel;

	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
	FABCharacterStat BaseStat;

	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
	FABCharacterStat ModifierStat;
};
  • ABCharacterStatComponent.h

 

  • 스텟 정보의 경우 csv파일을 통한 게임 싱글톤으로부터 제공받고, BaseStat, ModifierStat를 통해 관리해준다.
  • 전체 스텟값을 받아올 수 있는 GetTotalStat함수도 만들어 관리

 

#include "CharacterStat/ABCharacterStatComponent.h"
#include "GameData/ABGameSingleton.h"

// Sets default values for this component's properties
UABCharacterStatComponent::UABCharacterStatComponent()
{
	CurrentLevel = 1;
}


// Called when the game starts
void UABCharacterStatComponent::BeginPlay()
{
	Super::BeginPlay();
	
	SetLevelStat(CurrentLevel);
	SetHp(BaseStat.MaxHp);
}

// 주어진 레벨에서 스텟 값을 변경하도록 설정
void UABCharacterStatComponent::SetLevelStat(int32 InNewLevel)
{
	// GameSingleton에서 데이터를 가져오기
	CurrentLevel = FMath::Clamp(InNewLevel, 1, UABGameSingleton::Get().CharacterMaxLevel);
	BaseStat = UABGameSingleton::Get().GetCharacterStat(CurrentLevel);
	check(BaseStat.MaxHp > 0.0f);
}
  • ABCharacterStatComponent.cpp

 

UCLASS()
class ARENABATTLE_API AABStageGimmick : public AActor
{
	// ...

	// CurrentStageNum 관련 Getter, Setter
public:
	FORCEINLINE int32 GetStageNum() const { return CurrentStageNum; }
	FORCEINLINE void SetStageNum(int32 NewStageNum) { CurrentStageNum = NewStageNum; }
	
// Stage Stat
protected:
	UPROPERTY(VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
	int32 CurrentStageNum;
}
  • ABStageGimmick.h

 

  • 스테이지 기믹에도 스텟을 활용하여 로직을 구성해준다.
  • 현재 스테이지 레벨에 따라 NPC의 데이터를 업데이트해주기

 

// 게이트의 트리거가 발동된다면
void AABStageGimmick::OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	// ...

	// 아무것도 없다고 판정되었을 때
	if (!bResult)
	{
		AABStageGimmick* NewGimmick = GetWorld()->SpawnActor<AABStageGimmick>(NewLocation, FRotator::ZeroRotator);
		if (NewGimmick)
		{
			NewGimmick->SetStageNum(CurrentStageNum + 1);
		}
	}
}

// NPC 소환
void AABStageGimmick::OnOpponentSpawn()
{
	const FVector SpawnLocation = GetActorLocation() + FVector::UpVector * 88.0f;
	// OpponentClass를 TSubclassOf로 한정시켰기 때문에 캐릭터에 있는 ABCharacterNonPlayer를 상속받은 캐릭터에 한정해서만 액터를 스폰시키게 된다.
	AActor* OpponentActor = GetWorld()->SpawnActor(OpponentClass, &SpawnLocation, &FRotator::ZeroRotator);
	AABCharacterNonPlayer* ABOpponentCharacter = Cast<AABCharacterNonPlayer>(OpponentActor);

	// 캐스팅이 정상적으로 된다면, OnDestroyed 델리게이트에 함수를 바인드
	if (ABOpponentCharacter)
	{
		ABOpponentCharacter->OnDestroyed.AddDynamic(this, &AABStageGimmick::OnOpponentDestroyed);
		// NPC가 생성될 때 CurrentStageNum에 따라 Level을 설정할 수 있도록
		ABOpponentCharacter->SetLevel(CurrentStageNum);
	}
}
  • ABStageGimmick.cpp

 

  • 게이트의 트리거가 발동된다면 StageCurrentStageNum을 증가 시켜준다.
  • NPC가 생성되면 레벨을 CurrentStageNum을 활용하여 Level을 설정할 수 있도록 해준다.

 

실제 코드를 빌드후 실행해본 결과
적용된 csv 데이터 테이블

 

  • 하지만 위와 같은 코드를 작성하고 나면, 다음스테이지에 가게 될 경우 CurrentStageNum은 증가하면서 NPCMaxHp는 증가하지만 CurrentHp는 그대로인 모습을 확인 할 수 있다.
  • 위와 같은 현상이 일어나는 이유는 다음과 같다. 액터의 생성을 SpawnActor로 사용하게 된다면 액터와 액터가 가지고 있는 모든 컴포넌트의 BeginPlay 함수가 바로 호출이 됨.
  • 그렇기 때문에 SpawnActor로 NPC를 생성하자, ABCharacterStatComponent에서의 BeginPlay가 실행이 되고 그 안에 있는 SetLevelStat함수와 SetHp함수가 실행되게 된다.

 

// Called when the game starts
void UABCharacterStatComponent::BeginPlay()
{
	Super::BeginPlay();
	
	SetLevelStat(CurrentLevel);
	SetHp(BaseStat.MaxHp);
}
  • ABCharacterStatComponent.cpp

 

  • CurrentLevel은 아직 이전 레벨이기 때문에 1레벨 데이터로 NPC가 생성될 것이고, 이후 ABStageGimmick에서는 기믹을 이미 SpawnActor를 호출해서 BeginPlay를 실행한 상태에서 SetLevel을 진행했기 때문에

 

// NPC 소환
void AABStageGimmick::OnOpponentSpawn()
{
	const FVector SpawnLocation = GetActorLocation() + FVector::UpVector * 88.0f;
	// OpponentClass를 TSubclassOf로 한정시켰기 때문에 캐릭터에 있는 ABCharacterNonPlayer를 상속받은 캐릭터에 한정해서만 액터를 스폰시키게 된다.
	AActor* OpponentActor = GetWorld()->SpawnActor(OpponentClass, &SpawnLocation, &FRotator::ZeroRotator);
	AABCharacterNonPlayer* ABOpponentCharacter = Cast<AABCharacterNonPlayer>(OpponentActor);

	// 캐스팅이 정상적으로 된다면, OnDestroyed 델리게이트에 함수를 바인드
	if (ABOpponentCharacter)
	{
		ABOpponentCharacter->OnDestroyed.AddDynamic(this, &AABStageGimmick::OnOpponentDestroyed);
		// NPC가 생성될 때 CurrentStageNum에 따라 Level을 설정할 수 있도록
		ABOpponentCharacter->SetLevel(CurrentStageNum);
	}
}
  • ABStageGimmick.cpp

 

액터의 생성과 지연 생성의 프로세스

 

  • 언리얼에서는 액터를 지연 생성하는 SpawnActorDeferred라는 함수를 제공한다. 마지막에 FinishSpawning 함수를 호출해줘야지만 BeginPlay가 호출이 된다.
  • FinishSpawning 함수를 호출하기 전에 초기화를 진행하면 BeginPlay로 설정되어 있는 SetHp함수를 올바로 실행할 수 있게 된다.

 

// NPC 소환
void AABStageGimmick::OnOpponentSpawn()
{
	const FTransform SpawnTransform(GetActorLocation() + FVector::UpVector * 88.0f);
	// OpponentClass를 TSubclassOf로 한정시켰기 때문에 캐릭터에 있는 ABCharacterNonPlayer를 상속받은 캐릭터에 한정해서만 액터를 스폰시키게 된다.
	// SpawnActor -> SpawnActorDeferred 지연생성으로 변경
	AABCharacterNonPlayer* ABOpponentCharacter = GetWorld()->SpawnActorDeferred<AABCharacterNonPlayer>(OpponentClass, SpawnTransform);

	// 캐스팅이 정상적으로 된다면, OnDestroyed 델리게이트에 함수를 바인드
	if (ABOpponentCharacter)
	{
		ABOpponentCharacter->OnDestroyed.AddDynamic(this, &AABStageGimmick::OnOpponentDestroyed);
		// NPC가 생성될 때 CurrentStageNum에 따라 Level을 설정할 수 있도록
		ABOpponentCharacter->SetLevel(CurrentStageNum);
		// SpawnActorDeferred 지연 생성 완료, 이후 BeginPlay 함수 실행
		ABOpponentCharacter->FinishSpawning(SpawnTransform);
	}
}
  • ABStageGimmick.cpp

 

  • SpawnActorSpawnActorDeferred함수로 변경시켜주고, 인자로 FTransform을 받기 때문에 그에 맞게 수정해준다.
  • 이후, SetLevel을 실행해준 뒤, FinishSpawning을 실행해서 지연 생성을 완료해주고 BeginPlay함수가 실행되도록해준다.
  • 다음과 같이 스폰 코드가 초기값이 반영이 되어야 한다면 지연생성 코드로 바꿔주는 것이 좋다.
    • void AABStageGimmick::OnOpponentSpawn()
      • SpawnActor →SpawnActorDeferred로 변경
    • void AABStageGimmick::SpawnRewardBoxes()
      • SpawnActor → SpawnActorDeferred로 변경
    • 생성자에 있던 Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABItemBox::OnOverlapBegin);void AABItemBox::PostInitializeComponents() 에 위치하도록 변경하면서 지연생성 후 바인딩 되도록 변경

 

 

정상적으로 Stat이 적용된 모습을 볼 수 있다.

 

  • Weapon추가 스탯을 지정해주고, NPCMesh를 랜덤하게 나올 수 있도록 설정하기
#pragma once

#include "CoreMinimal.h"
#include "Item/ABItemData.h"
#include "GameData/ABCharacterStat.h"
#include "ABWeaponItemData.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABWeaponItemData : public UABItemData
{
	GENERATED_BODY()

	// ...

	UPROPERTY(EditAnywhere, Category = Stat)
	FABCharacterStat ModifierStat;
};
  • UABWeaponItemData.h

 

Weapon Data Asset

  • WeaponItemDataModifierStat 변수를 추가해서 획득하게 되면 Weapon의 스텟이 적용될 수 있도록 설정해준다.

 

ArenaBattle>Config>DefalutArenaBattle.ini

  • [Script/ArenaBattle.ABCharacterNonPlayer]
    • C++ 객체이고, ArenaBattle 모듈의 ABCharcterNonPlayer라는 언리얼 오브젝트를 가리키고 있다.
  • +NPCMeshes
    • NPCMeshes라는 TArray변수가 ABCharcterNonPlayer에 선언이 되어 있다면 이것의 값을 지정하는 것

 

#pragma once

#include "CoreMinimal.h"
#include "Character/ABCharacterBase.h"
#include "Engine/StreamableManager.h"		// 비동기 위함
#include "ABCharacterNonPlayer.generated.h"

/**
 * 
 */
// Config 폴더의 DefaultArenaBattle.ini를 사용해서 불러들이겠다는 의미
UCLASS(config=ArenaBattle)
class ARENABATTLE_API AABCharacterNonPlayer : public AABCharacterBase
{
	GENERATED_BODY()
	
public:
	AABCharacterNonPlayer();

protected:
	virtual void PostInitializeComponents() override;

protected:
	void SetDead() override;

	// NPC Mesh 로딩이 완료되었을 때 신호를 받는 함수
	void NPCMeshLoadCompleted();

	// UPROPERTY에 config를 붙이게 되면 해당 config 파일로부터 데이터를 불러오겠다는 의미
	// 경로가 있기 때문에 FSoftObjectPath
	UPROPERTY(config)
	TArray<FSoftObjectPath> NPCMeshes;

	// 비동기 위한 FStreamableHandle
	TSharedPtr<FStreamableHandle> NPCMeshHandle;
};
  • AABCharacterNonPlayer.h

 

  • PostInitializeComponents() 함수를 오버라이드 해주고, ini 파일에 나와 있는 것처럼 NPCMeshes라는 TArray 변수를 만들어 준다. 경로값이 들어가기 때문에 자료형은 FSoftObjectPath
  • 비동기 로딩을 위한 헤더 추가와 TSharedPtr<FStreamableHandle> NPCMeshHandle; 를 추가해준다.

 

#include "Character/ABCharacterNonPlayer.h"
#include "Engine/AssetManager.h"		// 에셋 매니저, 비동기 로드 위한

AABCharacterNonPlayer::AABCharacterNonPlayer()
{
	GetMesh()->SetHiddenInGame(true);
}

void AABCharacterNonPlayer::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	// NPCMeshes가 제대로 들어왔는지 확인
	ensure(NPCMeshes.Num() > 0);
	int32 RandIndex = FMath::RandRange(0, NPCMeshes.Num() - 1);
	// 에셋 매니저를 사용해서 비동기 방식으로 AsyncLoad로 진행, 비동기가 끝나면 NPCMeshLoadCompleted 함수를 실행할 수 있도록 바인드
	NPCMeshHandle = UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(NPCMeshes[RandIndex], FStreamableDelegate::CreateUObject(this, &AABCharacterNonPlayer::NPCMeshLoadCompleted));
}

// ...

// 비동기가 끝나면 NPCMeshLoadCompleted 함수 실행
void AABCharacterNonPlayer::NPCMeshLoadCompleted()
{
	// 로딩이 완료된 후 Handle이 유효한가?
	if (NPCMeshHandle.IsValid())
	{
		//GetLoadedAsset를 사용해서 Mesh 가져오기
		USkeletalMesh* NPCMesh = Cast<USkeletalMesh>(NPCMeshHandle->GetLoadedAsset());
		if (NPCMesh)
		{
			GetMesh()->SetSkeletalMesh(NPCMesh);
			GetMesh()->SetHiddenInGame(false);
		}
	}

	// Handle은 사용 완료후 해지
	NPCMeshHandle->ReleaseHandle();
}
  • AABCharacterNonPlayer.cpp

 

  • PostInitializeComponents()에서 Mesh가 정상적으로 들어왔는지 확인 후 랜덤으로 Mesh를 비동기 로딩한다.
  • 비동기 로딩이 끝나면 발생하는 델리게이트에 AABCharacterNonPlayer::NPCMeshLoadCompleted()를 바인딩해주고, Mesh를 적용시켜준다.
  • Mesh를 적용시켜주고 나면 NPCMeshHandle은 해지해준다.

 

 

NPC Mesh가 랜덤하게 적용되는 모습

 

 

 

 

 

 

 

 

 

해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part2 -언리얼 게임 프레임웍의 이해>
강의를 수강하고 정리한 내용입니다.
 

이득우의 언리얼 프로그래밍 Part2 - 언리얼 게임 프레임웍의 이해 | 이득우 - 인프런

청강문화산업대학교에서 언리얼 엔진, 게임 수학, UEFN 게임제작을 가르치고 있습니다. - 이득우의 언리얼 C++ 프로그래밍, 넥슨 코리아 공식 교육 교재 선정 2023 - 스마일게이트 언리얼 프로그래

www.inflearn.com

 

 

 

 

728x90