O(logN)의 시간복잡도를 가진 알고리즘을 사용해야 하는데, 이분탐색을 사용해서 계산을 했다.
// AO_GameplayAbility_Traversal.h
UCLASS()
class AO_API UAO_GameplayAbility_Traversal : public UGameplayAbility
{
GENERATED_BODY()
public:
UAO_GameplayAbility_Traversal();
protected:
virtual void OnAvatarSet(
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilitySpec& Spec) override;
virtual bool CanActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayTagContainer* SourceTags = nullptr,
const FGameplayTagContainer* TargetTags = nullptr,
FGameplayTagContainer* OptionalRelevantTags = nullptr) const override;
virtual void ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
virtual void EndAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
bool bReplicateEndAbility,
bool bWasCancelled) override;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Traversal")
TObjectPtr<UChooserTable> AnimChooserTable;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Traversal|Debug")
int32 DrawDebugLevel = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Traversal|Debug")
float DrawDebugDuration = 5.f;
private:
FTraversalCheckResult TraversalResult;
UPROPERTY()
TObjectPtr<AActor> Owner = nullptr;
UPROPERTY()
TObjectPtr<ACharacter> Character = nullptr;
UPROPERTY()
TObjectPtr<UCharacterMovementComponent> CharacterMovement = nullptr;
private:
UFUNCTION()
void OnMontageCompleted();
bool TryTraversal();
bool GetTraversalCheckInputs(FTraversalCheckInput& OutTraversalInput);
bool DetectTraversal();
bool EvaluateTraversal(TArray<UObject*>& EvaluateObjects);
bool SelectTraversal(const TArray<UObject*>& EvaluateObjects);
void UpdateWarpTargets();
bool RunCapsuleTrace(
const FVector& StartLocation,
const FVector& EndLocation,
float Radius,
float HalfHeight,
FHitResult& OutHit,
FColor DebugHitColor,
FColor DebugTraceColor);
};
InstancePerActor로 지정을 해줬고, NetExecutionPolicy는 파쿠르 애니메이션을 실행시켜주는 동작이기 때문에 UX를 위해서 LocalPredicted로 지정을 했다.Ability.Movement.Traversal로 지정했고, 캐릭터가 파쿠르를 실행 중이라면 추후에 다른 동작을 제한하기 위해서 Status.Action.Traversal 태그를 ActivationOwnedTags로 지정했다.UAO_GameplayAbility_Traversal::UAO_GameplayAbility_Traversal()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
const FGameplayTagContainer TraversalTag(FGameplayTag::RequestGameplayTag(FName("Ability.Movement.Traversal")));
SetAssetTags(TraversalTag);
ActivationOwnedTags.AddTag(FGameplayTag::RequestGameplayTag(FName("Status.Action.Traversal")));
}
BeginPlay()라는 생명주기 함수를 거치지 않았다. Ability에서는 OnAvatarSet()이라는 Ability가 캐릭터에 처음 부착될 때 실행되는 함수가 있어서 해당 함수에서 캐싱을 진행했다.checkf()를 통해 예외처리를 진행하고자 한다.void UAO_GameplayAbility_Traversal::OnAvatarSet(const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilitySpec& Spec)
{
Super::OnAvatarSet(ActorInfo, Spec);
Owner = ActorInfo->AvatarActor.Get();
checkf(Owner, TEXT("Failed to cast ActorInfo to AActor"));
Character = Cast<ACharacter>(Owner);
checkf(Character, TEXT("Failed to cast ActorInfo to ACharacter"));
CharacterMovement = Character->GetCharacterMovement();
checkf(CharacterMovement, TEXT("Failed to get CharacterMovementComponent"));
}
CanActivateAbility() 함수를 통해 파쿠르 Ability를 실행해도 되는지 검사를 실행했다.IsActive()를 통해 검사를 실행했다.ActivateAbility() 함수에서 TryTraversal()을 통해 파쿠르 가능 계산 여부와 애니메이션 선택 로직을 구현했는데, 그러다보니 실행된 것으로 결정되서 캐릭터에서 TryActivateAbilitiesByTag()를 했을 때 값이 항상 true가 반환이 되었다.CanActivateAbility() 함수에서 애니메이션 선택까지 결정을 하게 변경했다.CanActivateAbility()에서는 클래스의 내부 함수를 부를 때, const_cast<>(this)를 통해 캐스팅해서 함수를 실행시켜줘야 했다.bool UAO_GameplayAbility_Traversal::CanActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags,
const FGameplayTagContainer* TargetTags, FGameplayTagContainer* OptionalRelevantTags) const
{
if (!Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags))
{
return false;
}
if (IsActive())
{
return false;
}
if (!const_cast<UAO_GameplayAbility_Traversal*>(this)->TryTraversal())
{
return false;
}
return true;
}
ActivateAbility()에서는 기존의 몽타주를 실행시켜주는 과정을 실행했다.UAbilityTask_PlayMontageAndWait를 활용해서 몽타주 재생 및 동기화를 맞추고자 했다. 몽타주 재생이 종료되면 Ability를 종료시키기 위해서 바인딩도 했다.void UAO_GameplayAbility_Traversal::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
UpdateWarpTargets();
if (CharacterMovement)
{
CharacterMovement->bIgnoreClientMovementErrorChecksAndCorrection = true;
CharacterMovement->bServerAcceptClientAuthoritativePosition = true;
}
UAbilityTask_PlayMontageAndWait* MontageTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
this,
NAME_None,
TraversalResult.ChosenMontage,
TraversalResult.PlayRate,
NAME_None,
true,
1.0f,
TraversalResult.StartTime);
MontageTask->OnCompleted.AddDynamic(this, &UAO_GameplayAbility_Traversal::OnMontageCompleted);
MontageTask->OnInterrupted.AddDynamic(this, &UAO_GameplayAbility_Traversal::OnMontageCompleted);
MontageTask->OnBlendOut.AddDynamic(this, &UAO_GameplayAbility_Traversal::OnMontageCompleted);
MontageTask->OnCancelled.AddDynamic(this, &UAO_GameplayAbility_Traversal::OnMontageCompleted);
MontageTask->ReadyForActivation();
if (CharacterMovement)
{
CharacterMovement->SetMovementMode(MOVE_Flying);
}
if (ActorInfo->IsNetAuthority())
{
if (TObjectPtr<UCapsuleComponent> CapsuleComponent = Cast<UCapsuleComponent>(
Owner->GetComponentByClass(UCapsuleComponent::StaticClass())))
{
CapsuleComponent->IgnoreComponentWhenMoving(TraversalResult.HitComponent, true);
}
}
}
EndAbility()에서 구현했고, 캐릭터 무브먼트 모드를 변경하고, 서버 권위를 다시 복구해줬다.RetriggerableDelay()를 통해 0.2초 정도를 기다린 후에 서버 권위를 복구해줬는데, 이에 대한 이유는 바로 복구해주면 서버와 클라의 몽타주 재생 이후에 위치 차이로 인해 순간 텔포하는 현상이 매우 심하게 발생했다. 이를 완화하기 위해서 0.2초 정도를 Delay했고, 이를 통해 서버와 클라의 위치 정도 차이를 보정했다.void UAO_GameplayAbility_Traversal::EndAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
bool bReplicateEndAbility, bool bWasCancelled)
{
if (IsEndAbilityValid(Handle, ActorInfo))
{
if (ActorInfo->IsNetAuthority())
{
if (Character)
{
if (TObjectPtr<UCapsuleComponent> CapsuleComponent = Cast<UCapsuleComponent>(
Character->GetComponentByClass(UCapsuleComponent::StaticClass())))
{
CapsuleComponent->IgnoreComponentWhenMoving(TraversalResult.HitComponent, false);
}
}
}
if (CharacterMovement)
{
if (TraversalResult.ActionType == ETraversalActionType::Vault)
{
CharacterMovement->SetMovementMode(MOVE_Falling);
}
else
{
CharacterMovement->SetMovementMode(MOVE_Walking);
}
UKismetSystemLibrary::RetriggerableDelay(this, 0.2f, FLatentActionInfo());
CharacterMovement->bIgnoreClientMovementErrorChecksAndCorrection = false;
CharacterMovement->bServerAcceptClientAuthoritativePosition = false;
}
Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}
}
void AAO_PlayerCharacter::StartJump()
{
if (AbilitySystemComponent->TryActivateAbilitiesByTag(
FGameplayTagContainer(FGameplayTag::RequestGameplayTag(FName("Ability.Movement.Traversal")))))
{
return;
}
Jump();
}
void AAO_PlayerCharacter::TriggerJump()
{
AbilitySystemComponent->TryActivateAbilitiesByTag(
FGameplayTagContainer(FGameplayTag::RequestGameplayTag(FName("Ability.Movement.Traversal"))));
}

정지해있는 상태(WASD 키 입력이 없을 때)에서 Sprint 키를 입력하면 스태미나가 감소하는 현상이 있었다.

이를 해결하기 위해 기존의 Sprint 동작을 실행할 때, 캐릭터의 속력이 매우 작아서 0에 가깝다면 Sprint Ability가 실행이 되지 않게 CanActivateAbility() 함수에서 제어를 했다.
bool UAO_GameplayAbility_Sprint::CanActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags,
const FGameplayTagContainer* TargetTags, FGameplayTagContainer* OptionalRelevantTags) const
{
if (!Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags))
{
return false;
}
const TObjectPtr<ACharacter> Character = Cast<ACharacter>(ActorInfo->AvatarActor.Get());
checkf(Character, TEXT("Failed to cast AvatarActor to ACharacter"));
const TObjectPtr<UCharacterMovementComponent> CharacterMovement = Character->GetCharacterMovement();
checkf(CharacterMovement, TEXT("Failed to get CharacterMovementComponent"));
if (CharacterMovement->Velocity.SizeSquared2D() < KINDA_SMALL_NUMBER)
{
return false;
}
return true;
}
ActivateAbility()는 처음에 Ability를 실행할 때만 동작하는 함수이기 때문에 해당 로직이 정상적으로 작동하지 않았다.InputPressed() 함수가 Trigger될 때마다 동작되는 것으로 알고 있어서 해당 함수에 속력이 0이 되면 Ability가 종료되는 로직을 작성했다.void UAO_GameplayAbility_Sprint::InputPressed(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
Super::InputPressed(Handle, ActorInfo, ActivationInfo);
const TObjectPtr<ACharacter> Character = Cast<ACharacter>(ActorInfo->AvatarActor.Get());
checkf(Character, TEXT("Failed to cast AvatarActor to ACharacter"));
const TObjectPtr<UCharacterMovementComponent> CharacterMovement = Character->GetCharacterMovement();
checkf(CharacterMovement, TEXT("Failed to get CharacterMovementComponent"));
if (CharacterMovement->Velocity.SizeSquared2D() < KINDA_SMALL_NUMBER)
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
}
}

DamageComponent로 공격하는 함수를 실행시키게끔 구현을 해서 DamageEffectClass를 통해 데미지를 가하는 로직으로 작성을 했었다.DamageComponent에 의존적으로 작동할 것으로 보여서 Ability로 분리를 해서 구조를 다시 잡고자 했다.DamageEffectClass를 통해 데미지를 가하고, 해당 액터에게 공격받았다는 Event를 발생UAbilityTask_PlayMontageAndWait를 활용해서 몽타주를 실행시켜주는 Ability다.// AO_GameplayAbility_MeleeAttack_Base.h
/**
* 기본 근접 공격 어빌리티.
* - 몽타주 재생만 담당 (실제 데미지 판정은 AO_GameplayAbility_MeleeHitConfirm 에서 수행)
*/
UCLASS()
class AO_API UAO_GameplayAbility_MeleeAttack_Base : public UGameplayAbility
{
GENERATED_BODY()
public:
UAO_GameplayAbility_MeleeAttack_Base();
protected:
virtual void ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
virtual void EndAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
bool bReplicateEndAbility,
bool bWasCancelled) override;
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Attack")
TObjectPtr<UAnimMontage> AttackMontage;
protected:
UFUNCTION()
void OnMontageCompleted();
UFUNCTION()
void OnMontageCancelled();
};
GameplayEffect, 데미지의 양을 저장할 구조체를 FAO_MeleeHitTraceParams로 선언했다.// AO_MeleeHitEventPayload.h
USTRUCT(BlueprintType)
struct AO_API FAO_MeleeHitTraceParams
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FVector TraceStart = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FVector TraceEnd = FVector::ZeroVector;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float TraceRadius = 25.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TSubclassOf<UGameplayEffect> DamageEffectClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float DamageAmount = 0.f;
};
UCLASS()
class AO_API UAO_MeleeHitEventPayload : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadWrite)
FAO_MeleeHitTraceParams Params;
};
UAO_NotifyState_MeleeCollision에서 DamageComponent를 통해 데미지를 가하는 로직이 아니라 GameplayEvent 기반으로 변경했다. 이를 위해 헤더에서는 실행시킬 Event의 태그를 받았다.class AO_API UAO_NotifyState_MeleeCollision : public UAnimNotifyState
{
GENERATED_BODY()
public:
UAO_NotifyState_MeleeCollision();
...
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "GAS|Damage")
FGameplayTag HitConfirmEventTag;
}
void UAO_NotifyState_MeleeCollision::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
float TotalDuration, const FAnimNotifyEventReference& EventReference)
{
...
if (!SocketName.IsNone())
{
StartLocation = MeshComp->GetSocketLocation(SocketName);
}
else
{
StartLocation = MeshComp->GetComponentLocation();
}
}
UAO_MeleeHitEventPayload에 트레이스/데미지 데이터를 저장하고 해당 데이터와 함께 GameplayEvent를 동작하게끔 했다.Owner에게 Event를 발생하게끔 했다.void UAO_NotifyState_MeleeCollision::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
const FAnimNotifyEventReference& EventReference)
{
Super::NotifyEnd(MeshComp, Animation, EventReference);
if (!MeshComp || !OwningActor.IsValid() || !DamageEffectClass)
{
return;
}
AActor* Owner = OwningActor.Get();
if (!Owner || !Owner->HasAuthority())
{
return;
}
FVector EndLocation = StartLocation;
if (!SocketName.IsNone())
{
EndLocation = MeshComp->GetSocketLocation(SocketName);
}
else
{
EndLocation = MeshComp->GetComponentLocation();
}
UAO_MeleeHitEventPayload* Payload = NewObject<UAO_MeleeHitEventPayload>(Owner);
checkf(Payload, TEXT("Failed to create UAO_MeleeHitEventPayload"));
Payload->Params.TraceStart = StartLocation;
Payload->Params.TraceEnd = EndLocation;
Payload->Params.TraceRadius = TraceRadius;
Payload->Params.DamageEffectClass = DamageEffectClass;
Payload->Params.DamageAmount = DamageAmount;
FGameplayEventData EventData;
EventData.EventTag = HitConfirmEventTag;
EventData.Instigator = Owner;
EventData.OptionalObject = Payload;
EventData.EventMagnitude = DamageAmount;
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(Owner, HitConfirmEventTag, EventData);
}
// AO_GameplayAbility_MeleeHitConfirm.h
/**
* 노티파이에서 보낸 이벤트를 받아 실제 히트 판정과 데미지 적용을 담당.
*/
UCLASS()
class AO_API UAO_GameplayAbility_MeleeHitConfirm : public UGameplayAbility
{
GENERATED_BODY()
public:
UAO_GameplayAbility_MeleeHitConfirm();
protected:
virtual void ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Trace")
TEnumAsByte<ECollisionChannel> TraceChannel;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Trace")
bool bIgnoreInstigator = true;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "HitReact")
FGameplayTag HitReactEventTag;
protected:
void DoMeleeHitConfirm(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayEventData* EventData);
void ApplyDamageToActor(AActor* TargetActor, const FAO_MeleeHitTraceParams& Params, const FGameplayAbilityActorInfo* ActorInfo);
void SendHitReactEvent(AActor* TargetActor, AActor* InstigatorActor, float DamageAmount);
};
Event.Combat.Melee.Confirm라는 태그의 이벤트가 발생하면 작동하게끔 AbilityTriggers에 추가했고, HitReact Event 태그도 초기 값으로 Event.Combat.HitReact을 지정했다.UAO_GameplayAbility_MeleeHitConfirm::UAO_GameplayAbility_MeleeHitConfirm()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::ServerOnly;
TraceChannel = ECC_Pawn;
FAbilityTriggerData TriggerData;
TriggerData.TriggerTag = FGameplayTag::RequestGameplayTag(FName("Event.Combat.Melee.Confirm"));
TriggerData.TriggerSource = EGameplayAbilityTriggerSource::GameplayEvent;
AbilityTriggers.Add(TriggerData);
HitReactEventTag = FGameplayTag::RequestGameplayTag(FName("Event.Combat.HitReact"));
}
DamageComponent에서 Trace를 통해 충돌 검사를 실시하는 로직을 그대로 작성했다.void UAO_GameplayAbility_MeleeHitConfirm::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
if (!ActorInfo->IsNetAuthority())
{
EndAbility(Handle, ActorInfo, ActivationInfo, false, true);
return;
}
if (!TriggerEventData)
{
EndAbility(Handle, ActorInfo, ActivationInfo, false, true);
return;
}
DoMeleeHitConfirm(ActorInfo, TriggerEventData);
EndAbility(Handle, ActorInfo, ActivationInfo, true, false);
}
void UAO_GameplayAbility_MeleeHitConfirm::DoMeleeHitConfirm(const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayEventData* EventData)
{
checkf(ActorInfo, TEXT("Failed to get ActorInfo"));
checkf(ActorInfo->AvatarActor.IsValid(), TEXT("Failed to get AvatarActor"));
const UAO_MeleeHitEventPayload* Payload = Cast<UAO_MeleeHitEventPayload>(EventData->OptionalObject);
if (!Payload)
{
return;
}
const FAO_MeleeHitTraceParams& Params = Payload->Params;
UWorld* World = GetWorld();
checkf(World, TEXT("Failed to get World"));
TArray<FHitResult> HitResults;
TArray<AActor*> ActorsToIgnore;
if (bIgnoreInstigator)
{
ActorsToIgnore.Add(ActorInfo->AvatarActor.Get());
}
UKismetSystemLibrary::SphereTraceMulti(
World,
Params.TraceStart,
Params.TraceEnd,
Params.TraceRadius,
UEngineTypes::ConvertToTraceType(TraceChannel),
false,
ActorsToIgnore,
EDrawDebugTrace::ForDuration,
HitResults,
true);
TSet<TWeakObjectPtr<AActor>> UniqueHitActors;
for (const auto& Hit : HitResults)
{
AActor* HitActor = Hit.GetActor();
if (!HitActor) continue;
if (UniqueHitActors.Contains(HitActor)) continue;
UniqueHitActors.Add(HitActor);
ApplyDamageToActor(HitActor, Params, ActorInfo);
}
}
void UAO_GameplayAbility_MeleeHitConfirm::SendHitReactEvent(AActor* TargetActor, AActor* InstigatorActor,
float DamageAmount)
{
if (!TargetActor || !HitReactEventTag.IsValid())
{
return;
}
FGameplayEventData HitEvent;
HitEvent.EventTag = HitReactEventTag;
HitEvent.Instigator = InstigatorActor;
HitEvent.Target = TargetActor;
HitEvent.EventMagnitude = DamageAmount;
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(TargetActor, HitReactEventTag, HitEvent);
}
// AO_GameplayAbility_HitReact.h
/**
* 피격 리액션 전용 Ability
* - Event.Combat.HitReact 이벤트를 트리거로 발동
*/
UCLASS()
class AO_API UAO_GameplayAbility_HitReact : public UGameplayAbility
{
GENERATED_BODY()
public:
UAO_GameplayAbility_HitReact();
protected:
virtual void ActivateAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
virtual void EndAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
bool bReplicateEndAbility,
bool bWasCancelled) override;
protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "HitReact")
TObjectPtr<UAnimMontage> HitReactMontage;
UFUNCTION()
void OnMontageCompleted();
UFUNCTION()
void OnMontageCancelled();
};
Event.Combat.HitReact 태그의 Event가 발생하면 작동하게끔 AbilityTriggers에 부여했다는 점이다.UAO_GameplayAbility_HitReact::UAO_GameplayAbility_HitReact()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
FAbilityTriggerData TriggerData;
TriggerData.TriggerTag = FGameplayTag::RequestGameplayTag(FName("Event.Combat.HitReact"));
TriggerData.TriggerSource = EGameplayAbilityTriggerSource::GameplayEvent;
AbilityTriggers.Add(TriggerData);
}
at() 멤버 함수를 통해 런타임 오류를 방지할 수 있음iterator 기반의 STL의 알고리즘 및 이터레이터 기능을 활용할 수 있음