GAS 핵심 개념
GAS란?
- Gameplay Ability System의 약자
- 복잡한 RPG/액션 게임에서 캐릭터의 스킬, 스탯, 버프를 체계적으로 관리하는 프레임워크
- 장점
- 멀티플레이 친화적 : 어트리뷰트나 디버프의 리플리케이션을 내부 로직을 통해 처리해줌
- 확장성 : 새로운 스탯, 스킬 추가가 쉬움
- 재사용성 : 같은 Effect를 여러 Ability에서 활용 가능
- 데이터 중심 설계 : 스킬/버프를 블루프린트에서 손쉽게 제작 가능
- GAS 동작 플로우
┌─── Player Input ───┐
│ (스킬 키 누름) │
└──────┬─────────────┘
│
┌──────▼─────────────┐
│ AbilitySystem │
│ Component │ ← 모든 것의 중심!
│ (지휘관 역할) │
└──┬──────────────┬──┘
│ │
┌────▼───┐ ┌───▼────┐
│Ability│ │ Effect │
│ 관리 │ │관리 │
└────┬───┘ └───┬────┘
│ │
┌────▼─────────────▼────┐
│ AttributeSet │
│ (실제 수치 저장소) │
└───────────────────────┘
1. ASC (Ability System Component)
- 정의
UAbilitySystemComponent는 GAS의 중추 역할을 하는 컴포넌트
- 역할
- 캐릭터가 어떤 능력을 가지고 있는지 관리
- Ability 실행 요청을 처리
- Attibute 값들을 추적 및 동기화
- Effect 적용 및 제거 관리
- 네트워크 동기화
- 특징
- 주로
Character나 Pawn 클래스에 붙음
- 멀티플레이 네트워킹(서버 권한, 클라이언트 예측)까지 책임짐
2. Attribute
- 정의
- 체력, 마나, 스태미나, 공격력, 방어력 등 수치화된 캐릭터 능력치
- 특징
- 일반적으로
float 값을 저장
Health, Mana, Strength, MoveSpeed 등 다양한 값을 저장
- GAS는 이 값들의 변경을 자동으로 감지하고 브로드캐스트 -> UI 갱신에 활용
- 계산 과정
- Base Value : 기본값 (레벨업, 장비로 영구 변경)
- Current Value : 현재값 (버프/디버프로 임시 변경)
- 더하기 (Add)
- 곱하기 (Multiply)
- 덮어쓰기 (Override)
- 항상 위의 같은 순서로 스탯이 계산됨
3. Ability
- 정의
- 특징
- 단순 점프/달리기부터 복잡한 콤보 스킬까지 표현 가능
- Prediction 기능 내장 -> 클라이언트에서도 입력 지연 없이 스킬 실행이 보임
- 실행 흐름
- CanActivate : 스킬 사용 조건 확인
- Commit : 자원 소모, 쿨다운 적용
- Activate : 실제 스킬 효과 실행
- End : 스킬 종료, 정리 작업
4. Effect
- 정의
- 캐릭터의 Attribute 값에 영향을 주는 데이터
- 특징
- 공격력 증가, 이동속도 감소, 체력 회복, 지속 피해 등 가능
- 수치 변경 방식
- Additive (더하기/빼기)
- Multiplicative (곱셈 배수)
- Override (값 덮어쓰기)
- 지속 시간/주기
- Instant : 즉시 적용 후 사라짐 (예 : 데미지, 힐)
- Duration : 일정 시간 지속 (예 : 독, 버프)
- Infinite : 수동 제거 시까지 (예 : 장비 효과)
흐름 예시
플레이어 입력 -> ASC -> Ability -> Effect -> Attribute
힐링 포션 사용
- 플레이어가 힐링 스킬 키를 입력
- ASC가 힐링 Ability 실행 가능한지 판단
- Ability가 마나를 소모하고 힐링 Effect 적용
- Effect가 체력 증가
- Attribute에서 Health 값 변경
- UI에서 체력바 업데이트
GAS 샘플 코드
기본 설정
1. 플러그인 설정
- Edit -> Plugins -> Gameplay Abilities 플러그인 활성화
2. 모듈 추가
// Build.cs
PublicDependencyModuleNames.AddRange(new string[]
{
...
// GAS
"GameplayAbilities", "GameplayTags", "GameplayTasks",
});
- GameplayAbilities : ASC와 Ability의 핵심
- GameplayTags : Effect를 구분하고 관리하는 태그 시스템
- GameplayTasks : Ability가 시간이 걸리는 작업을 할 때 필요
대시 스킬 구현
1. GASCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AbilitySystemInterface.h"
#include "AbilitySystemComponent.h"
#include "GASCharacter.generated.h"
class UMyAttributeSet;
UCLASS()
class GASDEMO_API AGASCharacter : public ACharacter, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
AGASCharacter();
// IAbilitySystemInterface
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
protected:
// GAS 핵심 컴포넌트들
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "GAS")
class UAbilitySystemComponent* AbilitySystemComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "GAS")
class UMyAttributeSet* AttributeSet;
// Ability 설정
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GAS")
TArray<TSubclassOf<UGameplayAbility>> StartupAbilities;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GAS")
TSubclassOf<UGameplayAbility> DashAbilityClass;
virtual void BeginPlay() override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
// Input 함수들
UFUNCTION()
void InputDash();
UFUNCTION(BlueprintCallable, Category = "GAS")
void PrintDebugInfo();
private:
// 기본 이동 함수들
void MoveForward(float Value);
void MoveRight(float Value);
};
2. GASCharacter.cpp
#include "GASCharacter.h"
#include "MyAttributeSet.h"
#include "Engine/Engine.h"
AGASCharacter::AGASCharacter()
{
PrimaryActorTick.bCanEverTick = false;
// ASC 생성
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComp"));
// AttributeSet 생성
AttributeSet = CreateDefaultSubobject<UMyAttributeSet>(TEXT("AttributeSet"));
}
UAbilitySystemComponent* AGASCharacter::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}
void AGASCharacter::BeginPlay()
{
Super::BeginPlay();
if (AbilitySystemComponent)
{
// GAS 초기화 - 가장 중요!
AbilitySystemComponent->InitAbilityActorInfo(this, this);
// 시작 Ability들 부여
for (const auto& AbilityClass : StartupAbilities)
{
if (AbilityClass)
{
AbilitySystemComponent->
GiveAbility(FGameplayAbilitySpec(AbilityClass, 1, 0, this));
UE_LOG(LogTemp, Log, TEXT("Gave ability: %s"), *AbilityClass->GetName());
}
}
UE_LOG(LogTemp, Warning, TEXT("GAS Initialized!"));
}
}
void AGASCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 기본 이동
PlayerInputComponent->BindAxis("Move Forward / Backward", this, &AGASCharacter::MoveForward);
PlayerInputComponent->BindAxis("Move Right / Left", this, &AGASCharacter::MoveRight);
PlayerInputComponent->BindAxis("Turn Right / Left Mouse", this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAxis("Look Up / Down Mouse", this, &APawn::AddControllerPitchInput);
// Jump
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
// Dash
PlayerInputComponent->BindAction("Dash", IE_Pressed, this, &AGASCharacter::InputDash);
}
void AGASCharacter::InputDash()
{
if (AbilitySystemComponent && DashAbilityClass)
{
bool bSuccess = AbilitySystemComponent->TryActivateAbilityByClass(DashAbilityClass);
UE_LOG(LogTemp, Log, TEXT("Dash result: %s"), bSuccess ? TEXT("Success") : TEXT("Failed"));
}
}
void AGASCharacter::PrintDebugInfo()
{
if (!AttributeSet)
{
UE_LOG(LogTemp, Error, TEXT("No AttributeSet"));
return;
}
float Health = AttributeSet->GetHealth();
float MaxHealth = AttributeSet->GetMaxHealth();
float Mana = AttributeSet->GetMana();
float Stamina = AttributeSet->GetStamina();
FString DebugInfo = FString::Printf(TEXT("Health: %.0f/%.0f, Mana: %.0f, Stamina: %.0f"),
Health, MaxHealth, Mana, Stamina);
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Green, DebugInfo);
}
UE_LOG(LogTemp, Warning, TEXT("%s"), *DebugInfo);
}
// 기본 이동 함수들
void AGASCharacter::MoveForward(float Value)
{
if (Controller && Value != 0.0f)
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
AddMovementInput(Direction, Value);
}
}
void AGASCharacter::MoveRight(float Value)
{
if (Controller && Value != 0.0f)
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(Direction, Value);
}
}
3. MyAttributeSet.h
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "MyAttributeSet.generated.h"
// 매크로 중요 - Getter/Setter 자동 생성
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
UCLASS()
class GASDEMO_API UMyAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
UMyAttributeSet();
// Health
UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Health)
UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_MaxHealth)
FGameplayAttributeData MaxHealth;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxHealth)
// Mana
UPROPERTY(BlueprintReadOnly, Category = "Mana", ReplicatedUsing = OnRep_Mana)
FGameplayAttributeData Mana;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Mana)
UPROPERTY(BlueprintReadOnly, Category = "Mana", ReplicatedUsing = OnRep_MaxMana)
FGameplayAttributeData MaxMana;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxMana)
// Stamina
UPROPERTY(BlueprintReadOnly, Category = "Stamina", ReplicatedUsing = OnRep_Stamina)
FGameplayAttributeData Stamina;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, Stamina)
UPROPERTY(BlueprintReadOnly, Category = "Stamina", ReplicatedUsing = OnRep_MaxStamina)
FGameplayAttributeData MaxStamina;
ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxStamina)
protected:
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);
UFUNCTION()
virtual void OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth);
UFUNCTION()
virtual void OnRep_Mana(const FGameplayAttributeData& OldMana);
UFUNCTION()
virtual void OnRep_MaxMana(const FGameplayAttributeData& OldMaxMana);
UFUNCTION()
virtual void OnRep_Stamina(const FGameplayAttributeData& OldStamina);
UFUNCTION()
virtual void OnRep_MaxStamina(const FGameplayAttributeData& OldMaxStamina);
};
4. MyAttributeSet.cpp
#include "MyAttributeSet.h"
#include "Net/UnrealNetwork.h"
UMyAttributeSet::UMyAttributeSet()
{
// 기본값 설정
InitHealth(100.0f);
InitMaxHealth(100.0f);
InitMana(50.0f);
InitMaxMana(50.0f);
InitStamina(100.0f);
InitMaxStamina(100.0f);
}
void UMyAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 레플리케이션 설정
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Health, COND_None, REPNOTIFY_Alwways);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Mana, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, MaxMana, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Stamina, COND_None, REPNOTIFY_Always);
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, MaxStamina, COND_None, REPNOTIFY_Always);
}
void UMyAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Health, OldHealth);
}
void UMyAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldMaxHealth)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, MaxHealth, OldMaxHealth);
}
void UMyAttributeSet::OnRep_Mana(const FGameplayAttributeData& OldMana)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Mana, OldMana);
}
void UMyAttributeSet::OnRep_MaxMana(const FGameplayAttributeData& OldMaxMana)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, MaxMana, OldMaxMana);
}
void UMyAttributeSet::OnRep_Stamina(const FGameplayAttributeData& OldStamina)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Stamina, OldStamina);
}
void UMyAttributeSet::OnRep_MaxStamina(const FGameplayAttributeData& OldMaxStamina)
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, MaxStamina, OldMaxStamina);
}
5. DashAbility.h
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "DashAbility.generated.h"
UCLASS()
class GASDEMO_API UDashAbility : public UGameplayAbility
{
GENERATED_BODY()
public:
UDashAbility();
protected:
// 대시 설정값들
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Dash")
float DashDistance = 1000.0f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Dash")
float DashDuration = 0.2f;
// 실제 스킬 로직
virtual void ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData
) override;
// ability 종료시 로직
virtual void EndAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
bool bReplicateEndAbility,
bool bWasCancelled
) override;
private:
UFUNCTION()
void OnDashComplete();
FTimerHandle DashTimerHandle;
UPROPERTY()
ACharacter* DashingCharacter;
};
6. DashAbility.cpp
#include "DashAbility.h"
#include "GameFramework/Character.h"
#include "Engine/Engine.h"
UDashAbility::UDashAbility()
{
// NonInstanced, InstancedPerActor, InstancedPerExecution
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
DashingCharacter = nullptr;
}
void UDashAbility::ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
// Cost와 Cooldown 체크
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, false);
return;
}
// 캐릭터 가져오기
DashingCharacter = Cast<ACharacter>(ActorInfo->AvatarActor.Get());
if (!DashingCharacter)
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, false);
return;
}
UE_LOG(LogTemp, Log, TEXT("Dash Ability Activated!"));
// 대시 실행 - 앞으로 발사
FVector ForwardVector = DashingCharacter->GetActorForwardVector();
FVector DashVelocity = ForwardVector * DashDistance;
DashingCharacter->LaunchCharacter(DashVelocity, true, true);
// 일정 시간 후 종료
FTimerDelegate TimerDelegate;
TimerDelegate.BindUFunction(this, FName("OnDashComplete"));
DashingCharacter->GetWorldTimerManager().SetTimer(
DashTimerHandle,
TimerDelegate,
DashDuration,
false
);
}
void UDashAbility::OnDashComplete()
{
UE_LOG(LogTemp, Log, TEXT("Dash completed"));
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}
void UDashAbility::EndAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
bool bReplicateEndAbility,
bool bWasCancelled)
{
// 정리 작업
if (DashingCharacter)
{
DashingCharacter->GetWorldTimerManager().ClearTimer(DashTimerHandle);
}
DashingCharacter = nullptr;
UE_LOG(LogTemp, Log, TEXT("Dash Ability Ended"));
Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}
출처 : 팀스파르타 내일배움캠프