5.1 眩晕 - Stun
通常在眩晕效果下,我们希望取消Character
所有的处于激活状态的GameplayAbilities
,同时阻止新的GameplayAbility
的激活甚至是移动。示例项目中的流星GameplayAbility
就会给其命中的目标施加一个眩晕的效果。
要取消目标身上激活的GameplayAbilities
,我们可以在相应GameplayTag
被添加时时调用AbilitySystemComponent->CancelAbilities()
。
要实现阻止新的GameplayAbilities
的激活的效果,可以在其他技能上把眩晕的GameplayTag
设置到他们的Activation Blocked Tags
的GameplayTagContainer
上。
要阻止角色的移动,我们可以重写CharacterMovementComponent
的GetMaxSpeed()
函数,将其当所有者拥有眩晕的GameplayTag
时令这个函数返回0。
5.2 冲刺 - Sprint
示例项目中还提供了一个关于冲刺的例子,即当Left Shift
按下时角色可以跑得更快一些。
快速移动可以由CharacterMovementComponent
负责预测处理,具体就是通过网络发送一个标记到服务器。参考GDCharacterMovementComponent.h/cpp
。
GA
会处理Left Shift
对应的输入响应,通知角色移动组件去开启或者停止冲刺,并且预测性得消耗体力值。参考GA_Sprint_BP
。
5.3 瞄准 - Aim Down Sights
示例项目中,瞄准的做法和冲刺的做法基本一致,只是把加速换成了减速。
具体参阅GDCharacterMovementComponent.h/cpp
中关于减速的内容。
参阅GA_AimDownSight_BP
中关于输入的处理的内容。当然,瞄准并不会消耗体力。
5.4 生命偷取 - Lifesteal
我将生命偷取内置在伤害计算的ExecutionCalculation
内。GameplayEffect
会有一个专门的GameplayTag
,比如Effect.CanLifesteal
这样的。ExecutionCalculation
会去检查GameplayEffectSpec
是否有这样的一个GameplayTag
。如果这个GameplayTag
存在,那么ExecutionCalculation
会去创建一个动态的Instant
类型的GameplayEffect
,并且给它一个增加生命值的modifier
,然后把他应用到Source
的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
激活的时候设置相同的random seed
。每次去激活GameplayAbility
的时候你都需要设置random seed
,防止客户端预测激活失败并且随机数序列和服务器不同步。
随机数种子设置方法 | 描述 |
---|---|
使用激活预测键 | GameplayAbility 的激活预测键是一个int16类型的数据,并且保证在客户端和服务器的Activation() 是同步的。你可以将它作为客户端和服务器的random seed 。这种方式的不好的地方在于这个预测键总是在游戏开始时从0开始,并且在每次生成键的时候持续增长,这意味着虽然数字是随机的,但是每次游玩过程中得到的整个数列却不是随机的。意思是这种方法提供的随机性有限。 |
当你激活 GameplayAbility 时,通过事件负载发送种子 | 即使用事件激活GameplayAbility 并且由客户端向服务器通过复制的事件负载来发送随机生成的种子。这种方法能够提供足够的随机性,但是客户端就变得更加容易被攻击从而每次只去发送相同的种子值。而且通过事件激活GameplayAbilities 将无法和输入进行绑定。 |
如果你的随机编程很小,大部分玩家都不会之一到每次游戏的随机数序列是相同的,那么第一种使用预测键的方法足够用了。如果你需要做一些稍复杂的需要防黑客的事情,也许使用Server Initiated
的 GameplayAbility
更加合适,这样服务器可以创建预测键或是创建可以由事件负载发送的random seed
。
5.6 暴击 - Critical Hits
我将暴击效果的实现内置在了ExecutionCalculation
中。GameplayEffect
会有一个专门的GameplayTag
,比如说像Effect.CanCrit
。ExecutionCalculation
会去检查GameplayEffectSpec
是否拥有这个GameplayTag
。如果GameplayTag
存在的话,ExecutionCalculation
会去生成一个随机的数字对应着暴击几率(从Source
中取到的对应的Attribute
值),然后加到暴击伤害(另外一个从Source
中取到的对应的Attribute
值)中。因为我并没有去预测伤害,我并不需要去担心随机数生成的同步问题,因为ExecutionCalculation
只会在服务器上运行。如果你希望预测性得去使用MMC
来处理伤害计算,那么你就需要从GameplayEffectSpec->GameplayEffectContext->GameplayAbilityInstance
中拿到random seed
的引用。
可以参考GASShooter项目中是如何实现爆头的。其本质就是本小节的内容,当然它并没有暴击率的设计,而是去检查FHitResult
中命中的骨头的名称。
5.7 非可叠加游戏效果(仅取对目标影响最大者的游戏效果) - Non-Stacking Gameplay Effects but Only the Greatest Magnitude Actually Affects the Target
Paragon里的减速效果并不会堆叠。每一个减速实例正常应用并且正常追踪其生命周期,但是只有值最大的那一个才会在真正影响Character
。GAS利用AggregatorEvaluateMetaData
将这个效果做得开箱即用。可以参考AggregatorEvaluateMetaData()
中的具体实现。
5.8 游戏暂停时生成目标数据 - Generate Target Data While Game is Paused
如果你需要为你的玩家在利用WaitTargetData
AbilityTask
来生成TargetData
时暂停游戏,我建议不去暂停游戏,而是使用slomo 0
。
5.9 单按键交互系统 - One Button Interaction System
GASShooter实现了一个一键交互系统,其中玩家可以通过按下"E"来与可交互物进行交互,比如复活玩家,打开武器箱,以及打开或者关闭滑动门。