PART 4 - 강의 4/8
GameplayAbility 개발
스킬과 액션 시스템의 핵심 구현
01
GameplayAbility 기본 클래스
프로젝트 공통 베이스 어빌리티
MyGameplayAbility.h
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "MyGameplayAbility.generated.h"
/**
* 프로젝트 공통 GameplayAbility 베이스 클래스
*/
UCLASS()
class MYGAME_API UMyGameplayAbility : public UGameplayAbility
{
GENERATED_BODY()
public:
UMyGameplayAbility();
// 입력 바인딩용 ID
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Ability")
int32 AbilityInputID = 0;
// 자동 활성화 레벨
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Ability")
int32 RequiredLevel = 1;
// 활성화 시 자동으로 적용할 태그
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Ability")
FGameplayTagContainer ActivationOwnedTags;
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;
// 어빌리티 활성화 가능 여부
virtual bool CanActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayTagContainer* SourceTags,
const FGameplayTagContainer* TargetTags,
FGameplayTagContainer* OptionalRelevantTags) const override;
// 헬퍼 함수들
UFUNCTION(BlueprintCallable, Category = "Ability")
ACharacter* GetCharacterFromActorInfo() const;
UFUNCTION(BlueprintCallable, Category = "Ability")
APlayerController* GetPlayerControllerFromActorInfo() const;
UFUNCTION(BlueprintCallable, Category = "Ability")
UAbilitySystemComponent* GetASCFromActorInfo() const;
};
MyGameplayAbility.cpp
#include "MyGameplayAbility.h"
#include "AbilitySystemComponent.h"
#include "GameFramework/Character.h"
#include "GameFramework/PlayerController.h"
UMyGameplayAbility::UMyGameplayAbility()
{
// 인스턴싱 정책: 액터당 하나의 인스턴스
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
// 네트워크 실행 정책: 로컬 예측 (반응성 향상)
NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
// 기본 취소 및 차단 태그 설정
// - Dead 상태에서 차단
// - 같은 어빌리티 실행 중 차단
}
void UMyGameplayAbility::ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
// 비용과 쿨다운 커밋
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
// 활성화 태그 추가
if (UAbilitySystemComponent* ASC = GetASCFromActorInfo())
{
ASC->AddLooseGameplayTags(ActivationOwnedTags);
}
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
}
void UMyGameplayAbility::EndAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
bool bReplicateEndAbility,
bool bWasCancelled)
{
// 활성화 태그 제거
if (UAbilitySystemComponent* ASC = GetASCFromActorInfo())
{
ASC->RemoveLooseGameplayTags(ActivationOwnedTags);
}
Super::EndAbility(Handle, ActorInfo, ActivationInfo,
bReplicateEndAbility, bWasCancelled);
}
bool UMyGameplayAbility::CanActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayTagContainer* SourceTags,
const FGameplayTagContainer* TargetTags,
FGameplayTagContainer* OptionalRelevantTags) const
{
if (!Super::CanActivateAbility(Handle, ActorInfo, SourceTags,
TargetTags, OptionalRelevantTags))
{
return false;
}
// 추가 조건 검사 (예: 레벨 체크)
// ...
return true;
}
ACharacter* UMyGameplayAbility::GetCharacterFromActorInfo() const
{
return Cast<ACharacter>(GetAvatarActorFromActorInfo());
}
APlayerController* UMyGameplayAbility::GetPlayerControllerFromActorInfo() const
{
if (const FGameplayAbilityActorInfo* ActorInfo = GetCurrentActorInfo())
{
return ActorInfo->PlayerController.Get();
}
return nullptr;
}
UAbilitySystemComponent* UMyGameplayAbility::GetASCFromActorInfo() const
{
if (const FGameplayAbilityActorInfo* ActorInfo = GetCurrentActorInfo())
{
return ActorInfo->AbilitySystemComponent.Get();
}
return nullptr;
}
02
근접 공격 어빌리티
몽타주와 히트 감지 연동
MeleeAttackAbility.h
#pragma once
#include "CoreMinimal.h"
#include "MyGameplayAbility.h"
#include "MeleeAttackAbility.generated.h"
UCLASS()
class MYGAME_API UMeleeAttackAbility : public UMyGameplayAbility
{
GENERATED_BODY()
public:
UMeleeAttackAbility();
protected:
virtual void ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
// 적용할 데미지 이펙트
UPROPERTY(EditDefaultsOnly, Category = "Damage")
TSubclassOf<UGameplayEffect> DamageEffect;
// 공격 몽타주
UPROPERTY(EditDefaultsOnly, Category = "Animation")
UAnimMontage* AttackMontage;
// 공격 범위
UPROPERTY(EditDefaultsOnly, Category = "Combat")
float AttackRange = 200.0f;
UPROPERTY(EditDefaultsOnly, Category = "Combat")
float AttackRadius = 50.0f;
// 기본 데미지 (SetByCaller로 전달)
UPROPERTY(EditDefaultsOnly, Category = "Combat")
float BaseDamage = 20.0f;
private:
// 몽타주 완료 콜백
UFUNCTION()
void OnMontageCompleted();
UFUNCTION()
void OnMontageCancelled();
// 히트 감지 (AnimNotify에서 호출)
UFUNCTION()
void PerformAttackTrace();
};
MeleeAttackAbility.cpp
#include "MeleeAttackAbility.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "Abilities/Tasks/AbilityTask_PlayMontageAndWait.h"
#include "Abilities/Tasks/AbilityTask_WaitGameplayEvent.h"
#include "Kismet/KismetSystemLibrary.h"
UMeleeAttackAbility::UMeleeAttackAbility()
{
AbilityInputID = 1; // 기본 공격 슬롯
}
void UMeleeAttackAbility::ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
// 몽타주 재생 태스크
if (AttackMontage)
{
UAbilityTask_PlayMontageAndWait* MontageTask =
UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
this,
NAME_None,
AttackMontage,
1.0f // 재생 속도
);
MontageTask->OnCompleted.AddDynamic(this,
&UMeleeAttackAbility::OnMontageCompleted);
MontageTask->OnInterrupted.AddDynamic(this,
&UMeleeAttackAbility::OnMontageCancelled);
MontageTask->OnCancelled.AddDynamic(this,
&UMeleeAttackAbility::OnMontageCancelled);
MontageTask->ReadyForActivation();
}
// GameplayEvent 대기 (AnimNotify에서 트리거)
UAbilityTask_WaitGameplayEvent* EventTask =
UAbilityTask_WaitGameplayEvent::WaitGameplayEvent(
this,
FGameplayTag::RequestGameplayTag(FName("Event.Montage.AttackHit"))
);
EventTask->EventReceived.AddDynamic(this,
&UMeleeAttackAbility::OnAttackHitEvent);
EventTask->ReadyForActivation();
}
void UMeleeAttackAbility::PerformAttackTrace()
{
AActor* AvatarActor = GetAvatarActorFromActorInfo();
if (!AvatarActor) return;
FVector Start = AvatarActor->GetActorLocation();
FVector End = Start + AvatarActor->GetActorForwardVector() * AttackRange;
TArray<FHitResult> HitResults;
TArray<AActor*> IgnoreActors;
IgnoreActors.Add(AvatarActor);
// 스피어 트레이스
bool bHit = UKismetSystemLibrary::SphereTraceMulti(
GetWorld(),
Start,
End,
AttackRadius,
UEngineTypes::ConvertToTraceType(ECC_Pawn),
false,
IgnoreActors,
EDrawDebugTrace::ForDuration, // 디버그용
HitResults,
true
);
if (bHit && DamageEffect)
{
for (const FHitResult& Hit : HitResults)
{
AActor* HitActor = Hit.GetActor();
if (!HitActor) continue;
// 대상 ASC 가져오기
UAbilitySystemComponent* TargetASC =
UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitActor);
if (TargetASC)
{
// 데미지 이펙트 스펙 생성
FGameplayEffectSpecHandle SpecHandle =
MakeOutgoingGameplayEffectSpec(DamageEffect, GetAbilityLevel());
if (SpecHandle.IsValid())
{
// SetByCaller로 데미지 값 전달
SpecHandle.Data->SetSetByCallerMagnitude(
FGameplayTag::RequestGameplayTag(FName("Data.Damage")),
BaseDamage
);
// 대상에게 이펙트 적용
ApplyGameplayEffectSpecToTarget(
CurrentSpecHandle,
CurrentActorInfo,
CurrentActivationInfo,
SpecHandle,
TargetASC
);
}
}
}
}
}
void UMeleeAttackAbility::OnMontageCompleted()
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo,
true, false);
}
void UMeleeAttackAbility::OnMontageCancelled()
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo,
true, true);
}
03
인스턴싱 정책
어빌리티 인스턴스 관리 전략
| 정책 | 설명 | 사용 사례 |
|---|---|---|
| NonInstanced | CDO만 사용, 인스턴스 없음 | 단순한 즉시 효과 |
| InstancedPerActor | 액터당 하나의 인스턴스 | 일반적인 스킬 (권장) |
| InstancedPerExecution | 실행마다 새 인스턴스 | 동시 활성화 필요 시 |
권장 설정
InstancedPerActor가 대부분의 경우에 적합합니다. 멤버 변수를 사용할 수 있고, 메모리 효율적이며, AbilityTask를 사용할 수 있습니다.
04
네트워크 실행 정책
멀티플레이어 어빌리티 실행 전략
| 정책 | 실행 위치 | 사용 사례 |
|---|---|---|
| LocalPredicted | 클라이언트 예측 + 서버 확인 | 일반 스킬 (권장) |
| LocalOnly | 로컬 클라이언트만 | UI 관련 어빌리티 |
| ServerInitiated | 서버에서 시작 | AI 어빌리티 |
| ServerOnly | 서버에서만 실행 | 치트 방지 필요 시 |
C++
// 어빌리티 생성자에서 설정
UMyAbility::UMyAbility()
{
// 로컬 예측: 클라이언트에서 즉시 실행, 서버가 확인
// 반응성 좋음, 롤백 가능
NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
// 보안이 중요한 어빌리티는 ServerOnly 사용
// NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::ServerOnly;
}
SUMMARY
핵심 요약
- 프로젝트 공통 베이스 어빌리티 클래스로 헬퍼 함수 제공
- CommitAbility()로 비용과 쿨다운 처리 후 진행
- AbilityTask로 비동기 작업 (몽타주, 이벤트 대기) 처리
- InstancedPerActor와 LocalPredicted가 일반적으로 권장
- SetByCaller로 동적 데미지 값 전달
PRACTICE
도전 과제
배운 내용을 직접 실습해보세요
실습 1: 기본 공격 어빌리티 구현
UGameplayAbility를 상속하여 GA_MeleeAttack을 구현하세요. ActivateAbility()에서 몽타주 재생, CommitAbility()로 코스트 소모, EndAbility()로 종료하는 전체 플로우를 완성하세요.
실습 2: 어빌리티 태그 기반 차단 시스템
GameplayTag를 활용하여 특정 어빌리티 실행 중 다른 어빌리티를 차단하세요. AbilityTags, CancelAbilitiesWithTag, BlockAbilitiesWithTag를 설정하여 공격 중 대시 불가 등의 규칙을 구현하세요.
심화 과제: 콤보 어빌리티 시스템
AbilityTask(WaitGameplayEvent, WaitInputPress, PlayMontageAndWait)를 조합하여 RPG 근접 콤보 시스템을 구현하세요. 타이밍 입력에 따라 다음 콤보 단계로 진행하고, 실패 시 콤보가 초기화되는 로직을 만드세요.