게임플레이 어빌리티(Gameplay Ability) 는 UGameplayAbility 클래스에서 파생된 것으로, 게임 내 어빌리티가 하는 일, 사용 비용(있는 경우), 사용할 수 있는 상황 및 시점 등을 정의합니다. 게임플레이 어빌리티는 비동기 실행되는 인스턴스된 오브젝트로 존재할 수 있기에 캐릭터 애니메이션, 파티클과 사운드 이펙트, 플레이어 입력이나 실행 도중 벌어지는 캐릭터 상호작용에 따른 분기처럼 전문화된 다단계 태스크를 실행할 수 있습니다. 네트워크를 통한 자체 리플리케이트나, (클라이언트 측 예측 지원을 포함해서) 클라이언트 또는 서버 머신에서의 실행이나, 심지어 변수 동기화 및 원격 프로시저 콜(Remote Procedure Calls, RPC) 호출도 가능합니다. 또한 쿨다운 및 사용 비용, 플레이어 입력, 애님 몽타주가 있는 애니메이션, 액터에 부여되는 어빌리티 자체에 대한 반응을 구현하는 확장 가능한 함수 기능 등 게임 세션 중에 엔진이 게임플레이 어빌리티를 구현하는 방식에 있어 유연성을 제공합니다.
어빌리티 부여 및 철회
액터가 어빌리티를 사용하기 전에 어빌리티 시스템 컴포넌트에 해당 어빌리티를 부여해야 합니다. 다음 어빌리티 시스템 컴포넌트 함수는 어빌리티에 액세스를 부여합니다.
-
GiveAbility:FGameplayAbilitySpec으로 추가할 어빌리티를 나타내며,FGameplayAbilitySpecHandle을 반환합니다. -
GiveAbilityAndActivateOnce:FGameplayAbilitySpec으로 추가할 어빌리티를 나타내며,FGameplayAbilitySpecHandle을 반환합니다. 어빌리티는 반드시 인스턴스되고 서버에서 실행할 수 있어야 합니다. 서버에서 어빌리티 실행을 시도한 후에는FGameplayAbilitySpecHandle이 반환됩니다. 어빌리티가 필수 조건을 충족하지 못했거나 실행할 수 없었던 경우, 반환 값은 유효하지 않게 되어 어빌리티 시스템 컴포넌트가 어빌리티를 부여받지 못합니다.
다음은 어빌리티 시스템 컴포넌트에서 어빌리티에 대한 액세스를 철회하는 함수로, 어빌리티를 부여받았을 때 반환된 FGameplayAbilitySpecHandle 을 사용합니다.
-
ClearAbility: 지정한 어빌리티를 어빌리티 시스템 컴포넌트에서 제거합니다. -
SetRemoveAbilityOnEnd: 지정한 어빌리티 실행이 완료되면 어빌리티 시스템 컴포넌트에서 제거합니다. 어빌리티가 실행 중이지 않은 경우 즉시 제거합니다. 어빌리티가 실행 중인 경우, 그 입력을 즉시 지워 플레이어가 더 이상 재활성화 또는 상호작용하지 못하도록 합니다. -
ClearAllAbilities: 어빌리티 시스템 컴포넌트에서 모든 어빌리티를 제거합니다.FGameplayAbilitySpecHandle이 필요하지 않은 유일한 함수입니다.
기본 사용
게임플레이 어빌리티가 액터의 어빌리티 시스템 컴포넌트에 부여된 이후의 기본 실행 주기는 다음과 같습니다.
-
CanActivateAbility는 호출자가 어빌리티 실행을 시도하지 않아도 어빌리티 사용 가능 여부를 알려줍니다. 예를 들어, 유저 인터페이스에서 플레이어가 사용할 수 없는 아이콘을 회색 처리하여 비활성화하거나, 캐릭터에 사운드 또는 파티클 이펙트를 재생하여 특정 어빌리티를 사용할 수 있음을 표시해야 할 수도 있습니다. CallActivateAbility는 어빌리티에 관련된 게임 코드를 실행하지만, 어빌리티의 사용 가능 여부는 검사하지 않습니다. 이 함수는CanActivateAbility검사와 어빌리티 실행 사이에 약간의 로직이 필요한 경우에 주로 호출됩니다.-
사용자가 어빌리티의 커스텀 함수 기능으로 오버라이드해야 하는 메인 코드는 C++ 함수
ActivateAbility또는 블루프린트 이벤트 Activate Ability입니다. -
게임플레이 어빌리티는 액터나 컴포넌트와는 달리 주 작업을 '틱' 함수에서 수행하지 않습니다. 대신 활성화 중에 대부분의 작업을 비동기 처리하는 어빌리티 태스크를 실행합니다. 그런 다음 C++에서 델리게이트에 후킹하거나 블루프린트에서 출력 실행 핀에 노드를 연결하여 해당 태스크의 출력을 처리합니다.
-
CommitAbility는 Activate 내에서 호출한 경우 어빌리티 실행 비용을 적용합니다. 즉 게임플레이 어트리뷰트(Gameplay Attribute)에서 '마나', '스태미나' 등 게임 시스템에 맞는 리소스를 빼고 쿨다운을 적용합니다. -
CancelAbility는 어빌리티 취소 메커니즘을 제공합니다. 어빌리티의CanBeCanceled함수가 요청을 거부할 수 있습니다.CommitAbility와 달리, 이 함수는 어빌리티 자체 외부 호출자에서 사용할 수 있습니다. 취소가 성공하면 게임플레이 어빌리티 취소 시(On Gameplay Ability Cancelled)로 브로드캐스트한 뒤 해당 어빌리티를 종료하기 위한 표준 코드 경로로 들어가, 어빌리티에 특수한 클린업 코드를 실행할 기회를 주거나 정상적으로 종료했을 때와 다른 작동을 하도록 할 수 있습니다.
-
-
TryActivateAbility는 어빌리티를 실행하는 전형적인 방식입니다. 이 함수는CanActivateAbility를 호출해서 어빌리티를 즉시 실행할 수 있는지 여부를 판단하고, 가능하다면CallActivateAbility를 호출합니다. EndAbility(C++) 또는 End Ability 노드(블루프린트)는 어빌리티 실행을 마치면 어빌리티를 종료합니다. 어빌리티가 취소된 경우UGameplayAbility클래스에서 취소 프로세스의 일부로 자동 처리하지만, 다른 모든 경우에는 개발자가 C++ 함수를 호출하거나 어빌리티의 블루프린트 그래프에 노드를 추가해야 합니다. 어빌리티를 정상적으로 종료하지 못하면 게임플레이 어빌리티 시스템은 어빌리티가 아직 실행 중인 것으로 믿으며, 향후 해당 어빌리티 또는 그 어빌리티가 차단한 다른 어빌리티를 사용하지 못하게 될 수 있습니다. 예를 들어 게임에서 체력 회복약 마시기(Drink Health Potion)라는 게임플레이 어빌리티가 정상 종료되지 못한 경우, 해당 어빌리티를 사용하는 캐릭터는 다른 약 마시기, 질주하기, 사다리 오르기 등 체력 회복약 마시기가 방지하는 다른 액션을 취할 수 없게 됩니다. 이러한 어빌리티 차단은 게임플레이 어빌리티 시스템이 캐릭터가 아직 약을 마시는 중이라고 간주하므로 무한히 계속됩니다.
언리얼 엔진 프로젝트에서 게임플레이 어빌리티를 구성하는 방법에 대한 자세한 내용은 액션 RPG의 게임플레이 어빌리티를 확인하세요.
태그
게임플레이 태그(Gameplay Tag) 는 여러 게임플레이 어빌리티의 상호작용 방식을 결정하는 데 도움이 됩니다. 각 어빌리티에는 어빌리티의 비헤이비어에 영향을 미칠 수 있는 방식으로 어빌리티를 식별 및 분류하는 태그 세트가 있으며, 다른 어빌리티와의 인터랙션을 지원하기 위한 게임플레이 태그 컨테이너(Gameplay Tag Container) 및 게임플레이 태그 쿼리(Gameplay Tag Query)도 있습니다.
| 게임플레이 태그 변수 | 목적 |
|---|---|
| 태그로 어빌리티 취소(Cancel Abilities With Tag) | 이 어빌리티가 실행되는 동안 이미 실행 중인 어빌리티의 태그가 제공된 목록과 일치하면 취소합니다. |
| 태그로 어빌리티 차단(Block Abilities With Tag) | 이 어빌리티가 실행되는 동안 일치하는 태그가 있는 다른 어빌리티의 실행을 방지합니다. |
| 활성화 소유 태그(Activation Owned Tags) | 이 어빌리티가 실행되는 동안 해당 어빌리티의 오너에 이 태그 세트가 부여됩니다. |
| 활성화 필수 태그(Activation Required Tags) | 이 어빌리티는 활성 상태인 액터 또는 컴포넌트에 이러한 태그가 모두 있을 때만 활성화할 수 있습니다. |
| 활성화 차단 태그(Activation Blocked Tags) | 이 어빌리티는 활성 상태인 액터 또는 컴포넌트에 이러한 태그가 하나도 없을 때만 활성화할 수 있습니다. |
| 타깃 필수 태그(Target Required Tags) | 이 어빌리티는 타깃 액터 또는 컴포넌트에 이 태그가 모두 있을 때만 활성화할 수 있습니다. |
| 타깃 차단 태그(Target Blocked Tags) | 이 어빌리티는 타깃 액터 또는 컴포넌트에 이 태그가 하나도 없을 때만 활성화할 수 있습니다. |
리플리케이션
게임플레이 어빌리티는 내부 상태 및 게임플레이 이벤트 리플리케이션을 지원하며, 리플리케이션을 끄면 네트워크 대역폭과 CPU 사이클을 절약할 수 있습니다. 어빌리티의 게임플레이 어빌리티 리플리케이션 정책(Gameplay Ability Replication Policy) 을 '예' 또는 '아니요'로 설정하여 어빌리티가 네트워크를 통해 자체 인스턴스를 리플리케이트할지, 상태를 업데이트할지 또는 게임플레이 이벤트를 전송할지 제어할 수 있습니다. 리플리케이트하는 어빌리티가 있는 멀티플레이어 게임의 경우 리플리케이션 처리 방식에 대한 옵션이 몇 가지 있는데, 이를 게임플레이 네트워크 실행 정책(Gameplay Net Execution Policy) 이라고 합니다.
-
로컬 예측(Local Predicted): 반응성과 정확성 사이에 균형이 잘 잡힌 옵션입니다. 클라이언트가 명령을 내리면 로컬 클라이언트에서 어빌리티가 즉시 실행되지만, 어빌리티의 실제 영향이 어땠는지에 대한 최종 결정은 서버가 내리고 클라이언트를 오버라이드할 수 있습니다. 실제로 클라이언트는 서버에 어빌리티 실행 권한을 요청하지만, 클라이언트 관점의 결과를 서버가 동의할 것으로 예상하고 로컬에서 진행합니다. 클라이언트는 로컬에서 어빌리티의 비헤이비어를 예측하므로, 클라이언트의 예측이 서버와 모순되지 않는다면 지연 없이 완전히 매끄럽게 느껴질 것입니다.
-
로컬만(Local Only): 클라이언트가 단순히 로컬에서 어빌리티를 실행합니다. 서버에 리플리케이트하지 않지만, 해당 서버를 사용하는 클라이언트가 호스트이거나(물리적 서버 머신에서 플레이) 싱글 플레이어 게임이라면 서버에서도 실행되기는 합니다. 서버 머신에서 플레이하는 클라이언트가 없는 데디케이티드 서버 게임에는 적용되지 않습니다. 이 어빌리티로 클라이언트가 영향을 주는 모든 것은 일반 리플리케이션 프로토콜을 따르며, 여기에는 서버에서 보정을 받을 가능성이 포함됩니다.
-
서버 시작(Server Initiated): 서버에서 시작하는 어빌리티가 클라이언트에 전파됩니다. 종종 클라이언트 관점에서 서버의 실제 상황과 더욱 정확히 맞지만, 어빌리티를 사용하는 클라이언트에서는 로컬 예측이 없어 딜레이가 관측됩니다. 이러한 딜레이가 매우 짧기는 하지만, 특히 압박이 심한 상황에서 신속히 수행해야 하는 액션과 같은 일부 어빌리티 타입은 로컬 예측 모드만큼 매끄러운 느낌은 들지 않을 것입니다.
-
서버만(Server Only): '서버만' 어빌리티는 서버에서만 실행되며, 클라이언트로 리플리케이트되지 않습니다. 이 어빌리티가 변경하는 모든 변수는 평소처럼 리플리케이트됩니다. 이는 어빌리티가 서버 권한 데이터에 영향을 미칠 수 있으며 그런 다음 클라이언트에 전파된다는 뜻입니다. 이러한 방식을 통해 어빌리티 자체가 서버에서만 실행되더라도 어빌리티는 클라이언트가 관측하는 이펙트를 계속 보유할 수 있습니다.
인스턴싱 정책
게임플레이 어빌리티를 실행하면 보통 해당 어빌리티 타입의 새 오브젝트가 스폰되어 어빌리티 진행 상황을 추적합니다. 배틀로얄, MOBA, MMO 또는 RTS 게임에서 백 단위를 넘는 플레이어나 AI 캐릭터 간에 전투가 벌어지는 등의 경우에는 어빌리티 실행 빈도가 매우 높아질 수 있으므로, 어빌리티 오브젝트 생성이 매우 빨라져 퍼포먼스에 부정적 영향을 미칠 수 있습니다. 이를 해결하기 위해 어빌리티에 3가지 인스턴트화 정책 중 하나를 선택하여 효율성과 함수 기능 사이에서 균형을 맞출 수 있습니다. 지원하는 3가지 인스턴싱 타입은 다음과 같습니다.
-
실행별 인스턴스(Instanced per Execution): 어빌리티를 실행할 때마다 어빌리티의 오브젝트 사본을 스폰합니다. 이 정책의 장점은 블루프린트 그래프와 멤버 변수를 자유롭게 사용할 수 있으며, 실행 시작 시 모든 것이 디폴트값으로 초기화된다는 점입니다. 가장 간단하게 구현할 수 있는 인스턴싱 정책이지만, 대규모 오버헤드가 수반되기 때문에 자주 실행되지 않는 어빌리티에 이상적입니다. 예를 들어 MOBA의 '궁극기'는 이 정책의 합리적인 사용 사례입니다. 실행 쿨다운이 보통 60~90초로 긴 편이며, 이러한 어빌리티를 사용할 수 있는 캐릭터의 수가 보통 10명 정도로 얼마 안 되기 때문입니다. 컴퓨터로 제어되는 '미니언'이 사용하는 기본 공격 어빌리티에는 좋지 않습니다. 미니언은 한 번에 수백 마리가 존재할 수도 있으며, 각 미니언이 기본 공격을 상당히 자주 할 수 있으므로 새 오브젝트를 신속히 생성하고 리플리케이트할 것이기 때문입니다.
-
액터별 인스턴스(Instanced per Actor): 어빌리티를 처음 실행하면 액터마다 어빌리티의 인스턴스를 하나 스폰하며, 향후 실행 시 해당 인스턴스를 재사용합니다. 따라서 어빌리티를 실행할 때마다 멤버 변수를 지워야 하지만, 여러 번의 실행에서 정보를 절약할 수 있습니다. 액터별 인스턴스는 어빌리티에 리플리케이트된 오브젝트를 통해 변수 변화와 RPC를 처리할 수 있지만, 실행할 때마다 새 오브젝트를 스폰하느라 네트워크 대역폭과 CPU 시간이 낭비되지 않기 때문에 리플리케이션에 이상적입니다. 대규모 전투와 같이 규모가 큰 상황에서 퍼포먼스가 뛰어난 정책으로, 어빌리티를 사용하는 많은 수의 액터가 처음 어빌리티를 사용할 때만 오브젝트를 스폰하기 때문입니다.
-
인스턴스 없음(Non-Instanced): 전체 카테고리에서 가장 효율적인 인스턴싱 정책입니다. 어빌리티를 실행할 때 오브젝트를 스폰하는 대신 클래스 디폴트 오브젝트를 사용합니다. 하지만 이러한 효율성으로 인해 몇 가지 제한 사항이 발생합니다. 우선, 이 정책은 C++로만 작성한 어빌리티 전용입니다. 블루프린트 그래프는 오브젝트 인스턴스가 필요하기 때문입니다. 인스턴스가 없는 어빌리티의 블루프린트 클래스를 생성할 수는 있지만, 노출된 프로퍼티의 디폴트값을 변경할 때만 사용할 수 있습니다. 또한 어빌리티는 실행 중에 멤버 변수를 변경하거나 델리게이트를 바인딩해서는 안 되며, 변수를 리플리케이트하거나 RPC를 처리할 수도 없습니다. 내부적인 변수 저장과 데이터 리플리케이션이 필요 없는 어빌리티에만 사용해야 합니다. 다만 어빌리티의 사용자에 어트리뷰트를 설정하는 것은 가능합니다. 대규모 RTS 또는 MOBA 타이틀에서 유닛이 사용하는 기본 공격 등 자주 실행되며 여러 캐릭터에 사용되는 어빌리티에 특히 잘 맞습니다.
게임플레이 이벤트로 트리거
게임플레이 이벤트(Gameplay Event) 는 일반 채널을 통하지 않고도 어떤 컨텍스트의 데이터 페이로드를 전송하여 게임플레이 어빌리티를 직접 트리거하도록 전달될 수 있는 데이터 구조체입니다. 일반적인 방법은 액터에 게임플레이 이벤트 전송(Send Gameplay Event To Actor)을 호출하고 IAbilitySystemInterface 인터페이스와 게임플레이 이벤트에 필요한 컨텍스트 정보를 구현하는 액터를 제공하는 것이지만, 어빌리티 시스템 컴포넌트에서 게임플레이 이벤트 처리(Handle Gameplay Event)를 바로 호출해도 됩니다. 게임플레이 어빌리티를 호출하는 정상적인 경로는 아니므로, 어빌리티에 필요할 수 있는 컨텍스트 정보는 FGameplayEventData 데이터 구조체로 전달합니다. 이 구조체는 범용이므로 특정 게임플레이 이벤트 또는 어빌리티용으로 확장되지 않지만, 어떤 사례에서든 충분히 사용할 수 있습니다. 다형성 ContextHandle 필드에 필요한 부가 정보를 제공하면 됩니다.
게임플레이 이벤트가 게임플레이 어빌리티를 트리거하면, Activate Ability 코드 경로를 통하는 대신 Activate Ability From Event를 사용하므로 부가 컨텍스트 데이터를 파라미터로 제공할 수 있습니다. 어빌리티가 게임플레이 이벤트에 반응하도록 하려면 반드시 이 코드 경로를 처리해야 하지만, 여기서도 유의할 점은 게임플레이 어빌리티의 블루프린트에 구현되고 나면 Activate Ability From Event가 Activate Ability를 대신하여 모든 활성화 트래픽을 받게 된다는 것입니다.