ActionRPG解读(攻击逻辑)
发起攻击的逻辑
InputAction NormalAttack
玩家鼠标左键绑定的是InputAction NormalAttack
Do Melee Attack
先判断是否可以攻击。如果可以攻击,再判断是否正在攻击。
如果正在攻击,则调用Jump Section for Combo
触发Combo。
如果不在攻击,则触发Activate Abilities with Item Slot
,传入Current Weapon Slot
,即当前所持武器的Slot。
Current Weapon Slot
是在Weapon Attach Method中设置的。
ActivateAbilitiesWithItemSlot
之前在AddSlottedGameplayAbilities
中已经设置好了Slot到技能的Handle之间的映射。
使用Slot来取得当前Slot对应武器技能的Handle,调用TryActivateAbility
来触发技能,这里的技能就是攻击。
连击逻辑
Jump Section for Combo
这里的逻辑是从一个JumpSectionNS
类型的变量JumpSectionNotify
中获得JumpSections
,然后从中随机选一个作为下一个Section。
JumpSectionNS
JumpSectionNotify
不是在角色的蓝图中设置的。搜索JumpSectionNS
,打开蓝图。
发现果然是在这里设置了角色的JumpSectionNotify
变量。
这个通知来自哪个蒙太奇呢?可以进行调试康康,右键Jump Section Notify
节点选择watch this value,然后找个节点右键选择add breakpoint下个断点,运行游戏,进行攻击,游戏断下。
发现来自角色攻击蒙太奇AM_Attack_Axe的JumpSectionNS通知。
因此可以知道,第一个JumpSectionNS会跳到Combo2,而第二个JumpSectionNS会跳到Combo3,实现完整的连击。
确定目标的逻辑
Weapon_Axe
注意到之前分析GameInstance的时候说AddDefaultInventory会加入默认的物品,默认物品在蓝图中进行了设置,包含1个斧头。
查看该资产:
之前分析FillSlottedAbilitySpecs知道了会拿到Slot上的Item的GrantedAbility赋予玩家。
这里先分析这把初始武器的GrantedAbility,也就是GA_PlayerAxeMelee。
先看继承关系,下图中用红色箭头表示继承关系:
GA_PlayerAxeMelee继承于GA_MeleeBase,GA_MeleeBase继承于GA_AbilityBase,GA_AbilityBase继承于URPGGamePlayAbility,URPGGamePlayAbility继承于UGameplayAbility,查看UGameplayAbility的头文件,发现以下提示:
// -----------------------------------------------------------------------------------
//
// The important functions:
//
// CanActivateAbility() - const function to see if ability is activatable. Callable by UI etc
//
// TryActivateAbility() - Attempts to activate the ability. Calls CanActivateAbility(). Input events can call this directly.
// - Also handles instancing-per-execution logic and replication/prediction calls.
//
// CallActivateAbility() - Protected, non virtual function. Does some boilerplate 'pre activate' stuff, then calls ActivateAbility()
//
// ActivateAbility() - What the abilities *does*. This is what child classes want to override.
//
// CommitAbility() - Commits reources/cooldowns etc. ActivateAbility() must call this!
//
// CancelAbility() - Interrupts the ability (from an outside source).
//
// EndAbility() - The ability has ended. This is intended to be called by the ability to end itself.
//
// -----------------------------------------------------------------------------------
这里说ActivateAbility定义了abilities想要做的事,需要由子类来重写。
查看ActivateAbility的默认实现,这里提供三种实现方式,第一种是在蓝图中实现K2_ActivateAbility(蓝图中显示的名称也是ActivateAbility),第二种是蓝图中实现K2_ActivateAbilityFromEvent,第三种是在C++子类中重写ActivateAbility。这里三种方式都强调了要记得调用CommitAbility。
GA_MeleeBase
GA_MeleeBase中实现了K2_ActivateAbility。
首先调用CommitAbility处理cost和cooldown。如果不在冷却中并且资源足够,则接着播放一个Montage,并且等待事件,如果收到事件,就调用ApplyEffectContainer,并传入了EventTag和EventData,根据EventTag决定要触发哪些GameEffect,根据EventData决定GameEffect的具体逻辑。当Montage混出或中断或取消时,都调用EndAbility执行结束技能逻辑。
GA_PlayerAxeMelee中配置的Montage为AM_Attack_Axe,查看该Montage:
右键WeaponAttackNS,选择Open Notify Blueprint
WeaponAttackNS
发现调用了角色武器的BeginWeaponAttack事件,并且传入了EventTag,这里EventTag的默认值为Event.Montage.Shared.WeaponHit,也就是攻击事件的Tag。
WeaponActor::BeginWeaponAttack
这里设置了AttackEventTag,并且设置为正在攻击,然后开启碰撞检测,从而能够检测到武器和其他物体的接触。自然地,我们需要查看BeginOverlap事件。
WeaponActor::ActorBeginOverlap
果然就是在这里向角色发送了一个事件。注意到发送事件时携带了两个参数,第一个是EventTag(Event.Montage.Shared.WeaponHit),第二个是Payload,也就是GameplayEventData,其中将Target设置为了OtherActor(用于确定被攻击到的敌人)。
这里使用一个DoOnce,并在EndOverlap时进行Reset,是为了实现这样的逻辑:当打中第一个敌人后,武器与第一个敌人处于Overlapping状态期间不能再攻击到其他敌人,直到与第一个敌人脱离Overlap,才能继续攻击到其他的敌人。
对目标造成伤害的逻辑
ApplyEffectContainer
回到之前的GA_MeleeBase:
这里将EventData(其中包含了被攻击到的对象)和EventTag(Event.Montage.Shared.WeaponHit)传入ApplyEffectContainer函数。
该函数将参数传给了MakeEffectContainerSpec构造一个FRPGGameplayEffectContainerSpec,然后传给ApplyEffectContainerSpec进行攻击效果的触发。
MakeEffectContainerSpec
EffectContainerMap是一个编辑器中编辑的Map:
查看GA_PlayerAxeMelee中对EffectContainerMap的配置:
这里是值得学习的地方。
使用一个Map来封装事件类Tag到对应事件要触发的GE的映射。
RPGGamplayEffectContainer包含TargetType和GE,TargetType决定如何获取GE的作用对象。
根据我们之前的发现,这里的ContainerTag为Event.Montage.Shared.WeaponHit(也就是攻击到敌人的事件Tag),因此会取出第一个元素的Value(TargetType为RPGTargetType_UseEventData,GE为GE_PlayerAxeMelee),传入MakeEffectContainerSpecFromContainer
MakeEffectContainerSpecFromContainer
这里会取出TargetType,拿到其CDO,调用CDO的GetTargets方法。
根据之前的分析,TargetType为RPGTargetType_UseEventData,也就是调用RPGTargetType_UseEventData的GetTargets方法。
之前在分析WeaponActor::ActorBeginOverlap时知道了Target就是被武器攻击到的对象,所以这里会走下面那个分支。因此这里成功拿到了被攻击到的Actor,然后就是调用AddTargets往Spec中加入目标对象。
随后再构造出将要触发的GE,加入Spec。
ApplyEffectContainerSpec
逐个取出GE进行应用到目标上,也就是让被攻击的对象执行被攻击的效果。
根据前面的分析,这里应用的GE为GE_PlayerAxeMelee
GE_PlayerAxeMelee
以下为继承关系:
但其实GE_PlayerAxeMelee和GE_MeleeBase都与GE_DamageBase保持一致。
查看GE_DamageBase:
这是立即触发一次的效果。使用了一个C++中定义的RPGDamageExecution(继承于UGameplayEffectExecutionCalculation)作为CalculationClass来计算造成的伤害。
RPGDamageExecution
父类UGameplayEffectExecutionCalculation中定义了一个Execute方法,在GE执行时进行调用,用来处理复杂的计算逻辑。
因为这是一个BlueprintNativeEvent,所以C++中要实现Execute_Implementation方法。
这里写了一堆代码,只是为了拿到三个值来计算出造成的伤害,然后将造成的伤害加到Damage中。
光看这里还是不知道伤害是如何造成的,需要跟踪URPGAttributeSet中的Damage属性才能发现是在PostGameplayEffectExecute处理的扣血逻辑。
查看源码会发现PreGameplayEffectExecute是在GE修改Attribute之前进行的,PostGameplayEffectExecute是在GE修改Attribute之后进行的。
这里还可以学习一下如何捕获Source和Target的Attribute。
struct RPGDamageStatics
{
DECLARE_ATTRIBUTE_CAPTUREDEF(DefensePower);
DECLARE_ATTRIBUTE_CAPTUREDEF(AttackPower);
DECLARE_ATTRIBUTE_CAPTUREDEF(Damage);
RPGDamageStatics()
{
// Capture the Target's DefensePower attribute. Do not snapshot it, because we want to use the defense value at the moment we apply the execution.
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, DefensePower, Target, false);
// Capture the Source's AttackPower. We do want to snapshot this at the moment we create the GameplayEffectSpec that will execute the damage.
// (imagine we fire a projectile: we create the GE Spec when the projectile is fired. When it hits the target, we want to use the AttackPower at the moment
// the projectile was launched, not when it hits).
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, AttackPower, Source, true);
// Also capture the source's raw Damage, which is normally passed in directly via the execution
DEFINE_ATTRIBUTE_CAPTUREDEF(URPGAttributeSet, Damage, Source, true);
}
};
static const RPGDamageStatics& DamageStatics()
{
static RPGDamageStatics DmgStatics;
return DmgStatics;
}
URPGDamageExecution::URPGDamageExecution()
{
RelevantAttributesToCapture.Add(DamageStatics().DefensePowerDef);
RelevantAttributesToCapture.Add(DamageStatics().AttackPowerDef);
RelevantAttributesToCapture.Add(DamageStatics().DamageDef);
}
总结
至此我们分析清楚了攻击的逻辑。
回顾一遍:
- 输入事件触发一个NormalAttack,如果正在攻击,则触发连击,否则激活当前Slot对应的Ability。
- 当Ability激活时,会开启一个“播放蒙太奇并等待事件”的任务。
- 在攻击的蒙太奇中,通过Notify来开启武器的攻击检测逻辑,当武器接触到敌人时,向玩家发送事件。
- Ability收到该事件后会将被攻击的敌人作为目标,应用GE。
- GE中计算伤害并写入玩家的属性集,由属性集的PostGameplayEffectExecute执行扣血逻辑。
原文链接: https://www.cnblogs.com/iku-iku-iku/p/17076862.html
欢迎关注
微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍
原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/314834
非原创文章文中已经注明原地址,如有侵权,联系删除
关注公众号【高性能架构探索】,第一时间获取最新文章
转载文章受原作者版权保护。转载请注明原作者出处!