5. 常见实现的Abilities和Effects
5.1 Stun(眩晕)
当我们实现眩晕效果时,通常需要:
- 取消角色当前所有激活的
GameplayAbilities - 阻止新的
GameplayAbility激活 - 阻止角色移动
示例项目中的 Meteor(陨石) GameplayAbility 在击中目标时应用眩晕效果。
具体实现方式:
- 当眩晕效果被添加到目标时,调用
AbilitySystemComponent->CancelAbilities()取消目标当前所有激活的GameplayAbilities - 将眩晕效果的
GameplayTag添加到GameplayAbility的Activation Blocked Tags中,以阻止新的GameplayAbility激活 - 重写
CharacterMovementComponent的GetMaxSpeed()函数,当拥有者带有眩晕GameplayTag时返回 0 来阻止移动
5.2 Sprint(冲刺)
示例项目展示了如何实现冲刺功能 - 按住左Shift键时跑得更快。
具体实现:
-
更快的移动速度由
CharacterMovementComponent预测性处理,通过网络向服务器发送flag。详见GDCharacterMovementComponent.h/cpp -
GameplayAbility负责响应左Shift输入,告诉CharacterMovementComponent开始和停止冲刺,并在按住左Shift时预测性地(Predictively)消耗耐力值。详见GA_Sprint_BP
5.3 Aim Down Sights(瞄准)
示例项目中的瞄准实现方式与冲刺类似,但是是降低移动速度而不是提高。
-
预测性降低移动速度的实现见
GDCharacterMovementComponent.h/cpp -
输入处理的实现见
GA_AimDownSight_BP,瞄准不消耗耐力值
5.4 Lifesteal(生命偷取)
生命偷取在伤害 ExecutionCalculation 中处理。GameplayEffect 会带有一个类似 Effect.CanLifesteal 的 GameplayTag。ExecutionCalculation 检查 GameplayEffectSpec 是否有这个 Tag,如果存在,就创建一个动态的 Instant GameplayEffect,设置要恢复的生命值作为Modifier(修改器),并将其应用到来源的 ASC。
if (SpecAssetTags.HasTag(FGameplayTag::RequestGameplayTag(FName("Effect.Damage.CanLifesteal"))))
{
float Lifesteal = Damage * LifestealPercent;
UGameplayEffect* GELifesteal = NewObject<UGameplayEffect>(GetTransientPackage(), FName(TEXT("Lifesteal")));
GELifesteal->DurationPolicy = EGameplayEffectDurationType::Instant;
int32 Idx = GELifesteal->Modifiers.Num();
GELifesteal->Modifiers.SetNum(Idx + 1);
FGameplayModifierInfo& Info = GELifesteal->Modifiers[Idx];
Info.ModifierMagnitude = FScalableFloat(Lifesteal);
Info.ModifierOp = EGameplayModOp::Additive;
Info.Attribute = UPAAttributeSetBase::GetHealthAttribute();
SourceAbilitySystemComponent->ApplyGameplayEffectToSelf(GELifesteal, 1.0f, SourceAbilitySystemComponent->MakeEffectContext());
}5.5 Generating a Random Number on Client and Server(在客户端和服务器上生成随机数)
有时在 GameplayAbility 中需要生成随机数,比如用于子弹后坐力或扩散。客户端和服务器都需要生成相同的随机数。为此,我们必须在 GameplayAbility 激活时设置相同的随机种子。
有两种设置种子的方法:
-
使用激活预测键(
Activation Prediction Key) -
通过事件负载在激活
GameplayAbility时发送种子
| 随机种子设置方法 | 描述 |
|---|---|
使用Activation Prediction Key | GameplayAbility的Activation Prediction Key是一个确保同步并且在客户端和服务端的Activation()中都可用的int16类型值。你可以在客户端和服务端中设置其作为随机数种子。该方法的缺点是每次游戏开始时Prediction Key总是从0开始并且会在生成key之后持续增加,这意味着每场游戏都有着及其相同的随机数序列,这可能满足或不满足你的随机数需要。 |
激活GameplayAbility时通过事件负载(Event Payload)发送种子 | 通过事件激活GameplayAbility并通过可同步的事件负载(Event Payload)从客户端发送随机生成的种子到服务端, 这允许更高的随机性, 但是客户端也容易被破解而每次只发送相同的种子。通过事件激活GameplayAbility也会阻止其从用户绑定激活。 |
如果您的随机偏差较小,大多数玩家不会注意到每场比赛中的序列是相同的,因此使用激活预测键作为 random seed 应该适合您。如果您做的是更复杂的任务,并且需要防止黑客篡改,也许使用 Server Initiated 的 GameplayAbility 更为合适,在这种情况下,服务器可以生成预测键或生成随机种子并通过事件负载发送。
5.6 Critical Hits(暴击)
暴击判定在伤害 ExecutionCalculation 中处理。GameplayEffect 会带有一个类似 Effect.CanCrit 的 GameplayTag。ExecutionCalculation 会检查 GameplayEffectSpec 是否带有这个 Tag。如果存在,ExecutionCalculation 会根据暴击几率(从Source捕获的Attribute)生成随机数,如果触发成功则增加暴击伤害(同样是从Source捕获的Attribute)。
由于我们不预测伤害计算,所以不需要担心客户端和服务器之间的随机数同步问题,因为 ExecutionCalculation 只在服务器端运行。如果你尝试使用 MMC 来预测性地计算伤害,你就需要从 GameplayEffectSpec->GameplayEffectContext->GameplayAbilityInstance 获取随机种子的引用。
可以参考 GASShooter 中的爆头是如何实现的。原理类似,但不依赖随机数判定,而是检查 FHitResult 中的骨骼名称。
5.7 不叠加但保留最大效果值的 GameplayEffects (Non-Stacking Gameplay Effects but Only the Greatest Magnitude Actually Affects the Target)
在Paragon中,减速效果不叠加。每个减速实例正常应用并跟踪其生命周期,但只有最大减速效果实际影响Target。GAS 通过 AggregatorEvaluateMetaData 提供了这种情况的解决方案。详见 AggregatorEvaluateMetaData() 。
5.8 游戏暂停时生成 Target Data
如果你需要在等待从玩家获取 TargetData (通过 WaitTargetData AbilityTask) 时暂停游戏,建议使用 slomo 0 而不是直接暂停。
5.9 One Button Interaction System(按钮交互系统)
GASShooter 实现了一个单键交互系统 - 玩家可以通过按住或点击’E’键与可交互物体互动,比如:
- 复活玩家
- 打开武器箱
- 打开或关闭滑动门