BlueRoseNote/03-UnrealEngine/Gameplay/GAS/(8)一种将所有逻辑都写进Ability的GAS Combo方案.md
2023-06-29 11:55:02 +08:00

204 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 前言
使用GAS实现Combo技能本人所知道的有两种思路<br>
**第一种:为每一段Combo创建一个单独的Ability定制自定义Asset与编辑器之后再通过自定义编辑器进行编辑。**
- 优点:最高的自由度,不容易出问题。
- 缺点如果有大量多段或者派生复杂的Combo那工作量将会相当爆炸。可以说除了工程量大管理麻烦没有别的缺点。
**第二种:根据GameplayEvent直接在Ability里直接实现Combo切换逻辑**
https://zhuanlan.zhihu.com/p/131381892
在此文中我分析了ActionRPG的攻击逻辑的同时自己也提出一种通过GameplayEvent实现Combat的方法。这个方法理论上没问题但实际落地的时候发现存在一个问题里面使用的PlayMontageAndWaitEvent是GameplayTasks是异步运行的而通过AnimNotify发送的SendGameplayEvent的函数确是同步函数。所以会导致PlayMontageAndWaitEvent节点接收不到任何事件的问题。经过一段事件的探索本人摸索到了另一种方案
- 优点一个起点这个Combo树一个Ability方便管理。可以尽可能地把Combo相关的逻辑都整合到一个Ability里。
- 缺点:自由度会有一定的限制;因为采用异步函数来管理事件,会导致一定的延迟。不适于格斗游戏这种对低延迟要求比较高项目。
## 2021.7.30更新
如果你使用GAS的输入绑定系统即在GiveAbility()注册时进行输入绑定参考GASDocument的4.6.2章节。可将后面介绍的ListenPlayerInputAction节点换成GAS自带的WaitInputPress或WaitInputRelease。
## 具体实现
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/ComboAbilityBP.png)
还需要设置AbilityTag与AbilityBlockTag解决狂按而导致的技能鬼畜问题。
### URPGAbilityTask_ListenPlayerInputAction
构建一个GameplayTasks来监听玩家输入,使用Character类获取InputComponent之后再进行对应Action的动态函数绑定/解绑。执行前需要判断是否是本地Controller。具体代码如下
```
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "Abilities/Tasks/AbilityTask.h"
#include "RPGAbilityTask_ListenPlayerInputAction.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE( FOnInputActionMulticast );
UCLASS()
class RPGGAMEPLAYABILITY_API URPGAbilityTask_ListenPlayerInputAction : public UAbilityTask
{
GENERATED_UCLASS_BODY()
UPROPERTY(BlueprintAssignable)
FOnInputActionMulticast OnInputAction;
virtual void Activate() override;
UFUNCTION(BlueprintCallable, Category="Ability|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"))
static URPGAbilityTask_ListenPlayerInputAction* ListenPlayerInputAction(UGameplayAbility* OwningAbility,FName ActionName, TEnumAsByte< EInputEvent > EventType, bool bConsume);
protected:
virtual void OnDestroy(bool AbilityEnded) override;
UPROPERTY()
FName ActionName;
UPROPERTY()
TEnumAsByte<EInputEvent> EventType;
/** 是否消耗输入消息不再传递到下一个InputComponent */
UPROPERTY()
bool bConsume;
int32 ActionBindingHandle;
};
```
```
#include "Abilities/AsyncTasks/RPGAbilityTask_ListenPlayerInputAction.h"
#include "Components/InputComponent.h"
URPGAbilityTask_ListenPlayerInputAction::URPGAbilityTask_ListenPlayerInputAction(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{}
URPGAbilityTask_ListenPlayerInputAction* URPGAbilityTask_ListenPlayerInputAction::ListenPlayerInputAction(UGameplayAbility* OwningAbility,FName ActionName, TEnumAsByte< EInputEvent > EventType, bool bConsume)
{
URPGAbilityTask_ListenPlayerInputAction* Task=NewAbilityTask<URPGAbilityTask_ListenPlayerInputAction>(OwningAbility);
Task->ActionName = ActionName;
Task->EventType = EventType;
Task->bConsume = bConsume;
return Task;
}
void URPGAbilityTask_ListenPlayerInputAction::Activate()
{
AActor* Owner=GetOwnerActor();
UInputComponent* InputComponent=Owner->InputComponent;
FInputActionBinding NewBinding(ActionName, EventType.GetValue());
NewBinding.bConsumeInput = bConsume;
NewBinding.ActionDelegate.GetDelegateForManualSet().BindLambda([this]()
{
if(OnInputAction.IsBound())
OnInputAction.Broadcast();
});
ActionBindingHandle=InputComponent->AddActionBinding(NewBinding).GetHandle();
}
void URPGAbilityTask_ListenPlayerInputAction::OnDestroy(bool AbilityEnded)
{
AActor* Owner = GetOwnerActor();
UInputComponent* InputComponent = Owner->InputComponent;
if (InputComponent)
{
InputComponent->RemoveActionBindingForHandle(ActionBindingHandle);
}
Super::OnDestroy(AbilityEnded);
}
```
### 控制MontageSection跳转
这里我创建一个控制跳转用Map:
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/ComboMap.png)
其中Key为当前Combo名称_攻击类型。之后根据Value值进行MontageSection跳转。
### Montage偏移
为了Combo之间有更好的过渡可以通过计算进入招式派生状态的经过时间最后在Section跳转时进行偏移以实现最佳的连贯性Combo动画中需要有对应的过渡区域设计。以下是我实现的MontageSection跳转+偏移函数:
```
void URPGAbilitySystemComponent::CurrentMontageJumpToSectionWithOffset(FName SectionName,float PositionOffset)
{
UAnimInstance* AnimInstance = AbilityActorInfo.IsValid() ? AbilityActorInfo->GetAnimInstance() : nullptr;
if ((SectionName != NAME_None) && AnimInstance && LocalAnimMontageInfo.AnimMontage)
{
// AnimInstance->Montage_JumpToSection(SectionName, LocalAnimMontageInfo.AnimMontage);
FAnimMontageInstance* MontageInstance = AnimInstance->GetActiveInstanceForMontage(LocalAnimMontageInfo.AnimMontage);
if (MontageInstance)
{
UAnimMontage* Montage=LocalAnimMontageInfo.AnimMontage;
const int32 SectionID = Montage->GetSectionIndex(SectionName);
if (Montage->IsValidSectionIndex(SectionID))
{
FCompositeSection & CurSection = Montage->GetAnimCompositeSection(SectionID);
const float NewPosition = CurSection.GetTime()+PositionOffset;
MontageInstance->SetPosition(NewPosition);
MontageInstance->Play(MontageInstance->GetPlayRate());
}
}
if (IsOwnerActorAuthoritative())
{
FGameplayAbilityRepAnimMontage& MutableRepAnimMontageInfo = GetRepAnimMontageInfo_Mutable();
MutableRepAnimMontageInfo.SectionIdToPlay = 0;
if (MutableRepAnimMontageInfo.AnimMontage)
{
// we add one so INDEX_NONE can be used in the on rep
MutableRepAnimMontageInfo.SectionIdToPlay = MutableRepAnimMontageInfo.AnimMontage->GetSectionIndex(SectionName) + 1;
}
AnimMontage_UpdateReplicatedData();
}
else
{
ServerCurrentMontageJumpToSectionName(LocalAnimMontageInfo.AnimMontage, SectionName);
}
}
}
float URPGAbilitySystemComponent::GetCurrentMontagePlaybackPosition() const
{
UAnimInstance* AnimInstance = AbilityActorInfo.IsValid() ? AbilityActorInfo->GetAnimInstance() : nullptr;
if (AnimInstance && LocalAnimMontageInfo.AnimMontage)
{
FAnimMontageInstance* MontageInstance = AnimInstance->GetActiveInstanceForMontage(LocalAnimMontageInfo.AnimMontage);
return MontageInstance->GetPosition();
}
return 0.f;
}
```
```
void URPGGameplayAbility::MontageJumpToSectionWithOffset(FName SectionName,float PositionOffset)
{
check(CurrentActorInfo);
URPGAbilitySystemComponent* AbilitySystemComponent = Cast<URPGAbilitySystemComponent>(GetAbilitySystemComponentFromActorInfo_Checked());
if (AbilitySystemComponent->IsAnimatingAbility(this))
{
//调用对应函数
AbilitySystemComponent->XXXXXX();
}
}
```
### 解决因狂按而导致的技能鬼畜问题
在保证可以实现其他技能强制取消当前技能后摇的情况下,我尝试过以下方法,结果都不太理想:
- 使用带有AbilityBlockTag的GE来控制指定时间段里无法重复使用当前技能。
- 使用Ability的CoolDown功能配合GE制作公共CD效果。时间短了依然会鬼畜
最后采用给当前Ability设置标签并且把这个标签作为AbilityBlockTag来实现。取消技能后摇的效果可以通过AnimNotify调用EndAbility来实现。
### 事件处理
#### Default
处理其他没有指定的事件一般都是应用GE的。与ActionRPG不同的地方在于
1. 同一个事件第二次接收Ability会移除第一次应用的GE这是为了实现类似攻击动作霸体的效果。
2. 因为数值计算只在服务端处理所以执行前需要使用HasAuthority判断一下。
#### WeaponHit
武器其中可攻击对象时的逻辑。除此之外可以使用自定义的UObject作为EventPayloadOptionalObject传递更多的数据以此可以实现命中弱点增加伤害等效果。
#### DerivationCombat
这里其实应该是Combo但之前打错懒得改了。这里主要是设置控制Combo是否可以派生的变量。