ue5.3.2
UAbilityTask太庞大暂时不说
UGameplayTask是基础 ,对应的应该有UGameplayTasksComponent
启动一个Task,通过调用UGameplayTasksComponent的static方法来run一个task,还有蓝图方法
代码如下
UFUNCTION(BlueprintCallable, DisplayName="Run Gameplay Task", meta=(ScriptName="RunGameplayTask"), Category = "Gameplay Tasks", meta = (AutoCreateRefTerm = "AdditionalRequiredResources, AdditionalClaimedResources", AdvancedDisplay = "AdditionalRequiredResources, AdditionalClaimedResources"))
static GAMEPLAYTASKS_API EGameplayTaskRunResult K2_RunGameplayTask(TScriptInterface<IGameplayTaskOwnerInterface> TaskOwner, UGameplayTask* Task, uint8 Priority, TArray<TSubclassOf<UGameplayTaskResource> > AdditionalRequiredResources, TArray<TSubclassOf<UGameplayTaskResource> > AdditionalClaimedResources);
static GAMEPLAYTASKS_API EGameplayTaskRunResult RunGameplayTask(IGameplayTaskOwnerInterface& TaskOwner, UGameplayTask& Task, uint8 Priority, FGameplayResourceSet AdditionalRequiredResources, FGameplayResourceSet AdditionalClaimedResources);
EGameplayTaskRunResult UGameplayTasksComponent::RunGameplayTask(IGameplayTaskOwnerInterface& TaskOwner, UGameplayTask& Task, uint8 Priority, FGameplayResourceSet AdditionalRequiredResources, FGameplayResourceSet AdditionalClaimedResources)
{
const FText NoneText = FText::FromString(TEXT("None"));
if (Task.GetState() == EGameplayTaskState::Paused || Task.GetState() == EGameplayTaskState::Active)
{
// return as success if already running for the same owner, failure otherwise
return Task.GetTaskOwner() == &TaskOwner
? (Task.GetState() == EGameplayTaskState::Paused ? EGameplayTaskRunResult::Success_Paused : EGameplayTaskRunResult::Success_Active)
: EGameplayTaskRunResult::Error;
}
// this is a valid situation if the task has been created via "Construct Object" mechanics
if (Task.GetState() == EGameplayTaskState::Uninitialized)
{
Task.InitTask(TaskOwner, Priority);
}
Task.AddRequiredResourceSet(AdditionalRequiredResources);
Task.AddClaimedResourceSet(AdditionalClaimedResources);
Task.ReadyForActivation();
switch (Task.GetState())
{
case EGameplayTaskState::AwaitingActivation:
case EGameplayTaskState::Paused:
return EGameplayTaskRunResult::Success_Paused;
break;
case EGameplayTaskState::Active:
return EGameplayTaskRunResult::Success_Active;
break;
case EGameplayTaskState::Finished:
return EGameplayTaskRunResult::Success_Active;
break;
}
return EGameplayTaskRunResult::Error;
}
另外一种调用的方法就是手动的调用void UGameplayTask::InitTask方法也可以
void UGameplayTask::InitTask(IGameplayTaskOwnerInterface& InTaskOwner, uint8 InPriority)
{
Priority = InPriority;
TaskOwner = &InTaskOwner;
TaskState = EGameplayTaskState::AwaitingActivation;
if (bClaimRequiredResources)
{
ClaimedResources.AddSet(RequiredResources);
}
// call owner.OnGameplayTaskInitialized before accessing owner.GetGameplayTasksComponent, this is required for child tasks
InTaskOwner.OnGameplayTaskInitialized(*this);
UGameplayTasksComponent* GTComponent = InTaskOwner.GetGameplayTasksComponent(*this);
TasksComponent = GTComponent;
bOwnedByTasksComponent = (TaskOwner.GetObject() == GTComponent);
// make sure that task component knows about new task
if (GTComponent && !bOwnedByTasksComponent)
{
//Task初始化事件通知
GTComponent->OnGameplayTaskInitialized(*this);
}
}
调用的时候需要指定IGameplayTaskOwnerInterface& Owner,目前所知道的被AAIController控制的APawn都会默认创建UGameplayTasksComponent对象,而UGameplayTasksComponent就是继承了IGameplayTaskOwnerInterface接口。所以NPC具有运行UGameplaytask的天然优势。
当然也可以任意AActor添加UGameplayTasksComponent。
另外需要非常注意的是,Task本身也是继承IGameplayTaskOwnerInterface接口的,也就是说
Task可以run Task
class UGameplayTask : public UObject, public IGameplayTaskOwnerInterface
void AAIController::OnPossess(APawn* InPawn)
{
// don't even try possessing pending-kill pawns
if (InPawn != nullptr && !IsValid(InPawn))
{
return;
}
Super::OnPossess(InPawn);
if (GetPawn() == nullptr || InPawn == nullptr)
{
return;
}
// not calling UpdateNavigationComponents() anymore. The PathFollowingComponent
// is now observing newly possessed pawns (via OnNewPawn)
if (PathFollowingComponent)
{
PathFollowingComponent->Initialize();
}
if (bWantsPlayerState)
{
ChangeState(NAME_Playing);
}
// a Pawn controlled by AI _requires_ a GameplayTasksComponent, so if Pawn
// doesn't have one we need to create it
if (CachedGameplayTasksComponent == nullptr)
{
UGameplayTasksComponent* GTComp = InPawn->FindComponentByClass<UGameplayTasksComponent>();
if (GTComp == nullptr)
{
GTComp = NewObject<UGameplayTasksComponent>(InPawn, TEXT("GameplayTasksComponent"));
GTComp->RegisterComponent();
}
CachedGameplayTasksComponent = GTComp;
}
..........................
}
void UGameplayTask::ReadyForActivation()
如果想立刻执行,而不是根据优先级排序执行
就要确保
FORCEINLINE bool RequiresPriorityOrResourceManagement() const { return bCaresAboutPriority == true || RequiredResources.IsEmpty() == false || ClaimedResources.IsEmpty() == false; }
这个返回是false就行
void UGameplayTask::ReadyForActivation()
{
//没有UGameplayTasksComponent 就endtask
if (UGameplayTasksComponent* TasksPtr = TasksComponent.Get())
{
//是否需要按权重执行
if (RequiresPriorityOrResourceManagement() == false)
{
PerformActivation();
}
else
{
TasksPtr->AddTaskReadyForActivation(*this);
}
}
else
{
EndTask();
}
}
先说不按权重的执行
void UGameplayTask::PerformActivation()
{
//如果此Task是在运行状态就直接Return
if (TaskState == EGameplayTaskState::Active)
{
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Warning
, TEXT("%s PerformActivation called while TaskState is already Active. Bailing out.")
, *GetName());
return;
}
TaskState = EGameplayTaskState::Active;
//激活task
Activate();
// Activate call may result in the task actually "instantly" finishing.
// If this happens we don't want to bother the TaskComponent
// with information on this task
if (IsFinished() == false)
{
//激活事件通知
TasksComponent->OnGameplayTaskActivated(*this);
}
}
void UGameplayTask::Activate()
{
//一般都要重载这个函数
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Verbose
, TEXT("%s Activate called, current State: %s")
, *GetName(), *GetTaskStateName());
}
然后看看需要根据权重优先级执行是什么样子
//好复杂,就是要根据优先级占有资源的互斥,来执行
void UGameplayTasksComponent::AddTaskReadyForActivation(UGameplayTask& NewTask)
{
UE_VLOG(this, LogGameplayTasks, Log, TEXT("AddTaskReadyForActivation %s"), *NewTask.GetName());
ensure(NewTask.RequiresPriorityOrResourceManagement() == true);
TaskEvents.Add(FGameplayTaskEventData(EGameplayTaskEvent::Add, NewTask));
// trigger the actual processing only if it was the first event added to the list
if (TaskEvents.Num() == 1 && CanProcessEvents())
{
ProcessTaskEvents();
}
}
void UGameplayTasksComponent::ProcessTaskEvents()
{
static const int32 MaxIterations = 16;
bInEventProcessingInProgress = true;
int32 IterCounter = 0;
while (TaskEvents.Num() > 0)
{
IterCounter++;
if (IterCounter > MaxIterations)
{
UE_VLOG(this, LogGameplayTasks, Error, TEXT("UGameplayTasksComponent::ProcessTaskEvents has exceeded allowes number of iterations. Check your GameplayTasks for logic loops!"));
TaskEvents.Reset();
break;
}
for (int32 EventIndex = 0; EventIndex < TaskEvents.Num(); ++EventIndex)
{
UE_VLOG(this, LogGameplayTasks, Verbose, TEXT("UGameplayTasksComponent::ProcessTaskEvents: %s event %s")
, *TaskEvents[EventIndex].RelatedTask.GetName(), GetGameplayTaskEventName(TaskEvents[EventIndex].Event));
if (!IsValid(&TaskEvents[EventIndex].RelatedTask))
{
UE_VLOG(this, LogGameplayTasks, Verbose, TEXT("%s is invalid"), *TaskEvents[EventIndex].RelatedTask.GetName());
// we should ignore it, but just in case run the removal code.
RemoveTaskFromPriorityQueue(TaskEvents[EventIndex].RelatedTask);
continue;
}
switch (TaskEvents[EventIndex].Event)
{
case EGameplayTaskEvent::Add:
if (TaskEvents[EventIndex].RelatedTask.TaskState != EGameplayTaskState::Finished)
{
AddTaskToPriorityQueue(TaskEvents[EventIndex].RelatedTask);
}
else
{
UE_VLOG(this, LogGameplayTasks, Error, TEXT("UGameplayTasksComponent::ProcessTaskEvents trying to add a finished task to priority queue!"));
}
break;
case EGameplayTaskEvent::Remove:
RemoveTaskFromPriorityQueue(TaskEvents[EventIndex].RelatedTask);
break;
default:
checkNoEntry();
break;
}
}
TaskEvents.Reset();
UpdateTaskActivations();
// task activation changes may create new events, loop over to check it
}
bInEventProcessingInProgress = false;
}
//本来以为Task的Active函数就是执行逻辑了,没想到到这里又根据不同属性分发到不同的容器中
//那就看看这三个容器分别是干啥的
void UGameplayTasksComponent::OnGameplayTaskActivated(UGameplayTask& Task)
{
// process events after finishing all operations
FEventLock ScopeEventLock(this);
//容器1,总容器来一个收一个
KnownTasks.Add(&Task);
if (Task.IsTickingTask())
{
check(TickingTasks.Contains(&Task) == false);
//容器2,看来准备在tick中用
TickingTasks.Add(&Task);
// If this is our first ticking task, set this component as active so it begins ticking
if (TickingTasks.Num() == 1)
{
UpdateShouldTick();
}
}
//这个能网络同步,牛
if (Task.IsSimulatedTask())
{
//容器3,能网络同步到客户端
const bool bWasAdded = AddSimulatedTask(&Task);
check(bWasAdded == true);
}
IGameplayTaskOwnerInterface* TaskOwner = Task.GetTaskOwner();
if (!Task.IsOwnedByTasksComponent() && TaskOwner)
{
TaskOwner->OnGameplayTaskActivated(Task);
}
}
KnownTasks就是收录了所有active的task,只是收集记录,并没有操作task
TickingTasks存放需要tick的task
然后在
void UGameplayTasksComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
SCOPE_CYCLE_COUNTER(STAT_TickGameplayTasks);
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// Because we have no control over what a task may do when it ticks, we must be careful.
// Ticking a task may kill the task right here. It could also potentially kill another task
// which was waiting on the original task to do something. Since when a tasks is killed, it removes
// itself from the TickingTask list, we will make a copy of the tasks we want to service before ticking any
int32 NumTickingTasks = TickingTasks.Num();
int32 NumActuallyTicked = 0;
switch (NumTickingTasks)
{
case 0:
break;
case 1:
{
UGameplayTask* TickingTask = TickingTasks[0];
if (IsValid(TickingTask))
{
TickingTask->TickTask(DeltaTime);
NumActuallyTicked++;
}
}
break;
default:
{
static TArray<UGameplayTask*> LocalTickingTasks;
LocalTickingTasks.Reset();
LocalTickingTasks.Append(TickingTasks);
for (UGameplayTask* TickingTask : LocalTickingTasks)
{
if (IsValid(TickingTask))
{
TickingTask->TickTask(DeltaTime);
NumActuallyTicked++;
}
}
}
break;
};
// Stop ticking if no more active tasks
if (NumActuallyTicked == 0)
{
TickingTasks.SetNum(0, false);
UpdateShouldTick();
}
}
SimulatedTasks,网络同步给simulate端
bool UGameplayTasksComponent::AddSimulatedTask(UGameplayTask* NewTask)
{
if (NewTask == nullptr)
{
return false;
}
if (SimulatedTasks.Find(NewTask) == INDEX_NONE)
{
SimulatedTasks.Add(NewTask);
SetSimulatedTasksNetDirty();
if (IsUsingRegisteredSubObjectList() && IsReadyForReplication())
{
AddReplicatedSubObject(NewTask, COND_SkipOwner);
}
return true;
}
return false;
}
/** Tasks that run on simulated proxies */
UPROPERTY(ReplicatedUsing = OnRep_SimulatedTasks)
TArray<TObjectPtr<UGameplayTask>> SimulatedTasks;
void UGameplayTasksComponent::OnRep_SimulatedTasks(const TArray<UGameplayTask*>& PreviousSimulatedTasks)
{
if (IsUsingRegisteredSubObjectList())
{
// Find if any tasks got removed
for (UGameplayTask* OldSimulatedTask : PreviousSimulatedTasks)
{
if (OldSimulatedTask)
{
const bool bIsRemoved = SimulatedTasks.Find(OldSimulatedTask) == INDEX_NONE;
if (bIsRemoved)
{
RemoveReplicatedSubObject(OldSimulatedTask);
}
}
}
}
//同步过来的Task并没有自动的Runtask,可以重载InitSimulatedTask函数来运行
for (UGameplayTask* SimulatedTask : GetSimulatedTasks())
{
if (SimulatedTask)
{
// If the task needs to be ticked and isn't yet.
if (SimulatedTask->IsTickingTask() && TickingTasks.Contains(SimulatedTask) == false)
{
SimulatedTask->InitSimulatedTask(*this);
TickingTasks.Add(SimulatedTask);
// If this is our first ticking task, set this component as active so it begins ticking
if (TickingTasks.Num() == 1)
{
UpdateShouldTick();
}
}
// See if it's a new task that needs to be registered
if (IsUsingRegisteredSubObjectList())
{
const bool bIsNew = PreviousSimulatedTasks.Find(SimulatedTask) == INDEX_NONE;
if (bIsNew)
{
AddReplicatedSubObject(SimulatedTask, COND_SkipOwner);
}
}
}
}
}
满足可网络同步的simulated的Task必须要重载两个方法才能同步 Task里面的属性
virtual void GetLifetimeReplicatedProps(TArray< class FLifetimeProperty >& OutLifetimeProps) const override;
virtual bool IsSupportedForNetworking() const override { return true; }
public:
float MontageDuration = 0.0f;
UPROPERTY(Replicated)
UAnimMontage* Montage = nullptr;
UPROPERTY(Replicated)
float PlayRate = 1;
UPROPERTY(Replicated)
FName PlaySection;
还有
this->bSimulatedTask = true;
this->bTickingTask = true;
void XXXXXX::InitSimulatedTask(UGameplayTasksComponent& InGameplayTasksComponent)
{
Super::InitSimulatedTask(InGameplayTasksComponent);
UGameplayTasksComponent* Temp = &InGameplayTasksComponent;
if (Temp != nullptr)
{
if (Temp->Implements<UGameplayTaskOwnerInterface>())
{
IGameplayTaskOwnerInterface* InterfaceIns = Cast<IGameplayTaskOwnerInterface>(Temp);
if (InterfaceIns)
{
InitTask(*InterfaceIns, GetPriority());
//因为是在客户端执行,所以就没必要设置bSimulatedTask为true否则就会在客户端的
//gameplaytaskscom上又要添加到simulate的数组里,其实到客户端,我们只是想要执行
//active里面的逻辑,其他的事越少越好
//最好是visual的效果类的逻辑需要在客户端play,才会用到simulate的gameplaytask
this->bSimulatedTask = false;
this->bTickingTask = false;
TaskState = EGameplayTaskState::Active;
Activate();
}
}
}
}
总结
在comp中调用方法把task和owner结合起来,并run task
comp只是管理task,比如需要tick的,需要同步的simulate task,和一些事件的分发
目前没有看到task是异步的,都是在game线程执行
貌似异步的现象,这个是被蓝图异步节点封装的Task
class GAMEPLAYTASKSEDITOR_API UK2Node_LatentGameplayTaskCall : public UK2Node_BaseAsyncTask
深度的源码我没有看,猜测是
执行的逻辑是具有异步特性的,完成和失败是需要回调通知
如果继承UGameplayTask并且写了static的BlueprintCallable方法就能被UK2Node_LatentGameplayTaskCall所捕获,就能在蓝图中直接调出异步节点。神奇,但是并没有深度看源码。比如
/** Wait specified time. This is functionally the same as a standard Delay node. */
UFUNCTION(BlueprintCallable, Category = "GameplayTasks", meta = (AdvancedDisplay = "TaskOwner, Priority", DefaultToSelf = "TaskOwner", BlueprintInternalUseOnly = "TRUE"))
static UGameplayTask_WaitDelay* TaskWaitDelay(TScriptInterface<IGameplayTaskOwnerInterface> TaskOwner, float Time, const uint8 Priority = 192);
UAbilityTask 和 UAITask的子类蓝图异步调用都是上面的情况。
UAbilityTask 是必须要又有UAbilitySystemComponent
UAITask没有特殊要求
UAITask 有公用的方法能方便的创建UAITask
template <class T>
static T* NewAITask(AAIController& AIOwner, IGameplayTaskOwnerInterface& InTaskOwner, FName InstanceName = FName())
{
return NewAITask<T>(*T::StaticClass(), AIOwner, InTaskOwner, InstanceName);
}
template <class T>
static T* NewAITask(AAIController& AIOwner, IGameplayTaskOwnerInterface& InTaskOwner, EAITaskPriority InPriority, FName InstanceName = FName())
{
return NewAITask<T>(*T::StaticClass(), AIOwner, InTaskOwner, InPriority, InstanceName);
}
template <class T>
static T* NewAITask(AAIController& AIOwner, FName InstanceName = FName())
{
return NewAITask<T>(*T::StaticClass(), AIOwner, AIOwner, InstanceName);
}
template <class T>
static T* NewAITask(AAIController& AIOwner, EAITaskPriority InPriority, FName InstanceName = FName())
{
return NewAITask<T>(*T::StaticClass(), AIOwner, AIOwner, InPriority, InstanceName);
}
template <class T>
static T* NewAITask(const UClass& Class, AAIController& AIOwner, IGameplayTaskOwnerInterface& InTaskOwner, FName InstanceName = FName())
{
T* TaskInstance = NewObject<T>(GetTransientPackage(), &Class);
TaskInstance->InstanceName = InstanceName;
TaskInstance->InitAITask(AIOwner, InTaskOwner);
return TaskInstance;
}
template <class T>
static T* NewAITask(const UClass& Class, AAIController& AIOwner, IGameplayTaskOwnerInterface& InTaskOwner, EAITaskPriority InPriority, FName InstanceName = FName())
{
T* TaskInstance = NewObject<T>(GetTransientPackage(), &Class);
TaskInstance->InstanceName = InstanceName;
TaskInstance->InitAITask(AIOwner, InTaskOwner, (uint8)InPriority);
return TaskInstance;
}
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
调用Run 一个behavior
bool UGameplayBehaviorSubsystem::TriggerBehavior(const UGameplayBehaviorConfig& Config, AActor& Avatar, AActor* SmartObjectOwner/* = nullptr*/)
{
UWorld* World = Avatar.GetWorld();
UGameplayBehavior* Behavior = World ? Config.GetBehavior(*World) : nullptr;
return Behavior != nullptr && TriggerBehavior(*Behavior, Avatar, &Config, SmartObjectOwner);
}
bool UGameplayBehaviorSubsystem::TriggerBehavior(UGameplayBehavior& Behavior, AActor& Avatar, const UGameplayBehaviorConfig* Config, AActor* SmartObjectOwner/* = nullptr*/)
{
UGameplayBehaviorSubsystem* Subsystem = GetCurrent(Avatar.GetWorld());
return Subsystem != nullptr && Subsystem->TriggerBehaviorImpl(Behavior, Avatar, Config, SmartObjectOwner);
}
bool UGameplayBehaviorSubsystem::TriggerBehaviorImpl(UGameplayBehavior& Behavior, AActor& Avatar, const UGameplayBehaviorConfig* Config, AActor* SmartObjectOwner/* = nullptr*/)
{
if (Behavior.Trigger(Avatar, Config, SmartObjectOwner))
{
Behavior.GetOnBehaviorFinishedDelegate().AddUObject(this, &UGameplayBehaviorSubsystem::OnBehaviorFinished);
FAgentGameplayBehaviors& AgentData = AgentGameplayBehaviors.FindOrAdd(&Avatar);
AgentData.Behaviors.Add(&Behavior);
return true;
}
return false;
}
这个函数把behavior和actor和smartobject actor给联系了起来
逻辑执行体Trigger,一般会重写这个方法
bool UGameplayBehavior::Trigger(AActor& Avatar, const UGameplayBehaviorConfig* Config, AActor* SmartObjectOwner/* = nullptr*/)
{
bTransientIsTriggering = true;
TransientAvatar = &Avatar;
TransientSmartObjectOwner = SmartObjectOwner;
if (bTriggerGeneric || bTriggerPawn || bTriggerCharacter)
{
// most common case, that's why we consider it first. Plus it's most specific
ACharacter* CharacterAvatar = bTriggerCharacter ? Cast<ACharacter>(&Avatar) : nullptr;
if (CharacterAvatar)
{
bTransientIsActive = true;
K2_OnTriggeredCharacter(CharacterAvatar, Config, SmartObjectOwner);
}
else if (bTriggerGeneric || bTriggerPawn)
{
APawn* PawnAvatar = bTriggerPawn ? Cast<APawn>(&Avatar) : nullptr;
if (PawnAvatar)
{
bTransientIsActive = true;
K2_OnTriggeredPawn(PawnAvatar, Config, SmartObjectOwner);
}
else if (bTriggerGeneric)
{
bTransientIsActive = true;
K2_OnTriggered(&Avatar, Config, SmartObjectOwner);
}
}
}
bTransientIsTriggering = false;
// bTransientIsActive might get changed by BP "end behavior" calls so we need to
// detect the behavior has finished synchronously and inform the caller
return bTransientIsActive;
}
那么如何run一个behavior 然后有 需要回调呢?
引擎已经写好了
class GAMEPLAYBEHAVIORSMARTOBJECTSMODULE_API UAITask_UseGameplayBehaviorSmartObject : public UAITask
关于instance
UGameplayBehavior* UGameplayBehaviorConfig::GetBehavior(UWorld& World) const
{
if (!BehaviorClass)
{
return nullptr;
}
UGameplayBehavior* BehaviorCDO = GetMutableDefault<UGameplayBehavior>(BehaviorClass);
return (BehaviorCDO && BehaviorCDO->IsInstanced(this))
? NewObject<UGameplayBehavior>(&World, BehaviorClass)
: BehaviorCDO;
}
//这个子类实现了,可以作为参考使用instance
bool UGameplayBehavior_BehaviorTree::NeedsInstance(const UGameplayBehaviorConfig* Config) const
{
const UGameplayBehaviorConfig_BehaviorTree* BTConfig = Cast<const UGameplayBehaviorConfig_BehaviorTree>(Config);
return BTConfig && BTConfig->ShouldStorePreviousBT();
}
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
它的子类除了UGameplayBehaviorSmartObjectBehaviorDefinition这个会和UGameplayBehaviorConfig、UGameplayBehavior有关联,很有用,其他的对我来说 都没用,因为我没用mass ai
UGameplayInteractionSmartObjectBehaviorDefinition又是一套很复杂的系统,
从界面上看能运行statetree
statetree里面解锁了很多个专属于 UGameplayInteractionSmartObjectBehaviorDefinition的task
目前还不清楚 咋运用
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
重点说说这个UGameplayBehaviorSmartObjectBehaviorDefinition
可以重载UGameplayBehaviorConfig和UGameplayBehavior 做出更多的功能
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
GAS系统 5.4.4
首先先给ASC组件一个GA,之后才能让这个组件激活运行这个GA,就是说首先我得有,才能激活
给ASC一个技能,是通过一下两个函数赋予的,只能在权威端添加。但是激活的时候可以在client端激活。肯定也能在server端直接激活。当然还得看GA的netExcuse的策略。比如说在client端激活serveronly的GA,就必须得走rpc,再比如说在server端执行local的GA,就得走client函数。具体看下面源码分析。
/**
* Grants a Gameplay Ability and returns its handle.
* This will be ignored if the actor is not authoritative.
*
* @param AbilityClass Type of ability to grant
* @param Level Level to grant the ability at
* @param InputID Input ID value to bind ability activation to.
*/
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Gameplay Abilities", meta = (DisplayName = "Give Ability", ScriptName = "GiveAbility"))
FGameplayAbilitySpecHandle K2_GiveAbility(TSubclassOf<UGameplayAbility> AbilityClass, int32 Level = 0, int32 InputID = -1);//注意give的都是ga 的class
/**
* Grants a Gameplay Ability, activates it once, and removes it.
* This will be ignored if the actor is not authoritative.
*
* @param AbilityClass Type of ability to grant
* @param Level Level to grant the ability at
* @param InputID Input ID value to bind ability activation to.
*/
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Gameplay Abilities", meta = (DisplayName = "Give Ability And Activate Once", ScriptName = "GiveAbilityAndActivateOnce"))
FGameplayAbilitySpecHandle K2_GiveAbilityAndActivateOnce(TSubclassOf<UGameplayAbility> AbilityClass, int32 Level = 0, int32 InputID = -1);
FGameplayAbilitySpecHandle UAbilitySystemComponent::K2_GiveAbility(TSubclassOf<UGameplayAbility> AbilityClass, int32 Level /*= 0*/, int32 InputID /*= -1*/)
{
// build and validate the ability spec 生成GA的描述,包括ga的类是什么,等级和输入等
FGameplayAbilitySpec AbilitySpec = BuildAbilitySpecFromClass(AbilityClass, Level, InputID);
// validate the class
if (!IsValid(AbilitySpec.Ability))
{
ABILITY_LOG(Error, TEXT("K2_GiveAbility() called with an invalid Ability Class."));
return FGameplayAbilitySpecHandle();
}
// grant the ability and return the handle. This will run validation and authority checks
return GiveAbility(AbilitySpec);
}
FGameplayAbilitySpecHandle UAbilitySystemComponent::GiveAbility(const FGameplayAbilitySpec& Spec)
{
if (!IsValid(Spec.Ability))//如果Spec的GA对象是非法的,这个对象有可能是CDO
{
ABILITY_LOG(Error, TEXT("GiveAbility called with an invalid Ability Class."));
return FGameplayAbilitySpecHandle();
}
if (!IsOwnerActorAuthoritative())//如果是client
{
ABILITY_LOG(Error, TEXT("GiveAbility called on ability %s on the client, not allowed!"), *Spec.Ability->GetName());
return FGameplayAbilitySpecHandle();
}
// If locked, add to pending list. The Spec.Handle is not regenerated when we receive, so returning this is ok.
if (AbilityScopeLockCount > 0)
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("%s: GiveAbility %s delayed (ScopeLocked)"), *GetNameSafe(GetOwner()), *GetNameSafe(Spec.Ability));
AbilityPendingAdds.Add(Spec);
return Spec.Handle;
}
ABILITYLIST_SCOPE_LOCK();//加锁,保证添加完成
FGameplayAbilitySpec& OwnedSpec = ActivatableAbilities.Items[ActivatableAbilities.Items.Add(Spec)];
//如果实例化策略是 需要实例化的,就new出来一个GA
if (OwnedSpec.Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerActor)
{
// Create the instance at creation time
CreateNewInstanceOfAbility(OwnedSpec, Spec.Ability);
}
OnGiveAbility(OwnedSpec);
//标记脏,同步一下ActivatableAbilities
MarkAbilitySpecDirty(OwnedSpec, true);
UE_LOG(LogAbilitySystem, Log, TEXT("%s: GiveAbility %s [%s] Level: %d Source: %s"), *GetNameSafe(GetOwner()), *GetNameSafe(Spec.Ability), *Spec.Handle.ToString(), Spec.Level, *GetNameSafe(Spec.SourceObject.Get()));
UE_VLOG(GetOwner(), VLogAbilitySystem, Log, TEXT("GiveAbility %s [%s] Level: %d Source: %s"), *GetNameSafe(Spec.Ability), *Spec.Handle.ToString(), Spec.Level, *GetNameSafe(Spec.SourceObject.Get()));
return OwnedSpec.Handle;
}
UGameplayAbility* UAbilitySystemComponent::CreateNewInstanceOfAbility(FGameplayAbilitySpec& Spec, const UGameplayAbility* Ability)
{
check(Ability);
check(Ability->HasAllFlags(RF_ClassDefaultObject));
AActor* Owner = GetOwner();
check(Owner);
//new 出一个GA的对象
UGameplayAbility * AbilityInstance = NewObject<UGameplayAbility>(Owner, Ability->GetClass());
check(AbilityInstance);
// Add it to one of our instance lists so that it doesn't GC.如果同步策略是需要同步的
if (AbilityInstance->GetReplicationPolicy() != EGameplayAbilityReplicationPolicy::ReplicateNo)
{
//这个是个结构体,是通过ActivatableAbilities这个变量属性同步
Spec.ReplicatedInstances.Add(AbilityInstance);
//AActor自有的 subobject同步
AddReplicatedInstancedAbility(AbilityInstance);
}
else
{
Spec.NonReplicatedInstances.Add(AbilityInstance);
}
return AbilityInstance;
}
void UAbilitySystemComponent::AddReplicatedInstancedAbility(UGameplayAbility* GameplayAbility)
{
TArray<TObjectPtr<UGameplayAbility>>& ReplicatedAbilities = GetReplicatedInstancedAbilities_Mutable();
if (ReplicatedAbilities.Find(GameplayAbility) == INDEX_NONE)
{
ReplicatedAbilities.Add(GameplayAbility);
if (IsUsingRegisteredSubObjectList() && IsReadyForReplication())
{
const ELifetimeCondition LifetimeCondition = bReplicateAbilitiesToSimulatedProxies ? COND_None : COND_ReplayOrOwner;
AddReplicatedSubObject(GameplayAbility, LifetimeCondition);
}
}
}
void UAbilitySystemComponent::OnGiveAbility(FGameplayAbilitySpec& Spec)
{
if (!Spec.Ability)
{
return;
}
const UGameplayAbility* SpecAbility = Spec.Ability;
if (SpecAbility->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerActor && SpecAbility->GetReplicationPolicy() == EGameplayAbilityReplicationPolicy::ReplicateNo)
{
// If we don't replicate and are missing an instance, add one
if (Spec.NonReplicatedInstances.Num() == 0)
{
CreateNewInstanceOfAbility(Spec, SpecAbility);
}
}
/*
这个变量是不同步的,一般都是invalid的,但是一旦合法说明这个GA是通过GE产生的
*/
// If this Ability Spec specified that it was created from an Active Gameplay Effect, then link the handle to the Active Gameplay Effect.
if (Spec.GameplayEffectHandle.IsValid())
{
UAbilitySystemComponent* SourceASC = Spec.GameplayEffectHandle.GetOwningAbilitySystemComponent();
UE_CLOG(!SourceASC, LogAbilitySystem, Error, TEXT("OnGiveAbility Spec '%s' GameplayEffectHandle had invalid Owning Ability System Component"), *Spec.GetDebugString());
if (SourceASC)
{
FActiveGameplayEffect* SourceActiveGE = SourceASC->ActiveGameplayEffects.GetActiveGameplayEffect(Spec.GameplayEffectHandle);
UE_CLOG(!SourceActiveGE, LogAbilitySystem, Error, TEXT("OnGiveAbility Spec '%s' GameplayEffectHandle was not active on Owning Ability System Component '%s'"), *Spec.GetDebugString(), *SourceASC->GetName());
if (SourceActiveGE)
{
SourceActiveGE->GrantedAbilityHandles.AddUnique(Spec.Handle);
SourceASC->ActiveGameplayEffects.MarkItemDirty(*SourceActiveGE);
}
}
}
//当前GA当满足一定条件被动激活的逻辑
for (const FAbilityTriggerData& TriggerData : Spec.Ability->AbilityTriggers)
{
FGameplayTag EventTag = TriggerData.TriggerTag;
//根据激活source的类型,来决定往哪个容器里面放
auto& TriggeredAbilityMap = (TriggerData.TriggerSource == EGameplayAbilityTriggerSource::GameplayEvent) ? GameplayEventTriggeredAbilities : OwnedTagTriggeredAbilities;
if (TriggeredAbilityMap.Contains(EventTag))
{
TriggeredAbilityMap[EventTag].AddUnique(Spec.Handle); // Fixme: is this right? Do we want to trigger the ability directly of the spec?
}
else
{
TArray<FGameplayAbilitySpecHandle> Triggers;
Triggers.Add(Spec.Handle);
TriggeredAbilityMap.Add(EventTag, Triggers);
}
//如果被动激活的类型不是event类型,就是当对应的TriggerTag NewOrRemoved 的时候
if (TriggerData.TriggerSource != EGameplayAbilityTriggerSource::GameplayEvent)
{
FOnGameplayEffectTagCountChanged& CountChangedEvent = RegisterGameplayTagEvent(EventTag);
// Add a change callback if it isn't on it already
if (!CountChangedEvent.IsBoundToObject(this))
{
MonitoredTagChangedDelegateHandle = CountChangedEvent.AddUObject(this, &UAbilitySystemComponent::MonitoredTagChanged);
}
}
//那如果是TriggerSource是Event的类型时候,就等待别人调用
//int32 UAbilitySystemComponent::HandleGameplayEvent(FGameplayTag EventTag, const FGameplayEventData* Payload)
//
}
// If there's already a primary instance, it should be the one to receive the OnGiveAbility call
//这里又有一个primaryinstance的概念。PrimaryInstance->OnGiveAbility 这个方法 肯定是能被重载的
UGameplayAbility* PrimaryInstance = Spec.GetPrimaryInstance();
if (PrimaryInstance)
{
PrimaryInstance->OnGiveAbility(AbilityActorInfo.Get(), Spec);
}
else
{
Spec.Ability->OnGiveAbility(AbilityActorInfo.Get(), Spec);
}
}
//那什么是primaryInstance呢
UGameplayAbility* FGameplayAbilitySpec::GetPrimaryInstance() const
{
//首先技能的实例化策略必须是InstancePerActor
if (Ability && Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerActor)
{
//俩容器一个是不同步的一个是同步的,选第一个,就是说第一个被实例化的对象。
if (NonReplicatedInstances.Num() > 0)
{
return NonReplicatedInstances[0];
}
if (ReplicatedInstances.Num() > 0)
{
return ReplicatedInstances[0];
}
}
return nullptr;
}
通过上面源码分析,添加一个GA最后的结果都是把GA的对象放到 ActivatableAbilities这个容器里面,而且这个变量是属性同步的,也就是说每个端都会有当前所具备的GA
添加一个GA完成了。
下面看一下如何主动的激活一个GA。添加一个GA必须是在author端添加,但是激活一个GA既可以在server端激活也可以在client端激活,就是不能在simulate端激活。激活的逻辑看下面源码分析。
激活一个GA 有两种方式 第一 就是手动Active 第二种是 通过event ,UAbilitySystemComponent::HandleGameplayEvent
/**
* 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);
bool UAbilitySystemComponent::TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool bAllowRemoteActivation)
{
//先是一大堆过滤,不满足条件的 return false
FGameplayTagContainer FailureTags;
FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(AbilityToActivate);
if (!Spec)
{
ABILITY_LOG(Warning, TEXT("TryActivateAbility called with invalid Handle"));
return false;
}
// don't activate abilities that are waiting to be removed
if (Spec->PendingRemove || Spec->RemoveAfterActivation)
{
return false;
}
UGameplayAbility* Ability = Spec->Ability;
if (!Ability)
{
ABILITY_LOG(Warning, TEXT("TryActivateAbility called with invalid Ability"));
return false;
}
const FGameplayAbilityActorInfo* ActorInfo = AbilityActorInfo.Get();
// make sure the ActorInfo and then Actor on that FGameplayAbilityActorInfo are valid, if not bail out.
if (ActorInfo == nullptr || !ActorInfo->OwnerActor.IsValid() || !ActorInfo->AvatarActor.IsValid())
{
return false;
}
const ENetRole NetMode = ActorInfo->AvatarActor->GetLocalRole();
// This should only come from button presses/local instigation (AI, etc).技能不能在模拟端激活
if (NetMode == ROLE_SimulatedProxy)
{
return false;
}
bool bIsLocal = AbilityActorInfo->IsLocallyControlled();
// Check to see if this a local only or server only ability, if so either remotely execute or fail,技能的执行策略是localOnly或者localPredicted,但是当前环境不是local就执行ClientTryActivateAbility
if (!bIsLocal && (Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalOnly || Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted))
{
if (bAllowRemoteActivation)
{
ClientTryActivateAbility(AbilityToActivate);
return true;
}
ABILITY_LOG(Log, TEXT("Can't activate LocalOnly or LocalPredicted ability %s when not local."), *Ability->GetName());
return false;
}
//如果当前是client 但是GA的执行策略是在server上执行,就调用rpc方法CallServerTryActivateAbility
if (NetMode != ROLE_Authority && (Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::ServerOnly || Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::ServerInitiated))
{
if (bAllowRemoteActivation)
{
FScopedCanActivateAbilityLogEnabler LogEnabler;
if (Ability->CanActivateAbility(AbilityToActivate, ActorInfo, nullptr, nullptr, &FailureTags))
{
// No prediction key, server will assign a server-generated key
CallServerTryActivateAbility(AbilityToActivate, Spec->InputPressed, FPredictionKey());
return true;
}
else
{
NotifyAbilityFailed(AbilityToActivate, Ability, FailureTags);
return false;
}
}
ABILITY_LOG(Log, TEXT("Can't activate ServerOnly or ServerInitiated ability %s when not the server."), *Ability->GetName());
return false;
}
//剩下的就是 当前是server 并且执行策略是server 就直接激活 ,你会发现不管是在服务器上还是在client上执行激活的是一个函数
return InternalTryActivateAbility(AbilityToActivate);
}
//client方法
void UAbilitySystemComponent::ClientTryActivateAbility_Implementation(FGameplayAbilitySpecHandle Handle)
{
FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(Handle);
if (!Spec)//如果client上没有,还得让sever去激活
{
// Can happen if the client gets told to activate an ability the same frame that abilities are added on the server
FPendingAbilityInfo AbilityInfo;
AbilityInfo.Handle = Handle;
AbilityInfo.bPartiallyActivated = false;
// This won't add it if we're currently being called from the pending list
PendingServerActivatedAbilities.AddUnique(AbilityInfo);
return;
}
InternalTryActivateAbility(Handle);//client上找到spec就激活,你会发现不管是在服务器上还是在client上执行激活的是一个函数
}
//client上调用让server执行
void UAbilitySystemComponent::CallServerTryActivateAbility(FGameplayAbilitySpecHandle AbilityHandle, bool InputPressed, FPredictionKey PredictionKey)
{
UE_CLOG(AbilitySystemLogServerRPCBatching, LogAbilitySystem, Display, TEXT("::CallServerTryActivateAbility %s %d %s"), *AbilityHandle.ToString(), InputPressed, *PredictionKey.ToString());
/** Queue this call up if we are in a batch window, otherwise just push it through now */
if (FServerAbilityRPCBatch* ExistingBatchData = LocalServerAbilityRPCBatchData.FindByKey(AbilityHandle))//如果已经在列表中了
{
if (ExistingBatchData->Started)
{
FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(AbilityHandle);
ABILITY_LOG(Warning, TEXT("::CallServerTryActivateAbility called multiple times for ability (%s) during a single batch."), Spec ? *GetNameSafe(Spec->Ability) : TEXT("INVALID"));
return;
}
ExistingBatchData->Started = true;
ExistingBatchData->InputPressed = InputPressed;
ExistingBatchData->PredictionKey = PredictionKey;
}
else
{
UE_CLOG(AbilitySystemLogServerRPCBatching, LogAbilitySystem, Display, TEXT(" NO BATCH IN SCOPE"));
ServerTryActivateAbility(AbilityHandle, InputPressed, PredictionKey);//没在那个列表中就直接调用server方法
}
}
void UAbilitySystemComponent::ServerTryActivateAbility_Implementation(FGameplayAbilitySpecHandle Handle, bool InputPressed, FPredictionKey PredictionKey)
{
InternalServerTryActivateAbility(Handle, InputPressed, PredictionKey, nullptr);
}
void UAbilitySystemComponent::InternalServerTryActivateAbility(FGameplayAbilitySpecHandle Handle, bool InputPressed, const FPredictionKey& PredictionKey, const FGameplayEventData* TriggerEventData)
{
//首先这是服务器代码
#if WITH_SERVER_CODE
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
if (DenyClientActivation > 0)
{
DenyClientActivation--;
ClientActivateAbilityFailed(Handle, PredictionKey.Current);
return;
}
#endif
ABILITYLIST_SCOPE_LOCK();//先来个锁
FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(Handle);
if (!Spec)
{
// Can potentially happen in race conditions where client tries to activate ability that is removed server side before it is received.
ABILITY_LOG(Display, TEXT("InternalServerTryActivateAbility. Rejecting ClientActivation of ability with invalid SpecHandle!"));
ClientActivateAbilityFailed(Handle, PredictionKey.Current);//没找到就通知客户端失败
return;
}
const UGameplayAbility* AbilityToActivate = Spec->Ability;
if (!ensure(AbilityToActivate))
{
ABILITY_LOG(Error, TEXT("InternalServerTryActiveAbility. Rejecting ClientActivation of unconfigured spec ability!"));
ClientActivateAbilityFailed(Handle, PredictionKey.Current);
return;
}
// Ignore a client trying to activate an ability requiring server execution
if (AbilityToActivate->GetNetSecurityPolicy() == EGameplayAbilityNetSecurityPolicy::ServerOnlyExecution ||
AbilityToActivate->GetNetSecurityPolicy() == EGameplayAbilityNetSecurityPolicy::ServerOnly)
{
ABILITY_LOG(Display, TEXT("InternalServerTryActiveAbility. Rejecting ClientActivation of %s due to security policy violation."), *GetNameSafe(AbilityToActivate));
ClientActivateAbilityFailed(Handle, PredictionKey.Current);//如果这个GA只能在服务器上执行,也告诉客户端失败,毕竟是从客户端active一个GA
return;
}
// Consume any pending target info, to clear out cancels from old executions
ConsumeAllReplicatedData(Handle, PredictionKey);
FScopedPredictionWindow ScopedPredictionWindow(this, PredictionKey);//技能预测功能回头看明白了再单独详细叙述
ensure(AbilityActorInfo.IsValid());
SCOPE_CYCLE_COUNTER(STAT_AbilitySystemComp_ServerTryActivate);
SCOPE_CYCLE_UOBJECT(Ability, AbilityToActivate);
UGameplayAbility* InstancedAbility = nullptr;
Spec->InputPressed = true;
// Attempt to activate the ability (server side) and tell the client if it succeeded or failed.
if (InternalTryActivateAbility(Handle, PredictionKey, &InstancedAbility, nullptr, TriggerEventData))//执行激活,你会发现不管是在服务器上还是在client上执行激活的是一个函数
{
// TryActivateAbility handles notifying the client of success
}
else
{
ABILITY_LOG(Display, TEXT("InternalServerTryActivateAbility. Rejecting ClientActivation of %s. InternalTryActivateAbility failed: %s"), *GetNameSafe(Spec->Ability), *InternalTryActivateAbilityFailureTags.ToStringSimple() );
ClientActivateAbilityFailed(Handle, PredictionKey.Current);
Spec->InputPressed = false;
MarkAbilitySpecDirty(*Spec);
}
#endif
}
//你会发现不管是在服务器上还是在client上执行激活的是一个函数,也就是说这个函数在服务器上也运行,在客户端上也运行
//所以这个函数会把各种情况都会考虑在内,所以看起来非常复杂
/**
* Attempts to activate the ability.
* -This function calls CanActivateAbility
* -This function handles instancing
* -This function handles networking and prediction
* -If all goes well, CallActivateAbility is called next.
*/
bool UAbilitySystemComponent::InternalTryActivateAbility(FGameplayAbilitySpecHandle Handle, FPredictionKey InPredictionKey, UGameplayAbility** OutInstancedAbility, FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, const FGameplayEventData* TriggerEventData)
{
const FGameplayTag& NetworkFailTag = UAbilitySystemGlobals::Get().ActivateFailNetworkingTag;
InternalTryActivateAbilityFailureTags.Reset();
if (Handle.IsValid() == false)
{
ABILITY_LOG(Warning, TEXT("InternalTryActivateAbility called with invalid Handle! ASC: %s. AvatarActor: %s"), *GetPathName(), *GetNameSafe(GetAvatarActor_Direct()));
return false;
}
FGameplayAbilitySpec* Spec = FindAbilitySpecFromHandle(Handle);
if (!Spec)
{
ABILITY_LOG(Warning, TEXT("InternalTryActivateAbility called with a valid handle but no matching ability was found. Handle: %s ASC: %s. AvatarActor: %s"), *Handle.ToString(), *GetPathName(), *GetNameSafe(GetAvatarActor_Direct()));
return false;
}
// Lock ability list so our Spec doesn't get destroyed while activating
ABILITYLIST_SCOPE_LOCK();
const FGameplayAbilityActorInfo* ActorInfo = AbilityActorInfo.Get();
// make sure the ActorInfo and then Actor on that FGameplayAbilityActorInfo are valid, if not bail out.
if (ActorInfo == nullptr || !ActorInfo->OwnerActor.IsValid() || !ActorInfo->AvatarActor.IsValid())
{
return false;
}
// This should only come from button presses/local instigation (AI, etc)
ENetRole NetMode = ROLE_SimulatedProxy;
// Use PC netmode if its there
if (APlayerController* PC = ActorInfo->PlayerController.Get())
{
NetMode = PC->GetLocalRole();
}
// Fallback to avataractor otherwise. Edge case: avatar "dies" and becomes torn off and ROLE_Authority. We don't want to use this case (use PC role instead).
else if (AActor* LocalAvatarActor = GetAvatarActor_Direct())
{
NetMode = LocalAvatarActor->GetLocalRole();
}
if (NetMode == ROLE_SimulatedProxy)
{
return false;//模拟端不激活直接return
}
bool bIsLocal = AbilityActorInfo->IsLocallyControlled();//简单来说就是游戏手柄操作的那个角色,如果不是pawn就判断是不是authority
UGameplayAbility* Ability = Spec->Ability;
if (!Ability)
{
ABILITY_LOG(Warning, TEXT("InternalTryActivateAbility called with invalid Ability"));
return false;
}
// Check to see if this a local only or server only ability, if so don't execute
if (!bIsLocal)
{
if (Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalOnly || (Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted && !InPredictionKey.IsValidKey()))
{
// If we have a valid prediction key, the ability was started on the local client so it's okay
UE_LOG(LogAbilitySystem, Warning, TEXT("%s: Can't activate %s ability %s when not local"), *GetNameSafe(GetOwner()), *UEnum::GetValueAsString<EGameplayAbilityNetExecutionPolicy::Type>(Ability->GetNetExecutionPolicy()), *Ability->GetName());
UE_VLOG(GetOwner(), VLogAbilitySystem, Warning, TEXT("Can't activate %s ability %s when not local"), *UEnum::GetValueAsString<EGameplayAbilityNetExecutionPolicy::Type>(Ability->GetNetExecutionPolicy()), *Ability->GetName());
if (NetworkFailTag.IsValid())
{
InternalTryActivateAbilityFailureTags.AddTag(NetworkFailTag);
NotifyAbilityFailed(Handle, Ability, InternalTryActivateAbilityFailureTags);
}
return false;
}
}
if (NetMode != ROLE_Authority && (Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::ServerOnly || Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::ServerInitiated))
{
UE_LOG(LogAbilitySystem, Warning, TEXT("%s: Can't activate %s ability %s when not the server"), *GetNameSafe(GetOwner()), *UEnum::GetValueAsString<EGameplayAbilityNetExecutionPolicy::Type>(Ability->GetNetExecutionPolicy()), *Ability->GetName());
UE_VLOG(GetOwner(), VLogAbilitySystem, Warning, TEXT("Can't activate %s ability %s when not the server"), *UEnum::GetValueAsString<EGameplayAbilityNetExecutionPolicy::Type>(Ability->GetNetExecutionPolicy()), *Ability->GetName());
if (NetworkFailTag.IsValid())
{
InternalTryActivateAbilityFailureTags.AddTag(NetworkFailTag);
NotifyAbilityFailed(Handle, Ability, InternalTryActivateAbilityFailureTags);
}
return false;
}
// If it's an instanced one, the instanced ability will be set, otherwise it will be null
UGameplayAbility* InstancedAbility = Spec->GetPrimaryInstance();
UGameplayAbility* AbilitySource = InstancedAbility ? InstancedAbility : Ability;
if (TriggerEventData)//关于trigger的处理,一般是由UAbilitySystemComponent::HandleGameplayEvent这个来给TriggerEventData赋值
{
if (!AbilitySource->ShouldAbilityRespondToEvent(ActorInfo, TriggerEventData))
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("%s: Can't activate %s because ShouldAbilityRespondToEvent was false."), *GetNameSafe(GetOwner()), *Ability->GetName());
UE_VLOG(GetOwner(), VLogAbilitySystem, Verbose, TEXT("Can't activate %s because ShouldAbilityRespondToEvent was false."), *Ability->GetName());
NotifyAbilityFailed(Handle, AbilitySource, InternalTryActivateAbilityFailureTags);
return false;
}
}
{
const FGameplayTagContainer* SourceTags = TriggerEventData ? &TriggerEventData->InstigatorTags : nullptr;
const FGameplayTagContainer* TargetTags = TriggerEventData ? &TriggerEventData->TargetTags : nullptr;
FScopedCanActivateAbilityLogEnabler LogEnabler;
//判断能不能激活,其中有根据TAG互斥丢弃的逻辑判断了
if (!AbilitySource->CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, &InternalTryActivateAbilityFailureTags))
{
// CanActivateAbility with LogEnabler will have UE_LOG/UE_VLOG so don't add more failure logs here
NotifyAbilityFailed(Handle, AbilitySource, InternalTryActivateAbilityFailureTags);
return false;
}
}
// If we're instance per actor and we're already active, don't let us activate again as this breaks the graph
if (Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerActor)
{
if (Spec->IsActive())//冲掉之前的instance,重新active
{
if (Ability->bRetriggerInstancedAbility && InstancedAbility)
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("%s: Ending %s prematurely to retrigger."), *GetNameSafe(GetOwner()), *Ability->GetName());
UE_VLOG(GetOwner(), VLogAbilitySystem, Verbose, TEXT("Ending %s prematurely to retrigger."), *Ability->GetName());
bool bReplicateEndAbility = true;
bool bWasCancelled = false;
InstancedAbility->EndAbility(Handle, ActorInfo, Spec->ActivationInfo, bReplicateEndAbility, bWasCancelled);
}
else
{
UE_LOG(LogAbilitySystem, Verbose, TEXT("Can't activate instanced per actor ability %s when their is already a currently active instance for this actor."), *Ability->GetName());
return false;
}
}
}
// Make sure we have a primary
if (Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerActor && !InstancedAbility)
{
UE_LOG(LogAbilitySystem, Warning, TEXT("InternalTryActivateAbility called but instanced ability is missing! NetMode: %d. Ability: %s"), (int32)NetMode, *Ability->GetName());
return false;
}
// Setup a fresh ActivationInfo for this AbilitySpec.
Spec->ActivationInfo = FGameplayAbilityActivationInfo(ActorInfo->OwnerActor.Get());
FGameplayAbilityActivationInfo &ActivationInfo = Spec->ActivationInfo;
// If we are the server or this is local only
if (Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalOnly || (NetMode == ROLE_Authority))//如果是本地执行或者当前是权威端
{
// if we're the server and don't have a valid key or this ability should be started on the server create a new activation key
bool bCreateNewServerKey = NetMode == ROLE_Authority &&
(!InPredictionKey.IsValidKey() ||
(Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::ServerInitiated ||
Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::ServerOnly));
if (bCreateNewServerKey)//如果是服务器端,1、InPredictionKey非法 2、NetExecutionPolicy::ServerInitiated 3、NetExecutionPolicy::ServerOnly 。123满足其中一个的就需要更新PredictionKeyWhenActivated
{
ActivationInfo.ServerSetActivationPredictionKey(FPredictionKey::CreateNewServerInitiatedKey(this));
}
else if (InPredictionKey.IsValidKey())
{
// Otherwise if available, set the prediction key to what was passed up
ActivationInfo.ServerSetActivationPredictionKey(InPredictionKey);
}
// we may have changed the prediction key so we need to update the scoped key to match
FScopedPredictionWindow ScopedPredictionWindow(this, ActivationInfo.GetActivationPredictionKey());
// ----------------------------------------------
// Tell the client that you activated it (if we're not local and not server only)
// ----------------------------------------------
if (!bIsLocal && Ability->GetNetExecutionPolicy() != EGameplayAbilityNetExecutionPolicy::ServerOnly)
{
if (TriggerEventData)
{
ClientActivateAbilitySucceedWithEventData(Handle, ActivationInfo.GetActivationPredictionKey(), *TriggerEventData);
}
else
{
ClientActivateAbilitySucceed(Handle, ActivationInfo.GetActivationPredictionKey());
}
// This will get copied into the instanced abilities
ActivationInfo.bCanBeEndedByOtherInstance = Ability->bServerRespectsRemoteAbilityCancellation;
}
// ----------------------------------------------
// Call ActivateAbility (note this could end the ability too!)
// ----------------------------------------------
// Create instance of this ability if necessary
if (Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerExecution)
{
InstancedAbility = CreateNewInstanceOfAbility(*Spec, Ability);
InstancedAbility->CallActivateAbility(Handle, ActorInfo, ActivationInfo, OnGameplayAbilityEndedDelegate, TriggerEventData);//一个GA真正的执行了
}
else
{
AbilitySource->CallActivateAbility(Handle, ActorInfo, ActivationInfo, OnGameplayAbilityEndedDelegate, TriggerEventData);//一个GA真正的执行了
}
}
else if (Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)//如果是client 并且是本地预测 则先告诉服务器rpc,执行GA,之后不用等待在客户端直接执行
{
// Flush server moves that occurred before this ability activation so that the server receives the RPCs in the correct order
// Necessary to prevent abilities that trigger animation root motion or impact movement from causing network corrections
if (!ActorInfo->IsNetAuthority())
{
ACharacter* AvatarCharacter = Cast<ACharacter>(ActorInfo->AvatarActor.Get());
if (AvatarCharacter)
{
UCharacterMovementComponent* AvatarCharMoveComp = Cast<UCharacterMovementComponent>(AvatarCharacter->GetMovementComponent());
if (AvatarCharMoveComp)
{
AvatarCharMoveComp->FlushServerMoves();//先强制同步一下movement的信息
}
}
}
// This execution is now officially EGameplayAbilityActivationMode:Predicting and has a PredictionKey
FScopedPredictionWindow ScopedPredictionWindow(this, true);
ActivationInfo.SetPredicting(ScopedPredictionKey);//设置预测key
// This must be called immediately after GeneratePredictionKey to prevent problems with recursively activating abilities
if (TriggerEventData)//先告诉服务器要激活
{
ServerTryActivateAbilityWithEventData(Handle, Spec->InputPressed, ScopedPredictionKey, *TriggerEventData);
}
else
{
CallServerTryActivateAbility(Handle, Spec->InputPressed, ScopedPredictionKey);
}
// When this prediction key is caught up, we better know if the ability was confirmed or rejected
ScopedPredictionKey.NewCaughtUpDelegate().BindUObject(this, &UAbilitySystemComponent::OnClientActivateAbilityCaughtUp, Handle, ScopedPredictionKey.Current);
//告诉服务器后,不用等待client直接执行
if (Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerExecution)
{
// For now, only NonReplicated + InstancedPerExecution abilities can be Predictive.
// We lack the code to predict spawning an instance of the execution and then merge/combine
// with the server spawned version when it arrives.
if (Ability->GetReplicationPolicy() == EGameplayAbilityReplicationPolicy::ReplicateNo)
{
InstancedAbility = CreateNewInstanceOfAbility(*Spec, Ability);
InstancedAbility->CallActivateAbility(Handle, ActorInfo, ActivationInfo, OnGameplayAbilityEndedDelegate, TriggerEventData);
}
else
{
ABILITY_LOG(Error, TEXT("InternalTryActivateAbility called on ability %s that is InstancePerExecution and Replicated. This is an invalid configuration."), *Ability->GetName() );
}
}
else
{
AbilitySource->CallActivateAbility(Handle, ActorInfo, ActivationInfo, OnGameplayAbilityEndedDelegate, TriggerEventData);
}
}
if (InstancedAbility)
{
if (OutInstancedAbility)
{
*OutInstancedAbility = InstancedAbility;
}
// UGameplayAbility::PreActivate actually sets this internally (via SetCurrentInfo) which happens after replication (this is only set locally). Let's cautiously remove this code.
if (CVarAbilitySystemSetActivationInfoMultipleTimes.GetValueOnGameThread())
{
InstancedAbility->SetCurrentActivationInfo(ActivationInfo); // Need to push this to the ability if it was instanced.
}
}
MarkAbilitySpecDirty(*Spec);
AbilityLastActivatedTime = GetWorld()->GetTimeSeconds();
UE_LOG(LogAbilitySystem, Log, TEXT("%s: Activated [%s] %s. Level: %d. PredictionKey: %s."), *GetNameSafe(GetOwner()), *Spec->Handle.ToString(), *GetNameSafe(AbilitySource), Spec->Level, *ActivationInfo.GetActivationPredictionKey().ToString());
UE_VLOG(GetOwner(), VLogAbilitySystem, Log, TEXT("Activated [%s] %s. Level: %d. PredictionKey: %s."), *Spec->Handle.ToString(), *GetNameSafe(AbilitySource), Spec->Level, *ActivationInfo.GetActivationPredictionKey().ToString());
return true;
}
//互斥阻挡丢弃逻辑
//先看看各种Tag的配置说明
/** Abilities with these tags are cancelled when this ability is executed */
UPROPERTY(EditDefaultsOnly, Category = Tags, meta=(Categories="AbilityTagCategory"))
FGameplayTagContainer CancelAbilitiesWithTag;//我来了,就cancel这些
/** Abilities with these tags are blocked while this ability is active */
UPROPERTY(EditDefaultsOnly, Category = Tags, meta=(Categories="AbilityTagCategory"))
FGameplayTagContainer BlockAbilitiesWithTag;//我来了,要block这些
/** Tags to apply to activating owner while this ability is active. These are replicated if ReplicateActivationOwnedTags is enabled in AbilitySystemGlobals. */
UPROPERTY(EditDefaultsOnly, Category = Tags, meta=(Categories="OwnedTagsCategory"))
FGameplayTagContainer ActivationOwnedTags;//我来了,要给owner设置这些Tag
/** This ability can only be activated if the activating actor/component has all of these tags */
UPROPERTY(EditDefaultsOnly, Category = Tags, meta=(Categories="OwnedTagsCategory"))
FGameplayTagContainer ActivationRequiredTags;//有这些Tag 我才能运行
/** This ability is blocked if the activating actor/component has any of these tags */
UPROPERTY(EditDefaultsOnly, Category = Tags, meta=(Categories="OwnedTagsCategory"))
FGameplayTagContainer ActivationBlockedTags;//有这些Tag我就被block
/** This ability can only be activated if the source actor/component has all of these tags */
UPROPERTY(EditDefaultsOnly, Category = Tags, meta=(Categories="SourceTagsCategory"))
FGameplayTagContainer SourceRequiredTags;//当source actor都有这些TAG 就激活我
/** This ability is blocked if the source actor/component has any of these tags */
UPROPERTY(EditDefaultsOnly, Category = Tags, meta=(Categories="SourceTagsCategory"))
FGameplayTagContainer SourceBlockedTags;//当source actor有一个这样的TAG 我就被block
/** This ability can only be activated if the target actor/component has all of these tags */
UPROPERTY(EditDefaultsOnly, Category = Tags, meta=(Categories="TargetTagsCategory"))
FGameplayTagContainer TargetRequiredTags;
/** This ability is blocked if the target actor/component has any of these tags */
UPROPERTY(EditDefaultsOnly, Category = Tags, meta=(Categories="TargetTagsCategory"))
FGameplayTagContainer TargetBlockedTags;
//如何理解block 和 missing呢?从源码来看,有阻挡我的tag就是block,不满足我需要的Tag就是Missing。
//但是不管是哪种情况这个函数返回的都是false
bool UGameplayAbility::DoesAbilitySatisfyTagRequirements(const UAbilitySystemComponent& AbilitySystemComponent, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, OUT FGameplayTagContainer* OptionalRelevantTags) const
{
bool bBlocked = false;
bool bMissing = false;
UAbilitySystemGlobals& AbilitySystemGlobals = UAbilitySystemGlobals::Get();
const FGameplayTag& BlockedTag = AbilitySystemGlobals.ActivateFailTagsBlockedTag;
const FGameplayTag& MissingTag = AbilitySystemGlobals.ActivateFailTagsMissingTag;
// Check if any of this ability's tags are currently blocked
if (AbilitySystemComponent.AreAbilityTagsBlocked(AbilityTags))
{
bBlocked = true;
}
// Check to see the required/blocked tags for this ability
if (ActivationBlockedTags.Num() || ActivationRequiredTags.Num())
{
static FGameplayTagContainer AbilitySystemComponentTags;
AbilitySystemComponentTags.Reset();
AbilitySystemComponent.GetOwnedGameplayTags(AbilitySystemComponentTags);
if (AbilitySystemComponentTags.HasAny(ActivationBlockedTags))//当前有任何block我的Tag
{
bBlocked = true;
}
if (!AbilitySystemComponentTags.HasAll(ActivationRequiredTags))//当前没有我reuired的Tag
{
bMissing = true;
}
}
if (SourceTags != nullptr)
{
if (SourceBlockedTags.Num() || SourceRequiredTags.Num())
{
if (SourceTags->HasAny(SourceBlockedTags))
{
bBlocked = true;
}
if (!SourceTags->HasAll(SourceRequiredTags))
{
bMissing = true;
}
}
}
if (TargetTags != nullptr)
{
if (TargetBlockedTags.Num() || TargetRequiredTags.Num())
{
if (TargetTags->HasAny(TargetBlockedTags))
{
bBlocked = true;
}
if (!TargetTags->HasAll(TargetRequiredTags))
{
bMissing = true;
}
}
}
if (bBlocked)
{
if (OptionalRelevantTags && BlockedTag.IsValid())
{
OptionalRelevantTags->AddTag(BlockedTag);
}
return false;
}
if (bMissing)
{
if (OptionalRelevantTags && MissingTag.IsValid())
{
OptionalRelevantTags->AddTag(MissingTag);
}
return false;
}
return true;
}
关于GE
触发GE有两个通道
第一手动调用
第二种 调用CommitAbility
void UGameplayAbility_Montage::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
return;
}
ActivateAbility中手动调用CommitAbility 来消耗GE
以上两个方法最终都会走到AbilitySystemComponent->ApplyGameplayEffectSpecToSelf
这个方法中的ExecuteGameplayEffect方法
FActiveGameplayEffectHandle UAbilitySystemComponent::ApplyGameplayEffectSpecToTarget(const FGameplayEffectSpec &Spec, UAbilitySystemComponent *Target, FPredictionKey PredictionKey)
{
SCOPE_CYCLE_COUNTER(STAT_AbilitySystemComp_ApplyGameplayEffectSpecToTarget);
UAbilitySystemGlobals& AbilitySystemGlobals = UAbilitySystemGlobals::Get();
if (!AbilitySystemGlobals.ShouldPredictTargetGameplayEffects())
{
// If we don't want to predict target effects, clear prediction key
PredictionKey = FPredictionKey();//如果设置的是不想有预测的effect就清空预测key
}
FActiveGameplayEffectHandle ReturnHandle;
if (Target)
{
//在target上执行effect,有可能在client上执行也有可能在server上执行
ReturnHandle = Target->ApplyGameplayEffectSpecToSelf(Spec, PredictionKey);
}
return ReturnHandle;
}
FActiveGameplayEffectHandle UAbilitySystemComponent::ApplyGameplayEffectSpecToSelf(const FGameplayEffectSpec &Spec, FPredictionKey PredictionKey)
{
#if WITH_SERVER_CODE
SCOPE_CYCLE_COUNTER(STAT_AbilitySystemComp_ApplyGameplayEffectSpecToSelf);
#endif
// Scope lock the container after the addition has taken place to prevent the new effect from potentially getting mangled during the remainder
// of the add operation
FScopedActiveGameplayEffectLock ScopeLock(ActiveGameplayEffects);//执行的时候锁一下
FScopeCurrentGameplayEffectBeingApplied ScopedGEApplication(&Spec, this);
const bool bIsNetAuthority = IsOwnerActorAuthoritative();
// Check Network Authority
if (!HasNetworkAuthorityToApplyGameplayEffect(PredictionKey))//关于预测key的处理
{
return FActiveGameplayEffectHandle();
}
// Don't allow prediction of periodic effects
if (PredictionKey.IsValidKey() && Spec.GetPeriod() > 0.f)
{
if (IsOwnerActorAuthoritative())
{
// Server continue with invalid prediction key
PredictionKey = FPredictionKey();
}
else
{
// Client just return now
return FActiveGameplayEffectHandle();
}
}
// Check if there is a registered "application" query that can block the application
for (const FGameplayEffectApplicationQuery& ApplicationQuery : GameplayEffectApplicationQueries)
{
const bool bAllowed = ApplicationQuery.Execute(ActiveGameplayEffects, Spec);
if (!bAllowed)
{
return FActiveGameplayEffectHandle();
}
}
// check if the effect being applied actually succeeds
if (!Spec.Def->CanApply(ActiveGameplayEffects, Spec))
{
return FActiveGameplayEffectHandle();
}
// Check AttributeSet requirements: make sure all attributes are valid
// We may want to cache this off in some way to make the runtime check quicker.
// We also need to handle things in the execution list
for (const FGameplayModifierInfo& Mod : Spec.Def->Modifiers)
{
if (!Mod.Attribute.IsValid())
{
ABILITY_LOG(Warning, TEXT("%s has a null modifier attribute."), *Spec.Def->GetPathName());
return FActiveGameplayEffectHandle();
}
}
// Clients should treat predicted instant effects as if they have infinite duration. The effects will be cleaned up later.
bool bTreatAsInfiniteDuration = GetOwnerRole() != ROLE_Authority && PredictionKey.IsLocalClientKey() && Spec.Def->DurationPolicy == EGameplayEffectDurationType::Instant;
// Make sure we create our copy of the spec in the right place
// We initialize the FActiveGameplayEffectHandle here with INDEX_NONE to handle the case of instant GE
// Initializing it like this will set the bPassedFiltersAndWasExecuted on the FActiveGameplayEffectHandle to true so we can know that we applied a GE
FActiveGameplayEffectHandle MyHandle(INDEX_NONE);
bool bInvokeGameplayCueApplied = Spec.Def->DurationPolicy != EGameplayEffectDurationType::Instant; // Cache this now before possibly modifying predictive instant effect to infinite duration effect.
bool bFoundExistingStackableGE = false;
FActiveGameplayEffect* AppliedEffect = nullptr;
FGameplayEffectSpec* OurCopyOfSpec = nullptr;
TUniquePtr<FGameplayEffectSpec> StackSpec;
{
if (Spec.Def->DurationPolicy != EGameplayEffectDurationType::Instant || bTreatAsInfiniteDuration)//当是duration或者 forever的类型的时候
{
AppliedEffect = ActiveGameplayEffects.ApplyGameplayEffectSpec(Spec, PredictionKey, bFoundExistingStackableGE);//添加到ActiveGameplayEffects列表中,每帧更新
if (!AppliedEffect)
{
return FActiveGameplayEffectHandle();
}
MyHandle = AppliedEffect->Handle;
OurCopyOfSpec = &(AppliedEffect->Spec);
// Log results of applied GE spec
if (UE_LOG_ACTIVE(VLogAbilitySystem, Log))
{
UE_VLOG(GetOwnerActor(), VLogAbilitySystem, Log, TEXT("Applied %s"), *OurCopyOfSpec->Def->GetFName().ToString());
for (const FGameplayModifierInfo& Modifier : Spec.Def->Modifiers)
{
float Magnitude = 0.f;
Modifier.ModifierMagnitude.AttemptCalculateMagnitude(Spec, Magnitude);
UE_VLOG(GetOwnerActor(), VLogAbilitySystem, Log, TEXT(" %s: %s %f"), *Modifier.Attribute.GetName(), *EGameplayModOpToString(Modifier.ModifierOp), Magnitude);
}
}
}
if (!OurCopyOfSpec)
{
StackSpec = MakeUnique<FGameplayEffectSpec>(Spec);
OurCopyOfSpec = StackSpec.Get();
UAbilitySystemGlobals::Get().GlobalPreGameplayEffectSpecApply(*OurCopyOfSpec, this);
OurCopyOfSpec->CaptureAttributeDataFromTarget(this);
}
// if necessary add a modifier to OurCopyOfSpec to force it to have an infinite duration
if (bTreatAsInfiniteDuration)
{
// This should just be a straight set of the duration float now
OurCopyOfSpec->SetDuration(UGameplayEffect::INFINITE_DURATION, true);
}
}
// Update (not push) the global spec being applied [we want to switch it to our copy, from the const input copy)
UAbilitySystemGlobals::Get().SetCurrentAppliedGE(OurCopyOfSpec);
// UE5.4: We are following the same previous implementation that there is a special case for Gameplay Cues here (caveat: may not be true):
// We are Stacking an existing Gameplay Effect. That means the GameplayCues should already be Added/WhileActive and we do not have a proper
// way to replicate the fact that it's been retriggered, hence the RPC here. I say this may not be true because any number of things could have
// removed the GameplayCue by the time we getting a Stacking GE (e.g. RemoveGameplayCue).
if (!bSuppressGameplayCues && !Spec.Def->bSuppressStackingCues && bFoundExistingStackableGE && AppliedEffect && !AppliedEffect->bIsInhibited)
{
ensureMsgf(OurCopyOfSpec, TEXT("OurCopyOfSpec will always be valid if bFoundExistingStackableGE"));
if (OurCopyOfSpec && OurCopyOfSpec->GetStackCount() > Spec.GetStackCount())
{
// Because PostReplicatedChange will get called from modifying the stack count
// (and not PostReplicatedAdd) we won't know which GE was modified.
// So instead we need to explicitly RPC the client so it knows the GC needs updating
UAbilitySystemGlobals::Get().GetGameplayCueManager()->InvokeGameplayCueAddedAndWhileActive_FromSpec(this, *OurCopyOfSpec, PredictionKey);
}
}
// Execute the GE at least once (if instant, this will execute once and be done. If persistent, it was added to ActiveGameplayEffects in ApplyGameplayEffectSpec)
// Execute if this is an instant application effect
if (bTreatAsInfiniteDuration)
{
// This is an instant application but we are treating it as an infinite duration for prediction. We should still predict the execute GameplayCUE.
// (in non predictive case, this will happen inside ::ExecuteGameplayEffect)
if (!bSuppressGameplayCues)
{
UAbilitySystemGlobals::Get().GetGameplayCueManager()->InvokeGameplayCueExecuted_FromSpec(this, *OurCopyOfSpec, PredictionKey);
}
}
else if (Spec.Def->DurationPolicy == EGameplayEffectDurationType::Instant)//如果是立即执行的模式
{
// This is a non-predicted instant effect (it never gets added to ActiveGameplayEffects)
ExecuteGameplayEffect(*OurCopyOfSpec, PredictionKey);
}
// Notify the Gameplay Effect (and its Components) that it has been successfully applied
Spec.Def->OnApplied(ActiveGameplayEffects, *OurCopyOfSpec, PredictionKey);
UAbilitySystemComponent* InstigatorASC = Spec.GetContext().GetInstigatorAbilitySystemComponent();
// Send ourselves a callback
OnGameplayEffectAppliedToSelf(InstigatorASC, *OurCopyOfSpec, MyHandle);
// Send the instigator a callback
if (InstigatorASC)
{
InstigatorASC->OnGameplayEffectAppliedToTarget(this, *OurCopyOfSpec, MyHandle);
}
return MyHandle;
}
void UAbilitySystemComponent::ExecuteGameplayEffect(FGameplayEffectSpec &Spec, FPredictionKey PredictionKey)
{
#if WITH_SERVER_CODE
SCOPE_CYCLE_COUNTER(STAT_AbilitySystemComp_ExecuteGameplayEffect);
#endif
// Should only ever execute effects that are instant application or periodic application
// Effects with no period and that aren't instant application should never be executed
check( (Spec.GetDuration() == UGameplayEffect::INSTANT_APPLICATION || Spec.GetPeriod() != UGameplayEffect::NO_PERIOD) );
if (UE_LOG_ACTIVE(VLogAbilitySystem, Log))
{
UE_VLOG(GetOwnerActor(), VLogAbilitySystem, Log, TEXT("Executed %s"), *Spec.Def->GetFName().ToString());
for (const FGameplayModifierInfo& Modifier : Spec.Def->Modifiers)
{
float Magnitude = 0.f;
Modifier.ModifierMagnitude.AttemptCalculateMagnitude(Spec, Magnitude);
UE_VLOG(GetOwnerActor(), VLogAbilitySystem, Log, TEXT(" %s: %s %f"), *Modifier.Attribute.GetName(), *EGameplayModOpToString(Modifier.ModifierOp), Magnitude);
}
}
ActiveGameplayEffects.ExecuteActiveEffectsFrom(Spec, PredictionKey);//通过ActiveGameplayEffects立即执行,这个时候有可能也在client上运行
}
/** This is the main function that executes a GameplayEffect on Attributes and ActiveGameplayEffects */
void FActiveGameplayEffectsContainer::ExecuteActiveEffectsFrom(FGameplayEffectSpec &Spec, FPredictionKey PredictionKey)
{
#if WITH_SERVER_CODE
SCOPE_CYCLE_COUNTER(STAT_ExecuteActiveEffectsFrom);
#endif
if (!Owner)
{
return;
}
FGameplayEffectSpec& SpecToUse = Spec;
// Capture our own tags.
// TODO: We should only capture them if we need to. We may have snapshotted target tags (?) (in the case of dots with exotic setups?)
SpecToUse.CapturedTargetTags.GetActorTags().Reset();
Owner->GetOwnedGameplayTags(SpecToUse.CapturedTargetTags.GetActorTags());
SpecToUse.CalculateModifierMagnitudes();//先试图计算
// ------------------------------------------------------
// Modifiers
// These will modify the base value of attributes
// ------------------------------------------------------
bool ModifierSuccessfullyExecuted = false;
ensureMsgf(SpecToUse.Modifiers.Num() == SpecToUse.Def->Modifiers.Num(), TEXT("GE Spec %s modifiers did not match the Definition. This indicates an error much earlier (when setting up the GESpec)"), *SpecToUse.ToSimpleString());
for (int32 ModIdx = 0; ModIdx < SpecToUse.Modifiers.Num(); ++ModIdx)
{
const FGameplayModifierInfo& ModDef = SpecToUse.Def->Modifiers[ModIdx];
if (UE::GameplayEffect::bUseModifierTagRequirementsOnAllGameplayEffects)
{
// Check tag requirements. This code path is for Instant & Periodic effects.
// Duration effects are a separate code path; they use aggregators and their requirements checks are in FAggregatorMod::UpdateQualifies.
if (!ModDef.SourceTags.IsEmpty() && !ModDef.SourceTags.RequirementsMet(Spec.CapturedSourceTags.GetActorTags()))
{
continue;
}
if (!ModDef.TargetTags.IsEmpty() && !ModDef.TargetTags.RequirementsMet(Spec.CapturedTargetTags.GetActorTags()))
{
continue;
}
}
FGameplayModifierEvaluatedData EvalData(ModDef.Attribute, ModDef.ModifierOp, SpecToUse.GetModifierMagnitude(ModIdx, true));
ModifierSuccessfullyExecuted |= InternalExecuteMod(SpecToUse, EvalData);//具体应用到属性上
}
// ------------------------------------------------------
// Executions
// This will run custom code to 'do stuff'
// ------------------------------------------------------
TArray< FGameplayEffectSpecHandle, TInlineAllocator<4> > ConditionalEffectSpecs;
bool GameplayCuesWereManuallyHandled = false;
for (const FGameplayEffectExecutionDefinition& CurExecDef : SpecToUse.Def->Executions)
{
bool bRunConditionalEffects = true; // Default to true if there is no CalculationClass specified.
if (CurExecDef.CalculationClass)
{
const UGameplayEffectExecutionCalculation* ExecCDO = CurExecDef.CalculationClass->GetDefaultObject<UGameplayEffectExecutionCalculation>();
check(ExecCDO);
// Run the custom execution
FGameplayEffectCustomExecutionParameters ExecutionParams(SpecToUse, CurExecDef.CalculationModifiers, Owner, CurExecDef.PassedInTags, PredictionKey);
FGameplayEffectCustomExecutionOutput ExecutionOutput;
ExecCDO->Execute(ExecutionParams, ExecutionOutput);
bRunConditionalEffects = ExecutionOutput.ShouldTriggerConditionalGameplayEffects();
// Execute any mods the custom execution yielded
TArray<FGameplayModifierEvaluatedData>& OutModifiers = ExecutionOutput.GetOutputModifiersRef();
const bool bApplyStackCountToEmittedMods = !ExecutionOutput.IsStackCountHandledManually();
const int32 SpecStackCount = SpecToUse.GetStackCount();
for (FGameplayModifierEvaluatedData& CurExecMod : OutModifiers)
{
// If the execution didn't manually handle the stack count, automatically apply it here
if (bApplyStackCountToEmittedMods && SpecStackCount > 1)
{
CurExecMod.Magnitude = GameplayEffectUtilities::ComputeStackedModifierMagnitude(CurExecMod.Magnitude, SpecStackCount, CurExecMod.ModifierOp);
}
ModifierSuccessfullyExecuted |= InternalExecuteMod(SpecToUse, CurExecMod);
}
// If execution handled GameplayCues, we dont have to.
if (ExecutionOutput.AreGameplayCuesHandledManually())
{
GameplayCuesWereManuallyHandled = true;
}
}
if (bRunConditionalEffects)
{
// If successful, apply conditional specs
for (const FConditionalGameplayEffect& ConditionalEffect : CurExecDef.ConditionalGameplayEffects)
{
if (ConditionalEffect.CanApply(SpecToUse.CapturedSourceTags.GetActorTags(), SpecToUse.GetLevel()))
{
FGameplayEffectSpecHandle SpecHandle = ConditionalEffect.CreateSpec(SpecToUse.GetEffectContext(), SpecToUse.GetLevel());
if (SpecHandle.IsValid())
{
ConditionalEffectSpecs.Add(SpecHandle);
}
}
}
}
}
// ------------------------------------------------------
// Invoke GameplayCue events
// ------------------------------------------------------
// If there are no modifiers or we don't require modifier success to trigger, we apply the GameplayCue.
const bool bHasModifiers = SpecToUse.Modifiers.Num() > 0;
const bool bHasExecutions = SpecToUse.Def->Executions.Num() > 0;
const bool bHasModifiersOrExecutions = bHasModifiers || bHasExecutions;
// If there are no modifiers or we don't require modifier success to trigger, we apply the GameplayCue.
bool InvokeGameplayCueExecute = (!bHasModifiersOrExecutions) || !Spec.Def->bRequireModifierSuccessToTriggerCues;
if (bHasModifiersOrExecutions && ModifierSuccessfullyExecuted)
{
InvokeGameplayCueExecute = true;
}
// Don't trigger gameplay cues if one of the executions says it manually handled them
if (GameplayCuesWereManuallyHandled)
{
InvokeGameplayCueExecute = false;
}
if (InvokeGameplayCueExecute && SpecToUse.Def->GameplayCues.Num())
{
// TODO: check replication policy. Right now we will replicate every execute via a multicast RPC
UE_LOG(LogGameplayEffects, Log, TEXT("Invoking Execute GameplayCue for %s"), *SpecToUse.ToSimpleString());
UAbilitySystemGlobals::Get().GetGameplayCueManager()->InvokeGameplayCueExecuted_FromSpec(Owner, SpecToUse, PredictionKey);
}
// Apply any conditional linked effects
for (const FGameplayEffectSpecHandle& TargetSpec : ConditionalEffectSpecs)
{
if (TargetSpec.IsValid())
{
Owner->ApplyGameplayEffectSpecToSelf(*TargetSpec.Data.Get(), PredictionKey);
}
}
// Notify the Gameplay Effect that it's been executed. This will allow the GameplayEffectComponents to augment behavior.
Spec.Def->OnExecuted(*this, Spec, PredictionKey);
}
bool FActiveGameplayEffectsContainer::InternalExecuteMod(FGameplayEffectSpec& Spec, FGameplayModifierEvaluatedData& ModEvalData)
{
SCOPE_CYCLE_COUNTER(STAT_InternalExecuteMod);
check(Owner);
bool bExecuted = false;
UAttributeSet* AttributeSet = nullptr;
UClass* AttributeSetClass = ModEvalData.Attribute.GetAttributeSetClass();
if (AttributeSetClass && AttributeSetClass->IsChildOf(UAttributeSet::StaticClass()))
{
AttributeSet = const_cast<UAttributeSet*>(Owner->GetAttributeSubobject(AttributeSetClass));
}
if (AttributeSet)
{
FGameplayEffectModCallbackData ExecuteData(Spec, ModEvalData, *Owner);
/**
* This should apply 'gamewide' rules. Such as clamping Health to MaxHealth or granting +3 health for every point of strength, etc
* PreAttributeModify can return false to 'throw out' this modification.
*/
if (AttributeSet->PreGameplayEffectExecute(ExecuteData))
{
float OldValueOfProperty = Owner->GetNumericAttribute(ModEvalData.Attribute);
ApplyModToAttribute(ModEvalData.Attribute, ModEvalData.ModifierOp, ModEvalData.Magnitude, &ExecuteData);
FGameplayEffectModifiedAttribute* ModifiedAttribute = Spec.GetModifiedAttribute(ModEvalData.Attribute);
if (!ModifiedAttribute)
{
// If we haven't already created a modified attribute holder, create it
ModifiedAttribute = Spec.AddModifiedAttribute(ModEvalData.Attribute);
}
ModifiedAttribute->TotalMagnitude += ModEvalData.Magnitude;
{
SCOPE_CYCLE_COUNTER(STAT_PostGameplayEffectExecute);
/** This should apply 'gamewide' rules. Such as clamping Health to MaxHealth or granting +3 health for every point of strength, etc */
AttributeSet->PostGameplayEffectExecute(ExecuteData);
}
#if ENABLE_VISUAL_LOG
if (FVisualLogger::IsRecording())
{
DebugExecutedGameplayEffectData DebugData;
DebugData.GameplayEffectName = Spec.Def->GetName();
DebugData.ActivationState = "INSTANT";
DebugData.Attribute = ModEvalData.Attribute;
DebugData.Magnitude = Owner->GetNumericAttribute(ModEvalData.Attribute) - OldValueOfProperty;
DebugExecutedGameplayEffects.Add(DebugData);
}
#endif // ENABLE_VISUAL_LOG
bExecuted = true;
}
}
else
{
// Our owner doesn't have this attribute, so we can't do anything
UE_LOG(LogGameplayEffects, Log, TEXT("%s does not have attribute %s. Skipping modifier"), *Owner->GetPathName(), *ModEvalData.Attribute.GetName());
}
return bExecuted;
}
void FActiveGameplayEffectsContainer::ApplyModToAttribute(const FGameplayAttribute &Attribute, TEnumAsByte<EGameplayModOp::Type> ModifierOp, float ModifierMagnitude, const FGameplayEffectModCallbackData* ModData)
{
CurrentModcallbackData = ModData;
float CurrentBase = GetAttributeBaseValue(Attribute);
float NewBase = FAggregator::StaticExecModOnBaseValue(CurrentBase, ModifierOp, ModifierMagnitude);
SetAttributeBaseValue(Attribute, NewBase);
if (CurrentModcallbackData)
{
// We expect this to be cleared for us in InternalUpdateNumericalAttribute
UE_LOG(LogGameplayEffects, Warning, TEXT("FActiveGameplayEffectsContainer::ApplyModToAttribute CurrentModcallbackData was not consumed For attribute %s on %s."), *Attribute.GetName(), *Owner->GetFullName());
CurrentModcallbackData = nullptr;
}
}
关于 cue 看起来还是传递 tag
接下来聊聊 预测功能,源码说了预测undo 还不健全,不建议使用.5.4.4 版本,具体看GameplayPrediction.h的注释部分
预测的原理是 在client 上触发一个技能,先调用rpc方法在服务器上active,同时把预测key也传到服务器上,在不等待服务器上确认的情况下,client先执行。当服务上运行失败的时候,就调用client方法end GA。
预测的标识是EGameplayAbilityNetExecutionPolicy::LocalPredicted
可以在 InternalTryActivateAbility中1838行开始看到 执行处理。
client上GA end会发送rpc方法告诉server 我end了
如果server上的这个GA 还在运行 就end
下面函数是在客户端和服务器上 cancel 和 end 同一个函数处理
void UAbilitySystemComponent::RemoteEndOrCancelAbility(FGameplayAbilitySpecHandle AbilityToEnd, FGameplayAbilityActivationInfo ActivationInfo, bool bWasCanceled)
{
FGameplayAbilitySpec* AbilitySpec = FindAbilitySpecFromHandle(AbilityToEnd);
if (AbilitySpec && AbilitySpec->Ability && AbilitySpec->IsActive())
{
// Handle non-instanced case, which cannot perform prediction key validation
if (AbilitySpec->Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::NonInstanced)
{
// End/Cancel the ability but don't replicate it back to whoever called us
if (bWasCanceled)
{
AbilitySpec->Ability->CancelAbility(AbilityToEnd, AbilityActorInfo.Get(), ActivationInfo, false);
}
else
{
AbilitySpec->Ability->EndAbility(AbilityToEnd, AbilityActorInfo.Get(), ActivationInfo, false, bWasCanceled);
}
}
else
{
TArray<UGameplayAbility*> Instances = AbilitySpec->GetAbilityInstances();
for (auto Instance : Instances)
{
UE_CLOG(Instance == nullptr, LogAbilitySystem, Fatal, TEXT("UAbilitySystemComponent::RemoteEndOrCancelAbility null instance for %s"), *GetNameSafe(AbilitySpec->Ability));
// Check if the ability is the same prediction key (can both by 0) and has been confirmed. If so cancel it.
if (Instance->GetCurrentActivationInfoRef().GetActivationPredictionKey() == ActivationInfo.GetActivationPredictionKey())
{
// Let the ability know that the remote instance has ended, even if we aren't about to end it here.
Instance->SetRemoteInstanceHasEnded();
if (Instance->GetCurrentActivationInfoRef().bCanBeEndedByOtherInstance)
{
// End/Cancel the ability but don't replicate it back to whoever called us
if (bWasCanceled)
{
ForceCancelAbilityDueToReplication(Instance);
}
else
{
Instance->EndAbility(Instance->CurrentSpecHandle, Instance->CurrentActorInfo, Instance->CurrentActivationInfo, false, bWasCanceled);
}
}
}
}
}
}
}
void UAbilitySystemComponent::ReplicateEndOrCancelAbility(FGameplayAbilitySpecHandle Handle, FGameplayAbilityActivationInfo ActivationInfo, UGameplayAbility* Ability, bool bWasCanceled)
{
if (Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted || Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::ServerInitiated)
{
// Only replicate ending if policy is predictive
if (GetOwnerRole() == ROLE_Authority)
{
if (!AbilityActorInfo->IsLocallyControlled())
{
// Only tell the client about the end/cancel ability if we're not the local controller
if (bWasCanceled)
{
ClientCancelAbility(Handle, ActivationInfo);
}
else
{
ClientEndAbility(Handle, ActivationInfo);
}
}
}
else if(Ability->GetNetSecurityPolicy() != EGameplayAbilityNetSecurityPolicy::ServerOnlyTermination && Ability->GetNetSecurityPolicy() != EGameplayAbilityNetSecurityPolicy::ServerOnly)
{
// This passes up the current prediction key if we have one
if (bWasCanceled)
{
ServerCancelAbility(Handle, ActivationInfo);
}
else
{
CallServerEndAbility(Handle, ActivationInfo, ScopedPredictionKey);
}
}
}
}
总的看起来gas 非常庞大和复杂 支持网络同步和预测,但是 还不是很完善。
自动同步的 有GA 所以需要同步的东西尽量写在GA中,GE重点是修改Attribute的值的作用,是在GA网络同步之后 在各自的端执行GE,GE貌似没有自己的通络同步功能。
再以网络同步的视角分析一遍
当必须在server上give一个GA的时候,是添加到ActivatableAbilities这个变量里面的,这个变量是属性同步的,只同步给owner,就是说这个时候server和主控端都有这个GA了 但是模拟端没有
如果这个GA 是个instance那么在GA spec中有个ReplicatedInstances和NonReplicatedInstances记录着自己的多个对象。随着ActivatableAbilities 同步到主控端。
服务器给完GA server和主控端都会有
然后激活一个技能,包括主控端激活和server端激活
在不同端激活的时候 GA 有excute策略,
其中有两种情况比较拧巴
第一 主控端激活只在服务器上运行的GA 这个时候必走rpc
第二server端激活localonly的GA 这个时候必须运行client函数。
现在有个问题 那模拟端怎么办,目前没看到 模拟端的逻辑,可能只有表现的时候才会同步模拟端,或者属性自动会同步到模拟端。
GE 。GA在哪个端运行 GE就在哪个端运行。
ASC组件中ActiveGameplayCues ,SpawnedAttributes,ReplicatedLooseTags 全部端都有的
详情看ASC的GetLifetimeReplicatedProps函数
所以网络同步 这块逻辑全用GA就行
GA策略
UENUM(BlueprintType)
namespace EGameplayAbilityInstancingPolicy
{
/**
* How the ability is instanced when executed. This limits what an ability can do in its implementation. For example, a NonInstanced
* Ability cannot have state. It is probably unsafe for an InstancedPerActor ability to have latent actions, etc.
*/
enum Type : int
{
// This ability is never instanced. Anything that executes the ability is operating on the CDO.
NonInstanced,
// Each actor gets their own instance of this ability. State can be saved, replication is possible.
InstancedPerActor,
// We instance this ability each time it is executed. Replication currently unsupported.
InstancedPerExecution,
};
}
UENUM(BlueprintType)
namespace EGameplayAbilityNetExecutionPolicy
{
/** Where does an ability execute on the network. Does a client "ask and predict", "ask and wait", "don't ask (just do it)" */
enum Type : int
{
// Part of this ability runs predictively on the local client if there is one
LocalPredicted UMETA(DisplayName = "Local Predicted"),
// This ability will only run on the client or server that has local control
LocalOnly UMETA(DisplayName = "Local Only"),
// This ability is initiated by the server, but will also run on the local client if one exists
ServerInitiated UMETA(DisplayName = "Server Initiated"),
// This ability will only run on the server
ServerOnly UMETA(DisplayName = "Server Only"),
};
}
UENUM(BlueprintType)
namespace EGameplayAbilityNetSecurityPolicy
{
/** What protections does this ability have? Should the client be allowed to request changes to the execution of the ability? */
enum Type : int
{
// No security requirements. Client or server can trigger execution and termination of this ability freely.
ClientOrServer UMETA(DisplayName = "Client Or Server"),
// A client requesting execution of this ability will be ignored by the server. Clients can still request that the server cancel or end this ability.
ServerOnlyExecution UMETA(DisplayName = "Server Only Execution"),
// A client requesting cancellation or ending of this ability will be ignored by the server. Clients can still request execution of the ability.
ServerOnlyTermination UMETA(DisplayName = "Server Only Termination"),
// Server controls both execution and termination of this ability. A client making any requests will be ignored.
ServerOnly UMETA(DisplayName = "Server Only"),
};
}
UENUM(BlueprintType)
namespace EGameplayAbilityReplicationPolicy
{
/** How an ability replicates state/events to everyone on the network */
enum Type : int
{
// We don't replicate the instance of the ability to anyone.
ReplicateNo UMETA(DisplayName = "Do Not Replicate"),
// We replicate the instance of the ability to the owner.
ReplicateYes UMETA(DisplayName = "Replicate"),
};
}
UENUM(BlueprintType)
namespace EGameplayAbilityTriggerSource
{
/** Defines what type of trigger will activate the ability, paired to a tag */
enum Type : int
{
// Triggered from a gameplay event, will come with payload
GameplayEvent,
// Triggered if the ability's owner gets a tag added, triggered once whenever it's added
OwnedTagAdded,
// Triggered if the ability's owner gets tag added, removed when the tag is removed
OwnedTagPresent,
};
}