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로 비동기 작업 (몽타주, 이벤트 대기) 처리
  • InstancedPerActorLocalPredicted가 일반적으로 권장
  • SetByCaller로 동적 데미지 값 전달
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: 기본 공격 어빌리티 구현

UGameplayAbility를 상속하여 GA_MeleeAttack을 구현하세요. ActivateAbility()에서 몽타주 재생, CommitAbility()로 코스트 소모, EndAbility()로 종료하는 전체 플로우를 완성하세요.

실습 2: 어빌리티 태그 기반 차단 시스템

GameplayTag를 활용하여 특정 어빌리티 실행 중 다른 어빌리티를 차단하세요. AbilityTags, CancelAbilitiesWithTag, BlockAbilitiesWithTag를 설정하여 공격 중 대시 불가 등의 규칙을 구현하세요.

심화 과제: 콤보 어빌리티 시스템

AbilityTask(WaitGameplayEvent, WaitInputPress, PlayMontageAndWait)를 조합하여 RPG 근접 콤보 시스템을 구현하세요. 타이밍 입력에 따라 다음 콤보 단계로 진행하고, 실패 시 콤보가 초기화되는 로직을 만드세요.