9. RPG Character Classes

groot616·2024년 7월 23일

9. RPG Character Classes

목차

  1. 요약
  2. CharacterClassInfo 생성
  3. 적이 사용할 Attribute 생성
  4. Attribute Modifier 추가
  5. Enemy Attributes 초기화

9.1 요약

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

9.2 CharacterClassInfo 생성

먼저 DataAsset 클래스 기반의 CharacterClassInfo 클래스를 생성하고 코드를 작성한다.

  • CharacterClassInfo.h
// 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
// CharacterClassInfo.cpp

...

FCharacterClassDefaultInfo UCharacterClassInfo::GetClassDefaultInfo(ECharacterClass CharacterClass)
{
	return CharacterClassInformation.FindChecked(CharacterClass);
}

에디터로 돌아와서 CharacterClassInfo 기반 DataAsset 블루프린트인 DA_CharacterClassInfo 를 생성한다.

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

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

9.3 적이 사용할 Attribute 생성

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

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

SecondaryAttributesVitalAttributes 는 기존에 사용하던 GE_AuraSecondaryAttributesGE_AuraVitalAttributes 를 사용할 것이므로 헷갈리지 않도록 파일명을 GE_SecondaryAttributesGE_VitalAttributes 로 변경하고 DefaultAttributes 폴더로 이동시킨다.

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

9.4 Attribute Modifier 추가

9.4.1 CurveTable 추가

먼저 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.Intelligence
    동일하게 키를 추가한 후 Auto 를 클릭하여 그래프를 스무스하게 변경한다.
    ( x : 1 , y : 15 ) , ( x : 5 , y : 19 ) , ( x : 10 , y : 21 ) , ( x : 20 , y : 25 ) , ( x : 40 , y : 40 )
  • Attributes.Primary.Resilience
    동일하게 키를 추가한 후 Auto 를 클릭하여 그래프를 스무스하게 변경한다.
    ( x : 1 , y : 11 ) , ( x : 40 , y : 20 )
  • Attributes.Primary.Vigor
    동일하게 키를 추가한 후 Auto 를 클릭하여 그래프를 스무스하게 변경한다.
    ( x : 1 , y : 7 ) , ( x : 40 , y : 14 )

GE_PrimaryAttributes_Elementalist 로 돌아와서 4개의 Attributes 에 대해 Modifiers 를 추가해준다.

  • AuraAttributeSet.Strength
  • AuraAttributeSet.Intelligence
  • AuraAttributeSet.Resilience
  • AuraAttributeSet.Vigor

9.4.2 CSV를 이용한 CurveTable 데이터 추출(Cubic Curve 적용안됨)

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

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

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

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

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

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

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

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

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

9.4.3 JSON을 이용한 CurveTable 데이터 추출(Cubic Curve 적용됨)

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

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

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

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

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

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

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

9.4.4 나머지 캐릭터에 대해 GE파일 Modifier 적용

  • GE_PrimaryAttributes_Ranger
    • AuraAttributes.Strength
    • AuraAttributes.Intelligence
    • AuraAttributes.Resilience
    • AuraAttributes.Vigor
  • `GE_PrimaryAttributes_W
    • AuraAttributes.Strength
    • AuraAttributes.Intellegence
    • AuraAttributes.Resilience
    • AuraAttributes.Vigor

추가
현재 적용중인 SecondaryAttributes 는 캐릭터가 레벨업을 할 때마다 그에 따라Attribute 수치를 변경하기 위해 InfiniteGameplayEffecct 로 설정해두었다.
하지만 적의 경우 그럴 필요가 없으므로 기존의 GE_SecondaryAttributes 를 복사하여 GE_SecondaryAttributes_Enemy 로 rename해준 다음

Ge_SecnodaryAttributes_EnemyDuration Policy : Instant 로 변경한다.

DA_CharacterClassInfo 로 가서 GE_SecondaryAttributes_Enemy 가 적용되도록 변경한다.

기존의 GE_SecondaryAttributesAura 전용이므로 해당 폴더로 이동시킨다.

9.5 Enemy Attributes 초기화

DataAsset 적이 어떤 Attribute 값을 가져야 할지와 같은 룰을 포함한다.
룰 관련은 GameMode와 관련있으므로 AuraGameplayModeBase 에 코드를 추가한다.

  • AuraGameplayModeBase.h
// 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
// 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
// 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
// AuraCharacterBase.h

...
protected:
	...
    // DefaultAttribute 초기화 함수
    virtual void InitializeDefaultAttributes() const;
    
    ...
  • AuraEnemy.h
// 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
// 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 를 적용시키려고 해서 발생
GamePlayEffectMMC_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 에서 디버그포인트를 주고 실행시 값들이 정상적으로 작동하는지 확인가능하다.

  • Warrior로 설정해둔 것 적용됨
  • GE_PrimaryAttributes_Warrior 적용됨
  • GE_SecondaryAttributes_Enemy 적용됨
  • GE_VitalAttributes 적용됨

    ModifiersVitalAttributes 에 속하는 HealthMana Attributes 가 있는 것까지 확인가능하다.

    뷰포트에 있는 새총을 든 적을 Details -> Character Class : Ranger 로 변경하면 Ranger 관련 Attribute가 적용된다.

0개의 댓글