
GAS란 무엇인가
GAS는 게임 내 캐릭터의 능력을 관리하는 프레임워크다. 다음과 같은 기능들을 구현할 수 있다:
- 파이어볼을 쏘는 스킬
- 체력 회복 포션 사용
- 독 데미지를 받는 디버프
- 이동속도 증가 버프
이러한 기능들을 체계적으로 관리하는 것이 가능하다.
GAS의 4대 핵심 구성요소
1. Ability System Component (ASC)
모든 것을 관리하는 중앙 관리자다. 캐릭터에 하나씩 부착하여 사용한다.
2. Gameplay Ability
실제 스킬이나 행동을 정의한다. "파이어볼 쏘기", "점프하기" 등이 이에 해당한다.
3. Gameplay Effect
능력이 만들어내는 효과다. "데미지 10", "5초간 이동속도 20% 증가" 등을 정의한다.
4. Attribute Set
캐릭터의 스탯을 정의한다. 체력, 마나, 공격력 등이 여기에 포함된다.
단계별 구현 가이드
Step 1: 프로젝트 설정
Build.cs 파일에 GAS 모듈을 추가해야 한다.
// YourProject.Build.cs
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine",
"GameplayAbilities", // 이것을 추가
"GameplayTags", // 이것을 추가
"GameplayTasks" // 이것을 추가
});
Step 2: 속성(Attribute) 만들기
캐릭터의 스탯을 정의한다. 가장 기본적인 체력과 마나를 구현한다.
// MyAttributeSet.h
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "MyAttributeSet.generated.h"
// 매크로를 사용하면 Get, Set 함수를 자동으로 생성한다
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \\
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \\
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \\
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \\
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
UCLASS()
class MYPROJECT_API UMyAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UMyAttributeSet();
// 현재 체력
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Health)
// 최대 체력
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxHealth)
// 현재 마나
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData Mana;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Mana)
// 최대 마나
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData MaxMana;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxMana)
};
// MyAttributeSet.cpp
#include "MyAttributeSet.h"
UMyAttributeSet::UMyAttributeSet()
{
// 초기값 설정
InitHealth(100.0f);
InitMaxHealth(100.0f);
InitMana(50.0f);
InitMaxMana(50.0f);
}
1. 매크로의 목적
ATTRIBUTE_ACCESSORS 매크로는 언리얼 엔진의 Gameplay Ability System(GAS)에서
ATTRIBUTE_ACCESSORS 매크로는 속성(Attribute)에 대한 접근자 함수들을 자동으로 생성하기 위해 만들어짐.
이 매크로 없이 모든 속성마다 접근자 함수를 직접 작성한다면 코드가 매우 길어지고 실수할 가능성이 높아짐.
GAS 속성 시스템의 특징
GAS에서 속성은 일반적인 변수와 다른 특별한 규칙을 가진다.
- 모든 속성은 FGameplayAttributeData 타입으로 저장됨.
- 직접 변수에 접근하지 않고 반드시 접근자 함수를 통해서만 접근해야 함.
- 네트워크 복제, 이벤트 발생 등이 자동으로 처리함.
매크로가 생성하는 4가지 함수
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
예를 들어 Health 속성에 대해 다음과 같은 함수들이 자동으로 생성된다.
1. 속성 정의 가져오기 함수
FGameplayAttribute GetHealthAttribute() const;
- GAS 시스템에서 속성을 참조할 때 사용
- 게임플레이 이펙트나 어빌리티에서 특정 속성을 타겟팅할 때 필요
2. 현재 값 가져오기 함수
float GetHealth() const;
- 현재 속성 값을 읽기 위해 사용
- 가장 자주 사용되는 함수
3. 값 설정하기 함수
void SetHealth(float NewVal);
- 속성 값을 안전하게 변경
- 네트워크 복제와 이벤트 발생이 자동으로 처리됨
4. 값 초기화하기 함수
void InitHealth(float NewVal);
- 속성의 초기값을 설정
- 주로 AttributeSet이 처음 생성될 때 사용.
GAS를 사용해야 하는 이유
1. 일관성 보장
모든 속성에 대해 동일한 접근 패턴을 유지할 수 있다.
2. 안전성 증대
직접 변수에 접근하는 대신 검증된 함수를 사용하여 예상치 못한 오류를 방지함.
3. 네트워킹 자동화
속성 변화가 네트워크로 자동 복제되어 멀티플레이어 게임에서 동기화가 원활함.
4. 이벤트 시스템 통합
값 변경 시 자동으로 이벤트가 발생하여 다른 시스템에서 반응할 수 있음.
5. 코드 간소화
반복적인 접근자 함수 작성을 피할 수 있어 개발 효율성이 크게 향상됨.
주의사항
- 매크로 사용 시 클래스 이름과 속성 이름을 정확히 입력해야 함.
- 속성은 반드시 FGameplayAttributeData 타입이어야 함.
- 네트워크 복제가 필요한 경우 ReplicatedUsing 설정을 잊지 말것.
Step 3: 캐릭터에 ASC 추가하기
캐릭터에 Ability System Component를 추가한다.
// MyAttributeSet.h
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "MyAttributeSet.generated.h"
// 매크로를 사용하면 Get, Set 함수를 자동으로 생성한다
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \\
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \\
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \\
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \\
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
UCLASS()
class MYPROJECT_API UMyAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UMyAttributeSet();
// 현재 체력
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Health)
// 최대 체력
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxHealth)
// 현재 마나
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData Mana;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Mana)
// 최대 마나
UPROPERTY(BlueprintReadOnly, Category = "Attributes")
FGameplayAttributeData MaxMana;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxMana)
};
// MyAttributeSet.cpp
#include "MyAttributeSet.h"
UMyAttributeSet::UMyAttributeSet()
{
// 초기값 설정
InitHealth(100.0f);
InitMaxHealth(100.0f);
InitMana(50.0f);
InitMaxMana(50.0f);
}
Step 3: 캐릭터에 ASC 추가하기
캐릭터에 Ability System Component를 추가한다.
// MyCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AbilitySystemInterface.h" // 인터페이스 필수
#include "MyCharacter.generated.h"
UCLASS()
class MYPROJECT_API AMyCharacter : public ACharacter, public IAbilitySystemInterface
{
GENERATED_BODY()
protected:
// Ability System Component
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Abilities")
class UAbilitySystemComponent* AbilitySystemComponent;
// 속성 세트
UPROPERTY()
class UMyAttributeSet* AttributeSet;
public:
AMyCharacter();
// 인터페이스 구현 (필수)
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
virtual void BeginPlay() override;
};
// MyCharacter.cpp
#include "MyCharacter.h"
#include "AbilitySystemComponent.h"
#include "MyAttributeSet.h"
AMyCharacter::AMyCharacter()
{
// ASC 생성
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
// AttributeSet 생성
AttributeSet = CreateDefaultSubobject<UMyAttributeSet>(TEXT("AttributeSet"));
}
void AMyCharacter::BeginPlay()
{
Super::BeginPlay();
if (AbilitySystemComponent)
{
// ASC 초기화 - 매우 중요하다
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
}
UAbilitySystemComponent* AMyCharacter::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}
Step 4: 첫 번째 능력 만들기
간단한 능력을 구현한다. 즉시 체력을 회복하는 능력이다.
// HealAbility.h
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "HealAbility.generated.h"
UCLASS()
class MYPROJECT_API UHealAbility : public UGameplayAbility
{
GENERATED_BODY()
public:
UHealAbility();
// 능력이 활성화될 때 호출된다
virtual void ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
protected:
// 회복량
UPROPERTY(EditDefaultsOnly, Category = "Ability")
float HealAmount = 30.0f;
};
// HealAbility.cpp
#include "HealAbility.h"
#include "AbilitySystemComponent.h"
#include "MyAttributeSet.h"
UHealAbility::UHealAbility()
{
// 인스턴싱 정책 설정 (능력당 하나의 인스턴스)
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}
void UHealAbility::ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
// 1. 능력 사용 가능한지 확인한다 (비용, 쿨다운 등)
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
// 2. ASC를 가져온다
UAbilitySystemComponent* ASC = ActorInfo->AbilitySystemComponent.Get();
if (!ASC)
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
// 3. 현재 체력을 가져온다
float CurrentHealth = ASC->GetNumericAttribute(UMyAttributeSet::GetHealthAttribute());
float MaxHealth = ASC->GetNumericAttribute(UMyAttributeSet::GetMaxHealthAttribute());
// 4. 새로운 체력을 계산한다
float NewHealth = FMath::Clamp(CurrentHealth + HealAmount, 0.0f, MaxHealth);
// 5. 체력을 설정한다
ASC->SetNumericAttributeBase(UMyAttributeSet::GetHealthAttribute(), NewHealth);
// 6. 능력을 종료한다
EndAbility(Handle, ActorInfo, ActivationInfo, true, false);
}
Step 5: 능력 부여하고 사용하기
캐릭터에게 능력을 부여하고 사용하는 방법이다.
// MyCharacter.cpp의 BeginPlay에 추가
void AMyCharacter::BeginPlay()
{
Super::BeginPlay();
if (AbilitySystemComponent)
{
// ASC 초기화
AbilitySystemComponent->InitAbilityActorInfo(this, this);
// 힐 능력 부여
if (HealAbilityClass) // 블루프린트에서 설정
{
FGameplayAbilitySpec AbilitySpec(HealAbilityClass, 1, 0, this);
AbilitySystemComponent->GiveAbility(AbilitySpec);
}
}
}
// 능력 사용 함수
void AMyCharacter::UseHealAbility()
{
if (AbilitySystemComponent && HealAbilityClass)
{
// 클래스로 능력 활성화를 시도한다
AbilitySystemComponent->TryActivateAbilityByClass(HealAbilityClass);
}
}
자주 사용하는 함수들
ASC 필수 함수들
// 1. 초기화 (BeginPlay에서 반드시 호출해야 한다)
AbilitySystemComponent->InitAbilityActorInfo(Owner, Avatar);
// 2. 능력 부여
FGameplayAbilitySpec Spec(AbilityClass, Level, InputID, this);
AbilitySystemComponent->GiveAbility(Spec);
// 3. 능력 활성화
AbilitySystemComponent->TryActivateAbilityByClass(AbilityClass);
AbilitySystemComponent->TryActivateAbilitiesByTag(TagContainer);
// 4. 속성값 가져오기/설정하기
float Health = AbilitySystemComponent->GetNumericAttribute(AttributeSet::GetHealthAttribute());
AbilitySystemComponent->SetNumericAttributeBase(AttributeSet::GetHealthAttribute(), NewValue);
// 5. 태그 확인
bool HasTag = AbilitySystemComponent->HasMatchingGameplayTag(Tag);
Ability 필수 함수들
// 1. 능력 시작 (비용 차감, 쿨다운 적용)
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
// 실패 처리
}
// 2. 능력 종료
EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
// 3. ASC 가져오기
UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo();
// 4. 캐릭터 가져오기
AActor* Avatar = GetAvatarActorFromActorInfo();
실전 예제: 파이어볼 스킬 만들기
실제 게임에서 사용할 수 있는 파이어볼 스킬을 구현한다.
// FireballAbility.h
UCLASS()
class UFireballAbility : public UGameplayAbility
{
GENERATED_BODY()
public:
UFireballAbility();
virtual void ActivateAbility(...) override;
protected:
// 데미지
UPROPERTY(EditDefaultsOnly)
float Damage = 50.0f;
// 마나 소모량
UPROPERTY(EditDefaultsOnly)
float ManaCost = 20.0f;
// 쿨다운 시간
UPROPERTY(EditDefaultsOnly)
float CooldownDuration = 5.0f;
// 발사체 클래스
UPROPERTY(EditDefaultsOnly)
TSubclassOf<class AFireballProjectile> ProjectileClass;
};
// FireballAbility.cpp
void UFireballAbility::ActivateAbility(...)
{
// 1. 마나를 확인한다
UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo();
float CurrentMana = ASC->GetNumericAttribute(UMyAttributeSet::GetManaAttribute());
if (CurrentMana < ManaCost)
{
// 마나가 부족하다
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
// 2. 능력을 커밋한다 (마나 소모, 쿨다운 적용)
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
// 3. 마나를 차감한다
ASC->SetNumericAttributeBase(
UMyAttributeSet::GetManaAttribute(),
CurrentMana - ManaCost
);
// 4. 발사체를 스폰한다
if (ACharacter* Character = Cast<ACharacter>(GetAvatarActorFromActorInfo()))
{
FVector SpawnLocation = Character->GetActorLocation() + Character->GetActorForwardVector() * 100.0f;
FRotator SpawnRotation = Character->GetControlRotation();
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = Character;
SpawnParams.Instigator = Character;
if (AFireballProjectile* Fireball = GetWorld()->SpawnActor<AFireballProjectile>(
ProjectileClass, SpawnLocation, SpawnRotation, SpawnParams))
{
Fireball->SetDamage(Damage);
}
}
// 5. 능력을 종료한다
EndAbility(Handle, ActorInfo, ActivationInfo, true, false);
}
GameplayEffect로 버프/디버프 만들기
GameplayEffect는 블루프린트에서 만드는 것이 편하지만, C++에서도 동적으로 생성할 수 있다.
// 5초간 이동속도 50% 증가 버프를 적용한다
void AMyCharacter::ApplySpeedBuff()
{
if (!AbilitySystemComponent) return;
// 1. Effect Context를 생성한다
FGameplayEffectContextHandle EffectContext = AbilitySystemComponent->MakeEffectContext();
EffectContext.AddSourceObject(this);
// 2. Effect Spec을 생성한다
FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(
SpeedBuffEffectClass, // 블루프린트에서 만든 Effect 클래스
1, // 레벨
EffectContext
);
if (SpecHandle.IsValid())
{
// 3. Effect를 적용한다
FActiveGameplayEffectHandle ActiveHandle =
AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
}
}
초보자가 자주 하는 실수들
1. InitAbilityActorInfo를 호출하지 않는 경우
// 잘못된 예
void AMyCharacter::BeginPlay()
{
Super::BeginPlay();
// ASC를 초기화하지 않고 사용하면 크래시가 발생한다
}
// 올바른 예
void AMyCharacter::BeginPlay()
{
Super::BeginPlay();
if (AbilitySystemComponent)
{
AbilitySystemComponent->InitAbilityActorInfo(this, this); // 필수다
}
}
2. CommitAbility를 호출하지 않는 경우
// 잘못된 예
void UMyAbility::ActivateAbility(...)
{
// 바로 능력을 실행한다 - 비용이 차감되지 않는다
DoSomething();
EndAbility(...);
}
// 올바른 예
void UMyAbility::ActivateAbility(...)
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo)) // 비용 확인 및 차감
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
DoSomething();
EndAbility(...);
}
3. EndAbility를 호출하지 않는 경우
// 잘못된 예
void UMyAbility::ActivateAbility(...)
{
DoSomething();
// EndAbility를 호출하지 않으면 능력이 계속 활성화 상태로 남는다
}
// 올바른 예
void UMyAbility::ActivateAbility(...)
{
DoSomething();
EndAbility(Handle, ActorInfo, ActivationInfo, true, false); // 필수다
}
디버깅 팁
1. 콘솔 명령어 활용
showdebug abilitysystem
이 명령어를 입력하면 현재 활성화된 능력, 효과, 태그 등을 화면에서 확인할 수 있다.
2. 로그 추가
UE_LOG(LogTemp, Warning, TEXT("Current Health: %f"),
AbilitySystemComponent->GetNumericAttribute(UMyAttributeSet::GetHealthAttribute()));
3. 블루프린트에서 확인
에디터에서 플레이 중에 캐릭터를 선택하고 Details 패널에서 Ability System Component를 찾으면
현재 상태를 실시간으로 확인할 수 있다.
마무리
GAS는 초기에는 복잡해 보이지만, 기본 구조를 이해하면 매우 강력한 시스템이다.
핵심은 다음과 같다
- ASC가 모든 것을 관리한다
- Ability는 행동을 정의한다
- Effect는 결과를 정의한다
- Attribute는 스탯을 정의한다
이 네 가지만 숙지하면 된다.
다음 단계로 학습할 내용은 다음과 같다
- GameplayTag 시스템 활용하기
- 복잡한 GameplayEffect 만들기
- Ability Task로 비동기 작업 처리하기
- 멀티플레이어에서 리플리케이션 처리하기
'Game DevTip > Unreal Engine' 카테고리의 다른 글
| UEG : Unreal Engine Generate Tools for macOS - 맥 전용 언리얼 프로젝트 솔루션 생성 툴 (1) | 2024.12.10 |
|---|---|
| 4. 언리얼 엔진 팀플 시 소스 컨트롤 관리법 : Git & SVN(1) (0) | 2024.12.09 |
| 3. 언리얼 엔진 싱글케스트 델리게이트 기초 (0) | 2024.12.05 |
| 2. 언리얼 엔진의 메모리 관리에 대해서 (0) | 2024.12.05 |
| 1. 언리얼 엔진 핫 리로딩에 관하여 (0) | 2024.12.05 |
댓글