GAS 기초

김혁·2025년 9월 9일

챌린지

목록 보기
11/14

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 적용 및 제거 관리
    • 네트워크 동기화
  • 특징
    • 주로 CharacterPawn 클래스에 붙음
    • 멀티플레이 네트워킹(서버 권한, 클라이언트 예측)까지 책임짐

2. Attribute

  • 정의
    • 체력, 마나, 스태미나, 공격력, 방어력 등 수치화된 캐릭터 능력치
  • 특징
    • 일반적으로 float 값을 저장
    • Health, Mana, Strength, MoveSpeed 등 다양한 값을 저장
    • GAS는 이 값들의 변경을 자동으로 감지하고 브로드캐스트 -> UI 갱신에 활용
  • 계산 과정
    • Base Value : 기본값 (레벨업, 장비로 영구 변경)
    • Current Value : 현재값 (버프/디버프로 임시 변경)
      1. 더하기 (Add)
      2. 곱하기 (Multiply)
      3. 덮어쓰기 (Override)
    • 항상 위의 같은 순서로 스탯이 계산됨

3. Ability

  • 정의
    • 캐릭터가 수행할 수 있는 행동/스킬
  • 특징
    • 단순 점프/달리기부터 복잡한 콤보 스킬까지 표현 가능
    • Prediction 기능 내장 -> 클라이언트에서도 입력 지연 없이 스킬 실행이 보임
  • 실행 흐름
    1. CanActivate : 스킬 사용 조건 확인
    2. Commit : 자원 소모, 쿨다운 적용
    3. Activate : 실제 스킬 효과 실행
    4. End : 스킬 종료, 정리 작업

4. Effect

  • 정의
    • 캐릭터의 Attribute 값에 영향을 주는 데이터
  • 특징
    • 공격력 증가, 이동속도 감소, 체력 회복, 지속 피해 등 가능
    • 수치 변경 방식
      • Additive (더하기/빼기)
      • Multiplicative (곱셈 배수)
      • Override (값 덮어쓰기)
    • 지속 시간/주기
      • Instant : 즉시 적용 후 사라짐 (예 : 데미지, 힐)
      • Duration : 일정 시간 지속 (예 : 독, 버프)
      • Infinite : 수동 제거 시까지 (예 : 장비 효과)

흐름 예시

플레이어 입력 -> ASC -> Ability -> Effect -> Attribute

힐링 포션 사용

  1. 플레이어가 힐링 스킬 키를 입력
  2. ASC가 힐링 Ability 실행 가능한지 판단
  3. Ability가 마나를 소모하고 힐링 Effect 적용
  4. Effect가 체력 증가
  5. Attribute에서 Health 값 변경
  6. 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);
}


출처 : 팀스파르타 내일배움캠프
profile
게임 개발자를 향해..

0개의 댓글