📌 Gameplay Ability System
✅Gameplay Ability System이란?
- 고도로 유연한 프레임워크:
- 능력, 상태, 어트리뷰트를 구축하고 복잡한 상호작용과 시각적인 효과를 유연하게 관리할 수 있는 시스템
- 손쉬운 구성과 재사용:
- GAS는 다양한 능력을 쉽게 구성하고 재사용 가능
- 복잡한 능력을 구현 가능
- 뛰어난 확장성과 유연성:
- 확장성과 유연성이 뛰어나 다양한 게임 장르에 맞게 적용 가능
- 멀티플레이 게임에서의 동기화 문제를 최소화 가능
✅Gameplay Ability System을 사용하는 이유는?
- 복잡한 Ability 관리:
- 게임 내 캐릭터가 사용할 액티브, 패시브 능력을 체계적으로 관리
- 활성화 및 비활성화, Cooldown 및 Cost를 쉽게 구현
- 구조화된 Attribute Set:
- 버프나 디버프를 유연하게 적용 가능
- 재구현을 통해 Attribute 값 커스터마이즈
- 체계적이면서 유연한 상호작용:
- 게임 내 액터간의 상호작용을 Gameplay Effect Class를 활용하여 전달
- 성능 최적화:
- 에픽게임즈에서 제작한 프레임워크로, 대규모 게임에서도 안정적인 성능 제공
- 개발 생산성 향상:
- 모듈화 되어 있어, 재사용성과 확장성 좋음
- 복잡한 게임 플레이 매커니즘을 빠르게 프로토타이핑 가능
📌 참고 영상 및 문서
📌 기존 입력 시스템의 한계
전통적인 Input Mapping 방식은 헤더에 InputAction 변수 선언 후, 각 액션에 대한 콜백 함수를 수동으로 바인딩 하고 에디터에서 각각의 액션에 대해 에셋 연결을 하였다.
이는 소규모 프로젝트에는 적합하지만, GAS 기반 대형 프로젝트에는 유지보수에 어려움이 많다. 따라서 GAS의 GameplayTags를 기반으로 입력 시스템을 설계 및 구현해보는 방법에 대해 알아보자.
✅ Gameplay Tag 개념 및 장점
- 계층 구조로 정의 가능(Player.Attack.Melee, Player.Attack.Ranged)
- GAS와 결합 시, 태그 간 우선순위, 상쇄 등 기능 처리에 유리
- 코드에서 직접 Tag를 정의하면 오타 방지 및 유지보수성 향상
📌 GAS 기반 입력 시스템 전체 설계 개요
- Gameplay Tags로 입력을 정의
- InputConfig Data Asset에서 태그 ↔ 입력 액션을 매핑
- Custom Input Component 생성하여 태그 기반 입력 처리
- Character 등에서 InputComponent를 활용해 바인딩
- 에디터에 에셋을 연결하여 마무리
✅ C++에서 Gameplay Tag 선언 및 정의
빈 C++ 클래스 생성
🔹 GameplayTags.h
// GameplayTags.h
#pragma once
#include "NativeGameplayTags.h"
namespace GameplayTags
{
/** Input Tags */
RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InputTag_Move)
RPG_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InputTag_Look)
}
🔹 GameplayTags.cpp
// GameplayTags.cpp
#include "GameplayTags.h"
namespace GameplayTags
{
/** Input Tags */
UE_DEFINE_GAMEPLAY_TAG(InputTag_Move, "InputTag.Move")
UE_DEFINE_GAMEPLAY_TAG(InputTag_Look, "InputTag.Look")
}
Tag 선언 및 정의
🔹 Build.cs에 모듈 추가
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class RPG : ModuleRules
{
public RPG(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine",
"InputCore",
"EnhancedInput",
"GameplayTags" // 태그 모듈 추가
});
PrivateDependencyModuleNames.AddRange(new string[] { });
}
}
"GameplayTags" 모듈 추가
🔎에디터 내에서 Gameplay Tag 확인
✅ Input Config Data Asset 생성
DataAsset을 C++ 클래스로 생성
🔹DataAsset_InputConfig.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h" // FGameplayTag 헤더
#include "DataAsset_InputConfig.generated.h"
class UInputAction;
class UInputMappingContext;
USTRUCT(BlueprintType)
struct FRPGInputActionConfig
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta = (Category = "InputTag"))
FGameplayTag InputTag;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
UInputAction* InputAction;
};
UCLASS()
class RPG_API UDataAsset_InputConfig : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
UInputMappingContext* DefaultMappingContext;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta = (TitleProperty = "InputTag"))
TArray<FRPGInputActionConfig> NativeInputActions;
UInputAction* FindNativeInputActionByTag(const FGameplayTag& InInputTag);
};
🔹 DataAsset_InputConfig.cpp
#include "DataAssets/Input/DataAsset_InputConfig.h"
UInputAction* UDataAsset_InputConfig::FindNativeInputActionByTag(const FGameplayTag& InInputTag)
{
for (const FRPGInputActionConfig& InputActionConfig : NativeInputActions)
{
if (InputActionConfig.InputTag == InInputTag && InputActionConfig.InputAction)
{
return InputActionConfig.InputAction;
}
}
return nullptr;
}
에디터에서 C++에서 만든 DataAsset 클래스 생성
InputTag와 InputAction 매핑
✅ Custom Input Component 생성
Enhanced Input Component를 부모로 하는 새 클래스 생성
🔹 RPGInputComponent.h(cpp 구현 없음)
#pragma once
#include "CoreMinimal.h"
#include "EnhancedInputComponent.h"
#include "DataAssets/Input/DataAsset_InputConfig.h" //InputConfig 헤더 추가
#include "RPGInputComponent.generated.h"
UCLASS()
class RPG_API URPGInputComponent : public UEnhancedInputComponent
{
GENERATED_BODY()
public:
template<class UserObject, typename CallbackFunc>
void BindNativeInputAction(
const UDataAsset_InputConfig* InInputConfig, // Input Action 정의가 담긴 Data Asset
const FGameplayTag& InInputTag, // 바인딩할 Action을 찾기 위한 Tag
ETriggerEvent TriggerEvent, // Pressed, Released 등 언제 바인딩 함수가 호출될지
UserObject* ContextObject, // 함수가 실행될 객체
CallbackFunc Func); // 바인딩할 동작 함수
};
template<class UserObject, typename CallbackFunc>
inline void URPGInputComponent::BindNativeInputAction(
const UDataAsset_InputConfig* InInputConfig,
const FGameplayTag& InInputTag,
ETriggerEvent TriggerEvent,
UserObject* ContextObject,
CallbackFunc Func)
{
checkf(InInputConfig, TEXT("RPGInputComponent: Input config data asset is null, can not proceed with binding"));
if (UInputAction* FoundAction = InInputConfig->FindNativeInputActionByTag(InInputTag))
{
BindAction(FoundAction, TriggerEvent, ContextObject, Func);
}
}
DataAsset_InputConfig에서 Input Action을 GameplayTag 기반으로 찾아 함수에 바인딩
Project Settings에서 Input→ Defualt Classes→ Default Input Component Class 설정을 방금 만들었던 Custom Input Component로 설정
✅ InputAction과 함수 바인딩
🔹 Character.h
#pragma once
#include "CoreMinimal.h"
#include "Characters/RPGBaseCharacter.h"
#include "WarriorHeroCharacter.generated.h"
class UDataAsset_InputConfig;
struct FInputActionValue;
UCLASS()
class RPG_API AWarriorHeroCharacter : public ARPGBaseCharacter
{
GENERATED_BODY()
public:
AWarriorHeroCharacter();
protected:
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
virtual void BeginPlay() override;
private:
#pragma region Components
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera", meta=(AllowPrivateAccess = "true"))
TObjectPtr<class USpringArmComponent> CameraBoom;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera", meta=(AllowPrivateAccess = "true"))
TObjectPtr<class UCameraComponent> FollowCamera;
#pragma endregion
#pragma region Inputs
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "CharacterData", meta = (AllowPrivateAccess = "true"))
UDataAsset_InputConfig* InputConfigDataAsset;
void Input_Move(const FInputActionValue& InputActionValue);
void Input_Look(const FInputActionValue& InputActionValue);
#pragma endregion
};
🔹 Character.cpp
#include "Characters/WarriorHeroCharacter.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "EnhancedInputSubsystems.h"
#include "DataAssets/Input/DataAsset_InputConfig.h"
#include "Components/Input/RPGInputComponent.h"
#include "RPGGameplayTags.h"
/** 생략 */
void AWarriorHeroCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
ULocalPlayer* LocalPlayer = GetController<APlayerController>()->GetLocalPlayer();
check(LocalPlayer);
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(LocalPlayer);
check(Subsystem);
Subsystem->AddMappingContext(InputConfigDataAsset->DefaultMappingContext, 0);
URPGInputComponent* RPGInputComponent = CastChecked<URPGInputComponent>(PlayerInputComponent);
RPGInputComponent->BindNativeInputAction(InputConfigDataAsset, RPGGameplayTags::InputTag_Move, ETriggerEvent::Triggered,this, &AWarriorHeroCharacter::Input_Move);
RPGInputComponent->BindNativeInputAction(InputConfigDataAsset, RPGGameplayTags::InputTag_Look, ETriggerEvent::Triggered,this, &AWarriorHeroCharacter::Input_Look);
}
void AWarriorHeroCharacter::Input_Move(const FInputActionValue& InputActionValue)
{
const FVector2D MovementVector = InputActionValue.Get<FVector2D>();
const FRotator MovementRotation(0.0f, Controller->GetControlRotation().Yaw, 0.0f);
if (MovementVector.Y != 0.0f)
{
const FVector ForwardDirection = MovementRotation.RotateVector(FVector::ForwardVector);
AddMovementInput(ForwardDirection, MovementVector.Y);
}
if (MovementVector.X != 0.0f)
{
const FVector RightDirection = MovementRotation.RotateVector(FVector::RightVector);
AddMovementInput(RightDirection, MovementVector.X);
}
}
void AWarriorHeroCharacter::Input_Look(const FInputActionValue& InputActionValue)
{
const FVector2D LookAxisVector = InputActionValue.Get<FVector2D>();
if (LookAxisVector.X != 0.0f)
{
AddControllerYawInput(LookAxisVector.X);
}
if (LookAxisVector.Y != 0.0f)
{
AddControllerPitchInput(LookAxisVector.Y);
}
}
앞서 만들었던 RPGInputComponent의 InputAction 바인딩 함수인 BindNativeInputAction(...)을 사용해서 실행 함수들(Input_Move, Input_Look)과 바인딩
BP_Character에서 DataAsset_InputConfig 에셋 할당
🔔 결과
이러한 DataAsset + GameplayTag + 커스텀 InputComponent를 통해 입력 시스템을 구현하는 방식은 확장성 및 유지 보수성, 모듈화 측면에서 아래와 같은 장점들이 있는 것 같다.
1. DataAsset_InputConfig을 통해 입력 설정(InputAction, InputMappingContext)을 에셋으로 분리해서 캐릭터 클래스에 불필요한 하드레퍼런스가 없어졌다.
2. GameplayTag를 통해 추상화 되어있어, 추후에 InputAction이 변경되거나 이름이 바뀌어도 GameplayTag만 유지하면 전체 시스템이 그대로 작동된다.
3. 템플릿 기반 커스텀 InputComponent로 다양한 캐릭터 클래스에서 공통 로직을 재사용할 수 있다.
4. GameplayTag 기반으로 나중에 "입력 차단" 이나 "입력 유형별 분기처리" 등도 비교적 간단히 구현이 가능하다.
5. 추후 GAS의 AbilitySystem에서 Tag를 기반으로 Ability 호출이 자연스럽게 연결된다.(GAS 친화적 구조)
'내배캠 > GAS(Gameplay Ability System)' 카테고리의 다른 글
GAS_06_Ability Input Action 바인딩 및 GA(무기장착) 활성화 (0) | 2025.05.06 |
---|---|
GAS_05_플레이어 전용 GameplayAbility (0) | 2025.05.05 |
GAS_04_Combat Component (0) | 2025.05.05 |
GAS_03_무기 스폰 및 캐릭터 초기 능력 설정 (0) | 2025.05.04 |
GAS_02_기본 애니메이션 (0) | 2025.05.02 |