ActionRPG解读(攻击逻辑)

ActionRPG解读(攻击逻辑)

发起攻击的逻辑

InputAction NormalAttack

玩家鼠标左键绑定的是InputAction NormalAttack

image-20221003103116498

Do Melee Attack

image-20221003103202401

先判断是否可以攻击。如果可以攻击,再判断是否正在攻击。

如果正在攻击,则调用Jump Section for Combo触发Combo。

image-20221003103304531

如果不在攻击,则触发Activate Abilities with Item Slot ,传入Current Weapon Slot,即当前所持武器的Slot。

Current Weapon Slot 是在Weapon Attach Method中设置的。

ActivateAbilitiesWithItemSlot

image-20221003102939453

之前在AddSlottedGameplayAbilities中已经设置好了Slot到技能的Handle之间的映射。

使用Slot来取得当前Slot对应武器技能的Handle,调用TryActivateAbility 来触发技能,这里的技能就是攻击。

连击逻辑

Jump Section for Combo

image-20230128001645226

这里的逻辑是从一个JumpSectionNS类型的变量JumpSectionNotify中获得JumpSections,然后从中随机选一个作为下一个Section。

JumpSectionNS

JumpSectionNotify不是在角色的蓝图中设置的。搜索JumpSectionNS,打开蓝图。

image-20230128002057023

发现果然是在这里设置了角色的JumpSectionNotify变量。

image-20230128002147793

这个通知来自哪个蒙太奇呢?可以进行调试康康,右键Jump Section Notify节点选择watch this value,然后找个节点右键选择add breakpoint下个断点,运行游戏,进行攻击,游戏断下。

image-20230128001426383

发现来自角色攻击蒙太奇AM_Attack_Axe的JumpSectionNS通知。

image-20230128002555496

因此可以知道,第一个JumpSectionNS会跳到Combo2,而第二个JumpSectionNS会跳到Combo3,实现完整的连击。

确定目标的逻辑

Weapon_Axe

注意到之前分析GameInstance的时候说AddDefaultInventory会加入默认的物品,默认物品在蓝图中进行了设置,包含1个斧头。

查看该资产:

image-20230127224447503

之前分析FillSlottedAbilitySpecs知道了会拿到Slot上的Item的GrantedAbility赋予玩家。

这里先分析这把初始武器的GrantedAbility,也就是GA_PlayerAxeMelee。

先看继承关系,下图中用红色箭头表示继承关系:

image-20230127225318126

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想要做的事,需要由子类来重写。

image-20230127231252193

查看ActivateAbility的默认实现,这里提供三种实现方式,第一种是在蓝图中实现K2_ActivateAbility(蓝图中显示的名称也是ActivateAbility),第二种是蓝图中实现K2_ActivateAbilityFromEvent,第三种是在C++子类中重写ActivateAbility。这里三种方式都强调了要记得调用CommitAbility。

GA_MeleeBase

GA_MeleeBase中实现了K2_ActivateAbility。

image-20230127232118111

首先调用CommitAbility处理cost和cooldown。如果不在冷却中并且资源足够,则接着播放一个Montage,并且等待事件,如果收到事件,就调用ApplyEffectContainer,并传入了EventTag和EventData,根据EventTag决定要触发哪些GameEffect,根据EventData决定GameEffect的具体逻辑。当Montage混出或中断或取消时,都调用EndAbility执行结束技能逻辑。

GA_PlayerAxeMelee中配置的Montage为AM_Attack_Axe,查看该Montage:

image-20230129005050789

右键WeaponAttackNS,选择Open Notify Blueprint

image-20230129005222866

WeaponAttackNS

image-20230129005407690

发现调用了角色武器的BeginWeaponAttack事件,并且传入了EventTag,这里EventTag的默认值为Event.Montage.Shared.WeaponHit,也就是攻击事件的Tag。

WeaponActor::BeginWeaponAttack

image-20230129005546642

这里设置了AttackEventTag,并且设置为正在攻击,然后开启碰撞检测,从而能够检测到武器和其他物体的接触。自然地,我们需要查看BeginOverlap事件。

WeaponActor::ActorBeginOverlap

image-20230129010115065

果然就是在这里向角色发送了一个事件。注意到发送事件时携带了两个参数,第一个是EventTag(Event.Montage.Shared.WeaponHit),第二个是Payload,也就是GameplayEventData,其中将Target设置为了OtherActor(用于确定被攻击到的敌人)。

这里使用一个DoOnce,并在EndOverlap时进行Reset,是为了实现这样的逻辑:当打中第一个敌人后,武器与第一个敌人处于Overlapping状态期间不能再攻击到其他敌人,直到与第一个敌人脱离Overlap,才能继续攻击到其他的敌人。

对目标造成伤害的逻辑

ApplyEffectContainer

回到之前的GA_MeleeBase:

image-20230127232118111

这里将EventData(其中包含了被攻击到的对象)和EventTag(Event.Montage.Shared.WeaponHit)传入ApplyEffectContainer函数。

image-20230129000342475

该函数将参数传给了MakeEffectContainerSpec构造一个FRPGGameplayEffectContainerSpec,然后传给ApplyEffectContainerSpec进行攻击效果的触发。

MakeEffectContainerSpec

image-20230129010908230

EffectContainerMap是一个编辑器中编辑的Map:

image-20230129000543384

查看GA_PlayerAxeMelee中对EffectContainerMap的配置:

image-20230129002904075

这里是值得学习的地方。

使用一个Map来封装事件类Tag到对应事件要触发的GE的映射。

RPGGamplayEffectContainer包含TargetType和GE,TargetType决定如何获取GE的作用对象。

根据我们之前的发现,这里的ContainerTag为Event.Montage.Shared.WeaponHit(也就是攻击到敌人的事件Tag),因此会取出第一个元素的Value(TargetType为RPGTargetType_UseEventData,GE为GE_PlayerAxeMelee),传入MakeEffectContainerSpecFromContainer

MakeEffectContainerSpecFromContainer

image-20230129011932079

这里会取出TargetType,拿到其CDO,调用CDO的GetTargets方法。

根据之前的分析,TargetType为RPGTargetType_UseEventData,也就是调用RPGTargetType_UseEventData的GetTargets方法。

image-20230129004909546

之前在分析WeaponActor::ActorBeginOverlap时知道了Target就是被武器攻击到的对象,所以这里会走下面那个分支。因此这里成功拿到了被攻击到的Actor,然后就是调用AddTargets往Spec中加入目标对象。

image-20230129011734805

随后再构造出将要触发的GE,加入Spec。

image-20230129011946718

ApplyEffectContainerSpec

image-20230129011451465

逐个取出GE进行应用到目标上,也就是让被攻击的对象执行被攻击的效果。

根据前面的分析,这里应用的GE为GE_PlayerAxeMelee

GE_PlayerAxeMelee

以下为继承关系:

image-20230130144341814

但其实GE_PlayerAxeMelee和GE_MeleeBase都与GE_DamageBase保持一致。

查看GE_DamageBase:

image-20230130150612709

这是立即触发一次的效果。使用了一个C++中定义的RPGDamageExecution(继承于UGameplayEffectExecutionCalculation)作为CalculationClass来计算造成的伤害。

RPGDamageExecution

父类UGameplayEffectExecutionCalculation中定义了一个Execute方法,在GE执行时进行调用,用来处理复杂的计算逻辑。

因为这是一个BlueprintNativeEvent,所以C++中要实现Execute_Implementation方法。

image-20230130153624563

这里写了一堆代码,只是为了拿到三个值来计算出造成的伤害,然后将造成的伤害加到Damage中。

光看这里还是不知道伤害是如何造成的,需要跟踪URPGAttributeSet中的Damage属性才能发现是在PostGameplayEffectExecute处理的扣血逻辑。

image-20230130153946761

查看源码会发现PreGameplayEffectExecute是在GE修改Attribute之前进行的,PostGameplayEffectExecute是在GE修改Attribute之后进行的。

image-20230130154024272

这里还可以学习一下如何捕获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);
}

总结

至此我们分析清楚了攻击的逻辑。

回顾一遍:

  1. 输入事件触发一个NormalAttack,如果正在攻击,则触发连击,否则激活当前Slot对应的Ability。
  2. 当Ability激活时,会开启一个“播放蒙太奇并等待事件”的任务。
  3. 在攻击的蒙太奇中,通过Notify来开启武器的攻击检测逻辑,当武器接触到敌人时,向玩家发送事件。
  4. Ability收到该事件后会将被攻击的敌人作为目标,应用GE。
  5. GE中计算伤害并写入玩家的属性集,由属性集的PostGameplayEffectExecute执行扣血逻辑。

原文链接: https://www.cnblogs.com/iku-iku-iku/p/17076862.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    ActionRPG解读(攻击逻辑)

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/314834

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月16日 下午1:32
下一篇 2023年2月16日 下午1:34

相关推荐