김하연 튜터님 강의 - ‘GAS 아키텍쳐 - GAS가 돌아가는 큰 그림 이해하기’
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 (적) ]
(내 스탯 변화) (상대 체력 감소)
-
Input Tag 전달: 플레이어의 입력은
Input.Action.Skill같은 태그 형태로 ASC에 전달 -
규칙 검사 (Policy Check): ASC는 현재 캐릭터의 상태(태그), 자원(마나), 쿨타임을 확인
-
실행 및 동기화: 조건이 충족되면 로직을 수행하고, 결과를 네트워크를 통해 모든 클라이언트에 전파함.
핵심 역할 상세
규칙 집행(Rule Enforcement)
- ASC는 어빌리티 실행 요청이 들어올 때마다 다음 3가지를 검사함.
- Can Activate: 현재 상태에서 실행 가능한가? (예: 공중에서 사용 불가)
- Cost Check: 필요한 자원이 있는가? (예: 마나 50 소모)
- Cooldown Check: 재사용 대기시간이 끝났는가?
- Can Activate: 현재 상태에서 실행 가능한가? (예: 공중에서 사용 불가)
- ASC는 어빌리티 실행 요청이 들어올 때마다 다음 3가지를 검사함.
네트워크 중개(Replication Bridge)
- 멀티플레이어 환경에서 복잡한 동기화 코드를 작성하지 않아도, ASC가 이를 대신 처리
Client Prediction: 클라이언트에서 먼저 연출을 실행하여 반응성을 높임.Server Authority: 서버에서 최종검증 후 결과를 확정.Automatic Replication: 어빌리티 실행 여부와 이펙트 적용 결과를 자동으로 다른 클라이언트에 전파.
- 멀티플레이어 환경에서 복잡한 동기화 코드를 작성하지 않아도, ASC가 이를 대신 처리
데미지 및 이펙트 파이프라인
- 외부에서 들어오는 모든 변화(데미지, 힐, 버프)는 ASC를 통해 Gameplay Effect(GE) 형태로 처리됨.
TakeDamage()함수 대신ApplyGameplayEffectToTarget()을 사용하여, 방어력 계산이나 무적 판정 등의 로직을 일관성 있게 처리함.
- 외부에서 들어오는 모든 변화(데미지, 힐, 버프)는 ASC를 통해 Gameplay Effect(GE) 형태로 처리됨.
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)을 결정할 때 엄격한 수학적 순서를 따릅니다.
계산 흐름도
[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를 메모리에 어떻게 생성할지 결정하는 중요한 최적화 옵션임.
Instanced Per Actor(기본값)
- 캐릭터마다 별도의 GA 객체 생성.
- 복잡한 로직, 변수 저장이 필요한 스킬에 사용.
- 캐릭터마다 별도의 GA 객체 생성.
Non-Instanced(최적화)
- 모든 캐릭터가 하나의 GA 객체(CDO)를 공유.
- 변수를 저장할 수 없으나 메모리 비용이 매우 낮음.
- 단순한 평타, 점프 등에 권장 (포트나이트 방식).
- 모든 캐릭터가 하나의 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: 덮어쓰기 (특정 값으로 고정)
- Add: 더하기 (데미지는 음수 값을 더함)
- Value: 적용할 수치 입력 (예:
10.0)
- Attribute: 변경할 속성 선택 (예:
모듈형 확장
UE 5.5부터 GE는 컴포넌트 조립 방식(Modular)으로 기능을 확장됨.
- Target Tags: 효과 적용 시 특정 태그 부착 (예:
State.Debuff.Poison). - Grant Abilities: 효과 적용 중 특정 어빌리티 부여 (예: 독에 걸리면 ‘해독’ 스킬 사용 가능).
- Immunity: 특정 효과에 대한 면역 부여.
이 외 고급 기능 (Automation)
- 복잡한 게임 로직을 코드 없이 옵션 설정만으로 구현합니다.
- Period (주기적 실행):
Duration시간 동안X초마다 효과를 반복 실행 - Stacking (중첩): 동일한 효과가 여러 번 적용될 때의 규칙 설정.
- Stack Limit (최대 중첩 수)
- Duration Refresh (시간 갱신 여부)
- Expiration Policy (만료 시 처리)
- Stack Limit (최대 중첩 수)
- Period (주기적 실행):
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. 플러그인 활성화
- 에디터 상단 메뉴 Edit → Plugins
- 검색창에 Gameplay Abilities 입력
- Gameplay Abilities 체크박스 활성화
- 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 모드 추천
(클라에서 이펙트 등은 재생하되, 결과를 서버와 맞춤)
- 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. 에디터 세팅과 테스트
- 블루프린트 생성 (상속)
- C++
AGASCharacter를 상속받는BP_GASCharacter를 생성 - C++
UDashAbility를 상속받는GA_Dash를 생성
- C++
- 데이터 연결 (Configuration)
GA_Dash파일을 열어서Dash Distance를 1000으로,Dash Duration을 0.2로 설정 (기획자의 영역)BP_GASCharacter를 열어서Startup Abilities목록에GA_Dash를 등록
- 입력 시스템 설정 (Enhanced Input)
- Input Action (
IA_Dash) 에셋을 생성 - Input Mapping Context (
IMC_Default)를 만들고 원하는 키를 연결 - 이걸 캐릭터 블루프린트의 슬롯에 끼워 넣기
- Input Action (
- Play In Editor (PIE)
- 게임을 실행하고 스페이스바를 눌러 캐릭터가 실제로 튀어나가는지 확인합니다.
- 게임을 실행하고 스페이스바를 눌러 캐릭터가 실제로 튀어나가는지 확인합니다.
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);
}
댓글남기기