204 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
		
		
			
		
	
	
			204 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| 
								 | 
							
								## 前言
							 | 
						|||
| 
								 | 
							
								使用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。
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								## 具体实现
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								还需要设置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:
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								其中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作为EventPayload(OptionalObject)传递更多的数据,以此可以实现命中弱点增加伤害等效果。
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								#### DerivationCombat
							 | 
						|||
| 
								 | 
							
								这里其实应该是Combo,但之前打错,懒得改了。这里主要是设置控制Combo是否可以派生的变量。
							 |