## GAS中的传递数据、解耦与其他技巧 本文大部分内容来着GASDocumentation与GASDocumentation_Chinese,因为感觉很重要所以在此简单归纳一下。 https://github.com/tranek/GASDocumentation https://github.com/BillEliot/GASDocumentation_Chinese ## 响应GameplayTags的变化 这个没什么好说的,直接上GASDocumentation_Chinese中的解释: >ASC提供了一个委托(Delegate)用于在GameplayTag添加或移除时触发, 其中EGameplayTagEventType参数可以明确是该GameplayTag添加/移除还是其TagMapCount发生变化时触发. ``` AbilitySystemComponent->RegisterGameplayTagEvent(FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")), EGameplayTagEventType::NewOrRemoved).AddUObject(this, &AGDPlayerState::StunTagChanged); ``` 回调函数拥有变化的GameplayTag参数和新的TagCount参数. ``` virtual void StunTagChanged(const FGameplayTag CallbackTag, int32 NewCount); ``` ## 向GameplayAbility传递数据 一共有4中方法: - 通过设置Ability中的Trigger,之后再在ActivateAbilityFromEvent事件中进行处理。 - 使用WaitGameplayEvent之类的AbilityTask对接收到的事件进行处理。 - 使用TargetData。 - 存储数据在OwnerActor或者AvatarActor中,之后再通过ActorInfo中的Owner或者Avatar来获取数据。 前两种都可以通过往Event中添加Payload来传递额外信息。Payload可以使用自定义的UObject。TargetData涉及到FGameplayAbilityTargetData、AGameplayAbilityTargetActor等东西,下一篇文章会说。 ## 通过Event/Tag来激活GameplayAbility Ability Trigger是一种不错的解耦方式,大致的使用步骤: 1. 在Ability中的设置Trigger中的Ability Trigger(在Class Defaults中)。 2. 实现ActivateAbilityFromEvent事件,通过Event中Payload获取自定义UObject中的信息。 3. Event的触发方式:通过调用SendGameplayEventToActor()向对应拥有ASC的Actor/Character发送GameplayEvent来触发对应Ability。 4. Tag的触发方式:通过GamplayEffect来添加对应Tag来触发对应Ability。(AddLooseGameplayTag()我没试过,但估计也是可以的) 使用Tag的好处在于Owned Tag Present,即让Ability与Tag保持一样的生命周期,不过很可惜Tag的触发方式并不支持Ability的AbilityTags。注册过程位于ASC的OnGiveAbility()中,主要是通过绑定MonitoredTagChanged事件来实现触发。 PS.本人使用这个功能实现了FightingStateHandle,即进入战斗从背上拔出武器,离开战斗则将武器放回背上。 ### GASShooter中的另类用法 这里我还想说一下GASShooter中使用Ability Trigger机制的另类用法:Interact,项目中宝箱与按钮门都使用了它。 这个系统主要由GA_InteractPassive、GA_InteractActive以及IGSInteractable接口。 #### IGSInteractable 首先所有可以互动的物体都需要继承并且按照对应需求实现对应的几个接口函数: - IsAvailableForInteraction - GetInteractionDuration - GetPreInteractSyncType - GetPostInteractSyncType - PreInteract - PostInteract - CancelInteraction - RegisterInteracter - UnregisterInteracter - InteractableCancelInteraction #### GA_InteractPassive与GA_InteractActive GA_InteractPassive与GA_InteractActive会在游戏开始时,在BP_HeroCharacter中注册。 GA_InteractPassive是一个GrantAbility,它会在游戏开始就被激活。它会执行UGSAT_WaitInteractableTarget,不断地进行射线判断。如果有可用的Interactable目标出现就会进行一些系列的判断,在绑定的InputAction按下后,最终给角色类ASC发送GameplayTask中设置的GameplayEvent(Payload中带有Interactable目标的TargetData)。 因为GA_InteractActive中设置了Ability Trigger,所以它会接收到GameplayEvent并且处理。Interactable触发后的逻辑都在这里处理(通过TargetData调用IGSInteractable接口函数)。 ## GameplayEffect的GrantAbilities 在GameplayEffect也有类似Ability Trigger,就是GrantAbilities,并且有多种移除Ability方式以供选择。但GameplayEffect需要符合一下要求: - StackingType需要为AggregateBySource/AggregateByTarget - DurationType需要为HasDuration/Infinite - 设置的Ability必须没有注册过(GiveAbility) 同时GameplayAbility类还需要实现OnAvatarSet()虚函数,该函数主要用于处理GrantAbility: ``` UCLASS() class RPGGAMEPLAYABILITY_API URPGGameplayAbility : public UGameplayAbility { GENERATED_BODY() public: { virtual void OnAvatarSet(const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilitySpec & Spec) override; protected: UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "RPGGameplayAbility") bool ActivateAbilityOnGranted; }; ``` ``` void URPGGameplayAbility::OnAvatarSet(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) { if (ActivateAbilityOnGranted) { ActorInfo->AbilitySystemComponent->TryActivateAbility(Spec.Handle, true); } } ``` 其中ActivateAbilityOnGranted用于判断这个Ability是不是GrantAbility,不然所有所有的Ability都会调用这个函数。 ## 运行时创建动态GameplayEffect ``` void UGameplayAbilityRuntimeGE::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) { if (HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo)) { if (!CommitAbility(Handle, ActorInfo, ActivationInfo)) { EndAbility(Handle, ActorInfo, ActivationInfo, true, true); } // Create the GE at runtime. UGameplayEffect* GameplayEffect = NewObject(GetTransientPackage(), TEXT("RuntimeInstantGE")); GameplayEffect->DurationPolicy = EGameplayEffectDurationType::Instant; // Only instant works with runtime GE. const int32 Idx = GameplayEffect->Modifiers.Num(); GameplayEffect->Modifiers.SetNum(Idx + 1); FGameplayModifierInfo& ModifierInfo = GameplayEffect->Modifiers[Idx]; ModifierInfo.Attribute.SetUProperty(UMyAttributeSet::GetMyModifiedAttribute()); ModifierInfo.ModifierMagnitude = FScalableFloat(42.f); ModifierInfo.ModifierOp = EGameplayModOp::Override; // Apply the GE. FGameplayEffectSpec* GESpec = new FGameplayEffectSpec(GameplayEffect, {}, 0.f); // "new", since lifetime is managed by a shared ptr within the handle ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, FGameplayEffectSpecHandle(GESpec)); } EndAbility(Handle, ActorInfo, ActivationInfo, false, false); } ``` ## 延长GE的持续时间 在GASDocument的4.5.16 中介绍了这个方法,直接上代码了: ``` bool UPAAbilitySystemComponent::SetGameplayEffectDurationHandle(FActiveGameplayEffectHandle Handle, float NewDuration) { if (!Handle.IsValid()) { return false; } const FActiveGameplayEffect* ActiveGameplayEffect = GetActiveGameplayEffect(Handle); if (!ActiveGameplayEffect) { return false; } FActiveGameplayEffect* AGE = const_cast(ActiveGameplayEffect); if (NewDuration > 0) { AGE->Spec.Duration = NewDuration; } else { AGE->Spec.Duration = 0.01f; } AGE->StartServerWorldTime = ActiveGameplayEffects.GetServerWorldTime(); AGE->CachedStartServerWorldTime = AGE->StartServerWorldTime; AGE->StartWorldTime = ActiveGameplayEffects.GetWorldTime(); ActiveGameplayEffects.MarkItemDirty(*AGE); ActiveGameplayEffects.CheckDuration(Handle); AGE->EventSet.OnTimeChanged.Broadcast(AGE->Handle, AGE->StartWorldTime, AGE->GetDuration()); OnGameplayEffectDurationChange(*AGE); return true; } ``` PS.本人使用使用这个技巧来刷新战斗状态的持续时间。 ## 其他技巧 ### 添加给ACS附加标签(不通过GE与GA) 这个方法不适合联机,只在本地起效果。可以作为一些用于本地效果控制。 ``` FGameplayTag DeadTag = FGameplayTag::RequestGameplayTag(FName("Status.Dead")); AbilitySystemComponent->AddLooseGameplayTag(DeadTag); ``` ### 动态修改Ability等级 - 移除旧Ability之后再添加新Ability - 增加GameplayAbilitySpec的等级:在服务端上找到GameplayAbilitySpec, 增加它的等级, 并将其标记为Dirty以同步到所属(Owning)客户端。 ### AbilityInputID绑定 GASDocument的项目定义了EGSAbilityInputID枚举用于定义所有Ability的输入绑定(名字需要与InputAction一一对应),并且在UGSGameplayAbility中设置AbilityInputID变量,用于输入绑定。 ``` for (TSubclassOf& StartupAbility : CharacterAbilities) { AbilitySystemComponent->GiveAbility( FGameplayAbilitySpec(StartupAbility, GetAbilityLevel(StartupAbility.GetDefaultObject()->AbilityID), static_cast(StartupAbility.GetDefaultObject()->AbilityInputID), this)); } ``` 这里还通过AbilityLevel来传递AbilityID。 PS.但本人感觉这个方法在一些动作游戏中这个方法可能会不太合适。