내배캠/GAS(Gameplay Ability System)

GAS_09_무기 공격 애니메이션 구현

동그래님 2025. 5. 7. 20:44

📌 Light Attack 콤보 구현

도끼 장착시 강공격/약공격 어빌리티를 추가하여 콤보 시스템 구현을 준비한다.

1. InputTag / AbilityTag 설정
2. GA_LightAttack 능력 생성 및 Tag Setting
3. InputAction 생성하고 IMC_Axe에 바인딩
4. DA_InputConfig에 InputAction과 Tag 바인딩
5. 무기 클래스에 GA와 InputTag 바인딩
6. 무기 장착 / 해제 Ability에 Attack Tag 블로킹 처리

✅ Light Attack_Input 및 Ability 세팅

🔹 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); // 공격 Input 추가
	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InputTag_HeavyAttack_Axe); // 공격 Input 추가

	/** 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); // 공격 Ability 추가
	RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Player_Ability_Attack_Heavy_Axe); // 공격 Ability 추가

	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);
}

🔹 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"); // 공격 Input 추가
	UE_DEFINE_GAMEPLAY_TAG(InputTag_HeavyAttack_Axe, "InputTag.HeavyAttack.Axe"); // 공격 Input 추가

	/** 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"); // 공격 Ability 추가
	UE_DEFINE_GAMEPLAY_TAG(Player_Ability_Attack_Heavy_Axe, "Player.Ability.Attack.Heavy.Axe"); // 공격 Ability 추가

	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");
}

 

 

WarriorHeroGameplayAbility를 상속 받는 "GA_Hero_LightAttackMaster" 이름으로 약공격 Ability 부모클래스를 만들고, 이를 상속 받는 "GA_Hero_LightAttack_Axe" 클래스를 생성해주었다.
LightAttackMaster에 Tag Setting 진행.

 

InputAction_LightAttackAxe를 생성하고, Axe 무기 전용 IMC에 바인딩

 

InputConfig에 "Light Attack" 관련 InputTag - InputAction 바인딩

 

무기 클래스에 InputTag와 GA 바인딩

 

"GA_Hero_EquipAxe", "GA_Hero_UnequipAxe" Ability 클래스에서 장비 착용 및 장비 해제 Ability가 활성화 되는 동안 공격을 하지 못하도록 Block 처리 해주었다.

 

✅ Combo Attack 로직

✔️ Instancing Policy

본격적으로 콤보 로직을 구현하기  GameAbility의 Instancing Policy(인스턴스화 정책)에 대해 알고 넘어가야 한다.
콤보 시스템에서 애니메이션을 독립적으로 만들려면 GA에 Instancing Policy 설정을 해줘야한다.
  • Instanced Per Execution:
    • 어빌리티 실행 시, 항상 새 인스턴스 생성
    • 변수는 항상 기본값으로 초기화됨
    • 재사용 안 함 → 메모리/성능 손해
    • Fire / Dash / 짧은 효과 등 매번 초기화가 필요한 Ability에 적합
  • Instanced Per Actor:
    • 어빌리티 인스턴스를 처음 1회만 생성
    • 이후 계속 재사용
    • 내부 상태 저장 가능
    • Combo / Charge / 지속 효과 등 상태 저장이 필요한 Ability에 적합
  • Non Instanced:
    • 인스턴스 생성 X
    • 로직은 전부 C++에서만 구현 가능
    • Blueprint에서는 변수 저장 불가
    • 성능이 제일 좋지만 유연성은 적음
    • 단순 / 빠름/ 최적화 우선이며 Blueprint 사용하지 않아도 될 때 적합

현재 콤보 어택을 구현하고 있는 LightAttack 부모 클래스에 Instancing Policy를 "Instanced Per Actor"로 설정 해준다.
이유는 내부에 Combo Attack Count라는 int 변수를 두고 공격을 실행 했을 때마다 Count 값을 증가시키고 현재 Count 값에 맞는 Anim Montage를 재생시켜야하기 때문이다.

 

"GA_Hero_LightAttackMontage"에 콤보 어택 로직을 다음과 같이 작성한다.
TMap을 사용해서 각 인덱스마다 Anim Montage를 저장해놓고 현재 Combo Count에 맞게 애니메이션을 재생시켜준다.

 

Combo 로직을 구현한 "GA_Hero_LightAttackMater" 클래스를 상속 받은 "GA_Hero_LightAttack_Axe"클래스에서 4개의 애니메이션을 저장할 준비를 한다.

 

이전에 장비를 장착/탈착 했던 애니메이션들은 이동하면서 해당 동작이 가능했어야 해서 UpperBody 슬롯에서만 재생되도록 구현하다보니 Root Motion을 비활성화 했어야 했다.
하지만 현재는 하체까지 공격하는 애니메이션이 적용되도록 할 것이기 때문에, 4가지 애니메이션 모두 Root Motion 활성화를 체크하도록 한다.

 

4개의 애니메이션 몽타주를 생성하고, 콤보 어택에 사용되기에 재생속도가 느려서 Rate Scale을 2.0으로 빠르게 설정해주었고, "FullBody"slot을 생성하여 적용시켜주었다. 
1~3번 애니메이션은 2.0의 속도, 마지막 4번 애니메이션은 1.5로 앞 동작보다는 느리게 해주었다.

 

다시 "GA_Hero_LightAttack"에 돌아와 Map컨테이너에 몽타주들을 바인딩한다.

 

메인 ABP클래스인 "ABP_Hero" AnimGraph에서 방금 생성했던 FullBody Slot을 최종 Output Pose 핀 앞에 연결해준다.

 

🔔 Light 콤보 어택 테스트

마우스 클릭을 천천히 하면 Montage_1만 나오는 모습을 볼 수 있고, 연속적으로 빠르게 누르면 Montage_1~4번까지 정상적으로 잘 나오는 모습을 확인할 수 있었다.

 

📌  Heavy Attack 구현

Light Attack을 구현하면서 Heavy Attack에 대한 GameplayTag도 선언하였었다.
앞서 Light Attack Ability를 인풋을 등록하고 캐릭터에 능력을 부여하는 것까지의 과정을 똑같이 Heavy Attack Ability 빠르게 적용시킨다.

1. GameplayAbility 생성 후 Tag Setting
2. InputAction 생성 후 IMC에 바인딩
3. DA_InputConfig에 InputTag - InputAction 바인딩
4. Weapon Class에 InputTag - GA 바인딩

 

✅ Heavy Attack Ability 생성 및 등록

GameplayAbility 생성 후 Tag Setting

 

InputAction 생성 후 IMC에 바인딩

 

DA_InputConfig에 InputTag - InputAction 바인딩

 

Weapon Class에 InputTag - GA 바인딩

 

✅ Heavy Attack Ability 기능 구현

앞서 Light Attack Ability 로직도 연속적으로 공격을 입력하면 Combo Attack이 나오도록 구현하였다.
Heavy Attack Ability도 마찬가지로 연속적으로 빠르게 입력을 하면 Combo Attack이 되도록 구현하도록 한다.

 

"GA_Hero_HeavyAttackMaster" 부모 클래스에 해당 로직을 작성한다. 로직은 LightAttack과 동일하다.

"AttackMontagesMap" - Integer를 Key로, AnimMonstage Object Class를 Value로 가지고 있다.
"HeavyAttackComboCount" - Default Value로 1을 가지고 있고 연타하면 최대 Map의 크기 만큼 1씩 증가한다.
"ComboCountResetTimerHandle" - 0.3초 이내에 추가 입력이 들어오지 않으면 ComboCount를 Reset 시켜주는데, 이때 사용한 TimerHandle 캐싱

 

Heavy Attack 부모 클래스에선 Asset Tag를 "Player.Ability.Attack.Heavy" 까지만 설정하고 자식 클래스에서 세부 무기까지 포함해서 "Player.Ability.Attack.Heavy.Axe"로 설정할 것이다.
Combo Attack Count 변수가 계속 지속적으로 관리되어야 하기 때문에, "Instanced Per Actor" 정책으로 설정해주었다.
해당 설정들은 "GA_Hero_LightAttackMaster" 클래스와 동일하다.

 

"GA_Hero_HeavyAttackMaster"클래스를 상속받은 Ability 클래스에서 Map 컨테이너에 해당하는 몽타주를 넣어준다.
앞서 말했듯이 자식클래스에선 AssetTag를 Axe까지 붙혀주었고 Instancing Policy도 부모와 동일한지 확인해준다.

 

🔔 Light / Heavy 콤보 어택 테스트

무기를 장착하고, Light Attack(1~4) 그리고 Heavy Attack(1~2) 몽타주가 정상적으로 재생되는것을 확인할 수있다.

 

📌  Light → Heavy(마지막 공격)으로 전환하는 Combo Attack 구현

현재 약공격은 4단계로 이루어져있고, 강공격은 2단계로 이루어져 있다.
약공격 3단계까지 수행한 후에 바로 강공격 2단계로 넘어갈 수 있도록 해서, 플레이어가 다른 공격 콤보 패턴을 사용할 수 있도록 해보려한다.

이를 위해선 LightAttack의 ComboCnt가 3단계 이후 라면 "JumpToFinisher"라는 GameplayTag를 캐릭터에 부여하고,
HeavyAttack Ability는 해당 태그가 존재한다면 현재 ComboCnt를 무시하고 즉시 HeavyAttack 2단계인 피니셔 동작을 실행할 수 있어야한다.
이후 피니셔 동작이 끝나면 태그를 제거하고 ComboCnt를 초기화 하도록 한다.

 

✅ GAS 기반 시스템 전용 Function Library 구현

GAS 시스템을 사용하다보면 ASC를 여러 클래스(Character, Ability 등)에서 가져와야 하는 경우가 있다.
그리고 현재 구현하는 상황처럼, 특정 Ability 활성화 중에 Tag를 부여하거나 제거해야할 일이 많이 생길 것이다.

이를 위해 코드 재사용성 증가와 로직 통일성, 블루프린트 로직 간결화 등의 이점을 가져가기 위해 Blueprint Function Library 클래스를 만들어 자주 사용되는 기능들을 구현할 것이다.

"Blueprint Function Library"를 상속 받는 C++클래스를 생성한다.

 

🔹 RPGFunctionLibrary.h

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "GameplayTagContainer.h"
#include "RPGFunctionLibrary.generated.h"

class URPGAbilitySystemComponent;

UENUM()
enum class ERPGConfirmType : uint8
{
	Yes,
	No
};

UCLASS()
class RPG_API URPGFunctionLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()
	
public:
	// Actor로부터 ASC 반환 함수
	static URPGAbilitySystemComponent* NativeGetRPGASC_FormActor(AActor* InActor);

	// ASC가 특정 Tag를 가지고 있는지 확인
	static bool NativeDoesActorHaveTag(AActor* InActor, FGameplayTag TagToCheck);

	// Actor에 Tag 추가
	UFUNCTION(BlueprintCallable, Category = "RPG|FunctionLibrary")
	static void AddGameplayTagToActorIfNone(AActor* InActor, FGameplayTag TagToAdd);

	// Actor에 Tag 제거
	UFUNCTION(BlueprintCallable, Category = "RPG|FunctionLibrary")
	static void RemoveGameplayTagFromActorIfFound(AActor* InActor, FGameplayTag TagToRemove);

	// Native 함수 사용해서 BP에서 Tag 가지고 있는지 여부를 Enum으로 반환(Branch 노드 사용 줄이기 위함)
	UFUNCTION(BlueprintCallable, Category = "RPG|FunctionLibrary", meta = (DisplayName = "Does Actor Have Tag", ExpandEnumAsExecs = "OutConfirmType"))
	static void BP_DoesActorHaveTag(AActor* InActor, FGameplayTag TagToCheck, ERPGConfirmType& OutConfirmType);

};
C++ 클래스 내부에서만 사용되는 함수 네이밍은 접두어로 Native로 구분해주었다.
"BP_DoesActorHaveTag" 함수의 경우 Enum을 BP에서 분기 OutPin으로 설정하여 Branch 노드를 사용하지 않아도 되어 블루프린트 로직을 간결하게 한다.

🔹 RPGFunctionLibrary.cpp

#include "RPGFunctionLibrary.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystem/RPGAbilitySystemComponent.h"

URPGAbilitySystemComponent* URPGFunctionLibrary::NativeGetRPGASC_FormActor(AActor* InActor)
{
	check(InActor);
	
	return CastChecked<URPGAbilitySystemComponent>(UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(InActor));
}

bool URPGFunctionLibrary::NativeDoesActorHaveTag(AActor* InActor, FGameplayTag TagToCheck)
{
	URPGAbilitySystemComponent* ASC = NativeGetRPGASC_FormActor(InActor);

	return ASC->HasMatchingGameplayTag(TagToCheck);
}

void URPGFunctionLibrary::AddGameplayTagToActorIfNone(AActor* InActor, FGameplayTag TagToAdd)
{
	URPGAbilitySystemComponent* ASC = NativeGetRPGASC_FormActor(InActor);

	if (!ASC->HasMatchingGameplayTag(TagToAdd))
	{
		ASC->AddLooseGameplayTag(TagToAdd);
	}
}

void URPGFunctionLibrary::RemoveGameplayTagFromActorIfFound(AActor* InActor, FGameplayTag TagToRemove)
{
	URPGAbilitySystemComponent* ASC = NativeGetRPGASC_FormActor(InActor);

	if (ASC->HasMatchingGameplayTag(TagToRemove))
	{
		ASC->RemoveLooseGameplayTag(TagToRemove);
	}
}

void URPGFunctionLibrary::BP_DoesActorHaveTag(AActor* InActor, FGameplayTag TagToCheck, ERPGConfirmType& OutConfirmType)
{
	OutConfirmType = NativeDoesActorHaveTag(InActor, TagToCheck) ? ERPGConfirmType::Yes : ERPGConfirmType::No;
}

 

앞서 만든 함수들을 블루프린트 전역에서 사용할 수 있게 되었다.

 

✅ GAS 기반 시스템 전용 Function Library 구현

✔️ Gameplay 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); // 마지막 공격 전환 Tag 추가
}

🔹 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"); // 마지막 공격 전환 Tag 추가

}

 

🔹 GA_Hero_LightAttackMaster

Light Attack Ability 로직을 다음과 같이 수정한다.

1. 현재 Combo Count가 마지막 전이라면(총 4개의 공격 콤보 중 3번째 공격까지 한 상황), 라이브러리 함수를 사용해서 Character에 Tag를 부여한다.
2. Combo Count가 Reset 될 때, 부여했던 Tag를 삭제한다.

 

🔹 GA_Hero_HeavyAttackMaster

1. Heavy Attack Ability가 활성화 되었을 때, 현재 Character에 마지막 공격으로 전환 Tag가 있는지 확인한다.
2. 만약 태그가 있다면 현재 Combo Count를 마지막 단계로 설정하고 해당 몽타주를 재생한다.
3. 태그가 없다면 현재 Combo Count에 맞는 몽타주를 재생한다.
4. Combo Count를 초기화 하고 이때도 마찬가지로 마지막 공격 전환 Tag를 캐릭터에게서 제거한다.

 

🔔 Jump To Finisher 공격 콤보 테스트

좌(Light_1~3 & Heavy_2) / 우(Heavy_1~2)

Light Attack_1~3 이후 바로 Heavy Attack을 시전하면 바로 Heavy Attack의 마무리 동작인 위에서 내려찍는 애니메이션이 정상적으로 재생된다.

 

📌  Attack 몽타주에 효과 주기

✅ 슬로우 모션 효과

에디터에서 "AnimNotifyState"를 상속받는 블루프린트 클래스를 생성해준다.

 

좌측 상단에서 두 개의 함수를 Override 해준다.

 

Begin 함수에서 MeshComponent에 Dilation 설정을 해준다.
TimeDilation을 변수로 승격시키고, Default 값을 0.2로 준다.

 

End 함수에서 Time Dilation을 1.0으로 다시 기본 값으로 설정해준다.

 

Light / Heavy Attack의 마지막 콤보 애니메이션에 방금 만든 AnimNotifyState_SlowMotion을 적절한 위치에 배치해준다.

 

🔔 슬로우 모션 적용 테스트

Light / Heavy Attack 모두, 마지막 공격에 슬로우 모션이 걸리면서 강하게 힘을 모아 공격하는 듯한 연출이 가능해졌다.