GAS_10_Enemy 클래스 생성

2025. 5. 8. 16:17·내배캠/GAS(Gameplay Ability System)

📌 적 캐릭터 생성

✅ Enemy 관련 클래스 생성

Enemy 관련 (Character, Start Up Data, Gameplay Ability, Combat Component) 클래스를 생성해준다.

 

✅ Enemy 관련 클래스 구현

📍 EnemyCharacter 클래스

더보기

🔹 EnemyCharacter.h

#pragma once

#include "CoreMinimal.h"
#include "Characters/RPGBaseCharacter.h"
#include "RPGEnemyCharacter.generated.h"

class UEnemyCombatComponent;

UCLASS()
class RPG_API ARPGEnemyCharacter : public ARPGBaseCharacter
{
	GENERATED_BODY()
	
public:
	ARPGEnemyCharacter();

protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Combat")
	TObjectPtr<UEnemyCombatComponent> EnemyCombatComponent;

public:
	FORCEINLINE UEnemyCombatComponent* GetEnemyCombatComponent() const { return EnemyCombatComponent; }
};

🔹 EnemyCharacter.cpp

#include "Characters/RPGEnemyCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/Combat/EnemyCombatComponent.h"

ARPGEnemyCharacter::ARPGEnemyCharacter()
{
	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;

	bUseControllerRotationPitch = false;
	bUseControllerRotationRoll = false;
	bUseControllerRotationYaw = false;

	GetCharacterMovement()->bUseControllerDesiredRotation = false;
	GetCharacterMovement()->bOrientRotationToMovement = true;
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 180.0f, 0.0f);
	GetCharacterMovement()->MaxWalkSpeed = 300.0f;
	GetCharacterMovement()->BrakingDecelerationWalking = 1000.0f;

	EnemyCombatComponent = CreateDefaultSubobject<UEnemyCombatComponent>(TEXT("EnemyCombatComponent"));
}

📍 EnemyGameplayAbility  클래스

더보기

🔹 EnemyGameplayAbility.h

#pragma once

#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/RPGGameplayAbility.h"
#include "RPGEnemyGameplayAbility.generated.h"

class ARPGEnemyCharacter;
class UEnemyCombatComponent;

UCLASS()
class RPG_API URPGEnemyGameplayAbility : public URPGGameplayAbility
{
	GENERATED_BODY()
	
public:
	// Enemy 캐릭터 반환
	UFUNCTION(BlueprintPure, Category = "RPG|Ability")
	ARPGEnemyCharacter* GetEnemyCharacterFromActorInfo();

	// Enemy CombatComponent 반환
	UFUNCTION(BlueprintPure, Category = "RPG|Ability")
	UEnemyCombatComponent* GetEnemyCombatComponentFromActorInfo();

private:
	TWeakObjectPtr<ARPGEnemyCharacter> CachedRPGEnemyCharacter;
};

🔹 EnemyGameplayAbility.cpp

#include "AbilitySystem/Abilities/RPGEnemyGameplayAbility.h"
#include "Characters/RPGEnemyCharacter.h"

ARPGEnemyCharacter* URPGEnemyGameplayAbility::GetEnemyCharacterFromActorInfo()
{
	if (!CachedRPGEnemyCharacter.IsValid())
	{
		CachedRPGEnemyCharacter = Cast<ARPGEnemyCharacter>(CurrentActorInfo->AvatarActor);
	}

	return CachedRPGEnemyCharacter.IsValid() ? CachedRPGEnemyCharacter.Get() : nullptr;
}

UEnemyCombatComponent* URPGEnemyGameplayAbility::GetEnemyCombatComponentFromActorInfo()
{
	return GetEnemyCharacterFromActorInfo()->GetEnemyCombatComponent();
}
우선 EnemyCharacter 클래스와 EnemyGameplayAbility 클래스에 기본적인 구현을 진행한다.

 

에디터로 돌아와 "RPGEnemyCharacter" C++ 클래스를 상속 받는 BP클래스를 생성한다.
"BP_EnemyCharacter_Base"라 명명하고, 이 클래스는 모든 Enemy 클래스의 최상위 부모 클래스가 된다.

 

다음과 같은 상속 구조로 Enemy Character BP 클래스를 생성한다.

 

✅ Enemy / ABP 세팅

최하단 자식 클래스인 "BP_Gruntling_Guardian" 클래스에 Mesh와 Collision 설정을 적절히 해준다.

 

Enemy의 애니메이션의 경우 대부분 다 비슷하기 때문에 각각 개별적으로 만들어 주지 않고, Template을 만들어준다.
Template ABP의 경우 스켈레톤을 할당할 필요는 없지만 부모 클래스는 지정해야한다. 이전에 만들어두었던 Character AnimIntacne클래스로 설정한다.
이전에 부모 클래스 Character Anim Instance에 만들어 두었던 Data들을 확인할 수 있다.

 

다음과 같이 Enemy 전용 ABP Template클래스에 기본적인 로직을 구현한다.

 

해당 ABP의 자식 클래스를 생성하고 만들고자 하는 Enemy Skeleton을 지정한다.

 

자식 클래스에서 BlendSpace 1D를 생성해 할당하고, Enemy Character 클래스에 해당 ABP를 할당해준다.

 

📌 Enemy에 Ability 부여

✅ Enemy / StartUpData 세팅

기존에  플레이어 캐릭터도 "DataAsset_HeroStartUpData" 클래스를 상속 받은 "DA_Hero" 클래스를 만들어 Ability들을 저장하고 부여했었다.
Enemy 캐릭터도 마찬가지로 StartUpData를 구성해준다.

 

🔹 DataAsset_EnemyStartUpData.h

#pragma once

#include "CoreMinimal.h"
#include "DataAssets/StartUpData/DataAsset_StartUpDataBase.h"
#include "DataAsset_EnemyStartUpData.generated.h"

class URPGEnemyGameplayAbility;

UCLASS()
class RPG_API UDataAsset_EnemyStartUpData : public UDataAsset_StartUpDataBase
{
	GENERATED_BODY()

public:
	// Enemy 캐릭터의 ASC에 초기 능력을 부여
	virtual void GiveToAbilitySystemComponent(URPGAbilitySystemComponent* InRPG_ASC, int32 ApplyLevel = 1) override;

private:
	UPROPERTY(EditDefaultsOnly, Category = "StartUpData")
	TArray<TSubclassOf<URPGEnemyGameplayAbility>> EnemyCombatAbilities;
};

 

Enemy의 경우 Input이 따로 없기 때문에, 바로 TArray<TSubclassOf<URPGGameplayAbility>> 타입으로 배열을 선언해주었다. 

🔹 DataAsset_EnemyStartUpData.cpp

#include "DataAssets/StartUpData/DataAsset_EnemyStartUpData.h"
#include "AbilitySystem/RPGAbilitySystemComponent.h"
#include "AbilitySystem/Abilities/RPGEnemyGameplayAbility.h"

void UDataAsset_EnemyStartUpData::GiveToAbilitySystemComponent(URPGAbilitySystemComponent* InRPG_ASC, int32 ApplyLevel)
{
	Super::GiveToAbilitySystemComponent(InRPG_ASC, ApplyLevel);

	if (!EnemyCombatAbilities.IsEmpty())
	{
		for (const TSubclassOf<URPGEnemyGameplayAbility>& AbilityClass : EnemyCombatAbilities)
		{
			if (!AbilityClass) continue;

			FGameplayAbilitySpec AbilitySpec(AbilityClass);
			AbilitySpec.SourceObject = InRPG_ASC->GetAvatarActor();
			AbilitySpec.Level = ApplyLevel;

			InRPG_ASC->GiveAbility(AbilitySpec);
		}
	}
}

 

에디터에서 해당 클래스를 상속 받은 "DA_Guardian" 클래스를 생성해준다.
내부를 보면 Enemy Combat Abilities 배열이 추가되어있는 것을 확인할 수 있다.

 

✅ Enemy / StartUpData 로딩

이전에 플레이어 캐릭터는 PossessedBy 시점에, 데이터를 동기 로딩하였었다.
현재 이 프로젝트는 싱글 플레이로, 플레이어 캐릭터 인스턴스가 1개임이 확정적이기 때문에 즉시 사용가능하도록 하기 위함이었다.
하지만 AI의 경우 어느정도의 인스턴스가 생성될지 모르고, 대량의 로딩이 한꺼번에 이루어질 경우 hitch(게임이 멈추고나 끊김 현상)가 발생할 수 있어 비동기 로딩으로 진행하는 것이 안정적이다.
따라서 특정 시점에 BackGround에서 병렬적으로 로딩할 수 있도록 한다.

🔹 RPGEnemyCharacter.h

#pragma once

#include "CoreMinimal.h"
#include "Characters/RPGBaseCharacter.h"
#include "RPGEnemyCharacter.generated.h"

class UEnemyCombatComponent;

UCLASS()
class RPG_API ARPGEnemyCharacter : public ARPGBaseCharacter
{
	GENERATED_BODY()
	
public:
	ARPGEnemyCharacter();

protected:
	//~ Begin APawn Interface
	virtual void PossessedBy(AController* NewController) override;
	//~ End APawn Interface

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Combat")
	TObjectPtr<UEnemyCombatComponent> EnemyCombatComponent;
	
private:
	void InitEnemyStartUpData();

public:
	FORCEINLINE UEnemyCombatComponent* GetEnemyCombatComponent() const { return EnemyCombatComponent; }
};

🔹 RPGEnemyCharacter.cpp

#include "Characters/RPGEnemyCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/Combat/EnemyCombatComponent.h"
#include "Engine/AssetManager.h"
#include "DataAssets/StartUpData/DataAsset_EnemyStartUpData.h"

#include "WarriorDebugHelper.h"

ARPGEnemyCharacter::ARPGEnemyCharacter()
{
	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;

	bUseControllerRotationPitch = false;
	bUseControllerRotationRoll = false;
	bUseControllerRotationYaw = false;

	GetCharacterMovement()->bUseControllerDesiredRotation = false;
	GetCharacterMovement()->bOrientRotationToMovement = true;
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 180.0f, 0.0f);
	GetCharacterMovement()->MaxWalkSpeed = 300.0f;
	GetCharacterMovement()->BrakingDecelerationWalking = 1000.0f;

	EnemyCombatComponent = CreateDefaultSubobject<UEnemyCombatComponent>(TEXT("EnemyCombatComponent"));
}

void ARPGEnemyCharacter::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);

	InitEnemyStartUpData();
}

void ARPGEnemyCharacter::InitEnemyStartUpData()
{
	if (CharacterStartUpData.IsNull()) return;

	UAssetManager::GetStreamableManager().RequestAsyncLoad( // AssetManager를 통해 비동기 로딩 요청
		CharacterStartUpData.ToSoftObjectPath(),			// 불러올 자산의 경로
		FStreamableDelegate::CreateLambda(					// 로딩 완료 시 실행할 콜백 함수
			[this]()
			{
				if (UDataAsset_StartUpDataBase* LoadedData = CharacterStartUpData.Get())
				{
					LoadedData->GiveToAbilitySystemComponent(RPGAbilitySystemComponent);

					Debug::Print(TEXT("Enemy Start up Data Loaded"), FColor::Green);
				}
			}
		)
	);
}

 

컴파일 후, 에디터로 돌아와서 Enemy Character 클래스에 StartUpData를 할당해주었다.
플레이 했을 때 정상적으로 LoadData가 연결된 것을 확인할 수 있었다.

다만 현재 Possessed By 시점에 비동기 로딩을 하게끔 구현하였는데, Editor 환경에서 월드에 미리 배치 되어있는 적의 경우 StartUpData가 미리 로딩되어있을 수 있어서 정상적으로 비동기 로딩이 되었을 수 있다.
캐릭터 클래스에서 Soft Reference로 StartUpData를 가지고 있기 때문에 런타임에 Spawn된 적의 경우나 패키징된 상황에서 Possessed By 시점에 Data가 메모리에 올라와 있지 않아, 정상적으로 Ability가 등록 되지 않을 경우가 발생할 수 있다.

따라서 테스트를 해보고 이상이 있다면, BeginPlay에서 ㅁ

 

✅ Enemy / StartUpData 데이터 할당

✔️ Enemy Weapon Tag 추가

더보기

🔹 RPGGameplayTags.h

#pragma once

#include "NativeGameplayTags.h"

namespace RPGGameplayTags
{
	/** Input Tags */
	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InputTag_Move);
	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InputTag_Look);
	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InputTag_EquipAxe);
	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InputTag_UnequipAxe);
	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InputTag_LightAttack_Axe);
	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InputTag_HeavyAttack_Axe);

	/** Player Tags */
	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Player_Ability_Equip_Axe);
	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Player_Ability_Unequip_Axe);

	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Player_Ability_Attack_Light_Axe);
	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Player_Ability_Attack_Heavy_Axe);

	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Player_Weapon_Axe);

	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Player_Event_Equip_Axe);
	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Player_Event_Unequip_Axe);

	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Player_Status_JumpToFinisher);

	/** Enemy Tags */
	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Enemy_Weapon); // Enemy 무기 태그 추가
}

🔹 RPGGameplayTags.CPP

#include "RPGGameplayTags.h"

namespace RPGGameplayTags
{
	/** Input Tags */
	UE_DEFINE_GAMEPLAY_TAG(InputTag_Move, "InputTag.Move");
	UE_DEFINE_GAMEPLAY_TAG(InputTag_Look, "InputTag.Look");
	UE_DEFINE_GAMEPLAY_TAG(InputTag_EquipAxe, "InputTag.EquipAxe");
	UE_DEFINE_GAMEPLAY_TAG(InputTag_UnequipAxe, "InputTag.UnequipAxe");
	UE_DEFINE_GAMEPLAY_TAG(InputTag_LightAttack_Axe, "InputTag.LightAttack.Axe");
	UE_DEFINE_GAMEPLAY_TAG(InputTag_HeavyAttack_Axe, "InputTag.HeavyAttack.Axe");

	/** Player Tags */
	UE_DEFINE_GAMEPLAY_TAG(Player_Ability_Equip_Axe, "Player.Ability.Equip.Axe");
	UE_DEFINE_GAMEPLAY_TAG(Player_Ability_Unequip_Axe, "Player.Ability.Unequip.Axe");

	UE_DEFINE_GAMEPLAY_TAG(Player_Ability_Attack_Light_Axe, "Player.Ability.Attack.Light.Axe");
	UE_DEFINE_GAMEPLAY_TAG(Player_Ability_Attack_Heavy_Axe, "Player.Ability.Attack.Heavy.Axe");

	UE_DEFINE_GAMEPLAY_TAG(Player_Weapon_Axe, "Player.Weapon.Axe");

	UE_DEFINE_GAMEPLAY_TAG(Player_Event_Equip_Axe, "Player.Event.Equip.Axe");
	UE_DEFINE_GAMEPLAY_TAG(Player_Event_Unequip_Axe, "Player.Event.Unequip.Axe");

	UE_DEFINE_GAMEPLAY_TAG(Player_Status_JumpToFinisher, "Player.Status.JumpToFinisher");

	/** Enemy Tags */
	UE_DEFINE_GAMEPLAY_TAG(Enemy_Weapon, "Enemy.Weapon"); // Enemy 무기 태그 추가

}

 

✔️ Enemy 무기 클래스 생성

 

"RPGWeaponBase"를 상속 받아 "BP_Enemy_Weapon_Base" 클래스를 생성한 뒤, 이를 상속 받는 가디언 전용 무기 클래스를 생성한다.

 

✔️ Skeletal Mesh에서 Weapon Socket 생성

 

✔️ 무기 스폰 Ability 생성 및 데이터 할당

이전에 캐릭터 스폰 시, 무기를 스폰해 지정한 Mesh Socket에 부착하는 GameplayAbility를 만들었었다.
이를 상속받는 "GA_Guardian_SpawnWeapon" 어빌리티를 생성한다.
앞서 만들었던 무기 클래스, Socket Name, Weapon Tag, 장착 무기로 등록(true) 설정을 진행한다.

 

✔️ 캐릭터가 가지고 있는 StartUpData에 무기 스폰 Ability 등록

캐릭터 클래스에 할당되어있는 "DA_Guardian"을 더블클릭 후, 방금 만든 무기 스폰 Ability를 할당해준다.

 

🔔 Enemy 무기 스폰 Ability 동작 테스트

게임을 시작하면, 성공적으로 Enemy의 StartUpData가 비동기 로딩 되고, 내부 데이터에 GameAbility가 등록되며 자동으로 실행되어 무기가 정상적으로 스폰되었다.

'내배캠 > GAS(Gameplay Ability System)' 카테고리의 다른 글

GAS_12_피격 판정  (0) 2025.05.09
GAS_11_Attribute와 GameplayEffect  (0) 2025.05.08
GAS_09_무기 공격 애니메이션 구현  (0) 2025.05.07
GAS_08_무장 상태에 따른 Ability + IMC 설계  (0) 2025.05.07
GAS_07_무장 상태 애니메이션 전환 시스템 설계  (1) 2025.05.06
'내배캠/GAS(Gameplay Ability System)' 카테고리의 다른 글
  • GAS_12_피격 판정
  • GAS_11_Attribute와 GameplayEffect
  • GAS_09_무기 공격 애니메이션 구현
  • GAS_08_무장 상태에 따른 Ability + IMC 설계
동그래님
동그래님
  • 동그래님
    개발자 동그래
    동그래님
  • 전체
    오늘
    어제
    • 분류 전체보기 (210)
      • 공부 (51)
        • Code Cata (50)
      • 내배캠 (151)
        • TIL (50)
        • C++ (37)
        • Unreal Engine (48)
        • GAS(Gameplay Ability System.. (16)
      • Project (7)
        • Gunfire Paragon (5)
        • Arena Fighters (1)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.3
    동그래님
    GAS_10_Enemy 클래스 생성
    상단으로

    티스토리툴바