Client에서 Predict 된다는 의미는 Server에서 Gameplay Ability 활성화나 Gameplay Effect 등록에 대한 허가를 기다릴 필요가 없다는 것을 의미한다.
이는 작업을 수행할 권한을 부여해준 Server와 Gameplay Effect가 적용 될 Target을 Predict 하는 것이다.
Server는 Client가 활동하고 나서 네트워크 지연 시간이 지난 후 GameplayAbility를 실행한 후, Client에 Predict로 시행한 동작이 맞는지 틀린지 답해준다.
만약 Client가 틀렸다면, Client에서 잘못 Predict 한 것을 Server의 결과로 복구한다.
GAS에서의 Predict 관련된 코드들은 GameplayPrediction.h 파일에 있다.
Epic의 Predict에 대한 철학은 "해도 될 것 같은 것"에 사용하는 것이다.
예를 들어 Paragon이나 Fortnite는 Damage를 Predict 하지 않는다.
대부분 ExecutionCalculations를 통해 Predict 할 수 없는 Damage를 처리한다.
... we are also not all in on a "predict everything: seamlessly and automatically" solution. We still feel player prediction is best kept to a minimum (meaning: predict the minimum amount of stuff you can get away with).
Dave Ratti from Epic's comment from the new Network Prediction Plugin
Predict 가능한 것
Ability Activation
Triggered Events
GameplayEffect Application
Attribute Modification(Not Executions, only Attribute Modification)
GameplayTag Modification
Gameplay Cue Events
Montages
Movement(in UCharacterMovement)
Predict 불가능한 것
GameplayEffect Removal
GameplayEffect periodic Effect
추가 설명
GameplayEffect 등록은 Predict 할 수 있지만, 제거는 Predict 할 수 없다.
이 제약을 피하는 한가지 방법은 제거 대신 Inverse Effect를 Predict하는 것이다.
GameplayEffect 제거를 Predict 할 수 없기 때문에, 우리는 GameplayEffect Cooldown을 Predict 할 수 없다.
심지어 이건 Inverse도 없다.
Server에서 Replicate 된 Cooldown GE는 Client에 존재 할 것이고 이를 제거하려는 모든 시도는 Server에 의해 거부될 것이다.
이는 지연시간이 긴 유저가 서버로부터 Cooldown을 받는데 걸리는 시간이 길어 더 불리하다는 얘기이다.
Fortnite는 이를 피하기 위해 Cooldown GE 대신 Custom Bookkeeping을 사용하고 있다.
Instant GE를 자신에게 사용할 때에는 문제 없지만, 타인에게 사용할 때에는 값이 잠깐 튈 수 있다.
Instant GE를 Predict 하는 것은 Infinite GE처럼 취급되어 복구가 될 수 있다.
만약 Server의 GE가 적용되면, 아주 잠깐동안 동시에 2개의 GE가 존재하여 Modifier가 2번 적용될 수 있다.
이는 곧 정상화가 되겠지만, 일부 유저들은 이상현상을 감지할 수 있다.
PredictionKey
GA의 Prediction은 Client가 Gameplay Ability를 활성화 할 때 생성되는 Integer Identifier를 기반으로 동작한다.
Gameplay Ability가 활성화 될 때 Prediction Key가 생성된다.
이를 Activation Prediction Key라 지칭한다.
Client는 이 Key를 CallServerTryActivateAbility() 함수를 통해 Server에 전달한다.
Client는 이 Key가 Valid한 동안 등록된 모든 Gameplay Effect에 추가한다.
Client의 Prediction Key가 Scope를 벗어난다.
같은 Gameplay Ability에 Predict 한 Effect는 새 Scoped Prediction Window가 필요하다.
Serverrk Prediction Key를 Client로부터 전달 받는다.
Server가 Prediction Key를 등록된 모든 Gameplay Effect에 전달한다.
Server가 Prediction Key를 Client로 다시 Replicate 한다.
Client는 GE를 등록하는데 사용한 Prediction Key와 함께 GE를 Replicate 받는다.
만약 Client에서 등록한 GE 중 Replicate 받은 GE와 동일한 것이 있다면, Predict가 성공한 것이다.
Client가 Predict한 GE를 제거하기 전까지 한 Target에 대해 일시적으로 2개의 GE가 존재하게 된다.
Client가 Server로부터 Prediction Key를 받는다.
이것은 Replicated Prediction Key라 부른다.
이 Prediction Key는 이제 Stale 상태로 둔다.
Client는 Stale 상태의 Replicated Prediction Key을 이용해 모든 GE를 지운다.
Server로부터 Replicate 받은 GE는 유지가 될 것이다.
Client가 생성했지만 Server로부터 Replicated Prediction Key를 받지 못한 GE는 모두 Predict 실패 처리된다.
Prediction Key는 GameAbility가 Activation Prediction Key로 인해 활성화 되고 나서 Window라는 Atomic Group 명령이 수행되는 동안 Valid를 보장 받는다.
단순하게 한 Frame동안 Valid를 보장 받는다 생각하면 된다.
새로운 Scoped Prediction Window를 생성하는 Synch Point를 가지고 있지 않다면 예정되어 있는 Ability Task의 모든 Callback은 Valid한 Prediction Key를 가질 수 없다.
Creating New Prediction Windows in Ability
Ability Task에서 발생한 Callback에 대해 더 많은 Predict를 하기 위해, 새 Scoped Prediction Key를 이용해 새 Scoped Prediction Window를 만들 필요가 있다.
이는 Client와 Server 사이의 Synch Point로 여겨지기도 한다.
Input과 같은 일부 Ability Task에는 새 Scoped Prediction Window를 생성하는 기능이 내장되어 있다.
이는 Ability Task의 Callback의 단위 코드는 유효한 Scoped Prediction Key를 가지고 있다는 뜻이다.
만약 Ability Task 이후의 행동을 Predict 한다면, WaitDelay와 같이 Scoped Prediction Window를 만드는 코드가 없을 것이다.
때문에 OnlyServerWait 옵션과 함께 AbilityTask관련하여 WaitNetSync을 사용해야만 한다.
Client가 OnlyServerWait 옵션인 채로 WaitNetSync를 호출하면 다음 동작을 시행한다.
Gameplay Ability의 Activation Prediction Key를 기반으로 한 새 Scoped Prediction Key를 생성
새 Scoped Prediction Key를 Server에 RPC로 전달하고
등록된 GE에 새 Scoped Prediction Key 추가
Server가 OnlyServerWait 옵션인채로 WaitNetSync를 호출하면 다음 동작을 시행한다.
작업하기 전 Client로부터 새 Scoped Prediction Key를 받을 때까지 대기
이 Scoped Prediction Key는 GE에 등록된 Activation Prediction Keys와 일치하고, Client에 Stale 한 채로 Replicate 된다.
Scoped Prediction Key는 Scope를 벗어나기 전까지 유효하다.
이는 Scoped Prediction Window는 닫혀 있다는 것이다.
그렇기에 미루어지지 않는 Atomic 연산만이 새 Scoped Prediction Key를 사용할 수 있다.
WaitNetSync를 사용하면, Server의 Gameplay Ability가 Client로부터 패킷을 받을 때까지 대기상태에 빠질 수 있다.
이는 해커에게 취약점을 제공할 수 있다.
Predictively Spawning Actor
Client에서 Actor Spawn을 Predict 하는 것은 좀 더 복잡한 이슈이다.
적어도 GAS는 이러한 기능을 제공하지는 않는다.
중요한건 Replicate 되는 Actor을 Client와 Server 양쪽에 모드 Spawn 해야 한다는 것이다.
만약 Actor가 장식이거나 Gameplay 목적이 전혀 없다면, IsNetRelavantFor() 함수를 Override 하여 Server가 소유한 Client에 Replicate하지 않도록 하는 것도 방법이다.
Owner Client는 Local 상으로 Spawn을 하고, Server나 다른 Client는 Spawn을 하지 않는다.
/** GameplayCues can also come on their own. These take an optional effect context to pass through hit result, etc */
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
/** Add a persistent gameplay cue */
void AddGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void AddGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
/** Add gameplaycue for minimal replication mode. Should only be called in paths that would replicate gameplaycues in other ways (through GE for example) if not in minimal replication mode */
void AddGameplayCue_MinimalReplication(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
/** Remove a persistent gameplay cue */
void RemoveGameplayCue(const FGameplayTag GameplayCueTag);
/** Remove gameplaycue for minimal replication mode. Should only be called in paths that would replicate gameplaycues in other ways (through GE for example) if not in minimal replication mode */
void RemoveGameplayCue_MinimalReplication(const FGameplayTag GameplayCueTag);
/** Removes any GameplayCue added on its own, i.e. not as part of a GameplayEffect. */
void RemoveAllGameplayCues();
/** Handles gameplay cue events from external sources */
void InvokeGameplayCueEvent(const FGameplayEffectSpecForRPC& Spec, EGameplayCueEvent::Type EventType);
void InvokeGameplayCueEvent(const FGameplayTag GameplayCueTag, EGameplayCueEvent::Type EventType, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void InvokeGameplayCueEvent(const FGameplayTag GameplayCueTag, EGameplayCueEvent::Type EventType, const FGameplayCueParameters& GameplayCueParameters);
Local Gameplay Cues
ASC와 Gameplay Ability에서 제공하는 GameplayCue 실행 함수는 기본적으로 replicate 된다.
/**
* FScopedGameplayCueSendContext
* Add this around code that sends multiple gameplay cues to allow grouping them into a smalkler number of cues for more efficient networking
*/
struct GAMEPLAYABILITIES_API FScopedGameplayCueSendContext
{
FScopedGameplayCueSendContext();
~FScopedGameplayCueSendContext();
};
FlushPendingCues() 함수를 override하여 Custom Struct에서 몇몇 Custom Gamplay Tag로 batch 할수 있는 GameplayCue를 Merge하고, Client로 RPC 한다.
/**
* Enabling AbilitySystemAlwaysConvertGESpecToGCParams will mean that all calls to gameplay cues with GameplayEffectSpecs will be converted into GameplayCue Parameters server side and then replicated.
* This potentially saved bandwidth but also has less information, depending on how the GESpec is converted to GC Parameters and what your GC's need to know.
*/
int32 AbilitySystemAlwaysConvertGESpecToGCParams = 0;
static FAutoConsoleVariableRef CVarAbilitySystemAlwaysConvertGESpecToGCParams(TEXT("AbilitySystem.AlwaysConvertGESpecToGCParams"), AbilitySystemAlwaysConvertGESpecToGCParams, TEXT("Always convert a GameplayCue from GE Spec to GC from GC Parameters on the server"), ECVF_Default );
이 변수는 FGameplayEffectSpec를 FGameplayCueParameter로 변환하여 RPC에 전달한다.
UENUM(BlueprintType)
namespace EGameplayCueEvent
{
/** Indicates what type of action happened to a specific gameplay cue tag. Sometimes you will get multiple events at once */
enum Type : int
{
/** Called when a GameplayCue with duration is first activated, this will only be called if the client witnessed the activation */
OnActive,
/** Called when a GameplayCue with duration is first seen as active, even if it wasn't actually just applied (Join in progress, etc) */
WhileActive,
/** Called when a GameplayCue is executed, this is used for instant effects or periodic ticks */
Executed,
/** Called when a GameplayCue with duration is removed */
Removed
};
}
OnActive
GameplayCue가 활성화(Add) 될 때 호출
GameplayCue가 시작할 때 발생하는 모든 것에서 사용
단, Joiner가 늦어서 이 Event를 놓치더라도 WhileActive에서 보강해줄 수 있다.
WhileActive
GameplayCue가 동작 할 때 호출된다.
심지어 아직 등록이 완료되지 않더라도 동작만 하면 호출된다.
GameplayCueNotify_Actor가 Add 되거나 관련 작업이 생기면 OnActive와 같이 한번만 호출된다.
따라서 Tick이 아니다.
Actor가 GameplayCueNotify_Actor 범위 안에 들어올 때마다 한번씩 발생한다.
만약 Tick이 필요하다면, GameplayCueNotify_Actor::Tick()을 사용해야 한다.
Removed
GameplayCue가 제거 될 때 호출된다.
BP에서는 OnRemove Event가 이 기능과 관련 있다.
OnActive/WhileActive에서 추가한 것들을 반드시지워야 한다.
Actor가 GameplayCueNotify_Actor 범위 밖을 나갈 때마다 한번씩 발생한다.
Executed
Instant Effect나 Periodic Tick에서 GameplayCue가 실해오딜 때 호출된다.
BP에서는 OnExecute가 관련있다.
Gameplay Cue Reliability
일반적으로 GameplayCue는 Unreliable 함을 염두해야만 한다.
때문에 Gameplay에 직접적으로 영향을 주는 모든 겨우에 대해서는 적합하지 않다.
만약 GameplayCue를 Relaible하게 사용하고 싶다면, GameplayEffect으로부터 등록하여 WhileActive에서 FX를 추가하고, OnRevmoe에서 제거해야 한다.
Executed GameplayCues
이 경우에는 Unreliable Multicast로 등록이 되기 때문에 항상 Unreliable하다.
GameplayCues applied from GameplayEffect
Autonomous Proxy의 경우 OnActive, WhileActive, OnRemove에 대해 Reliable하다.
void FActiveGameplayEffectsContainer::PostReplicatedReceive(const FFastArraySerializer::FPostReplicatedReceiveParameters& Parameters)
{
// After the array has been replicated, invoke GC events ONLY if the effect is not inhibited
// We postpone this check because in the same net update we could receive multiple GEs that affect if one another is inhibited
if (Owner != nullptr)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_ActiveGameplayEffectsContainer_NetDeltaSerialize_CheckRepGameplayCues);
if ( LIKELY(UE::GameplayEffect::bSkipUnmappedReferencesCheckForGameplayCues) )
{
if (Owner->IsReadyForGameplayCues())
{
Owner->HandleDeferredGameplayCues(this);
}
}
else
{
// Keep this until we have actually deprecated the parameter just in case.
PRAGMA_DISABLE_DEPRECATION_WARNINGS
if (!Parameters.bHasMoreUnmappedReferences) // Do not invoke GCs when we have missing information (like AActor*s in EffectContext)
{
NumConsecutiveUnmappedReferencesDebug = 0;
if (Owner->IsReadyForGameplayCues())
{
Owner->HandleDeferredGameplayCues(this);
}
}
else
{
++NumConsecutiveUnmappedReferencesDebug;
constexpr uint32 HighNumberOfConsecutiveUnmappedRefs = 30;
ensureMsgf(NumConsecutiveUnmappedReferencesDebug < HighNumberOfConsecutiveUnmappedRefs, TEXT("%hs: bHasMoreUnmappedReferences is preventing GameplayCues from firing"), __func__);
UE_CLOG((NumConsecutiveUnmappedReferencesDebug % HighNumberOfConsecutiveUnmappedRefs) == 0, LogAbilitySystem, Error, TEXT("%hs: bHasMoreUnmappedReferences is preventing GameplayCues from firing (%u consecutive misses)"), __func__, NumConsecutiveUnmappedReferencesDebug);
}
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}
}
}
void UAbilitySystemComponent::HandleDeferredGameplayCues(const FActiveGameplayEffectsContainer* GameplayEffectsContainer)
{
for (const FActiveGameplayEffect& Effect : GameplayEffectsContainer)
{
if (Effect.bIsInhibited == false)
{
if (Effect.bPendingRepOnActiveGC)
{
InvokeGameplayCueEvent(Effect.Spec, EGameplayCueEvent::OnActive);
}
if (Effect.bPendingRepWhileActiveGC)
{
InvokeGameplayCueEvent(Effect.Spec, EGameplayCueEvent::WhileActive);
}
}
Effect.bPendingRepOnActiveGC = false;
Effect.bPendingRepWhileActiveGC = false;
}
}
void FActiveGameplayEffectsContainer::RemoveActiveGameplayEffectGrantedTagsAndModifiers(const FActiveGameplayEffect& Effect, bool bInvokeGameplayCueEvents)
{
// Update AttributeAggregators: remove mods from this ActiveGE Handle
if (Effect.Spec.GetPeriod() <= UGameplayEffect::NO_PERIOD)
{
for (const FGameplayModifierInfo& Mod : Effect.Spec.Def->Modifiers)
{
if (Mod.Attribute.IsValid())
{
if (const FAggregatorRef* RefPtr = AttributeAggregatorMap.Find(Mod.Attribute))
{
RefPtr->Get()->RemoveAggregatorMod(Effect.Handle);
}
}
}
}
// Update gameplaytag count and broadcast delegate if we are at 0
Owner->UpdateTagMap(Effect.Spec.Def->GetGrantedTags(), -1);
Owner->UpdateTagMap(Effect.Spec.DynamicGrantedTags, -1);
// Update our owner with the blocked ability tags this GameplayEffect adds to them
Owner->UnBlockAbilitiesWithTags(Effect.Spec.Def->GetBlockedAbilityTags());
// Update minimal replication if needed.
if (ShouldUseMinimalReplication())
{
Owner->RemoveMinimalReplicationGameplayTags(Effect.Spec.Def->GetGrantedTags());
Owner->RemoveMinimalReplicationGameplayTags(Effect.Spec.DynamicGrantedTags);
}
PRAGMA_DISABLE_DEPRECATION_WARNINGS
// Cancel/remove granted abilities
if (IsNetAuthority())
{
for (const FGameplayAbilitySpecDef& AbilitySpecDef : Effect.Spec.GrantedAbilitySpecs)
{
if (AbilitySpecDef.AssignedHandle.IsValid())
{
switch(AbilitySpecDef.RemovalPolicy)
{
case EGameplayEffectGrantedAbilityRemovePolicy::CancelAbilityImmediately:
{
Owner->ClearAbility(AbilitySpecDef.AssignedHandle);
break;
}
case EGameplayEffectGrantedAbilityRemovePolicy::RemoveAbilityOnEnd:
{
Owner->SetRemoveAbilityOnEnd(AbilitySpecDef.AssignedHandle);
break;
}
default:
{
// Do nothing to granted ability
break;
}
}
}
}
}
PRAGMA_ENABLE_DEPRECATION_WARNINGS
// Update GameplayCue tags and events
if (!Owner->bSuppressGameplayCues)
{
for (const FGameplayEffectCue& Cue : Effect.Spec.Def->GameplayCues)
{
// If we use Minimal/Mixed Replication, then this will cause EGameplayCueEvent::Removed
if (ShouldUseMinimalReplication())
{
for (const FGameplayTag& CueTag : Cue.GameplayCueTags)
{
Owner->RemoveGameplayCue_MinimalReplication(CueTag);
}
}
else
{
// Perform pseudo-RemoveCue (without affecting ActiveGameplayCues, as we were not inserted there - see AddActiveGameplayEffectGrantedTagsAndModifiers)
Owner->UpdateTagMap(Cue.GameplayCueTags, -1);
if (bInvokeGameplayCueEvents)
{
Owner->InvokeGameplayCueEvent(Effect.Spec, EGameplayCueEvent::Removed);
}
}
}
}
}
Simulated Proxy의 경우 WhileActive와 OnRemove가 Reliable하다.
아래 함수에서의 Replication이 WhileActive, OnRemove Event를 호출한다.
/** Replicated gameplaycues when in minimal replication mode. These are cues that would come normally come from ActiveGameplayEffects (but since we do not replicate AGE in minimal mode, they must be replicated through here) */
UPROPERTY(Replicated)
FActiveGameplayCueContainer MinimalReplicationGameplayCues;
OnActive Event는 Unreliable Multicast로 전달된다.
GameplayCues applied withou a GameplayEffect
Autonomous Proxy의 경우 OnRemove Event를 Reliable하게 받는다.
/*
* Grants an Ability.
* This will be ignored if the actor is not authoritative.
* Returns handle that can be used in TryActivateAbility, etc.
*
* @param AbilitySpec FGameplayAbilitySpec containing information about the ability class, level and input ID to bind it to.
*/
FGameplayAbilitySpecHandle GiveAbility(const FGameplayAbilitySpec& AbilitySpec);
void AGDCharacterBase::AddCharacterAbilities()
{
// Grant abilities, but only on the server
if (Role != ROLE_Authority || !AbilitySystemComponent.IsValid() || AbilitySystemComponent->bCharacterAbilitiesGiven)
{
return;
}
for (TSubclassOf<UGDGameplayAbility>& StartupAbility : CharacterAbilities)
{
AbilitySystemComponent->GiveAbility(
FGameplayAbilitySpec(StartupAbility, GetAbilityLevel(StartupAbility.GetDefaultObject()->AbilityID), static_cast<int32>(StartupAbility.GetDefaultObject()->AbilityInputID), this));
}
AbilitySystemComponent->bCharacterAbilitiesGiven = true;
}
/*
* Grants an ability and attempts to activate it exactly one time, which will cause it to be removed.
* Only valid on the server, and the ability's Net Execution Policy cannot be set to Local or Local Predicted
*
* @param AbilitySpec FGameplayAbilitySpec containing information about the ability class, level and input ID to bind it to.
* @param GameplayEventData Optional activation event data. If provided, Activate Ability From Event will be called instead of ActivateAbility, passing the Event Data
*/
FGameplayAbilitySpecHandle GiveAbilityAndActivateOnce(FGameplayAbilitySpec& AbilitySpec, const FGameplayEventData* GameplayEventData = nullptr);
FGameplayAbilitySpec으로 추가할 Ability를 나타냄
FGameplayAbilitySpecHandle을 반환
Ability는 반드시 Instance화되고 Server에서 실행할 수 있어야 한다.
Server에서 실행을 시도한 후에는 FGameplayAbilitySpecHandle을 반환한다.
Ability가 필수 조건을 충족하지 못하거나 실행 할 수 없는 경우, Handle이 유효하지 않아 ASC가 Ability를 부여받지 못한다.
Revoke Ability
입력받은 FGameplayAbilitySpecHandle은 모두 Grant 과정에서 반환한 것들을 사용한다.
/**
* Removes the specified ability.
* This will be ignored if the actor is not authoritative.
*
* @param Handle Ability Spec Handle of the ability we want to remove
*/
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Gameplay Abilities")
void ClearAbility(const FGameplayAbilitySpecHandle& Handle);
/** Wipes all 'given' abilities. This will be ignored if the actor is not authoritative. */
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="Gameplay Abilities")
void ClearAllAbilities();
/**
* Clears all abilities bound to a given Input ID
* This will be ignored if the actor is not authoritative
*
* @param InputID The numeric Input ID of the abilities to remove
*/
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Gameplay Abilities")
void ClearAllAbilitiesWithInputID(int32 InputID = 0);
/** Sets an ability spec to remove when its finished. If the spec is not currently active, it terminates it immediately. Also clears InputID of the Spec. */
void SetRemoveAbilityOnEnd(FGameplayAbilitySpecHandle AbilitySpecHandle);
지정한 Ability 실행이 완료되면 ASC에서 제거한다.
실행중이지 않는 경우 즉시 제거한다.
실행중인 경우 그 입력을 즉시 지워 Player가 더이상 재활성화/상호작용 못하도록 막는다.
CanActivateAbility
호출자가 Ability 실행을 시도하지 않아도 사용 가능 여부를 알려주는 함수
UI에서 Player가 사용할 수 없는 Icon을 Dimd 처리해야 하는 경우
Character에 특정 Particle/Sound Effect를 재생해 특정 Ability가 사용 가능한지 Notify해야 하는 경우
/** Returns true if this ability can be activated right now. Has no side effects */
virtual bool CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags = nullptr, const FGameplayTagContainer* TargetTags = nullptr, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr) const;
CallActivateAbility
Ability에 관련된 Game Code를 실행
하지만 사용 가능 여부를 검사하지 않는다.
CanActivateAbility 검사와 Ability 실행 사이에 약간의 로직이 필요한 경우에 실행
Actor/Component와 달리 주 작업이 Tick을 수행하지 않는다.
대신 활성화 중 비동기 작업을 시행할 수 있는 Ability Task를 지원 함.
C++에서는 Delegate로, BP에서는 실행 핀 노드로 해당 Task 출력을 처리한다.
/** Cancels the specified ability CDO. */
void CancelAbility(UGameplayAbility* Ability);
/** Cancels the ability indicated by passed in spec handle. If handle is not found among reactivated abilities nothing happens. */
void CancelAbilityHandle(const FGameplayAbilitySpecHandle& AbilityHandle);
/** Cancel all abilities with the specified tags. Will not cancel the Ignore instance */
void CancelAbilities(const FGameplayTagContainer* WithTags=nullptr, const FGameplayTagContainer* WithoutTags=nullptr, UGameplayAbility* Ignore=nullptr);
/** Cancels all abilities regardless of tags. Will not cancel the ignore instance */
void CancelAllAbilities(UGameplayAbility* Ignore=nullptr);
/** Cancels all abilities and kills any remaining instanced abilities */
virtual void DestroyActiveState();
TryActivateAbility
Ability를 실행하는 전형적인 방식
CanActivateAbility를 호출해 실행 여부를 판단한 뒤, 가능하면 CallActivateAbility를 호출
/**
* Attempts to activate every gameplay ability that matches the given tag and DoesAbilitySatisfyTagRequirements().
* Returns true if anything attempts to activate. Can activate more than one ability and the ability may fail later.
* If bAllowRemoteActivation is true, it will remotely activate local/server abilities, if false it will only try to locally activate abilities.
*/
UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilitiesByTag(const FGameplayTagContainer& GameplayTagContainer, bool bAllowRemoteActivation = true);
/**
* Attempts to activate the ability that is passed in. This will check costs and requirements before doing so.
* Returns true if it thinks it activated, but it may return false positives due to failure later in activation.
* If bAllowRemoteActivation is true, it will remotely activate local/server abilities, if false it will only try to locally activate the ability
*/
UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilityByClass(TSubclassOf<UGameplayAbility> InAbilityToActivate, bool bAllowRemoteActivation = true);
/**
* Attempts to activate the given ability, will check costs and requirements before doing so.
* Returns true if it thinks it activated, but it may return false positives due to failure later in activation.
* If bAllowRemoteActivation is true, it will remotely activate local/server abilities, if false it will only try to locally activate the ability
*/
UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool bAllowRemoteActivation = true);
/** Triggers an ability from a gameplay event, will only trigger on local/server depending on execution flags */
bool TriggerAbilityFromGameplayEvent(FGameplayAbilitySpecHandle AbilityToTrigger, FGameplayAbilityActorInfo* ActorInfo, FGameplayTag Tag, const FGameplayEventData* Payload, UAbilitySystemComponent& Component);
TriggetAbilityFromGameplayEvent를 사용하려면 해당 Gameplay Ability에 Trigger가 세팅되어 있어야 한다.
Gameplay Tag를 할당하고 Gameplay Event 옵션을 활성화 해야 한다.
Event를 사용하면 Data를 포함한 Payload에 전달할 수 있다.
GameplayTag가 추가/제거 되었을 때 GameplayAbility 활성화를 할 수 있다.
//UAbilitySystemBlueprintLibrary
/**
* This function can be used to trigger an ability on the actor in question with useful payload data.
* NOTE: GetAbilitySystemComponent is called on the actor to find a good component, and if the component isn't
* found, the event will not be sent.
*/
UFUNCTION(BlueprintCallable, Category = Ability, Meta = (Tooltip = "This function can be used to trigger an ability on the actor in question with useful payload data."))
static void SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload);
Passive Ability
자동으로 활성화 되고 계속해서 동작해야 하는 Gameplay Ability
UGameplayAbility::OnAvatarSet() 함수를 Override
Gameplay Ability가 등록되고 AvatarActor가 세팅되어 TryActivateAbility()가 호출 될 때 자동으로 호출된다.
/** Returns the list of all activatable abilities. Read-only. */
const TArray<FGameplayAbilitySpec>& GetActivatableAbilities() const
{
return ActivatableAbilities.Items;
}
/** Returns the list of all activatable abilities. */
TArray<FGameplayAbilitySpec>& GetActivatableAbilities()
{
return ActivatableAbilities.Items;
}
또한 GameplayAbilitySpecs를 수동으로 순회하는 대신 편의 함수를 제공한다.
/**
* Gets all Activatable Gameplay Abilities that match all tags in GameplayTagContainer AND for which
* DoesAbilitySatisfyTagRequirements() is true. The latter requirement allows this function to find the correct
* ability without requiring advanced knowledge. For example, if there are two "Melee" abilities, one of which
* requires a weapon and one of which requires being unarmed, then those abilities can use Blocking and Required
* tags to determine when they can fire. Using the Satisfying Tags requirements simplifies a lot of usage cases.
* For example, Behavior Trees can use various decorators to test an ability fetched using this mechanism as well
* as the Task to execute the ability without needing to know that there even is more than one such ability.
*/
void GetActivatableGameplayAbilitySpecsByAllMatchingTags(const FGameplayTagContainer& GameplayTagContainer, TArray < struct FGameplayAbilitySpec* >& MatchingGameplayAbilities, bool bOnlyAbilitiesThatSatisfyTagRequirements = true) const;
bOnlyAbilitiesThatSatisfyTagRequirements
주어진 Gameplay Tag 조건을 만족하고 현재 활성화 되어 있는 GameplayAbilitySpec만 반환할지 여부
한번 FGameplayAbilitySpec을 탐색하면 IsActive()를 호출할 수 있다.
Tags
여러 Gameplay Ability의 상호작용 방식을 결정
Tag Set
Ability의 Behaviour에 영향을 미칠수 있는 방식
Ability를 식별 및 분류
Gamplay Tag Container
다른 Ability와의 Interaction을 지원
Cancel Abilities With Tag
이 Ability가 실행되는 동안 이미 실행 중인 Ability의 Tag가 제공된 목록과 일치하면 그 Ability를 취소한다.
Block Abilities With Tag
이 Ability가 실행되는 동안 일치하는 Tag가 있는 다른 Ability의 실행을 방지한다.
Activation Owned Tags
이 Ability가 실행되는 동안 해당 Ability의 Owner에 이 Tag Set가 부여된다.
Activation Required Tags
이 Ability는 Active 상태인 Actor/Component에 모든 Tag가 있을 때에만 Activate 할 수 있다.
Activation Blocked Tags
이 Ability는 Active 상태인 Actor/Component에 모든 Tag가 없을 때에만 Activate 할 수 있다.
Target Required Tags
이 Ability는 Target Actor/Component에 이 Tag가 모두 있을 때에만 Activate 할 수 있다.
Target Blocked Tags
이 Ability는 Target Actor/Component에 이 Tag가 모두 없을 때에만 Actiavte 할 수 있다.
/**
* An activatable ability spec, hosted on the ability system component. This defines both what the ability is (what class, what level, input binding etc)
* and also holds runtime state that must be kept outside of the ability being instanced/activated.
*/
USTRUCT(BlueprintType)
struct GAMEPLAYABILITIES_API FGameplayAbilitySpec : public FFastArraySerializerItem
{
GENERATED_USTRUCT_BODY()
PRAGMA_DISABLE_DEPRECATION_WARNINGS
FGameplayAbilitySpec(const FGameplayAbilitySpec&) = default;
FGameplayAbilitySpec(FGameplayAbilitySpec&&) = default;
FGameplayAbilitySpec& operator=(const FGameplayAbilitySpec&) = default;
FGameplayAbilitySpec& operator=(FGameplayAbilitySpec&&) = default;
~FGameplayAbilitySpec() = default;
PRAGMA_ENABLE_DEPRECATION_WARNINGS
FGameplayAbilitySpec()
: Ability(nullptr), Level(1), InputID(INDEX_NONE), SourceObject(nullptr), ActiveCount(0), InputPressed(false), RemoveAfterActivation(false), PendingRemove(false), bActivateOnce(false)
{ }
/** Version that takes an ability class */
FGameplayAbilitySpec(TSubclassOf<UGameplayAbility> InAbilityClass, int32 InLevel = 1, int32 InInputID = INDEX_NONE, UObject* InSourceObject = nullptr);
/** Version that takes an ability CDO, this exists for backward compatibility */
FGameplayAbilitySpec(UGameplayAbility* InAbility, int32 InLevel = 1, int32 InInputID = INDEX_NONE, UObject* InSourceObject = nullptr);
/** Version that takes an existing spec def */
FGameplayAbilitySpec(FGameplayAbilitySpecDef& InDef, int32 InGameplayEffectLevel, FActiveGameplayEffectHandle InGameplayEffectHandle = FActiveGameplayEffectHandle());
/** Handle for outside sources to refer to this spec by */
UPROPERTY()
FGameplayAbilitySpecHandle Handle;
/** Ability of the spec (Always the CDO. This should be const but too many things modify it currently) */
UPROPERTY()
TObjectPtr<UGameplayAbility> Ability;
/** Level of Ability */
UPROPERTY()
int32 Level;
/** InputID, if bound */
UPROPERTY()
int32 InputID;
/** Object this ability was created from, can be an actor or static object. Useful to bind an ability to a gameplay object */
UPROPERTY()
TWeakObjectPtr<UObject> SourceObject;
/** A count of the number of times this ability has been activated minus the number of times it has been ended. For instanced abilities this will be the number of currently active instances. Can't replicate until prediction accurately handles this.*/
UPROPERTY(NotReplicated)
uint8 ActiveCount;
/** Is input currently pressed. Set to false when input is released */
UPROPERTY(NotReplicated)
uint8 InputPressed:1;
/** If true, this ability should be removed as soon as it finishes executing */
UPROPERTY(NotReplicated)
uint8 RemoveAfterActivation:1;
/** Pending removal due to scope lock */
UPROPERTY(NotReplicated)
uint8 PendingRemove:1;
/** This ability should be activated once when it is granted. */
UPROPERTY(NotReplicated)
uint8 bActivateOnce : 1;
/** Cached GameplayEventData if this ability was pending for add and activate due to scope lock */
TSharedPtr<FGameplayEventData> GameplayEventData = nullptr;
/** Activation state of this ability. This is not replicated since it needs to be overwritten locally on clients during prediction. */
UPROPERTY(NotReplicated)
FGameplayAbilityActivationInfo ActivationInfo;
/** Optional ability tags that are replicated. These tags are also captured as source tags by applied gameplay effects. */
UPROPERTY()
FGameplayTagContainer DynamicAbilityTags;
/** Non replicating instances of this ability. */
UPROPERTY(NotReplicated)
TArray<TObjectPtr<UGameplayAbility>> NonReplicatedInstances;
/** Replicated instances of this ability.. */
UPROPERTY()
TArray<TObjectPtr<UGameplayAbility>> ReplicatedInstances;
/**
* Handle to GE that granted us (usually invalid). FActiveGameplayEffectHandles are not synced across the network and this is valid only on Authority.
* If you need FGameplayAbilitySpec -> FActiveGameplayEffectHandle, then use AbilitySystemComponent::FindActiveGameplayEffectHandle.
*/
UPROPERTY(NotReplicated)
FActiveGameplayEffectHandle GameplayEffectHandle;
/** Passed on SetByCaller magnitudes if this ability was granted by a GE */
TMap<FGameplayTag, float> SetByCallerTagMagnitudes;
/** Returns the primary instance, used for instance once abilities */
UGameplayAbility* GetPrimaryInstance() const;
/** interface function to see if the ability should replicated the ability spec or not */
bool ShouldReplicateAbilitySpec() const;
/** Returns all instances, which can include instance per execution abilities */
TArray<UGameplayAbility*> GetAbilityInstances() const
{
TArray<UGameplayAbility*> Abilities;
Abilities.Append(ReplicatedInstances);
Abilities.Append(NonReplicatedInstances);
return Abilities;
}
/** Returns true if this ability is active in any way */
bool IsActive() const;
void PreReplicatedRemove(const struct FGameplayAbilitySpecContainer& InArraySerializer);
void PostReplicatedAdd(const struct FGameplayAbilitySpecContainer& InArraySerializer);
FString GetDebugString();
};
GameplayAbility가 활성화 가능한 Ability가 정의된 후에 ASC에 존재하게 된다.
GameAbility Class
GameAbility Level
Input Binding
GameplayAbility Class로부터 분리되어야만 하는 runtime state
GameAbility가 Server에서 부여되면, Server는 해당 Ability의 GameplayAbilitySpec을 활성화 되어야 할 대상 Client에 Replicate 한다.
GameplayAbilitySpec을 활성화 하면 Instance Policy에 따라 GameplayAbility Instance가 생성된다.
Passing Data to Abilities
GameplayAbility의 범용적인 패러다임은 Activate -> General Data -> Aply -> End이다.
하지만 때로는 이미 존재하는 Data를 기반으로 동작해야 하기도 하다.
GAS는 몇 가지 Gameplay Ability 외부의 데이터를 사용하는 옵션을 제공한다.
/**
* This function can be used to trigger an ability on the actor in question with useful payload data.
* NOTE: GetAbilitySystemComponent is called on the actor to find a good component, and if the component isn't
* found, the event will not be sent.
*/
UFUNCTION(BlueprintCallable, Category = Ability, Meta = (Tooltip = "This function can be used to trigger an ability on the actor in question with useful payload data."))
static void SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload);
Payload가 있는 Data를 포함한 Event로 Gameplay Ability를 활성화
Event의 Payload는 Client에서 Server로 local predict 된 Gameplay Ability를 Replicate한다.
다만 Ability를 Input Bind로 활성화 하는 것을 방지한다는 단점이 있다.
AbilityTask_WaitGameplayEvent
Gameplay Ability가 활성화 된 후 Payload Data를 Event로 받도록 설정
Event Payload를 보내는 과정은 Gameplay Ability가 Event로 활성화 되는 것과 같다.
Event가 Ability Task로 REplicate 되지 않고, 오직 LocalOnly나 ServerOnly만 가능하다는 단점이 있다.
Use TargetData
Custom Target Data를 선언하여 Client와 Server 사이에 임시 Data 전달
Store Data on the OwnerActor or AvatarActor
OwnerActor나 AvatarActor, 혹은 Reference를 갖을 수 있는 Object의 Replicated Varaiable에 저장
이 방법은 Input Bind로 활성화되는 Gameplay Ability로 작업하기에 가장 유연한 방법이다.
하지만 Data를 사용하는 때에 Replication으로 동기화가 되었음을 보장할 수 없다.
Replicated Variable을 설정 한 직후 Gameplay Ability를 활성화 하는 경우에 Packet loss로 인한 동기화 오류가 발생할 여지가 있다.
Ability Cost and Cooldown
Gameplay Ability는 선택적으로 Cost나 Cooldown 기능을 제공한다.
Cost
Instant GE로 실행할 때반드시 충족해야 하는ASC에서 사전에 선언된 Gameplay Ability가 Attribute의 요구량
Cooldown
Duration GE 가 만료된 이후 재실행 될 때까지 Gameplay Ability의 재실행을 방지하는Duration
CanActivateAbility() 함수에서 해당 Gameplay Ability를 소유한 ASC에서 Cost와 Cooldown을 체크한다.
/** Checks cost. returns true if we can pay for the ability. False if not */
virtual bool CheckCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr) const;
/** Checks cooldown. returns true if we can be used again. False if not */
virtual bool CheckCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr) const;
Activate()가 호출된 후, 선택적으로 Cost와 Cooldown 관련 함수가 호출된다.
virtual void EndServerAbilityRPCBatch(FGameplayAbilitySpecHandle AbilityHandle);
// Overridable function for sub classes
virtual void ServerAbilityRPCBatch_Internal(FServerAbilityRPCBatch& BatchInfo);
이 때 BatchInfo는 다음 Flag 정보를 포함한다.
Ability가 반드시 끝나야 하는지 여부
Activity가 활성화 되었을 때 Press Input이 발생했는지 여부
TargetData가 포함되었는지 여부
ServerAbilityRPCBatch_Internal은 Batch가 정상 동작하는지 확인하기 위해 Break Point를 걸기 매우 적절한 함수다.
/** Direct Input state replication. These will be called if bReplicateInputDirectly is true on the ability and is generally not a good thing to use. (Instead, prefer to use Generic Replicated Events). */
UFUNCTION(Server, reliable, WithValidation)
void ServerSetInputPressed(FGameplayAbilitySpecHandle AbilityHandle);
UFUNCTION(Server, reliable, WithValidation)
void ServerSetInputReleased(FGameplayAbilitySpecHandle AbilityHandle);
Instancing Policy
Gamplay Ability를 실행하면 보통 해당 Ability Type의 새 Object가 Spawn 되어 상황을 추적한다.
BR, MOBA, MMO, RTS 게임처럼 수백의 Player나 AI가 전투를 벌이는 경우, Ability 실행 빈도가 매우 높아져 Object 생성이 많아져 성능에 부정적인 영향을 줄 수 있다.
이를 해결하기 위해 Ability에 3가지 Instancing Policy를 제공해 성능과 기능 사이 균형을 맞출 수 있다.
Instanced per Execution
Ability를 실행할 때마다 Ability Object의 사본을 Spawn
장점
BP Graph와 멤버 변수를 자유롭게 사용 할 수 있음
실행 시작 시 모든 값이 Default로 초기화 된다.
가장 간단하게 구현할 수 있는 Instancing Polity
단점
대규모 Overhad가 수반되어 자주 실행되지 않는 Ability에 이상적
ex) MOBA의 궁극기
Instanced per Actor
Ability를 처음 실행하면 Actor마다 하나의 Ability Instance를 Spawn
향후 재실행 시 해당 Instance를 재사용
Ability를 실행할 때마다 멤버 변수를 지워야 하지만, 여러번 실행에서 정보를 절약할 수 있다.
각 Instance는 Ability에 Replicated Object를 통해 변수 변화와 RPC를 처리 할 수 있다.
하지만 실행할 때마다 새 Object를 Spawn하느라 네트워크 대역폭과 CPU 시간이 낭비되지 않는다.
때문에 Replication에 이상적이다.
규모가 큰 상황에서 Performance가 뛰어난 정책
많은 수의 Actor가 처음 Ability를 사용할 때에만 Object를 Spawn하기 때문
Non-Instanced
전체 Category 중 가장 효율적인 Instancing Policy
Ability를 실행할 때마다 Object Spawn 대신 Class Default Object를 사용
하지만 이런 효용성으로 인해 몇 가지 제약사항이 발생한다.
C++로만 작성이 가능하다.
Instance가 없는 Ability의 BP Class를 생성할 수는 있지만, 노출된 Property의 Default 값을 변경할 때에만 사용 가능하다.
Ability는 실행 중에 멤버 변수를 변경하거나 Delegate Bind를 하면 안된다.
변수를 Replicate 하거나 RPC를 처리할 수 없다.
내부적인 변수 저장과 Data Replication이 필요 없는 Ability에만 사용
다만 Ability의 사용자가 Attribute를 설정하는 것은 가능
ex) RTS나 MOBA 게임에서 유닛이 사용하는 기본 공격
Trigger with Gameplay Event
일반 Channel을 통하지않고도 어떤 Context의 Data Payload를 전송해 Ability를 직접 Trigger 하도록 전달될 수 있는 Struct
일반적으로 다음 기능을 제공
Actor에 SendGameplayEventToActor을 호출
IAbilitySystmInterface와 Gameplay Event에 필요한 Context 정보를 구현하는 Actor 제공
하지만 ASC에서 HandleGameplayEvent를 바로 호출해도 문제 없다.
Gameplay Ability를 호출하는 정상적인 경로가 아니기에 필요한 Context 정보는 FGameplayEventData 구조체로 전달한다.
이 구조체는 범용이기 때문에 특정 Gameplay Event나 Ability용으로 확장되지 않는다.
하지만 ContextHandle 관련 필드에 부가 정보를 제공하면 Ability나 Gameplay Event용으로 사용할 수 있다.
Gameplay Event가 Gameplay Ability를 Trigger하면, Activate Ability 대신 Activate Ability From Event를 사용한다.
Activate Ability From Event는 부가 Context Data를 parameter로 제공할 수 있다.
Ability가 Gameplay Event에 반응하려면 반드시 Activate Ability from Event로 처리되어야 한다.
하지만 BP에서 구현되고 나면 Activate Ability를 대신해 모든 Acitivate Traffic을 받게 된다.
/** Delegate for when an effect is applied */
DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnGameplayEffectAppliedDelegate, UAbilitySystemComponent*, const FGameplayEffectSpec&, FActiveGameplayEffectHandle);
/** Called on server whenever a GE is applied to self. This includes instant and duration based GEs. */
FOnGameplayEffectAppliedDelegate OnGameplayEffectAppliedDelegateToSelf;
/** Called on server whenever a GE is applied to someone else. This includes instant and duration based GEs. */
FOnGameplayEffectAppliedDelegate OnGameplayEffectAppliedDelegateToTarget;
/** Called on both client and server whenever a duration based GE is added (E.g., instant GEs do not trigger this). */
FOnGameplayEffectAppliedDelegate OnActiveGameplayEffectAddedDelegateToSelf;
/** Called on server whenever a periodic GE executes on self */
FOnGameplayEffectAppliedDelegate OnPeriodicGameplayEffectExecuteDelegateOnSelf;
/** Called on server whenever a periodic GE executes on target */
FOnGameplayEffectAppliedDelegate OnPeriodicGameplayEffectExecuteDelegateOnTarget;
/** This ASC has successfully applied a GE to something (potentially itself) */
void OnGameplayEffectAppliedToTarget(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);
void OnGameplayEffectAppliedToSelf(UAbilitySystemComponent* Source, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);
void OnPeriodicGameplayEffectExecuteOnTarget(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecExecuted, FActiveGameplayEffectHandle ActiveHandle);
void OnPeriodicGameplayEffectExecuteOnSelf(UAbilitySystemComponent* Source, const FGameplayEffectSpec& SpecExecuted, FActiveGameplayEffectHandle ActiveHandle);
위 함수들은 LocalRole에 따라 호출 여부가 다르다.
Authority: 언제나 항상 호출
Autonomous: 대상 GE의 Replication Mode가 Full이거나 Mixed인 경우에만 호출 됨
Simulated: 대상 GE의 Replication Mode가 Full일 때에만 호출 됨
Remove
GE는 GameplayAbilities와 ASC에서 RemoveActiveGameplayEffect가 포함된 함수들에 의해 제거된다.
또한 Duration, Infinite Ge는 제거될 때 Delegate를 받을 수 있다.
/** Contains all of the gameplay effects that are currently active on this component */
UPROPERTY(Replicated)
FActiveGameplayEffectsContainer ActiveGameplayEffects;
DECLARE_MULTICAST_DELEGATE_OneParam(FOnGivenActiveGameplayEffectRemoved, const FActiveGameplayEffect&);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnActiveGameplayEffectRemoved_Info, const FGameplayEffectRemovalInfo&);
/** Called when any gameplay effects are removed */
FOnGivenActiveGameplayEffectRemoved& OnAnyGameplayEffectRemovedDelegate();
/** Returns delegate structure that allows binding to several gameplay effect changes */
FOnActiveGameplayEffectRemoved_Info* OnGameplayEffectRemoved_InfoDelegate(FActiveGameplayEffectHandle Handle);
위 함수들은 LocalRole에 따라 호출 여부가 다르다.
Authority: 언제나 항상 호출
Autonomous: 대상 GE의 Replication Mode가 Full이거나 Mixed인 경우에만 호출 됨
Simulated: 대상 GE의 Replication Mode가 Full일 때에만 호출 됨
/** Gameplay effect duration policies */
UENUM()
enum class EGameplayEffectDurationType : uint8
{
/** This effect applies instantly */
Instant,
/** This effect lasts forever */
Infinite,
/** The duration of this effect will be specified by a magnitude */
HasDuration
};
/**
* Helper function to sum all of the mods in the specified array, using the specified modifier bias and evaluation parameters
*
* @param InMods Mods to sum
* @param Bias Bias to apply to modifier magnitudes
* @param Parameters Evaluation parameters
*
* @return Summed value of mods
*/
float FAggregatorModChannel::SumMods(const TArray<FAggregatorMod>& InMods, float Bias, const FAggregatorEvaluateParameters& Parameters)
{
float Sum = Bias;
for (const FAggregatorMod& Mod : InMods)
{
if (Mod.Qualifies())
{
Sum += (Mod.EvaluatedMagnitude - Bias);
}
}
return Sum;
}
/**
* Evaluates the channel's mods with the specified base value and evaluation parameters
*
* @param InlineBaseValue Base value to use for the evaluation
* @param Parameters Additional evaluation parameters to use
*
* @return Evaluated value based upon the channel's mods
*/
float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
for (const FAggregatorMod& Mod : Mods[EGameplayModOp::Override])
{
if (Mod.Qualifies())
{
return Mod.EvaluatedMagnitude;
}
}
float Additive = SumMods(Mods[EGameplayModOp::Additive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Additive), Parameters);
float Multiplicitive = SumMods(Mods[EGameplayModOp::Multiplicitive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Multiplicitive), Parameters);
float Division = SumMods(Mods[EGameplayModOp::Division], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Division), Parameters);
if (FMath::IsNearlyZero(Division))
{
ABILITY_LOG(Warning, TEXT("Division summation was 0.0f in FAggregatorModChannel."));
Division = 1.f;
}
return ((InlineBaseValue + Additive) * Multiplicitive) / Division;
}
Multiply/Divide Modifier는 둘 다 1의 Bias Value를 가지고 있다.
/** Notifies when GameplayEffectSpec is blocked by an ActiveGameplayEffect due to immunity */
DECLARE_MULTICAST_DELEGATE_TwoParams(FImmunityBlockGE, const FGameplayEffectSpec& /*BlockedSpec*/, const FActiveGameplayEffect* /*ImmunityGameplayEffect*/);
/** Immunity notification support */
FImmunityBlockGE OnImmunityBlockGameplayEffectDelegate;
GarantedApplicationImmunityTags
5.3 버전에서 Deprecated 됨
UImmunityGameplayEffectComponent를 대신 사용
GrantedApplicationImmunityQuery
5.3 버전에서 Deprecated 됨
UImmunityGameplayEffectComponent를 대신 사용
GameplayEffectSpec
GE의 인스턴스화로 취급할 수 있다.(이하 GESpec)
자신이 가르키는 GE의 Reference 만들어진 Level, 만든 주체에 대한 정보를 내포하고 있다.
이 정보들은 GE와 다르게 Runtime상에서 적용되기 전까지 자유롭게 생성/수정이 가능하다.
GE가 적용 될 때 GESpec이 GE로부터 생성되고 실질적으로 GESpec이 Target에 적용된다.
GESpec은 UASC::MakeOutgoingSpec()함수를 통해 GE로부터 생성된다.
/** Get an outgoing GameplayEffectSpec that is ready to be applied to other things. */
UFUNCTION(BlueprintCallable, Category = GameplayEffects)
virtual FGameplayEffectSpecHandle MakeOutgoingSpec(TSubclassOf<UGameplayEffect> GameplayEffectClass, float Level, FGameplayEffectContextHandle Context) const;
GESpec은 그 즉시 적용될 필요가 없다.
보통은 투사체에 GESpec을 전달하고 충돌 발생 시 연산에 사용하곤 한다.
GESpec이 성공적으로 적용되면 FActiveGameplayEffect를 반환한다.
GESpec에는 아래 내용들이 제공된다.
GESpec을 만드는데 사용된 GE 클래스
GESpec의 Level
보통 GESpec을 만드는데 사용한 Ability와 같지만 다를수도 있음
Duration
Default는 GE의 Duration이지만 값이 다를 수도 있다.
Period
Default는 GE의 Period이지만 다를 수도 있다.
현재 GESpec의 Stack Count
상한은 GE의 설정값을 따른다.
GameplayEffectContextHandle
이를 통해 누가 이 GeSpec을 만들었는지 인지할 수 있다.
GESpec이 생성 되었을 때 Snapshotting으로 인해 Capture되는 Attribute 들
DynamicGrantedTag
GESpec이 부여 될 때 GameplayTag에 GE가 추가로 부여해야 하는 GameplayEffectSpec
DynamicAssetTag
GESpec이 GE와 비교해 추가로 가져야 하는 Tag들
SetByCaller
SetByCallers
GESpec으로 하여금 GameplayTag나 FName과 관련된 float 값을 전달할 수 있도록 한다.
이들은 GESpec에 TMap 형태로 저장된다.
이 값들은 GE의 Modifier에 사용되거나, 일반적인 의미의 Ferring Floats로 사용될 수 있다.
Modifiers에서 사용
GE class보다 앞서 정의되어야만 한다.
GameTag 버전만 사용 가능하다.
GE class에 하나가 정의 되었는데 GESpec이 대응하는 Tag-float pair가 없는 경우, GESpec에서 Runtime Error가 발생하며 0을 반환한다.
Divide Operation에서 문제를 야기할 수 있다.
그 외에서 사용
어느 class보다도 앞서 정의 될 필요가 없다.
GESpec에 존재하지 않는 SetByCaller를 읽더라도 사용자로 하여금 Default 값과 warning을 반환할 수 있다.
관련 함수
보통 Ability 안에서 생성된 숫자 데이터들은 SetByCallers을 통해 GameplayEffectExecutionCalculations나 ModifierMagnitudeCalculations로 전달하는 것이 일반적이다.
SetByCaller를 BP에서 읽을 수 있으려면 BPLibrary에 Custom Node를 만들어 줘야 한다.
/** Sets the magnitude of a SetByCaller modifier */
void SetSetByCallerMagnitude(FName DataName, float Magnitude);
/** Sets the magnitude of a SetByCaller modifier */
void SetSetByCallerMagnitude(FGameplayTag DataTag, float Magnitude);
/** Returns the magnitude of a SetByCaller modifier. Will return 0.f and Warn if the magnitude has not been set. */
float GetSetByCallerMagnitude(FName DataName, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;
/** Returns the magnitude of a SetByCaller modifier. Will return 0.f and Warn if the magnitude has not been set. */
float GetSetByCallerMagnitude(FGameplayTag DataTag, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;
이 중 FName을 쓰는 것보다 GameplayTag를 사용하는 것을 권장한다.
GameplayEffectContext
GEContext는 GESpec의 Instigator와 TargetData를 가지고 있다.
이는 Subclass를 통해 임시 데이터를 다른 곳으로 전달하는 좋은 구조이다.
ModifierMagnitudeCalculations, GameplayEffectExecutionCalculations, AS, GameplayCues 등
/**
* Struct representing the definition of a custom execution for a gameplay effect.
* Custom executions run special logic from an outside class each time the gameplay effect executes.
*/
USTRUCT(BlueprintType)
struct GAMEPLAYABILITIES_API FGameplayEffectExecutionDefinition
{
GENERATED_USTRUCT_BODY()
/**
* Gathers and populates the specified array with the capture definitions that the execution would like in order
* to perform its custom calculation. Up to the individual execution calculation to handle if some of them are missing
* or not.
*
* @param OutCaptureDefs [OUT] Capture definitions requested by the execution
*/
void GetAttributeCaptureDefinitions(OUT TArray<FGameplayEffectAttributeCaptureDefinition>& OutCaptureDefs) const;
/** Custom execution calculation class to run when the gameplay effect executes */
UPROPERTY(EditDefaultsOnly, Category=Execution)
TSubclassOf<UGameplayEffectExecutionCalculation> CalculationClass;
/** These tags are passed into the execution as is, and may be used to do conditional logic */
UPROPERTY(EditDefaultsOnly, Category = Execution)
FGameplayTagContainer PassedInTags;
/** Modifiers that are applied "in place" during the execution calculation */
UPROPERTY(EditDefaultsOnly, Category = Execution)
TArray<FGameplayEffectExecutionScopedModifierInfo> CalculationModifiers;
/** Other Gameplay Effects that will be applied to the target of this execution if the execution is successful. Note if no execution class is selected, these will always apply. */
UPROPERTY(EditDefaultsOnly, Category = Execution)
TArray<FConditionalGameplayEffect> ConditionalGameplayEffects;
};
/** Array of executions that will affect the target of this effect */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = GameplayEffect)
TArray<FGameplayEffectExecutionDefinition> Executions;
Modifier에서 지원하는 정보 이상의 것이 필요할 때에는 사용
UGameplayEffectExecutionCalculation을 사용하면 GE의 Custom Behaviour를 정의
Modifier로 커버할 수 없는 복잡한 식을 정의할 때 유용
Application Requirements
GE를 적용하는 조건을 처리하는 부분
5.3 버전 기준으로 Deprecated
UCustomCanApplyGameplayEffectComponent를 대신 사용
Granted Abilities
GE를 통해 부여하는 Tag 혹은 Attribute
5.3 버전 기준으로 Deprecated
GetBlockedAbilityTags 함수를 통해 UTargetTagsGameplayEffectComponent를 대신 사용
Overflow Effects
/** Effects to apply when a stacking effect "overflows" its stack count through another attempted application. Added whether the overflow application succeeds or not. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Stacking|Overflow", meta = (EditConditionHides, EditCondition = "StackingType != EGameplayEffectStackingType::None"))
TArray<TSubclassOf<UGameplayEffect>> OverflowEffects;
/**
* FGameplayEffectCue
* This is a cosmetic cue that can be tied to a UGameplayEffect.
* This is essentially a GameplayTag + a Min/Max level range that is used to map the level of a GameplayEffect to a normalized value used by the GameplayCue system.
*/
USTRUCT(BlueprintType)
struct FGameplayEffectCue
{
GENERATED_USTRUCT_BODY()
FGameplayEffectCue()
: MinLevel(0.f)
, MaxLevel(0.f)
{
}
FGameplayEffectCue(const FGameplayTag& InTag, float InMinLevel, float InMaxLevel)
: MinLevel(InMinLevel)
, MaxLevel(InMaxLevel)
{
GameplayCueTags.AddTag(InTag);
}
/** The attribute to use as the source for cue magnitude. If none use level */
UPROPERTY(EditDefaultsOnly, Category = GameplayCue)
FGameplayAttribute MagnitudeAttribute;
/** The minimum level that this Cue supports */
UPROPERTY(EditDefaultsOnly, Category = GameplayCue)
float MinLevel;
/** The maximum level that this Cue supports */
UPROPERTY(EditDefaultsOnly, Category = GameplayCue)
float MaxLevel;
/** Tags passed to the gameplay cue handler when this cue is activated */
UPROPERTY(EditDefaultsOnly, Category = GameplayCue, meta = (Categories="GameplayCue"))
FGameplayTagContainer GameplayCueTags;
float NormalizeLevel(float InLevel)
{
float Range = MaxLevel - MinLevel;
if (Range <= KINDA_SMALL_NUMBER)
{
return 1.f;
}
return FMath::Clamp((InLevel - MinLevel) / Range, 0.f, 1.0f);
}
};
/** Cues to trigger non-simulated reactions in response to this GameplayEffect such as sounds, particle effects, etc */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GameplayCues")
TArray<FGameplayEffectCue> GameplayCues;
Particle이나 Sound 같은 Decoration Effect를 관리하는 효율적인 Network 방식
GAS로 제어할 수 있다.
GA나 GE로 트리거 할 수 있다.
모든 Gameplay Cue에는 GameplayCue로 시작하는 Gameplay Tag를 붙여야 한다.
/** Called when a GameplayCue is executed, this is used for instant effects or periodic ticks */
UFUNCTION(BlueprintNativeEvent, Category = "GameplayCueNotify")
bool OnExecute(AActor* MyTarget, const FGameplayCueParameters& Parameters);
/** Called when a GameplayCue with duration is first activated, this will only be called if the client witnessed the activation */
UFUNCTION(BlueprintNativeEvent, Category = "GameplayCueNotify")
bool OnActive(AActor* MyTarget, const FGameplayCueParameters& Parameters);
/** Called when a GameplayCue with duration is first seen as active, even if it wasn't actually just applied (Join in progress, etc) */
UFUNCTION(BlueprintNativeEvent, Category = "GameplayCueNotify")
bool WhileActive(AActor* MyTarget, const FGameplayCueParameters& Parameters);
/** Called when a GameplayCue with duration is removed */
UFUNCTION(BlueprintNativeEvent, Category = "GameplayCueNotify")
bool OnRemove(AActor* MyTarget, const FGameplayCueParameters& Parameters);
GA 와의 Interaction
GE가 GA와 상호작용할 때에 AS에서 제공하는 함수를 이용한다.
하지만 이는 기본적인 기능만 제공하고, Attribute의 값 변경에 대한 추가 작업이나 범위 지정을 하려면 AS의 함수를 Override 해야 한다.
/**
* Called just before modifying the value of an attribute. AttributeSet can make additional modifications here. Return true to continue, or false to throw out the modification.
* Note this is only called during an 'execute'. E.g., a modification to the 'base value' of an attribute. It is not called during an application of a GameplayEffect, such as a 5 ssecond +10 movement speed buff.
*/
virtual bool PreGameplayEffectExecute(struct FGameplayEffectModCallbackData &Data) { return true; }
/**
* Called just after a GameplayEffect is executed to modify the base value of an attribute. No more changes can be made.
* Note this is only called during an 'execute'. E.g., a modification to the 'base value' of an attribute. It is not called during an application of a GameplayEffect, such as a 5 ssecond +10 movement speed buff.
*/
virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData &Data) { }
/**
* An "On Aggregator Change" type of event could go here, and that could be called when active gameplay effects are added or removed to an attribute aggregator.
* It is difficult to give all the information in these cases though - aggregators can change for many reasons: being added, being removed, being modified, having a modifier change, immunity, stacking rules, etc.
*/
/**
* Called just before any modification happens to an attribute. This is lower level than PreAttributeModify/PostAttribute modify.
* There is no additional context provided here since anything can trigger this. Executed effects, duration based effects, effects being removed, immunity being applied, stacking rules changing, etc.
* This function is meant to enforce things like "Health = Clamp(Health, 0, MaxHealth)" and NOT things like "trigger this extra thing if damage is applied, etc".
*
* NewValue is a mutable reference so you are able to clamp the newly applied value as well.
*/
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) { }
/** Called just after any modification happens to an attribute. */
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) { }
/**
* This is called just before any modification happens to an attribute's base value when an attribute aggregator exists.
* This function should enforce clamping (presuming you wish to clamp the base value along with the final value in PreAttributeChange)
* This function should NOT invoke gameplay related events or callbacks. Do those in PreAttributeChange() which will be called prior to the
* final value of the attribute actually changing.
*/
virtual void PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const { }
/** Called just after any modification happens to an attribute's base value when an attribute aggregator exists. */
virtual void PostAttributeBaseChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) const { }
UCLASS()
class MYPROJECT_API UMyAttributeSet : public UAttributeSet
{
GENERATED_BODY()
public:
/** 퍼블릭 액세스 가능한 샘플 어트리뷰트 'Health'*/
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FGameplayAttributeData Health;
};
class MyAbilitySystemComponent : public UAbilitySystemComponent
{
....
/** 샘플 어트리뷰트 세트. */
UPROPERTY()
const UMyAttributeSet* AttributeSet;
....
}
// 이런 코드는 일반적으로 BeginPlay()에 나타나지만,
// 적절한 어빌리티 시스템 컴포넌트 구하기일 수 있습니다. 다른 액터에 있을 수도 있으므로 GetAbilitySystemComponent를 사용하여 결과가 유효한지 확인하세요.
AbilitySystemComponent* ASC = GetAbilitySystemComponent();
// AbilitySystemComponent가 유효한지 확인합니다. 실패를 허용할 수 없다면 if() 조건문을 check() 구문으로 대체합니다.
if (IsValid(ASC))
{
// 어빌리티 시스템 컴포넌트에서 UMyAttributeSet를 구합니다. 필요한 경우 어빌리티 시스템 컴포넌트가 UMyAttributeSet를 생성하고 등록할 것입니다.
AttributeSet = ASC->GetSet<UMyAttributeSet>();
// 이제 새 UMyAttributeSet에 대한 포인터가 생겨 나중에 사용할 수 있습니다. 초기화 함수가 있는 경우 여기서 호출하면 좋습니다.
}
float UMyAttributeSet::GetHealth() const
{
// Health의 현재 값을 반환하지만, 0 미만의 값은 반환하지 않습니다.
// 이 값은 Health에 영향을 미치는 모든 모디파이어가 고려된 후의 값입니다.
return FMath::Max(Health.GetCurrentValue(), 0.0f);
}
void UMyAttributeSet::SetHealth(float NewVal)
{
// 0 미만의 값을 받지 않습니다.
NewVal = FMath::Max(NewVal, 0.0f);
// 어빌리티 시스템 컴포넌트 인스턴스가 있는지 확인합니다. 항상 인스턴스가 있어야 합니다.
UAbilitySystemComponent* ASC = GetOwningAbilitySystemComponent();
if (ensure(ASC))
{
// 적절한 함수를 통해 현재 값이 아닌 베이스 값을 설정합니다.
// 그러면 적용한 모디파이어가 계속해서 적절히 작동합니다.
ASC->SetNumericAttributeBase(GetHealthAttribute(), NewVal);
}
}
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSet->GetHealthAttribute()).AddUObject(this, &AGASAbilityDemoCharacter::OnHealthChangedInternal);
Gameplay Effect와의 Interaction
GA의 값을 제어하는 가장 일반적인 방법은 관련 Gameplay Effect를 처리하는 것이다.
PostGameplayEffectExecute는 Attribute의 Base 값을 변경되는GameplayEffect가호출되기 전에 호출된다.
GameplayEffect가 호출(Execute) 되기 전에만 호출되고, 직접 접근해 수정할 때에는 호출되지 않는다.
또한 BaseValue가 아닌 current 값에 영향을 주는 GameplayEffect에서도 호출되지 않는다.
위 예시에서 PostGameplayEffectExecute 함수는 public 접근자가 지정되어야 한다.
함수 내에서 새로운 Body를 작성하되, Super::PostGameplayEffectExecute가 확실히 호출되어 있어야 한다.
class UMyAttributeSet : public AttributeSet
{
.....
public:
void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data) override;
.....
}
void UMyAttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
{
// 잊지 말고 부모 구현을 호출하세요.
Super::PostGameplayEffectExecute(Data);
// 프로퍼티 게터를 사용하여 이 호출이 Health에 영향을 미치는지 확인합니다.
if (Data.EvaluatedData.Attribute == GetHealthAttribute())
{
// 이 게임플레이 이펙트는 Health를 변경합니다. 적용하되 우선 값을 제한합니다.
// 이 경우 Health 베이스 값은 음수가 아니어야 합니다.
SetHealth(FMath::Max(GetHealth(), 0.0f));
}
}
Replication
Multiplayer의 경우, 여타 Property Replicate와 유사하게 AS을 통해 GA Replicate를 할 수 있다.
class UMyAttributeSet : public UAttributeSet
{
.....
/** 네트워크를 통해 새 Health 값이 도착할 때 호출됨 */
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);
/** 리플리케이트할 프로퍼티 표시 */
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
protected:
/** 샘플 어트리뷰트 'Health'*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
....
};
void UMyAttributeSet::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
// 디폴트 게임플레이 어트리뷰트 시스템의 repnotify 행동을 사용합니다.
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyAttributeSet, Health, OldHealth);
}
void UMyAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
// 부모 함수를 호출합니다.
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// Health에 대한 리플리케이션을 추가합니다.
DOREPLIFETIME_CONDITION_NOTIFY(UMyAttributeSet, Health, COND_None, REPNOTIFY_Always);
}
Replicate 할 Attribute에 PROPERTY에 ReplicatedUsing 지정자를 추가하고, Callback 함수를 선언한다.
Source File에서 Callback 함수를 정의한다.
Callback 함수 내에는 GAMEPLAYATTRIBUTE_REPNOTIFY 매크로로 Replicate 되는 Attribute에 대한 RepNotify 행동을 실행한다.
만약 이 AS가 처음 Replicate되는 Attribute라면, GetLifetimeReplicatedProps에 추가한다.
주요 함수
PreAttributeChange
/**
* Called just before any modification happens to an attribute. This is lower level than PreAttributeModify/PostAttribute modify.
* There is no additional context provided here since anything can trigger this. Executed effects, duration based effects, effects being removed, immunity being applied, stacking rules changing, etc.
* This function is meant to enforce things like "Health = Clamp(Health, 0, MaxHealth)" and NOT things like "trigger this extra thing if damage is applied, etc".
*
* NewValue is a mutable reference so you are able to clamp the newly applied value as well.
*/
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) { }
Attribute의 CurrentValue가 바뀌기 전에 호출되는 함수
NewValue를 이용해 CurrentValue를 Clamp한 상태로 적용할 때 사용 함.
if (Attribute == GetMoveSpeedAttribute())
{
// Cannot slow less than 150 units/s and cannot boost more than 1000 units/s
NewValue = FMath::Clamp<float>(NewValue, 150, 1000);
}
단, Attribute에 직접 접근하여 변경하는 것이 아닌 Macro에서 제공하는 Setter 함수나 GameplayEffects로만 호출된다.
그리고 Epic에서는 이 함수를 Attribute 변경에 대한 Delegate로 사용을 권장하지 않는다.
그저 Attribute 값의 보정(Clamp) 용도로 사용을 권장한다.
PostGameplayEffectExecute
/**
* Called just after a GameplayEffect is executed to modify the base value of an attribute. No more changes can be made.
* Note this is only called during an 'execute'. E.g., a modification to the 'base value' of an attribute. It is not called during an application of a GameplayEffect, such as a 5 ssecond +10 movement speed buff.
*/
virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData &Data) { }
Attribute의 BaseValue가 Instant GameplayEffect로 인해 변경될 때 호출되는 함수
GameplayEffect로 인해 Attribute가 변동될 때 좀 더 복잡한 작업을 수행할 수 있다.
함수가 호출된 시점에는 이미 Attribute 값이 변경이 된 후다.
하지만 아직 Client에는 Replicate 되지 않았다.
때문에 여기서 Clamping을 하는 것은 Client에서는 발생하지 않고, 그 결과만 받는다.
OnAttributeAggregatorCreate
/** Callback for when an FAggregator is created for an attribute in this set. Allows custom setup of FAggregator::EvaluationMetaData */
virtual void OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator) const { }
Attribute가 set에서 생성되어 Aggregator가 생성될 때.
FAggregatorEvaluateMetaData에 대한 Custom Setup을 할 수 있다.
Attribute에서 Modifier로 적용하는데 가장 작은 음수 값을 가져올 수 있다.
Paragon에서 여러 개의 감속 디버프가 걸릴 때 가장 낮은 비율로 적용할 때 이 기능을 사용
AGDPlayerState::AGDPlayerState()
{
// Create ability system component, and set it to be explicitly replicated
AbilitySystemComponent = CreateDefaultSubobject<UGDAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
AbilitySystemComponent->SetIsReplicated(true);
//...
}
class APACharacterBase : public AActor, public IAbilitySystemInterface
{
//~ IAbilitySystemInterface 시작
/** 어빌리티 시스템 컴포넌트를 반환합니다. */
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
//~ IAbilitySystemInterface 끝
/** 어빌리티 시스템 컴포넌트입니다. 게임플레이 어트리뷰트 및 게임플레이 어빌리티를 사용하려면 필요합니다. */
UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "Abilities")
UAbilitySystemComponent* AbilitySystemComponent;
}
void APACharacterBase::PossessedBy(AController * NewController)
{
Super::PossessedBy(NewController);
if (AbilitySystemComponent)
{
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
// ASC MixedMode replication requires that the ASC Owner's Owner be the Controller.
SetOwner(NewController);
}
UAbilitySystemComponent* APACharacterBase::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}
// Server only
void AGDHeroCharacter::PossessedBy(AController * NewController)
{
Super::PossessedBy(NewController);
AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
if (PS)
{
// Set the ASC on the Server. Clients do this in OnRep_PlayerState()
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
// AI won't have PlayerControllers so we can init again here just to be sure. No harm in initing twice for heroes that have PlayerControllers.
PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
}
//...
}
// Client only
void AGDHeroCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
if (PS)
{
// Set the ASC for clients. Server does this in PossessedBy.
AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());
// Init ASC Actor Info for clients. Server will init its ASC when it possesses a new Actor.
AbilitySystemComponent->InitAbilityActorInfo(PS, this);
}
// ...
}
ASC가 PlayerState에 있는 경우에는 명시적으로 Initialize 하는데 호출되는 함수가 달라진다.
Server: Pawn::PossessedBy()
Client: Pawn::Onrep_PlayerState()
이 모든 과정은 Client에 PlayerState가 있다는 것을 상정하고 작업된다.
만약 아래 로그가 발생하면, ASC가 Client에서 Initialize가 되지 않은 것이다.
LogAbilitySystem: Warning: Can't activate LocalOnly or LocalPredicted ability %s when not local!