RPG 게임에는 다양한 캐릭터들이 존재한다.
해당 캐릭터들을 일일히 관리하기엔 어려움이 존재하므로 공통된 Property 들은 묶어서 관리하고, 나머지는 UCharacterClassInfo 라는 DataAsset 을 통해 관리한다.
DataAsset 은 ENum 타입의 ECharacterClass , CurveTable , Attributes , GameplayEffect 등을 소유할 수 있다.
1. 모든 캐릭터 클래스에 대한 Information을 저장하는 UCharacterClassInfo 타입의 DataAsset 생성
2. 각 캐릭터 클래스 타입에 대한 상수를 가지고 있는 ENum 타입의 ECharacterClass 생성
3. Attribute 에 대한 CurveTable (Warrior, Archer와 같은 캐릭터 각각의 커브테이블)생성
4. 각 Attribute 들에 대한 GameplayEffect 생성
5. 공유하는 Attribute 나 Effect 생성
6. 위의 과정을 통해 만든 DataAsset 을 호출하는 함수 생성
먼저 DataAsset 클래스 기반의 CharacterClassInfo 클래스를 생성하고 코드를 작성한다.


// CharacterClassInfo.h
...
class UGameplayEffect;
...
UENUM(BlueprintType)
enum class ECharacterClass : uint8
{
Elementalist,
Warrior,
Ranger
};
USTRUCT(BlueprintType)
struct FCharacterClassDefaultInfo
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, Category = "Class Defaults")
TSubclassOf<UGameplayEffect> PrimaryAttributes;
};
...
public:
UPROPERTY(EditDefaultsOnly, Category = "Character Class Defaults")
TMap<ECharacterClass, FCharacterClassDefaultInfo> CharacterClassInformation;
UPROPERTY(EditDefaultsOnly, Category = "Common Class Defaults")
TSubclassOf<UGameplayEffect> SecondaryAttributes;
UPROPERTY(EditDefaultsOnly, Category = "Common Class Defaults")
TSubclassOf<UGameplayEffect> VitalAttributes;
FCharacterClassDefaultInfo GetClassDefaultInfo(ECharacterClass CharacterClass);
// CharacterClassInfo.cpp
...
FCharacterClassDefaultInfo UCharacterClassInfo::GetClassDefaultInfo(ECharacterClass CharacterClass)
{
return CharacterClassInformation.FindChecked(CharacterClass);
}
에디터로 돌아와서 CharacterClassInfo 기반 DataAsset 블루프린트인 DA_CharacterClassInfo 를 생성한다.



파일을 열고 Elements를 Elementalist , Warrior , Ranger 에 대해 추가하려고 하면 하나만 추가되고 아래와 같은 에러메세지가 뜨는데


가장 아래에 있는 요소로 순서대로 생성하도록 하면 된다.



먼저 구별을 위해 기존에 캐릭터가 사용하던 Attributes 들은 Aura 라는 폴더를 만들어 그곳으로 이동시킨다.

Enemy 라는 폴더를 만들고 그곳에 GameplayEffect 기반 블루프린트 GE_PrimaryAtributes_Elementalist , GE_PrimaryAttributes_Warrior , GE_PrimaryAttributes_Ranger 를 생성한다.


SecondaryAttributes 와 VitalAttributes 는 기존에 사용하던 GE_AuraSecondaryAttributes 와 GE_AuraVitalAttributes 를 사용할 것이므로 헷갈리지 않도록 파일명을 GE_SecondaryAttributes 와 GE_VitalAttributes 로 변경하고 DefaultAttributes 폴더로 이동시킨다.

다시 DA_CharacterClassInfo 로 돌아와 나머지 설정을 끝마친다.

먼저 CurveTable 기반 CT_PrimaryAttributes_Elementalist 을 추가한다.



어떤 Attributes 에 대한 커브인지 알기 쉽게 하기 위해 태그와 같은 이름으로 바꿔준다.

우클릭 -> Add Key 로 키를 추가해주고, 값을 수정한다.( x(Level) : 1 , y(수치) : 5 )


키를 하나 더 추가하고 x : 5 , y : 7 로 설정해주면 자동으로 그래프를 통해 각 x축에 대한 y값을 그래프로 나타내어 준다.


두 포인트를 선택한 후 우클릭 -> Auto 를 클릭하면 스무스하게 그래프가 바뀌게 된다.


키를 하나 더 추가하고 x : 10 , y : 9.5 로 설정해준 다음 Normalized View Mode 를 선택해주면 전체적으로 그래프를 확인할 수 있다.


다시 Absolute View Mode 를 선택하고, 키를 추가한 다음 x : 15 , y : 12.5 로 설정한다.
스무스하게 변형시킬 구간의 포인트를 선택하고 우클릭 후 Auto 를 선택해 그래프를 스무스하게 변경시킨다.

나머지 지점에 대해서도 키를 추가하고 Auto 를 통해 그래프를 스무스하게 변경한다.
( x : 20 , y : 14 ) , ( x : 40 , y : 25 )

다른 Attributes 에 대해서도 커브를 생성해야 한다.
Attributes.Primary.IntelligenceAuto 를 클릭하여 그래프를 스무스하게 변경한다.x : 1 , y : 15 ) , ( x : 5 , y : 19 ) , ( x : 10 , y : 21 ) , ( x : 20 , y : 25 ) , ( x : 40 , y : 40 )
Attributes.Primary.ResilienceAuto 를 클릭하여 그래프를 스무스하게 변경한다.x : 1 , y : 11 ) , ( x : 40 , y : 20 )
Attributes.Primary.VigorAuto 를 클릭하여 그래프를 스무스하게 변경한다.x : 1 , y : 7 ) , ( x : 40 , y : 14 )
GE_PrimaryAttributes_Elementalist 로 돌아와서 4개의 Attributes 에 대해 Modifiers 를 추가해준다.




9.4.1의 과정을 Warrior 과 Ranger 둘다 키를 추가하고 Auto 를 이용해 스무스하게 변경하는 과정은 작업 효율이 떨어진다. 값을 추출하여 적용하는 방법이 두가지 있는데 그중 하나가 CSV를 이용하는 것이다.
먼저 프로젝트가 있는 곳에 데이터를 저장할 폴더를 하나 생성한다.

데이터를 추출할 CT_PrimaryAttributes_Elementalist 를 우클릭하고 Export as CSV를 클릭한다.

데이터를 저장할 경로를, 방금 생성한 프로젝트가 위치하는 곳에 있는 Data 폴더로 지정하여 저장한다.

파일을 열어보면 아래와 같이 데이터가 저장되어 있는 것을 확인할 수 있다.
(Excel로 열면 표처럼 깔끔하게 나옴)

값을 수정하거나 추가하는 것도 가능하다.

변경점을 에디터에서 적용시키려면, CT_PrimaryAttributes_Elementalist 로 돌아와서 Reimport 버튼을 클릭하고, csv 파일을 선택하면 된다.


이 방법은 더이상 Interpolate 할 수 없으므로 그래프가 리니어하게 나온다.

기존의 csv 파일을 복사한 다음 이름을 변경하여 값을 수정할 수 있고, 이를 통해 커브테이블을 일일히 생성하지 않아도 된다.


이제 해당 값을 적용시킬 CT_PrimaryAttributes_Ranger 를 생성하고 ReImport 를 통해 해당 값을 적용시킨다.


JSON을 통한 CurveTable 추출도 가능하다.
추출할 CurveTable 을 선택하고 우클릭한 후 Export as JSON 을 클릭한다.



파일을 열면 아래와 JSON format 으로 데이터들이 저장되어 있다.

동일하게 복사하여 이름을 CT_PrimaryAttributes_Warrior 로 변경한 다음, 값들을 수정한다.


에디터로 돌아와서 Import 를 클릭하고 CT_PrimaryAttributes_Warrior 를 선택해준다.


그러면 옵션을 선택할 수 있도록 창이 표시된다.

다른 캐릭터들과 동일하게 CurveTable , Cubic 을 선택한 후 Apply 를 클릭한다.


파일을 열면 정상적으로 적용된 것을 확인할 수 있다.

GE_PrimaryAttributes_Ranger







추가
현재 적용중인SecondaryAttributes는 캐릭터가 레벨업을 할 때마다 그에 따라Attribute수치를 변경하기 위해InfiniteGameplayEffecct로 설정해두었다.
하지만 적의 경우 그럴 필요가 없으므로 기존의GE_SecondaryAttributes를 복사하여GE_SecondaryAttributes_Enemy로 rename해준 다음
Ge_SecnodaryAttributes_Enemy의Duration Policy : Instant로 변경한다.
DA_CharacterClassInfo로 가서GE_SecondaryAttributes_Enemy가 적용되도록 변경한다.
기존의GE_SecondaryAttributes는Aura전용이므로 해당 폴더로 이동시킨다.
DataAsset 적이 어떤 Attribute 값을 가져야 할지와 같은 룰을 포함한다.
룰 관련은 GameMode와 관련있으므로 AuraGameplayModeBase 에 코드를 추가한다.
// AuraGameplayModeBase.h
...
class UCharacterClassInfo;
...
...
public:
// Elementalist, Warrior, Ranger 선택가능한 CharacterClassInfo, 블루프린트에서 DA_CharacterClassInfo 할당
UPROPERTY(EditDefaultsOnly, Category = "Character Class Defaults")
TObjectPtr<UCharacterClassInfo> CharacterClassInfo;
적이 CharacterClassInfo 에 접근하고, Attribute를 적용시킬 수 있도록 AuraAbilitySystemLibrary 클래스에 값을 적용시키기 위한 코드를 작성한다.
// AuraAbilitySystemLibrary.h
...
#include "Data/CharacterClassInfo.h"
...
public:
...
// 적 클래스가 CharacterClassInfo에 접근하고, 값을 적용시킬 수 있도록 하는 함수
UFUNCTION(BlueprintCallable, Category = "AuraAbilitySystemLibrary|CharacterClassDefaults")
static void InitializeDefaultAttributes(const UObject* WorldContextObject, ECharacterClass CharacterClass, float Level, UAbilitySystemComponent* ASC);
// AuraAbilitySystemLibrary.cpp
...
#include "Game/AuraGameModeBase.h"
#include "AbilitySystem/AuraAbilitySystemComponent.h"
...
...
void UAuraAbilitySystemLibrary::InitializeDefaultAttributes(const UObject* WorldContextObject, ECharacterClass CharacterClass, float Level, UAbilitySystemComponent* ASC)
{
// GameMode에 생성해둔 CharacterClassInfo dataasset을 가져오기 위함
AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject));
if(AuraGameMode == nullptr) return;
// AuraGameMode의 CharacterClassInfo를 가져와서, FCharacterClassDefaultInfo 타입의 구조체에 저장
UCharacterClassInfo* CharacterClassInfo = AuraGameMode->CharacterClassInfo;
// TMap에 있는 ECharacterClass 타입의 Key(EX: Warrior)를 넣으면 FCharacterClassDefaultInfo 구조체 타입의 PrimaryAttribute(EX: Warrior) 리턴
FCharacterClassDefaultInfo ClassDefaultInfo = CharacterClassInfo->GetClassDefaultInfo(CharacterClass);
// ASC가 PrimaryAttributes에 대한 SpecHandle을 생성하고, 객체에 적용시킴
// 캐릭터나 객체가 PrimaryAttributes GameplayEffect를 자신에게 적용하도록 정의
const FGameplaySpecHandle PrimaryAttributesSpecHandle = ASC->MakeOutgoingSpec(ClassDefaultInfo.PrimaryAttributes, Level, ASC->MakeEffectContext());
ASC->ApplyGameplayEffectSpecToSelf(*PrimaryAttributesSpecHandle.Data.Get());
const FGameplaySpecHandle SecondaryAttributesSpecHandle = ASC->MakeOutgoingSpec(CharacterClassInfo->SecondaryAttributes, Level, ASC->MakeEffectContext());
ASC->ApplyGameplayEffectSpecToSelf(*SecondaryAttributesSpecHandle.Data.Get());
const FGameplaySpecHandle VitalAttributesSpecHandle = ASC->MakeOutgoingSpec(CharacterClassInfo->VitalAttributes, Level, ASC->MakeEffectContext());
ASC->ApplyGameplayEffectSpecToSelf(*VitalAttributesSpecHandle.Data.Get());
}
이제 AuraEnemy 클래스에서 Defalut Attribute ( Primary , Secondary , Vital )들을 초기화해야 한다.
그러기 위해서 AuraEnemy 클래스에 CharacterClassInfo 클래스를 추가한다.
추가로 초기화 관련 함수가 부모 클래스인 AuraCharacterBase 클래스에 존재한다.
해당 클래스에서 함수에 virtual 키워드를 추가하여 AuraEnemy 클래스에서 override할 수 있도록 코드를 수정한다.
// AuraCharacterBase.h
...
protected:
...
// DefaultAttribute 초기화 함수
virtual void InitializeDefaultAttributes() const;
...
// AuraEnemy.h
...
#include "AbilitySystem/Data/CharacterClassInfo.h"
...
...
protected:
virtual void BeginPlay() override;
virtual void InitAbilityActorInfo() override;
/** 코드 추가 */
// DefaultAttribute 초기화 함수
virtual void InitializeDefaultAttributes() const override;
/** 코드 추가 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character class Defaults")
int32 Level = 1;
/** 코드 추가 */
// 캐릭터 종류 변수, Warrior로 초기화
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Character class Defaults")
ECharacterClass CharacterClass = ECharacterClass::Warrior;
/** 코드 추가 */
...
// AuraEnemy.cpp
...
#include "AbilitySystem/AuraAbilitySystemLibrary.h"
...
...
void AAuraEnmey::InitializeDefaultAttributes() const
{
UAuraAbilitySystemLibrary::InitializeDefaultAttributes(this, CharacterClass, Level, AbilitySystemComponent);
}
컴파일 후 에디터의 BP_AuraGameMode 에서 CharacterClassInfo : DA_CharacterClassInfo 를 적용시켜준다.

추가
컴파일 후 실행시MMC_MaxHealth클래스에서 오류 발생
SourceObject설정 전에GameplayEffect를 적용시키려고 해서 발생
GamePlayEffect는MMC_MaxHealth클래스를 사용하는데,MMC_MaxHealth클래스는GetSourceObject()를 통해CombatInterface를 가져옴
40번째 라인의 코드에서 이미CombatInterface는 null값을 가지게 되므로 이어서 오류가 발생하게 됨.
CombatInterface를 얻기 위해SourceObject를 추가하면 해결됨
- AuraAbilitySystemLibrary.cpp
// AuraAbilitySystemLibrary.cpp ... void UAuraAbilitySystemLibrary::InitializeDefaultAttributes(const UObject* WorldContextObject, ECharacterClass CharacterClass, float Level, UAbilitySystemComponent* ASC) { AAuraGameModeBase* AuraGameMode = Cast<AAuraGameModeBase>(UGameplayStatics::GetGameMode(WorldContextObject)); if(AuraGameMode == nullptr) return; /** 코드 추가 */ // SourceObject 추가를 위함 AActor* AvatarActor = ASC->GetAvatarActor(); /** 코드 추가 */ UCharacterClassInfo* CharacterClassInfo = AuraGameMode->CharacterClassInfo; FCharacterClassDefaultInfo ClassDefaultInfo = CharacterClassInfo->GetClassDefaultInfo(CharacterClass); /** 코드 추가 */ FGameplayEffectContextHandle PrimaryAttributesContextHandle = ASC->MakeEffectContext(); PrimaryAttributesContextHandle.AddSourceObject(AvatarActor); /** 코드 추가 */ /** 코드 수정 */ const FGameplaySpecHandle PrimaryAttributesSpecHandle = ASC->MakeOutgoingSpec(ClassDefaultInfo.PrimaryAttributes, Level, PrimaryAttributesContextHandle); ASC->ApplyGameplayEffectSpecToSelf(*PrimaryAttributesSpecHandle.Data.Get()); /** 코드 수정 */ /** 코드 추가 */ FGameplayEffectContextHandle SecondaryAttributesContextHandle = ASC->MakeEffectContext(); SecondaryAttributesContextHandle.AddSourceObject(AvatarActor); /** 코드 추가 */ /** 코드 수정 */ const FGameplaySpecHandle SecondaryAttributesSpecHandle = ASC->MakeOutgoingSpec(CharacterClassInfo->SecondaryAttributes, Level, SecondaryAttributesContextHandle); ASC->ApplyGameplayEffectSpecToSelf(*SecondaryAttributesSpecHandle.Data.Get()); /** 코드 수정 */ /** 코드 추가 */ FGameplayEffectContextHandle VitalAttributesContextHandle = ASC->MakeEffectContext(); VitalAttributesContextHandle.AddSourceObject(AvatarActor); /** 코드 추가 */ /** 코드 수정 */ const FGameplaySpecHandle VitalAttributesSpecHandle = ASC->MakeOutgoingSpec(CharacterClassInfo->VitalAttributes, Level, VitalAttributesContextHandle); ASC->ApplyGameplayEffectSpecToSelf(*VitalAttributesSpecHandle.Data.Get()); /** 코드 수정 */ }
AuraAbilitySystemLibrary.cpp 에서 디버그포인트를 주고 실행시 값들이 정상적으로 작동하는지 확인가능하다.

GE_PrimaryAttributes_Warrior 적용됨
GE_SecondaryAttributes_Enemy 적용됨
GE_VitalAttributes 적용됨
Modifiers 에 VitalAttributes 에 속하는 Health 와 Mana Attributes 가 있는 것까지 확인가능하다.
Details -> Character Class : Ranger 로 변경하면 Ranger 관련 Attribute가 적용된다.

