GAS 文档阅读摘抄笔记
GAS 文档阅读摘抄笔记
Ability System Component
- 所有期望使用GameplayAbility, 包含Attribute, 或者接受GameplayEffect的Actor都必须附加ASC.
- ASC附加的Actor被引用作为该ASC的OwnerActor, 该ASC的物理代表Actor被称为AvatarActor. 比如MOBA游戏中玩家控制的英雄, 其中OwnerActor是PlayerState, AvatarActor是英雄的Character类.
- 绝大多数Actor的ASC都附加在其自身, 如果你的Actor会重生并且重生时需要持久化Attribute或GameplayEffect(比如MOBA中的英雄), 那么ASC理想的位置就是PlayerState.
- 如果ASC位于PlayerState, 那么你需要提高PlayerState的NetUpdateFrequency, 其默认是一个很低的值, 因此在客户端上发生Attribute和GameplayTag改变时会造成延迟或卡顿. 确保启用Adaptive Network Update Frequency(net.UseAdaptiveNetUpdateFrequency=1),之后对Actor的NetUpdateFrequency和MinNetUpdateFrequency的设置就可以自适应的改变复制频率。
- 在ASC的子类里函数里使用ABILITYLIST_SCOPE_LOCK()来防止在它的作用域里移除列表里的Ability.
//ABILITYLIST_SCOPE_LOCK会在栈里创建1个变量,构造函数里给ASC的AbilityScopeLockCount加1,表示增删锁的数量,析构函数里会减一表示释放锁。 //所有的锁被完全释放后,会应用缓存的PendingAdds和PendingRemove ABILITYLIST_SCOPE_LOCK(); for (FGameplayAbilitySpec& Spec : ActivatableAbilities.Items){...}
同步模式
ASC定义了三种不同的同步模式用于同步GameplayEffect, GameplayTag和GameplayCue: Full, Mixed和Minimal. Attribute由其AttributeSet同步.
同步模式 | 如何使用 | 描述 |
---|---|---|
Full | 单人 | 所有GameplayEffect都同步到客户端. |
Mixed | 多人, 玩家控制的Actor | GameplayEffect只同步到其所属客户端, 只有GameplayTag和GameplayCue同步到所有客户端. |
Minimal | 多人, AI控制的Actor | GameplayEffect从不同步到任何客户端, 只有GameplayTag和GameplayCue同步到所有客户端. |
- Mixed同步模式需要OwnerActor的Owner是Controller. PlayerState的Owner默认是Controller但是Character不是. 如果OwnerActor不是PlayerState时使用Mixed同步模式, 那么需要在OwnerActor中调用SetOwner()设置自己的Owner为Controller.
设置和初始化
- ASC一般在OwnerActor的构建函数中创建并且需要明确标记为Replicated. 这必须在C++中完成.
- OwnerActor和AvatarActor的ASC在服务端和客户端上均需初始化, 你应该在Pawn的Controller设置之后初始化(Possess之后), 单人游戏只需参考服务端的做法.
- 对于玩家控制的Character且ASC位于Pawn, 我一般在服务端Pawn的PossessedBy()函数中初始化, 在客户端PlayerController的AcknowledgePossession()函数中获取Pawn的ASC来初始化.
- 于玩家控制的Character且ASC位于PlayerState, 我一般在服务端Pawn的PossessedBy()函数中初始化, 在客户端PlayerController的OnRep_PlayerState()函数中初始化, 因为这里确保了PlayerState存在于客户端上。PlayerState的ASC初始化时,传入的OwnerActor和AvatarActor分别是this和Character,Character上的ASC初始化时,传入的2个参数都是this.
Gameplay Tags
- FGameplayTag是由GameplayTagManager注册的形似Parent.Child.Grandchild...的层级类标签, 这些标签对于分类和描述对象的状态非常有用
- 当给某个对象设置标签时, 如果它有ASC的话, 我们一般添加标签到ASC以与其交互.
- 多个GameplayTag可被保存于一个FGameplayTagContainer中, 相比TArray
,GameplayTagContainer做了一些很有效率的优化。 - 应该在项目设置里开启GamePlayTag的快速复制功能。此外GameplayTag编辑器可以选择填写普遍需要同步的GameplayTag以对其深度优化.
- 如果GameplayTag由GameplayEffect添加, 那么其就是可同步的. ASC允许你添加不可同步的LooseGameplayTag且必须手动管理.
- 对于标签的访问和比较,应该使用UGameplayTagManager提供的方法,相比直接使用字符串比较,GameplayTagManager实际上使用关系节点(父, 子等等)保存GameplayTag以获得更快的处理速度.
- UPROPERTY宏Meta = (Categories = "GameplayCue")用于在蓝图中过滤标签而只显示父标签为GameplayCue的GameplayTag
- 如果你想过滤函数中的GameplayTag参数, 使用UFUNCTION宏Meta = (GameplayTagFilter = "GameplayCue")
- 响应Tag的添加和删除:
AbilitySystemComponent->RegisterGameplayTagEvent(FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")), EGameplayTagEventType::NewOrRemoved).AddUObject(this, &AGDPlayerState::StunTagChanged);
Attribute
- 如果某项数值是属于某个Actor且游戏相关的, 你就应该考虑使用Attribute
- Attribute一般应该只能由GameplayEffect修改, 这样ASC才能预测(Predict)其改变
- Attribute也可以由AttributeSet定义并存于其中. AttributeSet用于同步那些标记为replication的Attribute.
- Attribute的类型FGameplayAttributeData由BaseValue和CurrentValue组成,CurrentValue用于记录当前值,BaseValue用于备份和恢复上一个值。
- 在PreAttributeChange中处理对CurrentValue的修改以及取值范围问题, 在PostGameplayEffectExecute中处理GE对BaseValue的修改.
- InstantGE可以永久性修改BaseValue,Duration&InfiniteGE可以修改CurrentValue,PeriodicGE被视为InstantGE并且可以修改BaseValue.
- Meta Attribute是临时的、可被任意GE修改、不可同步的,将于任意Attribute交互的临时属性。 比如伤害值作为Meta Attribute占位符, 而不是使用GE直接修改生命值Attribute, 使用这种方法, 伤害值就可以在GameplayEffectExecutionCalculation中由buff和debuff修改, 并且可以在AttributeSet中进一步操作。
UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate
可以为一个属性返回一个委托,这个委托会在属性值发生改变时广播。- 如果一个Attribute的值基于其他的Attribute,当其他属性变化,自己也自动变化,可以使用一个基于n个Attribute或MMC的InfiniteGE来让它自动推导.
TestAttrA = (TestAttrA + TestAttrB) * ( 2 * TestAttrC)
AttributeSet
- AttributeSet用于定义, 保存以及管理对Attribute的修改。开发者应该继承UAttributeSet. 在OwnerActor的构造函数中创建AttributeSet会自动注册到其ASC. 这必须在C++中完成.
- 你可以使用多个AttributeSet来表示按需添加到Actor的Attribute分组, 例如, 你可以有一个生命相关的AttributeSet, 一个魔法相关的AttributeSet等等。
- Attribute在内部被引用为AttributeSetClassName.AttributeName, 当你继承AttributeSet时, 所有父类的Attribute仍将保留父类名作为前缀.
- ASC中不应该有多个同一类的AttributeSet,否则ASC会随机选择一个操作。
- AttributeSet拥有一个Attribute, 并不意味着必须要使用它, 未使用的Attribute只占用很少的内存。
- AttributeSet可以在运行时从ASC上添加和移除,但是移除是有客户端奔溃的风险的。
- 对于可共享的物品如武器携带的弹药、盔甲耐久等,有3种方案可控权衡。
原文介绍 - Attribute只能使用C++在AttributeSet头文件中定义。建议使用
ATTRIBUTE_ACCESSORS(ClassName, PropertyName)
宏定义(在示例项目里)。 - AttributeSet的.cpp文件应该用预测系统(Prediction System)使用的
GAMEPLAYATTRIBUTE_REPNOTIFY
宏填充OnRep函数 - 为了配合预测系统,让客户端的OnRep在属性相同的情况下(因为有预测)依旧回调,需要在
GetLifetimeReplicatedProps
做一下处理。void UGDAttributeSetBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); //REPTNOTIFY_Always用于设置OnRep函数在客户端值已经与服务端同步的值相同的情况下触发(因为有预测), DOREPLIFETIME_CONDITION_NOTIFY(UGDAttributeSetBase, Health, COND_None, REPNOTIFY_Always); }
- Attribute的BaseValue和CurrentValue的初始化建议使用InstantGE,当然也可以使用
AttributeSet->InitHealth(100.0f);
,InitXX方法由ATTRIBUTE_ACCESSORS定义。 PreAttributeChange
用于在CurrentValue改变前修改查询Modifier时的返回值,这意味着像GameplayEffectExecutionCalculations和ModifierMagnitudeCalculations这种使用所有Modifier重新计算CurrentValue的函数需要再次执行值的限制操作。PreAttributeChange
不应该作为游戏逻辑的事件回调,而应该只做限制操作。游戏逻辑的事件应该使用UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute)
返回的代理。PostGameplayEffectExecute
仅在InstantGE对Attribute的BaseValue修改之后触发, 当GameplayEffect对其修改时, 这就是一个处理更多Attribute操作的有效位置.- 当PostGameplayEffectExecute()被调用时, 对Attribute的修改已经发生, 但是还没有被同步回客户端, 客户端只会接收到限制后的值.
OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator)
会在Aggregator为集合中的某个Attribute创建时触发。它允许FAggregatorEvaluateMetaData
的自定义设置。AggregatorEvaluateMetaData
用于Aggregator基于所有应用的Modifier评估Attribute的CurrentValue。默认情况下, AggregatorEvaluateMetaData只由Aggregator用于确定哪些Modifier是满足条件的。- 以MostNegativeMod_AllPositiveMods为例, 其允许所有正(Positive)Modifier但是限制负(Negative)Modifier(除了最负的那一个),即只允许将最负移动速度减速效果应用到玩家, 不满足条件的Modifier仍存于ASC中, 只是不被总合进最终的CurrentValue, 一旦条件改变, 它们之后就可能满足条件, 就像如果最负Modifier过期后, 下一个最负Modifier(如果存在的话)就是满足条件的.
void UGSAttributeSetBase::OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator) const { Super::OnAttributeAggregatorCreated(Attribute, NewAggregator); if (!NewAggregator) return; if (Attribute == GetMoveSpeedAttribute()) { //设置AggregatorEvaluateMetaData NewAggregator->EvaluationMetaData = &FAggregatorEvaluateMetaDataLibrary::MostNegativeMod_AllPositiveMods; } }
Gameplay Effect
- GE是GameplayAbility修改自己或他人Attribute和GameplayTag的容器。只是单纯的配置数据,使用时一般会创建很多的GE子类蓝图。
- GE通过Modifier和Execution(GameplayEffectExecutionCalculation)修改Attribute.
- GE有三种持续类型: 即刻(Instant), 持续(Duration)和无限(Infinite).
- GE可以添加、移除、执行GameplayeCue.
类型 | GameplayCue事件 | 何时使用 |
---|---|---|
Instant | Execute | 对Attribute中BaseValue立即进行的永久性修改. 其不会应用GameplayTag, 哪怕是一帧. |
Duration | Add & Remove | 对Attribute中CurrentValue的临时修改和当GameplayEffect过期或手动移除时, 应用将要被移除的GameplayTag. 持续时间是在UGameplayEffect类/蓝图中明确的. |
Infinite | Add & Remove | 对Attribute中CurrentValue的临时修改和当GameplayEffect移除时, 应用将要被移除的GameplayTag. 该类型自身永不过期且必须由某个Ability或ASC手动移除. |
- PeriodGE修改Attribute的BaseValue和执行GameplayCue时就被视为InstantGE, 这种类型的Effect对于像随时间推移的持续伤害(damage over time, DOT)很有用. 注意: 周期性的Effect不能被预测.
- GE一般不实例化, 而是从GameplayEffect的ClassDefaultObject创建一个GameplayEffectSpec,然后应用后被添加到ASC下名为ActiveGameplayEffects的特殊结构体容器.
- GE可以被GameplayAbility和ASC中的多个函数应用, 其通常是ApplyGameplayEffectTo的形式,可以通过绑定
AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf
委托监听Duration&InfiniteGE。服务器上总会被回调,Autonomous Proxy只会在Full和Mixed同步模式下对于同步的GameplayEffect调用该函数, Simulated Proxy只会在Full同步模式下调用该函数。 - GE可以被GameplayAbility和ASC中的多个函数移除, 其通常是RemoveActiveGameplayEffect的形式,课用过绑定
AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate()
的委托监听Duration&InfiniteGE。服务器上总会被回调,Autonomous Proxy只会在Full和Mixed同步模式下对于同步的GameplayEffect调用该函数, Simulated Proxy只会在Full同步模式下调用该函数。
Modifier
- Modifier可以修改Attribute并且是唯一可以预测性修改Attribute的方法. 一个GE可以有0个或多个Modifier, 每个Modifier通过某个指定的操作只能修改一个Attribute.
- Modifier可以指定4种操作:Add、Multiply、Divide、Override。最后CurrentValue按照定义的总和公式:
((InlineBaseValue + Additive) * Multiplicitive) / Division
计算,Override会优先覆盖最后应用的Modifier得出的最终值。 - 有四种类型的Modifier: ScalableFloat, AttributeBased, CustomCalculationClass, 和 SetByCaller, 它们全都生成一些浮点数, 用于之后基于各自的操作修改指定Modifier的Attribute.
Period GE的Period配置
- Period: 周期性GE的周期(单位秒),表示多少秒应用一次
- Execute Periodic Effect On Application: true/false, 表示在GE应用的瞬间就开始执行,或者等1个Period时间。
- Periodic Inhibition Policy: 在该GE失效一段时间后(可能由于GameTag被免疫),如何重新计算周期。
策略 描述 Reset Period 把免疫结束的那一刻作为新的周期开始点,等待下个周期应用效果 Execute and Reset Period 把免疫结束的那一刻作为新的周期开始点,并立即应用效果 Never Reset 按原来规定的时间周期继续执行
Stacking 叠加
- Stacking Type:多个GE应用到同一个Target的ASC上时,如何计算GE效果。
类型 描述 None GE不叠加,各自执行自己的计算 Aggregated By Target GE的目标有1个stackingGE实例 Aggregated By Source GE的每个来源都有1个stackingGE实例
以下的配置都是针对每个StackingGE实例的配置
- Stack Limit Count: GE叠加数量的上限,GE开启叠加后只有1个GESpec实例,但是会具有数量(层数)。
- Stack Duration Refresh Policy:叠加后,时长怎么处理:
策略 描述 RefreshOnSuccessfulApplication 按照最新的GE应用时间计算结束时间 NeverRefresh 仍然按照第一个GE计算的结束时间 - Stack Period Reset Policy: 叠加后,周期怎么处理:
策略 描述 ResetOnSuccessfulApplication 使用新叠加的GE重置周期 NeverReset 任然按照第一个GE的周期 - Stack Expiration Policy: 叠加后,结束时怎么处理:
策略 描述 ClearEntireStack 清除所有叠加数量 RemoveSingleStackAndRefreshDuration 层数-1,刷新Duration继续直到层数变为0 RefreshDuration 刷新Duration继续,相当于叠加的GE一直存在,主要用来在FActiveGameplayEffectsContainer::OnStackCountChange里手动处理层数和刷新时长
Gameplay Effect Tag
- Gameplay Effect Asset Tags用于说明GE,并不是GE生效后给目标添加的标签。
- GE是否能应用到目标,由Application Tag Requirements填写的标签是否满足决定。GE应用后可能是开启的,也可能是关闭的,这依赖于Ongoing Tag Requirements是否满足,应用期间如果依赖的标签不满足GE就会关闭,之后如果满足了,就会重新打开并重新应用它的Modifer.
- GE应用或移除时给ASC添加或删除的标签由Granted Tags决定。
- GE应用后,将会从该GE的Gameplay Effect Asset Tags和Granted Tags中移除Remove Gameplay Effects with Tags中填写的标签。
- GE的Immunity(免疫)基于GameplayTag实现,能有效组织其他GE的应用。尽管Application Tag Requirement也能从另一个角度达到此效果,但是使用Immunity可以提供UAbilitySystemComponent::OnImmunityBlockGameplayEffectDelegate委托。
- 免疫下的GrantedApplicationImmunityTags会检查源ASC(包括源Ability的AbilityTag, 如果有的话)是否包含特定的标签, 这是一种基于确定Character或源的标签对其所有GE提供免疫的方法.
- 免疫下的Granted Application Immunity Query, 会检查传入的GameplayEffectSpec是否与其查询条件相匹配, 从而阻止或允许其应用.
GameplayEffectSpec
- GameplayEffectSpec基于GE在创建,并且可在应用前修改GE的等级、周期、时长等,还可以增加GE的动态Tag。
- GESpec有SetByCaller用于传递某个Ability内部生成的数值数据到GameplayEffectExecutionCalculations或ModifierMagnitudeCalculations.一般通过
FGameplayEffectSpec::SetSetByCallerMagnitude 、GetSetByCallerMagnitude
使用,蓝图中提供了相应的方法。
GameplayEffectContext
GameplayEffectContext结构体存有关于GameplayEffectSpec创建者(Instigator)和TargetData的信息, 这也是一个很好的可继承结构体以在ModifierMagnitudeCalculation/GameplayEffectExecutionCalculation, AttributeSet和GameplayCue之间传递任意数据.
- 可以继承UAbilitySystemGlobals然后重写
FGameplayEffectContext* AllocGameplayEffectContext()
返回你继承FGameplayEffectContext自定义的类实例。
Modifier Magnitude Calculation
MMC是在GameplayEffect中作为Modifier使用的强有力的类, 它的功能类似GameplayEffectExecutionCalculation但是要逊色一些, 最重要的是它是可预测的. 它唯一要做的就是自CalculateBaseMagnitude_Implementation()返回浮点值, 你可以在C++和蓝图中继承并重写该函数.
- MMC里可以设置被捕获的attribute是否snapshot(快照),是否快照会影响要捕获的attribute的捕获时机(Attribute在GE创建或应用时捕获),同时选择snapshot的后该属性被Duration/Infinite的GE修改时,不会自动更新(这就是快照的含义)。
Snapshot | 源(Source)或目标(Target) | 在GameplayEffectSpec中捕获 | Attribute被无限(Infinite)或持续(Duration)GameplayEffect修改时自动更新 |
---|---|---|---|
是 | Source | 创建 | 否 |
是 | Target | 应用 | 否 |
否 | Source | 应用 | 是 |
否 | Target | 应用 | 是 |
- 捕获Attribute会自ASC现有的Modifier重新计算它们的CurrentValue, 该重新计算不会执行AbilitySet中的PreAttributeChange(), 因此所有的限制操作(Clamp)必须在这里重新处理.
Execution Calculation
GameplayEffectExecutionCalculation(Execution)是GE对ASC进行修改最强有力的方式,与MMC一样,它也可以捕获Attribute并选择性地为其创建Snapshot, 和MMC不同的是, 它可以修改多个Attribute并且基本上可以处理程序员想要做的任何事. 这种强有力和灵活性的负面就是它是不可预测的且必须在C++中实现.
- ExecutionCalculation只能由Instant和Periodic GE使用
快照 | Source或Target | 在GameplayEffectSpec中捕获 |
---|---|---|
是 | Source | 创建 |
是 | Target | 应用 |
否 | Source | 应用 |
否 | Target | 应用 |
- ExecCalc最普遍的应用场景是计算一个来自很多源(Source)和目标(Target)中Attribute伤害值的复杂公式.
- 除了捕获Attribute,还有别的发送数据到ExecCalc的方法,比如使用:SetByCaller、Backing Data Attribute Calculation Modifier、 Backing Data Temporary Variable Calculation Modifier、GameplayEffectContext。
CustomApplicationRequirement
CustomApplicationRequirement(CAR)类为设计师提供对于GameplayEffect是否可以应用的高阶控制, 而不是对GameplayEffect进行简单的GameplayTag检查.
Cost GE
- 开始的时候, 你通常会为每个有花费的GA都设置一个独一无二的Cost GE, 一个更高阶的技巧是对多个GA复用一个Cost GE, 只需修改自GA的Cost GE创建的GameplayEffectSpec中指定的数据(花费值是在GA上定义的), 这只作用于实例化的Ability.
- 复用CostGE有2种方法:1. 定义并使用MMC,在MMC里即时获取Ability的花费数值并计算。2. 重写UGameplayAbility::GetCostGameplayEffect(). 重写函数并在运行时即时的创建一个用来读取GameplayAbility中花费值的GameplayEffect.
Cooldown GE
- CoolDown基于Ability是否包含CoolDown的GameplayTag来实现, 所以Cooldown GE应该是一个不带有Modifier的Duration GE。
- 与Cost GE一样,CoolDown GE也可以复用
- 复用Cost GE有2中方法: 1. 使用SetByCaller设置GE的Duration Magnitude. 2. 使用MMC设置GE的Duration Magnitude。
- 使用
AbilitySystemComponent::GetActiveEffectsTimeRemainingAndDuration(const FGameplayEffectQuery&)
可以查询匹配传入的GameplayTag的GE剩余时间和总时长。注意在客户端上查询剩余冷却时间要求其可以接收同步的GE, 这依赖于它们ASC的同步模式. - ASC上可以监听GE的添加/移除,以及Tag的添加/移除。要监听冷却的开始结束,建议监听GE的添加来确认冷却的开始,监听Tag的移除来确认冷却的结束。注意在客户端上监听某个GameplayEffect添加或移除要求其可以接收同步的GameplayEffect, 这依赖于它们ASC的同步模式.
- 目前冷却时间不是真正可预测的.
- CoolDown和其他DurationGE的持续时间可以被修改(修改示例见原文档)。
GameplayEffect Containers
- Epic的Action RPG样例项目实现了一个名为FGameplayEffectContainer的结构体, 它不属于原生GAS, 但是对于包含GameplayEffect和TargetData极其好用
Gameplay Abilities
GameplayAbility(GA)是Actor在游戏中可以触发的一切行为和技能. 多个GameplayAbility可以在同一时刻激活, 例如奔跑和射击.
- GA运行在所属(Owning)客户端还是服务端取决于网络执行策略(Net Execution Policy)而不是Simulated Proxy.
- 不要使用Replication Policy. 这个名字会误导你并且你并不需要它. GameplayAbilitySpec默认会从服务端向所属(Owning)客户端同步, 上文提到过, GameplayAbility不会运行在Simulated Proxy上, 其使用AbilityTask和GameplayCue来replicate或者RPC视觉变化到Simulated Proxy.
- Server Respects Remote Ability Cancellation(服务器承认客户端GA结束(取消或完成)),该选项一般禁用,这表示客户端的GA由于玩家取消或者自然完成时, 就会强制它的服务端版本结束而不管其是否完成
- 建议禁用Replicate Input Directly,而是使用创建在已有输入相关的AbilityTask中的Generic Replicated Event(如果你的输入绑定在ASC)
绑定输入到ASC
- 为了绑定输入到ASC, 你必须首先创建一个枚举来将输入事件名称转换为byte, 枚举名必须准确匹配****项目设置中用于输入事件的名称, DisplayName就无所谓了.
- 除了分配的输入事件可以激活GA, ASC也接受设置外的Confirm和Cancel输入, 这些特殊输入被AbilityTask用来确定或取消像Target Actor这样的对象.
- 如果你的ASC位于Character, 那么就在SetupPlayerInputComponent()中包含用于绑定到ASC的函数.
- 如果你的ASC位于PlayerState, SetupPlayerInputComponent()中有一个潜在的竞争情况就是PlayerState还没有同步到客户端, 因此, 建议同时在SetupPlayerInputComponent()和OnRep_PlayerState()中绑定输入,利用bool值可以防止多次绑定。
- 如果你不想你的GA在按键按下时自动激活, 但是仍想将它们绑定到输入以与AbilityTask一起使用, 你可以在UGameplayAbility子类中添加一个新的布尔变量, bActivateOnInput, 其默认值为true并重写
UAbilitySystemComponent::AbilityLocalInputPressed
。
授予Ability
向ASC授予GA会将其添加到ASC的ActivatableAbilities列表, 从而允许其在满足GameplayTag需求时激活该GA.
- 我们在服务端授予GA, 之后其会自动同步GameplayAbilitySpec到所属(Owning)客户端, 其他客户端/Simulated proxy不会接受到GameplayAbilitySpec.
激活Ability
如果某个GA被分配给了一个输入事件, 那么当输入按键按下并且它的GameplayTag需求满足时, 它将会自动激活, 这可能并非总是激活GA的期望方式。
ASC提供了另外4种方法来定义如何激活GA.
UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilitiesByTag(const FGameplayTagContainer& GameplayTagContainer, bool bAllowRemoteActivation = true);
UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilityByClass(TSubclassOf<UGameplayAbility> InAbilityToActivate, bool bAllowRemoteActivation = true);
bool TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool bAllowRemoteActivation = true);
bool TriggerAbilityFromGameplayEvent(FGameplayAbilitySpecHandle AbilityToTrigger, FGameplayAbilityActorInfo* ActorInfo, FGameplayTag Tag, const FGameplayEventData* Payload, UAbilitySystemComponent& Component);
FGameplayAbilitySpecHandle GiveAbilityAndActivateOnce(const FGameplayAbilitySpec& AbilitySpec);
- 当从蓝图中的Event激活GA时, 你必须使用ActivateAbilityFromEvent节点, 并且标准的ActivateAbility节点不能出现在图表中, 如果ActivateAbility节点存在, 它就会一直被调用而不调用ActivateAbilityFromEvent节点.
- 不要忘记应该在GameplayAbility终止时调用EndAbility(), 除非你的GameplayAbility是像被动技能那样一直运行的GameplayAbility.
- 客户端调用TryActivateAbility后一直到ActivateAbility最终激活的过程中,如果是Local Predicted GA,过程中会调用CallServerTryActivateAbility,服务端收到CallServerTryActivateAbility后,和客户端一样开始从ServerTryActivateAbility开始执行,当调用到CanActivateAbility()后会根据返回ture/false,通知客户端是否激活成功(调用ClientActivateAbilitySucceed()/ClientActivateAbilityFailed()),客户端如果接收到服务端激活失败的消息,就会立即终止客户端的GA并撤销所有预测的修改.
- 被动GA应该在
OnAvatarSet
激活,同时被动GA一般有一个Server Only的网络执行策略。
取消Ability
- GA内部可以用
CancelAbility()
取消GA,他会调用EndAbility,然后标记WasCancelled = true;
- 外部GAS提供了下列一些取消GA函数:
/** 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();
CancelAllAbilities
无法取消非实例化的GA,需要依靠CancelAbility
获取激活的Active
- 你必须搜索ASC的ActivatableAbility列表(ASC拥有的已授予GameplayAbility)并找到一个与你正在寻找的资源或授予的GameplayTag相匹配的AbilitySpec.或者使用ASC提供的直接查询接口。
- FGameplayAbilitySpec有一个方法
IsActive()
可以获取GA当前是否激活。
实例化策略
GameplayAbility的实例化策略决定了当GA激活时是否和如何实例化.
- 分为:Instanced Per Actor、Instanced Per Execution、Non-Instanced。Instanced Per Actor最常用,Non-Instanced性能最好,但是一般只会用在小兵的基础攻击等频繁使用的简单Ability上面。
网络执行策略
GA的网络执行策略(Net Execution Policy)决定了谁该以什么顺序激活该GA.单人游戏要使用Server Only。
网络执行策略(Net Execution Policy) | 描述 |
---|---|
Local Only | GameplayAbility只运行在所属(Owning)客户端. 这对那些只需做本地视觉变化的Ability很有用. 单人游戏应该使用Server Only. |
Local Predicted | Local Predicted GameplayAbility首先在所属(Owning)客户端激活, 之后在服务端激活. 服务端版本会纠正客户端预测的所有不正确的地方. 参见Prediction. |
Server Only | GameplayAbility只运行在服务端. 被动GameplayAbility一般是Server Only. 单人游戏应该使用该项. |
Server Initiated | Server Initiated GameplayAbility首先在服务端激活, 之后在所属(Owning)客户端激活. 我个人使用的不多(如果有的话). |
Ability标签
GameplayAbility自带有内建逻辑的GameplayTagContainer. 这些GameplayTag都不进行同步.这是针对该GA的配置。
Game Ablity Spec
- 当GameplayAbility在服务端授予时, 服务端会同步GameplayAbilitySpec到所属(Owning)客户端, 因此可以激活它.
- 激活GameplayAbilitySpec会根据它的实例化策略(Instancing Policy)创建一个GameplayAbility实例(Non-Instanced GameplayAbility除外).
传递数据到Ability
- 有4种方式可以传递数据到Ability(参照文档), 其中大部分未同步或保证及时同步。使用TargetData是一种在客户端和服务端之间传递任意数据的好方法.
Ability的Cost和Cooldown
GameplayAbility默认带有对可选Cost和Cooldown的功能.
- Cost是指ASC激活GameplayAbility所必需的Attribute量,如果满足条件,则会应用Cost GE。CoolDown功能由Duration类型的Cooldown GE实现。
- GA在Activate()之前,会进行
UGameplayAbility::CheckCost()
和UGameplayAbility::CheckCooldown()
的判断。 - GA在Activate()之后,可以掉用
UGameplayAbility::CommitAbility()
提交Cost和Cooldown,他会调用CommitAbility()
和CommitCooldown()
。 - 有前摇的GA在CommitAbility()之前,有可能被其他GA提前Commit了,此时CommitAbility()可能失败,然后结束GA。
- 如果prediction key在提交时有效的话, 提交Cost和Cooldown是可以客户端预测的.
升级GA
升级GA有2种做法,区别是是否取消激活的GA
升级方法 | 描述 |
---|---|
升级时取消授予和重新授予 | 自ASC取消授予(移除)GameplayAbility, 并在下一等级于服务端上重新授予. 如果此时GameplayAbility是激活的, 那么就会终止它. |
增加GameplayAbilitySpec的等级 | 在服务端上找到GameplayAbilitySpec, 增加它的等级, 并将其标记为Dirty以同步到所属(Owning)客户端. 该方法不会在GameplayAbility激活时将其终止. |
AbilitySet
- GameplayAbilitySet是很方便的UDataAsset类, 其用于保存输入绑定和初始GameplayAbility列表, 该GameplayAbility列表用于拥有授予GameplayAbility逻辑的Character. 子类也可以包含额外的逻辑和属性
Ability批处理
GA的生命周期里,至少涉及3个客户端->服务端的RPC调用,CallServerTryActivateAbility(); ServerSetReplicatedTargetData()(可选); ServerEndAbility()
。
- 这些RPC如果在同一帧同一个原子组里调用,就可以批处理为1个RPC调用。
- ASC中批处理默认是禁止的,重写
ShouldDoServerAbilityRPCBatch()
打开。 - 如果要批处理激活的GA,在激活GA前必须创建FScopedServerAbilityRPCBatcher变量,在它的作用域内,所有激活的AC才会尝试批处理。这个变量会拦截域内的RPC然后打包进一个批处理结构体内,在出域的时候,会把这个RPC给服务端。
UAbilitySystemComponent::ServerAbilityRPCBatch_Internal
会接收发来的批处理结构体,是一个打断点查看批处理是否正确运行的好函数。
网络安全策略
- GA可以设置它的NetSecurityPolicy网络安全策略(EditDefautlsOnly)用来说明客户端触发GA的执行和退出是否被服务器忽略。
网络安全策略 描述 ClientOrServer 没有安全需求. 客户端或服务端可以自由地触发该Ability的执行和终止. ServerOnlyExecution 客户端对该Ability请求的执行会被服务端忽略, 但客户端仍可以请求服务端取消或结束该Ability. ServerOnlyTermination 客户端对该Ability请求的取消或结束会被服务端忽略, 但客户端仍可以请求执行该Ability. ServerOnly 服务端控制该Ability的执行和终止, 客户端的任何请求都会被忽略.
Ability Task
GameplayAbility只能在1帧里执行,这本身不够灵活。所以我们用AbilityTask实现随时间触发或者经过一段时间后触发的委托操作。GAS提供了大量的UAbilityTask供使用。
- 同时运行的AbilityTask数量最多不超过1000个。
自定义AbilityTask
- UAbilityTask主要包括:
- 供蓝图调用的静态创建函数,里边创建并返回类的实例。
- 暴露给蓝图的使用各项委托(需要注意所有委托只能是同一种委托类型,不论是否有参数)。
- 进行主要工作的Activate()函数,会绑定委托。
- 其他
- Ability不会运行在SimulatedProxy上,但是可以通过AbilityTask运行。为此需要设置
bSimulatedTask=true
,重写virtual void InitSimulatedTask
。 - 设置
bTickingTask = true
后可以在TickTask()
里拿到帧间隔做平滑处理。
使用AbilityTask
- cpp里可使用静态创建函数创建,绑定代理回调后,调用
Task->ReadyForActivation()
激活。 - 蓝图里使用AbilityTask后,
K2Node_LatentGameplayTaskCall
会自动调用Task->ReadyForActivation()
.
Root Motion Source Ability
- GAS自带的AbilityTask可以使用挂载在CharacterMovementComponent中的Root Motion Source随时间推移而移动Character, 像击退, 复杂跳跃, 吸引和猛冲.(4.25版本修复了已存的bug)
Gameplay Cue
定义
- GameplayCue(GC)执行非游戏逻辑相关的功能, 像音效, 粒子效果, 镜头抖动等等. GameplayCue一般是可同步(除非在客户端明确执行, 添加和移除), 同时也是可预测的.
- 我们可以在ASC中通过发送一个强制带有"GameplayCue"父名的相应GameplayTag,以及GameplayCueManager的事件类型(Execute, Add或Remove)来触发GameplayCue.
- GameplayCueNotify对象和其他实现IGameplayCueInterface的Actor可以基于GameplayCueTag来订阅这些事件.
- 有2个GameplayCueNotify的子类供使用:
- Instant和Periodic GE只有Execute事件,使用
GameplayCueNotify_Static
类订阅,适用于一次性效果(如打击效果)。 - Duration和Infinite GE有Add和Remove事件,使用
GameplayCueNotify_Actor
类订阅,适用于循环的声音和粒子特效。
- Instant和Periodic GE只有Execute事件,使用
- GameplayCueNotify技术上可以响应任何事件, 但是这是我们一般使用它的方式.
- 当使用GameplayCueNotify_Actor时, 要勾选Auto Destroy on Remove, 否则接下来对GameplayCueTag的添加(Add)调用将无法运行.
- 当使用除了Full以外的同步模式时,Add和Remove的GameplayCue事件会在监听服务器玩家上触发两次,1次是应用GE,一次是Server Multicast到客户端。而WhileActive事件仍会只触发一次。
- 有些GC可以通过客户端触发来进行优化, 而不是通过GE同步。
触发
- GE成功应用后,会自动触发GE里配置的GameplayCues(通过填入的GameplayCueTag寻找对应GC)。
- GC编辑触发的GameplayTag的时候,还能设置
IsOverride
,标识该标签的父标签触发的GC是否递归向上触发。true表示不递归触发。 - 手动触发:GA提供了接口添加和移除GC,这些接口暴露给了蓝图。ASC也在C++里提供了接口,你可以同在在子类暴露给蓝图。
客户端GameplayCue
从GameplayAbility和ASC中暴露的用于触发GameplayCue的函数默认是可同步的. 每个GameplayCue事件都是一个多播(Multicast)RPC. 这会导致大量RPC. GAS也强制在每次网络更新中最多能有两个相同的GameplayCueRPC. 我们可以通过使用client GameplayCue来避免这个问题.
- 本地GC的触发一般通过
UGameplayCueManager::HandleGameplayCue(...)
触发GC的WhileActive、Remove...等事件。 - GC遵循客户端添加客户端移除、服务器添加服务器移除的原则。
GameplayCueParameters
如果手动触发GC,你就必须填写传递GameplayCueParameters给GC。如果由GE触发,这个变量就会自动填充。
- GameplayCueParameters包含大量数据可供传递给GC。
Gameplay Cue Manager
- 游戏开始时GampelayCueManager会扫描游戏的全部目录以寻找GameplayCueNotify并加载进内存。可以修改
DefaultGame.ini
来修改默认的扫描路径。[/Script/GameplayAbilities.AbilitySystemGlobals] +GameplayCueNotifyPaths="/Game/GASShooter/Characters" # 项目路径Content/GASShooter/Characters +GameplayCueNotifyPaths="/Game/GASShooter/Weapons" # 项目路径Content/GASShooter/Weapons
- 地图开始时默认会加载游戏里所有的GC和他们依赖的资源,这可能会导致无效的资源内存占用,增加潜在的游戏无响应几率,导致第一次GC触发时出现延迟。如果要解决这个问题,需要自己继承UGameplayCueManager重写
ShouldAsyncLoadRuntimeObjectLibraries
方法,并在配置里指定GCManager类。
//GSGameplayCueManager.h
virtual bool ShouldAsyncLoadRuntimeObjectLibraries() const override
{
return false; //default is true
}
//DefaultGame.ini
[/Script/GameplayAbilities.AbilitySystemGlobals]
GlobalGameplayCueManagerClass="/Script/GASShooter.GSGameplayCueManager" // 项目路径Source/GasShooter/xxx/xxx/xxx/GSGameplayCueManager.h
阻止GameplayCue响应
- 为了阻止GE上配置的GC执行,我们可以在GameplayEffectExecutionCalculations中调用
OutExecutionOutput.MarkGameplayCuesHandledManually()
, 之后手动发送我们的GameplayCue事件到Target或Source的ASC中. - 如果想要某个指定的ASC中的GameplayCue永不触发,可以设置
AbilitySystemComponent->bSuppressGameplayCues = true;
GameplayCue的批处理
和GameplayAbility一样,每次GameplayCue触发都是一次不可靠的多播(NetMulticast)RPC. 在同一时刻触发多个GameplayCue的情况下, 有一些优化方法来将它们压缩成一个RPC或者通过发送更少的数据来节省带宽。
- 阻塞UGameplayCueManager::FlushPendingCues(),将排队的GC合并进自定义的结构体,手动RPC结构体到客户端。在客户端接收并解释结构体,然后执行对应的ClientGC。
- 通过设置AbilitySystem.AlwaysConvertGESpecToGCParams = true来将GameplayEffectSpec转换为FGameplayCueParameter结构体并且RPC它,而不是整个FGameplayEffectSpecForRPC, 这会节省带宽但是只有较少的信息。
Ability System Globals
AbilitySystemGlobals类保存有关GAS的全局信息. 大多数变量可以在DefaultGame.ini中设置. 一般你不需要和该类互动, 但是应该知道它的存在. 如果你需要继承像GameplayCueManager或GameplayEffectContext这样的对象, 就必须通过AbilitySystemGlobals来做.
- 想要继承AbilitySystemGlobals, 需要在DefaultGame.ini中设置类名:
[/Script/GameplayAbilities.AbilitySystemGlobals] AbilitySystemGlobalsClassName="/Script/ParagonAssets.PAAbilitySystemGlobals"
- 继承AbilitySystemGlobals后,你需要调用一次
UAbilitySystemGlobals::InitGlobalData()
来使用TargetData, 否则你会遇到关于ScriptStructCache的错误, 并且客户端会从服务端断开连接。将其放到UEngineSubsystem::Initialize()
是个好位置。
预测 Prediction
GAS的预测是在服务器许可前客户端激活GA和应用GE,服务器拒绝后客户端回滚以匹配服务器。
- 有些是可以“预测”的,有些不能预测,比如:GE的移除,GE的周期效果。
- Epic的理念是预测“不受伤害”的事情。
Prediction Key
GAS的预测体系建立在Prediction Key上面,它是客户端激活GA时生成的Integer标识符。
客户端 | 数据同步 | 服务端 |
---|---|---|
激活、P-Key | ||
CallServerTryActivateAbility | P-Key --> | receive P-Key |
add P-Key to all applying GE(c) | add P-Key to all applying GE(s) | |
P-Key out scope | ||
receive GE(s) using P-key | <-- GE(s) | sync GE(s) |
receive P-key(stale) | <-- P-Key | sync back P-Key |
rm GE(c) using P-Key(stale),keep GE(s) | ||
all GE(c) not using P-Key(stale) is wrong |
Scoped Prediction Window
FScopedPredictionWindow是一个“窗口”,在构造时窗口打开,同时生成新的预测Key,在析构时窗口关闭。在此期间,FPredictionKey UAbilitySystemComponent::ScopedPredictionKey
是有效Key,可以用来预测。
预测性的生成Actor
在客户端预测性地生成Actor是一项高级技术. GAS对此没有提供开箱即用的功能(SpawnActor AbilityTask只在服务端生成Actor)。
- 如果要生成的Actor只是装饰、不影响游戏逻辑。可以限制服务器同步到所属客户端,所属(Owning)客户端会拥有其本地生成的版本, 而服务端和其他客户端会拥有服务端同步的版本.
bool APAReplicatedActorExceptOwner::IsNetRelevantFor(const AActor * RealViewer, const AActor * ViewTarget, const FVector & SrcLocation) const
{
return !IsOwnedBy(ViewTarget);
}
- 如果生成的Actor影响了游戏逻辑, 像投掷物就需要预测伤害值, 那么你需要本文档范围之外的高级知识, 在Epic Games的Github上查看UnrealTournament是如何生成投掷物的, 它有一个只在所属(Owning)客户端生成且与服务端同步的投掷物.
Targeting
Target Data
FGameplayAbilityTargetData是用于通过网络传输定位数据的通用结构体,一般包括AAcotr/UObject的引用、FHitResult、和其他通用的Location/Direciton/OriginData。
- 本质上你可以继承它以增添想要的任何数据, 其可以简单理解为在客户端和服务端的GameplayAbility中传递数据
- TargetData一般由Target Actor或者手动创建, 供AbilityTask使用, 或者GE通过EffectContext使用. 因为其位于EffectContext中, 所以Execution, MMC, GameplayCue和AttributeSet的后端函数可以访问该TargetData.
- 我们一般不直接使用FGameplayAbilityTargetData,而是FGameplayAbilityTargetDataHandle.
Target Actor
AGameplayAbilityTargetActor用于展示正在定位的目标。比如AGameplayAbilityTargetActor_GroundTrace用地面贴花表示陨石技能的伤害区域效果。
- TargetActor默认是不可同步的. 当然这是可以修改的。
- 在TargetActor和WaitTargetData AbilityTask中存在大量委托, TargetActor响应输入来产生广播TargetData的准备, 确认或者取消委托, WaitTargetData监听TargetActor的TargetData的准备、确认和取消委托, 并将该信息转发回GameplayAbility和服务端
Target Data Filter
同时使用Make GameplayTargetDataFilter和Make Filter Handle节点, 你可以过滤玩家的Pawn或者只选择某个特定类.
Gameplay Effect Container Targeting
它提供了一个可选的产生TargetData的高效方法. 当EffectContainer在客户端和服务端上应用时, 定位会立即进行。不支持用户输入, 无需确认即可立即进行,** 不能取消, 并且不能从客户端向服务端发送数据(在两者上产生数据), 它对即时射线检测和碰撞**很有用。