18 분 소요

GAS 아키텍쳐에 대하여 알아보자

김하연 튜터님의 Notion 자료를 바탕으로 강의를 들으며
수정 및 재작성한 블로깅용 글

  • GAS는 소규모 게임보단
    대규모 게임 기반임
    • 초기 설정 및 학습 난이도가 높은 편
    • 또한, 연산량이 낮지 않음
  • GAS의 장점은 확장성과 유지보수성!
    • 오버 엔지니어링에는 유의하자

1. GAS 의 5대 요소 🍉

1-1. Gameplay Ability System (GAS)란?

  • Epic Games가 포트나이트 개발 과정에서 만든 시스템
  • 스킬, 버프, 디버프, 능력치를 체계적으로 관리하는 전투 프레임워크
  • 전통적인 RPG 캐릭터 시트를 엔진 수준에서 구조화한 시스템
[디지털 TRPG 캐릭터 시트 구조]

1. 관리자 (DM)  ➔ Ability System Component (ASC)
2. 능력치 (Stats) ➔ Attribute Set
3. 행동 (Actions) ➔ Gameplay Ability (GA)
4. 결과/변화 (Changes) ➔ Gameplay Effect (GE)
---------------------------------------------------
5. 언어/규칙 (Language) ➔ Gameplay Tags

1-2. Ability System Component (ASC) - 지휘관

  • ASC는 GAS의 모든 구성 요소를 총괄하고 관리하는 중앙 처리 장치
  • 캐릭터가 수행하는 모든 행동(Ability), 상태 변화(Effect), 능력치(Attribute)는 반드시 ASC를 거쳐서 처리됨.

작동 원리

과거의 직접적인 함수 호출(Call Function) 방식에서 벗어나,
태그(Tag)를 기반으로 한 느슨한 결합(Loose Coupling) 방식을 사용함.

[ 플레이어 입력 (Enhanced Input) ]
         
          (Input Tag 전달: "나 스킬 쓸래!")
         
[ ASC (심장 & 관제탑) ]
         
          (1. 태그 & 상태 확인: "지금   있나?")
         
    ┌───< 판단 >───┐
                  
     (No)          (Yes)
 [ 실패 ]       [ Ability 실행 (Activate) ]
(기절/쿨타임)      
                   ├───────────────────────┐
                                          
           [ 비용/쿨타임 적용 ]      [ 이펙트(GE) 적용 ]
           (마나 차감,  시작)    (데미지 계산  전달)
                                          
                                          
           [ Attribute Set ]       [ Target (적) ]
           (내 스탯 변화)          (상대 체력 감소)
  1. Input Tag 전달: 플레이어의 입력은 Input.Action.Skill 같은 태그 형태로 ASC에 전달

  2. 규칙 검사 (Policy Check): ASC는 현재 캐릭터의 상태(태그), 자원(마나), 쿨타임을 확인

  3. 실행 및 동기화: 조건이 충족되면 로직을 수행하고, 결과를 네트워크를 통해 모든 클라이언트에 전파함.

핵심 역할 상세

  • 규칙 집행 (Rule Enforcement)
    • ASC는 어빌리티 실행 요청이 들어올 때마다 다음 3가지를 검사함.
      • Can Activate: 현재 상태에서 실행 가능한가? (예: 공중에서 사용 불가)
      • Cost Check: 필요한 자원이 있는가? (예: 마나 50 소모)
      • Cooldown Check: 재사용 대기시간이 끝났는가?
  • 네트워크 중개 (Replication Bridge)
    • 멀티플레이어 환경에서 복잡한 동기화 코드를 작성하지 않아도, ASC가 이를 대신 처리
      • Client Prediction: 클라이언트에서 먼저 연출을 실행하여 반응성을 높임.
      • Server Authority: 서버에서 최종 검증 후 결과를 확정.
      • Automatic Replication: 어빌리티 실행 여부와 이펙트 적용 결과를 자동으로 다른 클라이언트에 전파.
  • 데미지 및 이펙트 파이프라인
    • 외부에서 들어오는 모든 변화(데미지, 힐, 버프)는 ASC를 통해 Gameplay Effect(GE) 형태로 처리됨.
    • TakeDamage() 함수 대신 ApplyGameplayEffectToTarget()을 사용하여, 방어력 계산이나 무적 판정 등의 로직을 일관성 있게 처리함.

1-3. Attribute Set - 살아있는 능력치 시스템

  • 게임 내 캐릭터의 능력치 (Stats)를 정의하고 관리하는 데이터 컨테이너
  • 단순한 변수 (float)가 아닌, FGameplayAttributeData 구조체를 사용하여 수치의 변화 내역, 네트워크 동기화, 계산 로직을 통합 관리

기존 방식(float) vs GAS 방식 비교

구분 일반 변수 방식 (float Health) GAS 방식 (FGameplayAttributeData)
구조 단순 숫자 값 하나만 저장 BaseValue와 CurrentValue 분리 저장
상태 관리 버프/디버프 적용 시 직접 연산 필요 Modifiers가 자동으로 계산 및 합산
복원 버프 종료 시 원상복구 로직 구현 필요 자동 복구 (Dirty Marking)
네트워크 별도의 리플리케이션 함수 필요 자동 동기화 및 예측(Prediction) 지원

핵심 구조: 두 개의 통장 (Base vs Current)

UPROPERTY(BlueprintReadOnly, ReplicateUsing = OnRep_Health)
FGameplayAttributeData Health;

GAS의 속성은 하나의 스탯에 대해 두 가지 값을 동시에 유지함.

  • BaseValue (기본값): 캐릭터의 영구적인 능력치. (레벨업, 영구 스탯 아이템 등으로만 변경)
  • CurrentValue (현재값): 모든 버프, 디버프, 장비 효과가 합산된 최종 값.

변경 규칙 (Duration Policy에 따른 차이)

Gameplay Effect(GE)의 지속성 정책에 따라 변경되는 값이 달라짐.

GE 종류 대상 값 동작 원리 예시
Instant BaseValue 값을 영구적으로 변경함 데미지(체력 감소), 영구 스탯 상승 물약
Duration CurrentValue BaseValue는 유지하고, Current만 잠시 변경 10초간 공격력 증가 버프
Infinite CurrentValue 장비 해제 등 특정 시점까지 값 유지 무기 장착 시 공격력 증가

버프(Duration)가 끝나면, GAS는 CurrentValue를 다시 계산함. 따라서 개발자가 “버프 끝났으니 -10 하세요”라는 코드를 짤 필요가 없다는 얘기

계산 파이프라인 (Calculation Pipeline)

GAS는 최종 값(CurrentValue)을 결정할 때 엄격한 수학적 순서를 따릅니다.

\[CurrentValue = (BaseValue + \sum Additive) \times \prod Multiplicative\]

계산 흐름도

[BaseValue: 100] (기본 공격력)
       
       
[1. Additive Modifiers] (더하기 연산 모음)
        ├─ 무기 공격력 (+50)
        └─ 디버프 (-20)
        -> 중간 합계: 130
       
[2. Multiplicative Modifiers] (곱하기 연산 모음)
        ├─ 분노 버프 (x 1.5)
        └─ 약화 저주 (x 0.5)
        -> 130 * 1.5 * 0.5
       
[CurrentValue: 97.5] (최종 결과)

UPROPERTY(BlueprintReadOnly, ReplicateUsing = OnRep_Health)
FGameplayAttributeData Health;
  • 규칙: 항상 덧셈을 먼저 모두 수행한 후, 그 결과에 곱셈을 수행함.

데이터 무결성 보호 (Clamping)

체력이 0 미만으로 떨어지거나, 최대 체력을 초과하는 것을 방지하기 위해 PreAttributeChange 함수를 사용

  • 시점: 속성 값이 변경되기 직전에 호출됨.
  • 역할: 들어오는 값을 검사하고, 범위 내로 Clamp(자르기)

1-4. Gameplay Ability (GA) - 액션의 정의와 흐름

  • 캐릭터가 수행하는 모든 행동 (Action)과 기술 (Skill)을 정의하는 클래스
  • 단순한 함수 호출이 아니라, 상태와 시간을 가지고 실행되는 객체

생명주기 (Lifecycle)

GA는 엄격한 실행 순서를 따르며,
모든 단계가 통과되어야 실행됩니다.

[1. TryActivate] (요청)
       │
       ▼
[2. CanActivate] (검사: 입국 심사)
       │ - 마나/쿨타임 확인
       │ - Tag 확인 (기절 상태? 무기 장착?)
       │
       ▼ (Pass)
[3. CommitAbility] (결제: 비용 지불)
       │ - Cost 차감 (마나)
       │ - Cooldown 적용
       │
       ▼
[4. ActivateAbility] (실행: 본 로직)
       │ ★ 핵심 구간 (Ability Task 실행)
       │
       ▼
[5. EndAbility] (종료: 뒷정리)
       │ - 메모리 해제
       │ - Tag 제거

핵심 메커니즘: Ability Task (시간 제어)

  • GA가 단순 함수와 다른 가장 큰 차이점은 비동기 대기(Latent Wait)가 가능하다는 점
  • 이를 수행하는 것이 Ability Task

Ability와 Task의 관계

  • 문제점: GA는 Activate() 호출 후 코드가 끝나면 즉시 소멸(End)하려 함.
  • 해결책: Task가 실행되는 동안 GA의 종료를 막고, 특정 사건(애니메이션 종료, 입력 해제 등)이 발생할 때까지 대기함.
    → Ability는 껍데기(Container)이고, Task는 그 안에서 시간을 소비하는 엔진(Engine)

구현 패턴 (Patterns)

사용하는 Task의 종류에 따라 스킬의 성격이 결정됨.

유형 설명 핵심 Task 예시
Active 즉시 실행 후 종료 PlayMontageAndWait 공격, 점프, 스킬 사용
Interaction 특정 입력/시간 동안 유지 WaitInputRelease 차징 공격, 부활시키기
Passive 게임 내내 백그라운드 실행 WaitAttributeChange 체력 30% 이하 시 방어 버프

메모리 최적화 (Instancing Policy)

GA를 메모리에 어떻게 생성할지 결정하는 중요한 최적화 옵션임.

  1. Instanced Per Actor (기본값)
    • 캐릭터마다 별도의 GA 객체 생성.
    • 복잡한 로직, 변수 저장이 필요한 스킬에 사용.
  2. Non-Instanced (최적화)
    • 모든 캐릭터가 하나의 GA 객체(CDO)를 공유.
    • 변수를 저장할 수 없으나 메모리 비용이 매우 낮음.
    • 단순한 평타, 점프 등에 권장 (포트나이트 방식).

1-5. Gameplay Effect (GE) - 변화를 정의하는 데이터

  • 게임 내에서 발생하는 모든 속성 (Attribute)의 변경상태 (State) 변화정의하는 데이터 에셋
  • 블루프린트 로직이나 C++ 코드가 아닌, 순수 데이터 (Data Asset) 형태로 존재하며, ASC에 의해 해석되고 실행

GE의 특징: 데이터 기반 설계 (Data-Driven)

Gameplay Effect는 프로그래밍(Logic)이 아닌 설정(Configuration)의 영역

  • 기획자가 프로그래머 없이 수치 조정 같은 밸런스 작업을 하기 위해
구분 일반적인 구현 방식 GAS 구현 방식 (GE)
구현 방법 C++ / BP 함수 (TakeDamage) 작성 데이터 에셋(GE_Damage) 생성 및 수치 입력
편집 환경 노드 그래프 또는 코드 에디터 디테일 패널 (속성 창)
작업 주체 프로그래머 기획자 (밸런스 조정 용이)
유지 보수 수치 변경 시 컴파일/재저장 필요 에셋 값만 수정하면 즉시 반영

핵심 설정 요소 (Configuration)

  • 지속성 정책 (Duration Policy): 이 효과가 속성에 영향을 미치는 시간적 규칙을 정의
정책 (Policy) 설명 속성 영향 예시
Instant 즉시 적용되고 종료됨 BaseValue 영구 변경 데미지, 즉시 회복
Duration 정해진 시간(X초) 동안만 유지됨 CurrentValue 임시 변경 이동속도 버프, 도트 데미지
Infinite 별도의 제거 명령 전까지 영구 유지 CurrentValue 임시 변경 장비 착용 스탯, 영구 상태이상
  • 수치 변경 연산 (Modifiers): 속성 값을 어떻게 변경할지 수학적 연산을 정의
    • Attribute: 변경할 속성 선택 (예: Health, Mana)
    • Operation: 연산 방식 선택
      • Add: 더하기 (데미지는 음수 값을 더함)
      • Multiply: 곱하기 (비율 퍼센트 적용)
      • Override: 덮어쓰기 (특정 값으로 고정)
    • Value: 적용할 수치 입력 (예: 10.0)

모듈형 확장

UE 5.5부터 GE는 컴포넌트 조립 방식(Modular)으로 기능을 확장됨.

  • Target Tags: 효과 적용 시 특정 태그 부착 (예: State.Debuff.Poison).
  • Grant Abilities: 효과 적용 중 특정 어빌리티 부여 (예: 독에 걸리면 ‘해독’ 스킬 사용 가능).
  • Immunity: 특정 효과에 대한 면역 부여.

이 외 고급 기능 (Automation)

  • 복잡한 게임 로직을 코드 없이 옵션 설정만으로 구현합니다.
    • Period (주기적 실행): Duration 시간 동안 X초마다 효과를 반복 실행
    • Stacking (중첩): 동일한 효과가 여러 번 적용될 때의 규칙 설정.
      • Stack Limit (최대 중첩 수)
      • Duration Refresh (시간 갱신 여부)
      • Expiration Policy (만료 시 처리)

1-6. Gameplay Tags - 시스템을 지배하는 공용어

  • GAS 내에서 객체의 상태(State)를 정의하고, 시스템 간의 상호작용 규칙을 결정하는 표준 식별자
  • 기존의 ‘boolean 변수나 Enum을 대체’하며, 계층형 구조(Hierarchy)로 이루어짐.

태그의 구조 (Hierarchy)

태그는 점(.)으로 구분된 계층 구조를 가짐.

  • 형식: Category.SubCategory.Specific
  • 예시
State
    ├── Debuff
        ├── Stun   (기절)
        └── Freeze (빙결)
    └── Buff
        └── Speed  (가속)
  • 상태 이상에 걸리지 않았는지 검사?
if(!bStunned ||
   !bFrozen ||
   ...)

기존 방식 vs GAS 태그 방식

구분 Boolean 방식 (기존) Gameplay Tag 방식 (GAS)
상태 관리 bIsStunned, bIsFrozen 등 변수 나열 State.Debuff.Stun 태그 하나로 관리
유지 보수 상태 추가 시 코드 수정 필수 데이터 추가만으로 해결 (코드 수정 X)

핵심 기능: 계층 매칭 (Hierarchy Matching)

부모 태그를 검사하면 자식 태그가 자동으로 포함되는 기능

  • 검사: 이 캐릭터가 State.Debuff 상태인가?
  • 보유: State.Debuff.Stun (자식) 보유 중
  • 결과: True (매칭 성공)
    • 모든 종류의 디버프를 일일이 나열할 필요 없이, 부모 태그 하나로 일괄 처리가 가능

어빌리티 제어 (Interaction)

코딩 없이 태그 설정만으로 스킬 간의 취소(Cancel)와 차단(Block) 규칙을 정의

기능 설명 예시 상황
Cancel 이 태그가 있는 행동을 강제 종료시킴 구르기 사용 시 → 캐스팅(Casting) 중인 마법 취소
Block 이 태그가 있는 동안 행동 불가 기절(Stun) 상태일 때 → 공격(Attack) 시도 차단

입력 태그 (Input Tag)

요즘은 입력 시스템(Enhanced Input)과 GAS를 태그로 연결함.

  • 개념: 물리적 키(Key)가 직접 함수를 호출하지 않고, Input Tag를 ASC에 전달하여 어빌리티를 트리거
  • 흐름: Q Key 입력 → Input.Action.Heal 태그 발생 → GA_Potion 실행
    (키 변경 과 같은 설정 세팅에 유리)

Tag를 몽타주와 연결하여
특정 부분에 ‘무적’효과 등을 부여할 수도 있음!

1-7. GAS 5대 요소의 협력 과정 (Data Flow)

[1. Input (입력)]
       │
       │ (Input Tag: "Input.Action.Heal")
       ▼
[2. ASC (중앙 관리자)] ◀─── [Tag & Attribute 검사]
       │                    (마나 충분? 기절 상태 아님?)
       │ (승인: Activate)
       ▼
[3. Ability (행동 실행)]
       │
       ├─ (ⓐ Commit) ──▶ [Cost & Cooldown GE 적용]
       │
       │ (ⓑ Ability Task: 시간 지연)
       ▼
[4. Animation (몽타주 재생 & 대기)]
       │
       │ (ⓒ 완료 시: OnCompleted)
       ▼
[5. Gameplay Effect (데이터 생성)]
       │
       │ (SpecHandle 전달: "Health +50")
       ▼
[6. Attribute Set (수치 반영)]
       │
       │ (BaseValue 변경 & Clamping)
       ▼
[7. UI / Networking (결과)]
       │
       └─▶ [체력바 갱신 (OnRep/Delegate)]

힐링 포션 사용 예시

플레이어가 ‘Q’ 키를 눌러 포션을 마시는 0.1초 동안의 내부 처리 과정입니다.

① 입력 및 요청 (Input & Tags)

  • 플레이어가 Q키 입력 → Input.Action.UsePotion 태그가 ASC로 전달됨.
  • ASC는 해당 태그에 매핑된 GA_Potion 어빌리티를 찾음.

② 실행 판정 (ASC & Tags)

  • 태그 검사: 현재 캐릭터에게 State.Debuff.Stun(기절) 태그가 있는가? (Block Tags 확인)
  • 비용 검사: 마나(Attribute)가 있는가? 쿨타임(Cooldown GE) 중인가?
  • ➜ 결과: 모두 통과 시 ActivateAbility() 호출.

③ 행동 및 시간 지연 (Ability & Task)

  • CommitAbility: 마나 차감 및 쿨타임 GE 즉시 적용.
  • Task_PlayMontageAndWait: 포션 마시는 애니메이션 재생 시작.
  • (Wait): Ability는 애니메이션이 끝날 때까지 대기 상태 유지.

④ 결과 적용 (Gameplay Effect)

  • 애니메이션 종료 시점(OnCompleted)에 GE_Heal 이펙트를 생성.
  • ASC에게 GE_Heal 적용 요청 (SpecHandle 전달).
    • *GE_Heal 정보: Duration=Instant, Modifier=Health Add 50*

⑤ 상태 변화 및 피드백 (Attribute & UI)

  • PreAttributeChange: 체력이 MaxHealth를 넘지 않도록 Clamp(제한) 처리.
  • 값 변경: Health 속성 값이 50 증가.
  • 네트워크: 서버에서 변경된 값이 클라이언트로 자동 복제(Replication).
  • UI 갱신: OnHealthChanged 델리게이트가 호출되어 HUD의 체력바가 즉시 갱신됨.

2. 캐릭터 대시 스킬 예제 코드 🛠️

Step 1. 환경설정

1-1. 플러그인 활성화

  1. 에디터 상단 메뉴 EditPlugins
  2. 검색창에 Gameplay Abilities 입력
  3. Gameplay Abilities 체크박스 활성화
  4. Restart Now 버튼 클릭하여 재시작

1-2. Build.cs 설정

GAS 3대장 모듈 (GameplayAbilities, GameplayTags, GameplayTasks)을 추가합니다.

// [ProjectName].Build.cs

using UnrealBuildTool;

public class GASDemo : ModuleRules
{
	public GASDemo(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[] 
        { 
            "Core", 
            "CoreUObject", 
            "Engine", 
            "InputCore", 
            "EnhancedInput",
            // GAS 필수 모듈 3대장 추가
            "GameplayAbilities", 
            "GameplayTags", 
            "GameplayTasks" 
        });

		PrivateDependencyModuleNames.AddRange(new string[] { });
	}
}

Step 2. 캐릭터 세팅

  • 이제 캐릭터에게 ASC (심장)를 이식함.
  • 아직 스탯이나 스킬은 없고, 시스템만 탑재하는 단계

1. GASCharacter.h

  • IAbilitySystemInterface를 상속받고,
    GetAbilitySystemComponent()를 오버라이드하는 것이 핵심
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
// 인터페이스와 컴포넌트 헤더 추가
#include "AbilitySystemInterface.h"
#include "AbilitySystemComponent.h"
#include "GASCharacter.generated.h"

// 인터페이스 상속 추가
UCLASS()
class GASDEMO_API AGASCharacter : public ACharacter, public IAbilitySystemInterface // 이것을 상속받지 않으면, 일부 내부 시스템 처리에 불리
{
    GENERATED_BODY()

public:
    AGASCharacter();

    // 인터페이스 필수 구현 함수
    virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;

protected:
    virtual void BeginPlay() override;

    // ASC 선언
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "GAS")
    TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
};

2. GASCharacter.cpp

  • ASC를 생성하고 초기화합니다.
#include "GASCharacter.h"

AGASCharacter::AGASCharacter()
{
    PrimaryActorTick.bCanEverTick = false;
    
    // ASC 생성
    AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
    // 멀티플레이어 동기화 설정
    AbilitySystemComponent->SetIsReplicated(true);
    AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
}

UAbilitySystemComponent* AGASCharacter::GetAbilitySystemComponent() const
{
    return AbilitySystemComponent;
}

void AGASCharacter::BeginPlay()
{
    Super::BeginPlay();

    // ASC 초기화 (Owner와 Avatar 설정)
    if (AbilitySystemComponent)
    {
        // 초기화 안하면 ASC 멈춤...
        AbilitySystemComponent->InitAbilityActorInfo(this, this);
    }
}
  • 멀티 플레이를 위해 Replicate 설정
    • Player 캐릭터라면 Mixed 모드 추천
      (클라에서 이펙트 등은 재생하되, 결과를 서버와 맞춤)

Step 3. 스탯 만들기

  • ASC가 관리할 AttributeSet을 만들어 보자. 체력(Health)을 예시로 작성

1. MyAttributeSet.h

  • 편리한 사용을 위해 매크로를 정의하고, 변수(FGameplayAttributeData)를 선언
#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();

    // 네트워크 복제를 위한 함수 오버라이드
    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

    // 체력(Health) 속성 정의
    UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "Vital")
    FGameplayAttributeData Health;
    ATTRIBUTE_ACCESSORS(UMyAttributeSet, Health) // 매크로 적용

    // 최대 체력(MaxHealth) 속성 정의
    UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxHealth, Category = "Vital")
    FGameplayAttributeData MaxHealth;
    ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxHealth)

protected:
    // 값이 변경될 때 클라이언트에서 호출될 함수
    UFUNCTION()
    void OnRep_Health(const FGameplayAttributeData& OldValue);

    UFUNCTION()
    void OnRep_MaxHealth(const FGameplayAttributeData& OldValue);
};
  • 하나의 스탯 개념을 위하여
    클래스를 만들어야 해줌

2. MyAttributeSet.cpp

  • 생성자에서 초기값을 잡고, 리플리케이션(동기화) 로직을 연결
#include "MyAttributeSet.h"
#include "Net/UnrealNetwork.h" // DOREPLIFETIME 매크로용

UMyAttributeSet::UMyAttributeSet()
{
    // 기본값 초기화 (매크로로 생성된 함수들)
    InitHealth(100.0f);
    InitMaxHealth(100.0f);
}

void UMyAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    // 네트워크 동기화 등록
    DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Health, COND_None, REPNOTIFY_Always);
    DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
}

void UMyAttributeSet::OnRep_Health(const FGameplayAttributeData& OldValue)
{
    // GAS 시스템에 값 변경 알림 (네트워크!)
    GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Health, OldValue);
}

void UMyAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldValue)
{
    GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, MaxHealth, OldValue);
}
  • GAS에선 예측 시스템이 존재하기에
    DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Health, COND_None, REPNOTIFY_Always);
    이 방식을 사용함

3. 캐릭터 (GASCharacter) 업데이트

  • 이제 만든 AttributeSet을 캐릭터에 붙여줌.
// GASCharacter.h (추가)
class UMyAttributeSet; // 전방 선언

// ... 기존 코드 ...
protected:
    // AttributeSet 포인터 추가
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "GAS")
    TObjectPtr<UMyAttributeSet> AttributeSet;
// GASCharacter.cpp (추가)
#include "MyAttributeSet.h" // 헤더 포함

AGASCharacter::AGASCharacter()
{
    // ... ASC 생성 코드 ...
    
    // AttributeSet 생성
    AttributeSet = CreateDefaultSubobject<UMyAttributeSet>(TEXT("AttributeSet"));
}
  • AttributeSet을 가짐으로서 그 내부 스탯들을 관리하는 Set을 가지게 됨

Step 4. 대시 스킬 구현

  • 마지막으로, 입력(Space Bar)을 받으면 앞으로 튀어 나가는 Dash Ability를 만들고 연결함.

1. DashAbility.h

  • GAS의 UGameplayAbility를 상속받아 대시 로직을 정의함.
// 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:
    // 어빌리티 실행 로직 (Activate)
    virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;

    // 어빌리티 종료 로직 (End)
    virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled) override;

    // Task 완료 콜백 함수
    UFUNCTION()
    void OnDashFinished();

protected:
    UPROPERTY(EditDefaultsOnly, Category = "Dash")
    float DashDistance = 1000.0f;

    UPROPERTY(EditDefaultsOnly, Category = "Dash")
    float DashDuration = 0.2f;
};

2. DashAbility.cpp

  • Activate -> Commit -> RootMotion -> End로 이어지는 흐름을 구현하기
// DashAbility.cpp
#include "DashAbility.h"
#include "GameFramework/Character.h"
#include "Abilities/Tasks/AbilityTask_ApplyRootMotionConstantForce.h"
#include "Abilities/Tasks/AbilityTask_WaitDelay.h"

UDashAbility::UDashAbility()
{
    // 중요: 캐릭터마다 별도 인스턴스 생성 (변수 저장 위함)
    // 액터별 할당
    InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}

void UDashAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
    // 1. 쿨타임/비용 확인 및 적용 (Commit) - 조건 확인
    if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
    {
        EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
        return;
    }

    // 2. 캐릭터 방향 계산
    ACharacter* Character = CastChecked<ACharacter>(ActorInfo->AvatarActor);
    FVector DashDir = Character->GetActorForwardVector();

    // 3. 루트 모션 태스크 실행 (힘 적용) - 캐릭터를 밀어주는 엔진
    // (Launch 안쓰는 이유는 이 함수가 멀티플레이 동기화를 해주기에)
    UAbilityTask_ApplyRootMotionConstantForce* RootMotionTask = UAbilityTask_ApplyRootMotionConstantForce::ApplyRootMotionConstantForce(
        this, 
        FName("Dash"), 
        DashDir, 
        DashDistance / DashDuration, // 속도 = 거리 / 시간
        DashDuration, 
        false, nullptr, 
        ERootMotionFinishVelocityMode::MaintainLastRootMotionVelocity, 
        FVector::ZeroVector, 
        0.f, true
    );
    
    if (RootMotionTask)
    {
        RootMotionTask->ReadyForActivation();
    }

    // 4. 대기 태스크 실행 (시간 경과 후 종료)
    UAbilityTask_WaitDelay* DelayTask = UAbilityTask_WaitDelay::WaitDelay(this, DashDuration);
    if (DelayTask)
    {
        DelayTask->OnFinish.AddDynamic(this, &UDashAbility::OnDashFinished);
        DelayTask->ReadyForActivation();
    }
}

void UDashAbility::OnDashFinished()
{
    // 5. 정상 종료
    EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}

void UDashAbility::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
    Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
    // (루트 모션 태스크는 Ability 종료 시 자동으로 정리됨)
}

3. 캐릭터(GASCharacter) 최종 업데이트: 입력 바인딩

  • 이제 스킬을 캐릭터에 등록(GiveAbility)하고, 입력과 연결
// GASCharacter.h (최종)
class UInputAction; // 전방 선언

// ... 기존 코드 ...

protected:
    // 시작 시 부여할 어빌리티 목록
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GAS")
    TArray<TSubclassOf<UGameplayAbility>> StartupAbilities;
    
    // 대시 입력 액션 (IA_Dash)
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
    TObjectPtr<UInputAction> IA_Dash;

    // 입력 처리 함수
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
    void HandleDashInput();
// GASCharacter.cpp (최종)
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"

void AGASCharacter::BeginPlay()
{
    Super::BeginPlay();

    if (AbilitySystemComponent)
    {
        AbilitySystemComponent->InitAbilityActorInfo(this, this);

        // 서버 권한(Authority)이 있을 때만 스킬 부여!
        if (HasAuthority())
        {
            for (TSubclassOf<UGameplayAbility> AbilityClass : StartupAbilities)
            {
                AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(AbilityClass, 1));
            }
        }
    }
}

void AGASCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

    // Enhanced Input 바인딩
    if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
    {
        EnhancedInputComponent->BindAction(IA_Dash, ETriggerEvent::Started, this, &AGASCharacter::HandleDashInput);
    }
}

void AGASCharacter::HandleDashInput()
{
    // Tag를 사용하여 Ability 실행 요청
    // (StartupAbilities에 DashAbility가 등록되어 있어야 함)
    FGameplayTagContainer TagContainer;
    TagContainer.AddTag(FGameplayTag::RequestGameplayTag(FName("Ability.Action.Dash"))); // 태그는 예시로 넣은것
}

Step 5. 에디터 세팅과 테스트

  1. 블루프린트 생성 (상속)
    • C++ AGASCharacter를 상속받는 BP_GASCharacter를 생성
    • C++ UDashAbility를 상속받는 GA_Dash를 생성
  2. 데이터 연결 (Configuration)
    • GA_Dash 파일을 열어서 Dash Distance를 1000으로, Dash Duration을 0.2로 설정 (기획자의 영역)
    • BP_GASCharacter를 열어서 Startup Abilities 목록에 GA_Dash를 등록
  3. 입력 시스템 설정 (Enhanced Input)
    • Input Action (IA_Dash) 에셋을 생성
    • Input Mapping Context (IMC_Default)를 만들고 원하는 키를 연결
    • 이걸 캐릭터 블루프린트의 슬롯에 끼워 넣기
  4. Play In Editor (PIE)
    • 게임을 실행하고 스페이스바를 눌러 캐릭터가 실제로 튀어나가는지 확인합니다.
  5. ShowDebug AbilitySystem (GAS의 신의 명령어)
    • 콘솔(~ 키)을 열고 이 명령어를 쳐보자!
    • 그러면 화면 옆에 ASC의 내부 상태(현재 체력, 적용된 태그, 실행 중인 스킬)가 실시간 텍스트로 뜸.
    • “대시를 쓸 때 Ability.Action.Dash 태그가 켜졌다가 꺼지는가?” 를 눈으로 확인

3. 전체 완성 코드 😶‍🌫️

Build.cs 설정

using UnrealBuildTool;

public class GASDemo : ModuleRules
{
	public GASDemo(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[] 
		{ 
			"Core", 
			"CoreUObject", 
			"Engine", 
			"InputCore", 
			"EnhancedInput",
			// ★ GAS 필수 3대장 모듈 확인
			"GameplayAbilities", 
			"GameplayTags", 
			"GameplayTasks" 
		});

		PrivateDependencyModuleNames.AddRange(new string[] { });
	}
}
  • 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;
class UInputAction;
class UInputMappingContext;
class UGameplayAbility;

UCLASS()
class GASDEMO_API AGASCharacter : public ACharacter, public IAbilitySystemInterface
{
	GENERATED_BODY()

public:
	AGASCharacter();

	// ★ IAbilitySystemInterface 구현 (Getter)
	virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;

protected:
	virtual void BeginPlay() override;
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

public:
	// ASC 컴포넌트
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "GAS")
	TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;

	// AttributeSet
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "GAS")
	TObjectPtr<UMyAttributeSet> AttributeSet;

protected:
	// 시작 시 부여할 스킬 목록
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GAS")
	TArray<TSubclassOf<UGameplayAbility>> StartupAbilities;

	// 대시 스킬 (클래스 정보)
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "GAS")
	TSubclassOf<UGameplayAbility> DashAbilityClass;

	// --- Enhanced Input ---
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	TObjectPtr<UInputMappingContext> DefaultMappingContext;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	TObjectPtr<UInputAction> IA_Move;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	TObjectPtr<UInputAction> IA_Look;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	TObjectPtr<UInputAction> IA_Dash;

	// 입력 함수
	void Move(const struct FInputActionValue& Value);
	void Look(const struct FInputActionValue& Value);
	void Dash(const struct FInputActionValue& Value);
};

2. GASCharacter.cpp

#include "GASCharacter.h"
#include "MyAttributeSet.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "GameFramework/CharacterMovementComponent.h"

AGASCharacter::AGASCharacter()
{
	PrimaryActorTick.bCanEverTick = false;
	bReplicates = true;

	// 1. ASC 생성 (Mixed 모드는 멀티플레이어 PlayerState용 표준)
	AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
	AbilitySystemComponent->SetIsReplicated(true);
	AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);

	// 2. AttributeSet 생성
	AttributeSet = CreateDefaultSubobject<UMyAttributeSet>(TEXT("AttributeSet"));
}

UAbilitySystemComponent* AGASCharacter::GetAbilitySystemComponent() const
{
	return AbilitySystemComponent;
}

void AGASCharacter::BeginPlay()
{
	Super::BeginPlay();

	// 3. ASC 초기화 (OwnerActor = Self, AvatarActor = Self)
	if (AbilitySystemComponent)
	{
		AbilitySystemComponent->InitAbilityActorInfo(this, this);

		// 스킬 부여는 반드시 서버(Authority)에서만 실행!
		if (HasAuthority())
		{
			for (TSubclassOf<UGameplayAbility> AbilityClass : StartupAbilities)
			{
				if (AbilityClass)
				{
					AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(AbilityClass, 1));
				}
			}
		}
	}
}

void AGASCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	// 4. 입력 매핑 및 바인딩 (이곳이 가장 안전한 위치입니다)
	if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			Subsystem->AddMappingContext(DefaultMappingContext, 0);
		}
	}

	if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
	{
		EnhancedInputComponent->BindAction(IA_Move, ETriggerEvent::Triggered, this, &AGASCharacter::Move);
		EnhancedInputComponent->BindAction(IA_Look, ETriggerEvent::Triggered, this, &AGASCharacter::Look);
		EnhancedInputComponent->BindAction(IA_Dash, ETriggerEvent::Started, this, &AGASCharacter::Dash);
	}
}

void AGASCharacter::Move(const FInputActionValue& Value)
{
	FVector2D MovementVector = Value.Get<FVector2D>();
	if (Controller != nullptr)
	{
		const FRotator Rotation = Controller->GetControlRotation();
		const FRotator YawRotation(0, Rotation.Yaw, 0);
		const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
		const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

		AddMovementInput(ForwardDirection, MovementVector.Y);
		AddMovementInput(RightDirection, MovementVector.X);
	}
}

void AGASCharacter::Look(const FInputActionValue& Value)
{
	FVector2D LookAxisVector = Value.Get<FVector2D>();
	if (Controller != nullptr)
	{
		AddControllerYawInput(LookAxisVector.X);
		AddControllerPitchInput(LookAxisVector.Y);
	}
}

void AGASCharacter::Dash(const FInputActionValue& Value)
{
	// 5. ASC를 통해 대시 스킬 발동 요청
	// (실무에선 태그로 하지만, 입문 강의에선 Class 기반 실행이 가장 직관적임)
	if (AbilitySystemComponent && DashAbilityClass)
	{
		AbilitySystemComponent->TryActivateAbilityByClass(DashAbilityClass);
	}
}

3. MyAttributeSet.h

#pragma once

#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "MyAttributeSet.generated.h"

// 편의성 매크로 (보일러플레이트 코드 제거용)
#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();
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

	// Health
	UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "Vital")
	FGameplayAttributeData Health;
	ATTRIBUTE_ACCESSORS(UMyAttributeSet, Health)

	// MaxHealth
	UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxHealth, Category = "Vital")
	FGameplayAttributeData MaxHealth;
	ATTRIBUTE_ACCESSORS(UMyAttributeSet, MaxHealth)

protected:
	UFUNCTION()
	void OnRep_Health(const FGameplayAttributeData& OldValue);

	UFUNCTION()
	void OnRep_MaxHealth(const FGameplayAttributeData& OldValue);

	// 값 변경 전 보정(Clamping)
	virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
};

4. MyAttributeSet.cpp

#include "MyAttributeSet.h"
#include "Net/UnrealNetwork.h"

UMyAttributeSet::UMyAttributeSet()
{
	InitHealth(100.0f);
	InitMaxHealth(100.0f);
}

void UMyAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	// REPNOTIFY_Always: 값이 롤백되어도 UI 갱신을 보장함
	DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Health, COND_None, REPNOTIFY_Always);
	DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always);
}

void UMyAttributeSet::OnRep_Health(const FGameplayAttributeData& OldValue)
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Health, OldValue);
}

void UMyAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldValue)
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, MaxHealth, OldValue);
}

void UMyAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
	Super::PreAttributeChange(Attribute, NewValue);

	// 체력이 0 ~ MaxHealth 범위를 벗어나지 않도록 강제 (Clamping)
	if (Attribute == GetHealthAttribute())
	{
		NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxHealth());
	}
}

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:
	// 어빌리티 활성화 (실행)
	virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;

	// 어빌리티 종료
	virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled) override;

	// 태스크 완료 콜백
	UFUNCTION()
	void OnDashFinished();

protected:
	UPROPERTY(EditDefaultsOnly, Category = "Dash")
	float DashDistance = 1000.0f;

	UPROPERTY(EditDefaultsOnly, Category = "Dash")
	float DashDuration = 0.2f;
};

6. DashAbility.cpp

#include "DashAbility.h"
#include "GameFramework/Character.h"
#include "Abilities/Tasks/AbilityTask_ApplyRootMotionConstantForce.h"
#include "Abilities/Tasks/AbilityTask_WaitDelay.h"

UDashAbility::UDashAbility()
{
	// 캐릭터마다 개별 상태 저장을 위해 Instancing 필수
	InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}

void UDashAbility::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;
	}

	ACharacter* Character = CastChecked<ACharacter>(ActorInfo->AvatarActor);
	FVector DashDir = Character->GetActorForwardVector();

	// 2. 루트 모션 태스크 (캐릭터 이동 힘 적용)
	UAbilityTask_ApplyRootMotionConstantForce* RootMotionTask = UAbilityTask_ApplyRootMotionConstantForce::ApplyRootMotionConstantForce(
		this,
		FName("Dash"),
		DashDir,
		DashDistance / DashDuration,
		DashDuration,
		false, nullptr,
		ERootMotionFinishVelocityMode::MaintainLastRootMotionVelocity,
		FVector::ZeroVector,
		0.f, true
	);

	if (RootMotionTask)
	{
		RootMotionTask->ReadyForActivation();
	}

	// 3. 딜레이 태스크 (대시 지속시간만큼 대기)
	UAbilityTask_WaitDelay* DelayTask = UAbilityTask_WaitDelay::WaitDelay(this, DashDuration);
	if (DelayTask)
	{
		DelayTask->OnFinish.AddDynamic(this, &UDashAbility::OnDashFinished);
		DelayTask->ReadyForActivation();
	}
}

void UDashAbility::OnDashFinished()
{
	// 4. 종료 처리
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}

void UDashAbility::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
	Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}

댓글남기기