5、常见实现的能力和效果

5. 常见实现的AbilitiesEffects

5.1 Stun(眩晕)

当我们实现眩晕效果时,通常需要:

  • 取消角色当前所有激活的 GameplayAbilities
  • 阻止新的 GameplayAbility 激活
  • 阻止角色移动

示例项目中的 Meteor(陨石) GameplayAbility 在击中目标时应用眩晕效果。

具体实现方式:

  • 当眩晕效果被添加到目标时,调用 AbilitySystemComponent->CancelAbilities() 取消目标当前所有激活的 GameplayAbilities
  • 将眩晕效果的 GameplayTag 添加到 GameplayAbilityActivation Blocked Tags 中,以阻止新的 GameplayAbility 激活
  • 重写 CharacterMovementComponentGetMaxSpeed() 函数,当拥有者带有眩晕 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.CanLifestealGameplayTagExecutionCalculation 检查 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 激活时设置相同的随机种子

有两种设置种子的方法:

  1. 使用激活预测键(Activation Prediction Key)

  2. 通过事件负载在激活 GameplayAbility 时发送种子

随机种子设置方法描述
使用Activation Prediction Key GameplayAbilityActivation Prediction Key是一个确保同步并且在客户端和服务端的Activation()中都可用的int16类型值。你可以在客户端和服务端中设置其作为随机数种子。该方法的缺点是每次游戏开始时Prediction Key总是从0开始并且会在生成key之后持续增加,这意味着每场游戏都有着及其相同的随机数序列,这可能满足或不满足你的随机数需要。
激活GameplayAbility时通过事件负载(Event Payload)发送种子通过事件激活GameplayAbility并通过可同步的事件负载(Event Payload)从客户端发送随机生成的种子到服务端, 这允许更高的随机性, 但是客户端也容易被破解而每次只发送相同的种子。通过事件激活GameplayAbility也会阻止其从用户绑定激活。

如果您的随机偏差较小,大多数玩家不会注意到每场比赛中的序列是相同的,因此使用激活预测键作为 random seed 应该适合您。如果您做的是更复杂的任务,并且需要防止黑客篡改,也许使用 Server InitiatedGameplayAbility 更为合适,在这种情况下,服务器可以生成预测键或生成随机种子并通过事件负载发送。

5.6 Critical Hits(暴击)

暴击判定在伤害 ExecutionCalculation 中处理。GameplayEffect 会带有一个类似 Effect.CanCritGameplayTagExecutionCalculation 会检查 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中,减速效果不叠加。每个减速实例正常应用并跟踪其生命周期,但只有最大减速效果实际影响TargetGAS 通过 AggregatorEvaluateMetaData 提供了这种情况的解决方案。详见 AggregatorEvaluateMetaData()

5.8 游戏暂停时生成 Target Data

如果你需要在等待从玩家获取 TargetData (通过 WaitTargetData AbilityTask) 时暂停游戏,建议使用 slomo 0 而不是直接暂停。

5.9 One Button Interaction System(按钮交互系统)

GASShooter 实现了一个单键交互系统 - 玩家可以通过按住或点击’E’键与可交互物体互动,比如:

  • 复活玩家
  • 打开武器箱
  • 打开或关闭滑动门