目录
- 1. 由GA砍出的第一刀
- 2. 挥剑时的命中检测
- 3. 完善:UI显示当前血量
- 参考:
1. 由GA砍出的第一刀
有了前面章节的经验,我们可以很容易创建一个专用于攻击的GA:
其中PlayMontageAndWait
任务节点负责攻击动画及相应回调的绑定。
但是仅仅这样并不足以支撑整个技能,因为缺少伤害的触发和判定。比如说技能触发,砍中一的一刻,应该要有伤害GE的触发,可以使用射线检测进行砍中事件的检测,然后发送相应事件。所以最优先的,需要扩展技能任务PlayMontageAndWait
的功能,使其支持更多类型的回调(扩展为PlayMontageAndWaitForEvent
,使其具备监听特定事件的特性)。
参照GASDocumentation的PlayMontageAndWaitForEvent
的写法:
#pragma once
#include "CoreMinimal.h"
#include "Abilities/Tasks/AbilityTask.h"
#include "AT_PlayMontageAndWaitForEvent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FPlayMontageAndWaitForEventDelegate, FGameplayTag, EventTag, FGameplayEventData, EventData);
/**
*
*/
UCLASS()
class INSIDEGAS_API UAT_PlayMontageAndWaitForEvent : public UAbilityTask
{
GENERATED_BODY()
public:
UAT_PlayMontageAndWaitForEvent(const FObjectInitializer& ObjectInitializer);
virtual void Activate() override;
virtual void ExternalCancel() override;
virtual FString GetDebugString() const override;
virtual void OnDestroy(bool AbilityEnded) override;
UPROPERTY(BlueprintAssignable)
FPlayMontageAndWaitForEventDelegate OnCompleted;
UPROPERTY(BlueprintAssignable)
FPlayMontageAndWaitForEventDelegate OnBlendOut;
UPROPERTY(BlueprintAssignable)
FPlayMontageAndWaitForEventDelegate OnInterrupted;
UPROPERTY(BlueprintAssignable)
FPlayMontageAndWaitForEventDelegate OnCancelled;
UPROPERTY(BlueprintAssignable)
FPlayMontageAndWaitForEventDelegate EventReceived;
UFUNCTION(BlueprintCallable, Category = "Ability|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"))
static UAT_PlayMontageAndWaitForEvent* PlayMontageAndWaitForEvent(
UGameplayAbility* OwningAbility,
FName TaskInstanceName,
UAnimMontage* MontageToPlay,
FGameplayTagContainer EventTags,
float Rate = 1.f,
FName StartSection = NAME_None,
bool bStopWhenAbilityEnds = true,
float AnimRootMotionTranslationScale = 1.f);
private:
UPROPERTY()
UAnimMontage* MontageToPlay;
UPROPERTY()
FGameplayTagContainer EventTags;
UPROPERTY()
float Rate;
UPROPERTY()
FName StartSection;
UPROPERTY()
float AnimRootMotionTranslationScale;
UPROPERTY()
bool bStopWhenAbilityEnds;
bool StopPlayingMontage();
UAbilitySystemComponent* GetTargetASC();
void OnMontageBlendingOut(UAnimMontage* Montage, bool bInterrupted);
void OnAbilityCancelled();
void OnMontageEnded(UAnimMontage* Montage, bool bInterrupted);
void OnGameplayEvent(FGameplayTag EventTag, const FGameplayEventData* Payload);
FOnMontageBlendingOutStarted BlendingOutDelegate;
FOnMontageEnded MontageEndedDelegate;
FDelegateHandle CancelledHandle;
FDelegateHandle EventHandle;
};
相应的cpp文件:
#include "AT_PlayMontageAndWaitForEvent.h"
#include "AbilitySystemComponent.h"
#include "AbilitySystemGlobals.h"
#include "GameFramework/Character.h"
UAT_PlayMontageAndWaitForEvent::UAT_PlayMontageAndWaitForEvent(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
Rate = 1.f;
bStopWhenAbilityEnds = true;
}
UAbilitySystemComponent* UAT_PlayMontageAndWaitForEvent::GetTargetASC()
{
return Cast<UAbilitySystemComponent>(AbilitySystemComponent);
}
void UAT_PlayMontageAndWaitForEvent::OnMontageBlendingOut(UAnimMontage* Montage, bool bInterrupted)
{
if (Ability && Ability->GetCurrentMontage() == MontageToPlay)
{
if (Montage == MontageToPlay)
{
AbilitySystemComponent->ClearAnimatingAbility(Ability);
// Reset AnimRootMotionTranslationScale
ACharacter* Character = Cast<ACharacter>(GetAvatarActor());
if (Character && (Character->GetLocalRole() == ROLE_Authority ||
(Character->GetLocalRole() == ROLE_AutonomousProxy && Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)))
{
Character->SetAnimRootMotionTranslationScale(1.f);
}
}
}
if (bInterrupted)
{
if (ShouldBroadcastAbilityTaskDelegates())
{
OnInterrupted.Broadcast(FGameplayTag(), FGameplayEventData());
}
}
else
{
if (ShouldBroadcastAbilityTaskDelegates())
{
OnBlendOut.Broadcast(FGameplayTag(), FGameplayEventData());
}
}
}
void UAT_PlayMontageAndWaitForEvent::OnAbilityCancelled()
{
if (StopPlayingMontage())
{
// Let the BP handle the interrupt as well
if (ShouldBroadcastAbilityTaskDelegates())
{
OnCancelled.Broadcast(FGameplayTag(), FGameplayEventData());
}
}
}
void UAT_PlayMontageAndWaitForEvent::OnMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
if (!bInterrupted)
{
if (ShouldBroadcastAbilityTaskDelegates())
{
OnCompleted.Broadcast(FGameplayTag(), FGameplayEventData());
}
}
EndTask();
}
void UAT_PlayMontageAndWaitForEvent::OnGameplayEvent(FGameplayTag EventTag, const FGameplayEventData* Payload)
{
if (ShouldBroadcastAbilityTaskDelegates())
{
FGameplayEventData TempData = *Payload;
TempData.EventTag = EventTag;
EventReceived.Broadcast(EventTag, TempData);
}
}
UAT_PlayMontageAndWaitForEvent* UAT_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(UGameplayAbility* OwningAbility,
FName TaskInstanceName, UAnimMontage* MontageToPlay, FGameplayTagContainer EventTags, float Rate, FName StartSection, bool bStopWhenAbilityEnds, float AnimRootMotionTranslationScale)
{
UAbilitySystemGlobals::NonShipping_ApplyGlobalAbilityScaler_Rate(Rate);
UAT_PlayMontageAndWaitForEvent* MyObj = NewAbilityTask<UAT_PlayMontageAndWaitForEvent>(OwningAbility, TaskInstanceName);
MyObj->MontageToPlay = MontageToPlay;
MyObj->EventTags = EventTags;
MyObj->Rate = Rate;
MyObj->StartSection = StartSection;
MyObj->AnimRootMotionTranslationScale = AnimRootMotionTranslationScale;
MyObj->bStopWhenAbilityEnds = bStopWhenAbilityEnds;
return MyObj;
}
void UAT_PlayMontageAndWaitForEvent::Activate()
{
if (Ability == nullptr)
{
return;
}
bool bPlayedMontage = false;
UAbilitySystemComponent* GDAbilitySystemComponent = GetTargetASC();
if (GDAbilitySystemComponent)
{
const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo();
UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance();
if (AnimInstance != nullptr)
{
// Bind to event callback
EventHandle = GDAbilitySystemComponent->AddGameplayEventTagContainerDelegate(EventTags, FGameplayEventTagMulticastDelegate::FDelegate::CreateUObject(this, &UAT_PlayMontageAndWaitForEvent::OnGameplayEvent));
if (GDAbilitySystemComponent->PlayMontage(Ability, Ability->GetCurrentActivationInfo(), MontageToPlay, Rate, StartSection) > 0.f)
{
// Playing a montage could potentially fire off a callback into game code which could kill this ability! Early out if we are pending kill.
if (ShouldBroadcastAbilityTaskDelegates() == false)
{
return;
}
CancelledHandle = Ability->OnGameplayAbilityCancelled.AddUObject(this, &UAT_PlayMontageAndWaitForEvent::OnAbilityCancelled);
BlendingOutDelegate.BindUObject(this, &UAT_PlayMontageAndWaitForEvent::OnMontageBlendingOut);
AnimInstance->Montage_SetBlendingOutDelegate(BlendingOutDelegate, MontageToPlay);
MontageEndedDelegate.BindUObject(this, &UAT_PlayMontageAndWaitForEvent::OnMontageEnded);
AnimInstance->Montage_SetEndDelegate(MontageEndedDelegate, MontageToPlay);
ACharacter* Character = Cast<ACharacter>(GetAvatarActor());
if (Character && (Character->GetLocalRole() == ROLE_Authority ||
(Character->GetLocalRole() == ROLE_AutonomousProxy && Ability->GetNetExecutionPolicy() == EGameplayAbilityNetExecutionPolicy::LocalPredicted)))
{
Character->SetAnimRootMotionTranslationScale(AnimRootMotionTranslationScale);
}
bPlayedMontage = true;
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("UGDAbilityTask_PlayMontageAndWaitForEvent call to PlayMontage failed!"));
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("UGDAbilityTask_PlayMontageAndWaitForEvent called on invalid AbilitySystemComponent"));
}
if (!bPlayedMontage)
{
UE_LOG(LogTemp, Warning, TEXT("UGDAbilityTask_PlayMontageAndWaitForEvent called in Ability %s failed to play montage %s; Task Instance Name %s."), *Ability->GetName(), *GetNameSafe(MontageToPlay), *InstanceName.ToString());
if (ShouldBroadcastAbilityTaskDelegates())
{
//ABILITY_LOG(Display, TEXT("%s: OnCancelled"), *GetName());
OnCancelled.Broadcast(FGameplayTag(), FGameplayEventData());
}
}
SetWaitingOnAvatar();
}
void UAT_PlayMontageAndWaitForEvent::ExternalCancel()
{
check(AbilitySystemComponent);
OnAbilityCancelled();
Super::ExternalCancel();
}
void UAT_PlayMontageAndWaitForEvent::OnDestroy(bool AbilityEnded)
{
// Note: Clearing montage end delegate isn't necessary since its not a multicast and will be cleared when the next montage plays.
// (If we are destroyed, it will detect this and not do anything)
// This delegate, however, should be cleared as it is a multicast
if (Ability)
{
Ability->OnGameplayAbilityCancelled.Remove(CancelledHandle);
if (AbilityEnded && bStopWhenAbilityEnds)
{
StopPlayingMontage();
}
}
UAbilitySystemComponent* GDAbilitySystemComponent = GetTargetASC();
if (GDAbilitySystemComponent)
{
GDAbilitySystemComponent->RemoveGameplayEventTagContainerDelegate(EventTags, EventHandle);
}
Super::OnDestroy(AbilityEnded);
}
bool UAT_PlayMontageAndWaitForEvent::StopPlayingMontage()
{
const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo();
if (!ActorInfo)
{
return false;
}
UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance();
if (AnimInstance == nullptr)
{
return false;
}
// Check if the montage is still playing
// The ability would have been interrupted, in which case we should automatically stop the montage
if (AbilitySystemComponent && Ability)
{
if (AbilitySystemComponent->GetAnimatingAbility() == Ability
&& AbilitySystemComponent->GetCurrentMontage() == MontageToPlay)
{
// Unbind delegates so they don't get called as well
FAnimMontageInstance* MontageInstance = AnimInstance->GetActiveInstanceForMontage(MontageToPlay);
if (MontageInstance)
{
MontageInstance->OnMontageBlendingOutStarted.Unbind();
MontageInstance->OnMontageEnded.Unbind();
}
AbilitySystemComponent->CurrentMontageStop();
return true;
}
}
return false;
}
FString UAT_PlayMontageAndWaitForEvent::GetDebugString() const
{
UAnimMontage* PlayingMontage = nullptr;
if (Ability)
{
const FGameplayAbilityActorInfo* ActorInfo = Ability->GetCurrentActorInfo();
UAnimInstance* AnimInstance = ActorInfo->GetAnimInstance();
if (AnimInstance != nullptr)
{
PlayingMontage = AnimInstance->Montage_IsActive(MontageToPlay) ? MontageToPlay : AnimInstance->GetCurrentActiveMontage();
}
}
return FString::Printf(TEXT("PlayMontageAndWaitForEvent. MontageToPlay: %s (Currently Playing): %s"), *GetNameSafe(MontageToPlay), *GetNameSafe(PlayingMontage));
}
这样就得到一个全新的节点(相比于原生的PlayMontageAndWait
,多了待接收事件的Tag的过滤器,以及相应的EventReceived
回调):
如此,我们对劈砍技能进一步玩善,添加砍中后的一系列处理,如伤害,命中特效:
伤害的处理是在一个特定的伤害GE中去进行处理的,写法在前面章节提到过。
同样,我这里是简要处理,并没有为角色赋予攻击力抑或是伤害能力的属性,这里暂时使用一个hardcode来充当伤害。
命中特效是利用GC来实现,更具体来说,是利用GCN_Burst_Latent
这个GAS内置的Actor类,我们将碰撞那一刻的位置和法线传入,令特效和声效等能够在希望的位置触发。更具体的,这里使用了一个Niagara特效,一个声效,一个CameraShake
效果,还有一个ForceFeedback(手柄的震动力反馈):
GA里的逻辑部分到这里就算是准备就绪,我们下面要考虑砍中那一刻相关的内容了。
2. 挥剑时的命中检测
在武器下设置检测点(SceneComponent
),只要适时根据依照这个轨迹线去探测是否集中某个物体就可以按照需要发送命中事件。
在BeginPlay做轨迹球的初始化,并利用变量WeaponTraceControl
来作为检测的开关(默认关闭):
相应的Tick阶段利用WeaponTraceControl
变量进行流程控制:
开启检测后,如果有命中,则利用SendGameplayEventToActor
发送命中事件,这里使用DoOnce
,意义在一次挥剑我只希望去单次触发命中事件;WeaponHitResult
用来存储命中结果,供GA访问使用(如使用命中的位置,法线等信息)。此外在关闭检测后,需要完成一些清理工作。
最后利用Montage
里的NotifyState
来进行射线检测控制的开关:
主要就是在ReceivedNotifyBegin
和ReceivedNotifyEnd
两个时间节点控制WeaponTraceControl
变量:
到这里,一个能造成伤害,能触发攻击命中效果的技能就完成了。
3. 完善:UI显示当前血量
血条的核心在于血量与最大血量的比值,这个比值和ProgressBar
结合可以很快构建出血条的效果:
然后利用WidgetComponent
挂载到角色身上简单配置即可。
参考:
属性 - Attributes
属性集 - Attribute Set
游戏效果 - Gameplay Effects
游戏技能 - Gameplay Abilities
技能任务 - Ability Tasks
游戏反馈 - Gameplay Cues