BlueRose
文章97
标签28
分类7
ActionRPG中的攻击逻辑与其他实现方法

ActionRPG中的攻击逻辑与其他实现方法

ActionRPG中的逻辑

逻辑位于BP_PlayerCharacter中,通过按键事件执行ActivateAbilitiesWithTags(定义于ARPGCharacterBase,本质上是执行AbilitySystemComponent的TryActivateAbilitiesByTag函数)、ActivateAbilitiesWithItemSlot(定义于ARPGCharacterBase,本质上通过查询ItemSlot来激活对应的Ability,使用了AbilitySystemComponent的TryActivateAbility函数)来施放“Ability”。

Ability类

在ActionRPG中攻击Ability以PlayMontageAndWaitForEvent为主,这个节点是一个GameplayTask,具体的介绍可以参考我之前的文章。攻击Ability的基类为Abilities/Shared/GA_MeleeBase。

一种是武器攻击判定,在Ability类中通过PlayMontageAndWaitForEvent节点播放Montage,之后通过在Montage中设置的AnimNotify(WeaponAttackNS)调用对应角色的WeaponActor的EventBeginWeaponAttack,来开启WeaponActor的碰撞判定。之后将碰撞的Actor信息通过SendGameplayEventToActor发送给角色。

另一种是范围技能,直接通过AnimNotify(RangeAttackNS)向对应角色发送EventTag来触发(SendGameplayEventToActor)。

PlayMontageAndWaitForEvent事件处理

PlayMontageAndWaitForEvent之后会接收到信息(本质是角色的AbilitySystemComponent接收到事件,之后再广播委托),Event会包含发起者与目标对象,之后调用ApplyEffectContainer函数,将Ability类中设置的GameplayEffect应用到与之对应的target上(通过设置对应的URPGTargetType派生类)。

URPGTargetType

在GA_MeleeBase中默认使用的目标对象获取类为URPGTargetType_UseEventData。可以看得出是直接使用了EventData。

void URPGTargetType_UseEventData::GetTargets_Implementation(ARPGCharacterBase* TargetingCharacter, AActor* TargetingActor, FGameplayEventData EventData, TArray<FHitResult>& OutHitResults, TArray<AActor*>& OutActors) const
{
    const FHitResult* FoundHitResult = EventData.ContextHandle.GetHitResult();
    if (FoundHitResult)
    {
        OutHitResults.Add(*FoundHitResult);
    }
    else if (EventData.Target)
    {
        OutActors.Add(const_cast<AActor*>(EventData.Target));
    }
}

除了c++ 实现的URPGTargetType,ActionRPG还使用蓝图实现了TargetType_SphereTrace(其他皆只为参数修改)。本质上还是碰撞判断,所以个人还是推荐使用C++来实现,以获得较好的性能。

WeaponActor

其中攻击判断逻辑位于WeaponActor中的ActorBeginOverlap事件,其他逻辑主要为开启与关闭碰撞判定。

Instigator为Actor中伤害系统(Damage)中的组成部分,可以理解为伤害的发起对象。在WeaponActor中即为装备了该武器的角色类。

攻击判定逻辑大致为:

  1. 判断碰撞到武器是否为武器本身。
  2. 如果不是,且处于攻击过程中,就向WeaponActor发送GameplayEvent(通过)。

WeaponActor蓝图

ActionRPG中的Combo实现

官方的ActionRPG是将连击逻辑中跳转控制放在Character类中。

在播放Montage的过程中触发AnimNotify,AnimNotify会设置BP_PlayerCharacter类中的JumpSectionNotify引用(还会设置一个Bool变量)。并在需要跳转MontageSection时读取SectionName以进行跳转。

DoMeleeAttack中的逻辑为:如果符合若干前置条件,并且没有施放GA_MeleeBase及其子类能力,就施放指定Melee能力。如果正在施放Melee能力,则进行Montage跳转(调用AnimInstance的MontageSetNextSection)。

具体的实现请参考ActionRPG案例中的Blueprints/AnimNotifies/JumpSectionNS与Blueprints/BP_PlayerCharacter中的DoMeleeAttack函数。

本人尝试的方法

本人想把所有逻辑都写进Ability中,尝试了以下几个思路:

通过AbilityActorInfo获取AnimInstance,之后绑定AnimationInstance中的OnPlayMontageNotifyBegin与End委托,来获取Notify信息。

这个方法因为OnPlayMontageNotifyBegin与End委托没有暴露给蓝图,并且还需要所以需要编写c++有点麻烦。另外我还写了个AnimNotifyState类,重写若干虚函数以绑定这两种委托。因为默认只有UAnimNotify_PlayMontageNotifyWindow与UAnimNotify_PlayMontageNotify会触发这两种委托。这里先介绍一下:

ActorInfo
是一个包含各个组件指针的类,换句话说就是一个GAS组件拥有者的相关组件指针类(应该是在Component初始化时填充数据)。

/** Cached off data about the owning actor that abilities will need to frequently access (movement component, mesh component, anim instance, etc) */
TSharedPtr<FGameplayAbilityActorInfo>    AbilityActorInfo;

你可以在Ability蓝图类中调用GetActorInfo获取,之后再通过ActorInfo获取AnimInstance操作Montage。

UAnimInstance* AnimInstance = AbilityActorInfo.IsValid() ? AbilityActorInfo->GetAnimInstance() : nullptr;

当然也可以使用

/** Returns the avatar actor for this component */
AActor* GetAvatarActor() const;

进行获取。

但上面介绍的方法依然需要将Montage跳转逻辑写在角色类中,这主要是因为调用TryActivateAbilityByClass函数会重复创建Ability实例。所以

我写了个新的TryActivateAbilityByClassOrReturnInstance函数,当存在指定类型的Ability实例就不会创建新的实例而是返回这个实例。

这样我们就可以把所有逻辑都写在Ability类中了。但是这个方法需要编写若干c++代码,比如增加GamplayAbility存在实例时的事件、增加若干TryActivateAbility系列函数,这对只会蓝图的开发开发者不太友好。所以下面我将介绍另一种方法:

使用GameplayEvent控制

这个方法的好处在于不用写c++代码,能把技能相关代码都写进Ability类中。在外部使用Event对Ability进行控制,易于观察与控制,同时也进行了解耦。

  1. 在AnimNotify中使用GetOwner获取Character后作为SendEventToActor的参数使用,并在SendEventToActor的参数中填入对应的GameplayEvent标签与所需的Payload结构体。
  2. 在角色类中对应的输入事件中首先判断是否存在对应标签的Ability(参考Blueprints/BP_Character中的IsUsingMelee),之后根据结果执行TryActivateAbilityByClass构建Ability或者调用SendGameplayEventToActor发送Combo事件。
  3. 在对应Ability中RPGAbilityTask_PlayMontageAndWaitForEvent节点的RecivedEvent下,判断接收到EventTag并执行对应的逻辑,比如:接收到ComboEvent则进行Montage跳转,接收到HitEvent则将DamageEffect应用到目标上。

如果你有强迫症,想把所有逻辑都写进Ability中,那就把这个方法与上一种方法合起来使用吧。

有关SendGameplayEventToActor

1.SendEventToActor所用的GameplayEventTag必须是Event的子标签。 其实是因为ActionRPG的PlayMontageAndWaitForEvent节点加了过滤事件(EventTag形参)

2.SendEventToActor的Payload形参中的OptionalObject可以用来传递自定义的UObject,以此来达到传递自定义数据的目的。具体操作为:使用ConstructObjectFromClass节点对自己定义的UObject(在UObject中定义各种数据变量)进行实例化(C++好像是NewObject),再对实例进行修改后(修改变量或者调用函数操作)连入OptionalObject。之后就可以在RecivedEvent委托中获取这个UObject。
3.SendEventToActor的目标Actor必须继承IAbilitySystemInterface接口。

结语

以上就是ActionRPG与本人Demo的攻击判断逻辑,但有一些还是需要改进:例如对于使用骨骼模型的PhysicalAsset进行攻击判断的方案,这套逻辑容易造成重复判定。

在看源码的时候发现了个不错的GameplayAbility文档:
https://github.com/tranek/GASDocumentation配合最近挺火的https://www.deepl.com/translator翻译,效果更佳。

除了文档之外,它还包含了一个小项目,我大致看了一下,推荐在学习过ActionRPG后在学习这个项目。
这个项目使用AbilityInput绑定与GameplayCue,使用Tag进行技能施放与阻止施放判断,同时有更多的c++代码,对于ActionRPG是一个很好的补充。

另外我还找到一个中文版(然而并不全),大致看了一下应该是学习了这个项目后写的笔记,英语不太好的可以参考下:
https://tianqi.li/2020/02/29/gas-document.html