ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [UE5] GAS를 활용한 캐릭터 공격
    UE5 2025. 1. 3. 14:17

    캐릭터 콤보 공격 구현을 위한 기획

    • 공격 시작 후 유효 시간 내에 추가 공격 입력을 넣으면, 다음 공격 모션을 발동
    • 콤보 공격에 대한 정보는 ABComboActionData에서 불러들임
    • AT를 발동하고 입력 점검 타이머도 함께 발동
    • 입력 점검 타이머가 발동되면 다음 공격 입력이 있는지 검사
    • 다음 공격 입력이 있으면 다음 공격 모션을 발동하고 다시 입력 점검 타이머를 발동
    // Fill out your copyright notice in the Description page of Project Settings.
    
    
    #include "GA/ABGA_Attack.h"
    #include "Character/ABCharacterBase.h"
    #include "Abilities/Tasks/AbilityTask_PlayMontageAndWait.h"
    #include "ArenaBattleGAS.h"
    #include "GameFramework/CharacterMovementComponent.h"
    #include "Character/ABComboActionData.h"
    
    UABGA_Attack::UABGA_Attack()
    {
    	InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
    }
    
    // UAbilityTask_PlayMontageAndWait를 통해 몽타주를 재생하고, 첫 번째 콤보 섹션을 시작
    // StartComboTimer를 호출하여 콤보 입력 가능 시간을 설정
    void UABGA_Attack::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
    {
    	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
    
    	AABCharacterBase* ABCharacter = CastChecked<AABCharacterBase>(ActorInfo->AvatarActor.Get());
    	CurrentComboData = ABCharacter->GetComboActionData();
    	ABCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
    
    	UAbilityTask_PlayMontageAndWait* PlayAttackTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(this, TEXT("PlayAttack"), ABCharacter->GetComboActionMontage(), 1.0f, GetNextSection());
    	PlayAttackTask->OnCompleted.AddDynamic(this, &UABGA_Attack::OnCompletedCallback);
    	PlayAttackTask->OnInterrupted.AddDynamic(this, & UABGA_Attack::OnInterruptedCallback);
    	PlayAttackTask->ReadyForActivation();
    
    	StartComboTimer();
    }
    
    // 플레이어 입력이 발생하면 HasNextComboInput 플래그가 설정
    void UABGA_Attack::InputPressed(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
    {
    	if (!ComboTimerHandle.IsValid())
    	{
    		HasNextComboInput = false;
    	}
    	else
    	{
    		HasNextComboInput = true;
    	}
    }
    
    void UABGA_Attack::CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility)
    {
    	Super::CancelAbility(Handle, ActorInfo, ActivationInfo, bReplicateCancelAbility);
    }
    
    void UABGA_Attack::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
    {
    	Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
    
    	AABCharacterBase* ABCharacter = CastChecked<AABCharacterBase>(ActorInfo->AvatarActor.Get());
    	ABCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
    
    	CurrentComboData = nullptr;
    	CurrentCombo = 0;
    	HasNextComboInput = false;
    }
    
    void UABGA_Attack::OnCompletedCallback()
    {
    	bool bReplicatedEndAbility = true;
    	bool bWasCanceled = false;
    	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCanceled);
    }
    
    void UABGA_Attack::OnInterruptedCallback()
    {
    	bool bReplicatedEndAbility = true;
    	bool bWasCanceled = true;
    	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCanceled);
    }
    
    // 다음 섹션 이름 계산
    FName UABGA_Attack::GetNextSection()
    {
    	CurrentCombo = FMath::Clamp(CurrentCombo + 1, 1, CurrentComboData->MaxComboCount);
    	FName NextSection = *FString::Printf(TEXT("%s%d"), *CurrentComboData->MontageSectionNamePrefix, CurrentCombo);
    	return NextSection;
    }
    
    // 타이머 설정 및 콤보 입력 대기
    void UABGA_Attack::StartComboTimer()
    {
    	int32 ComboIndex = CurrentCombo - 1;
    	ensure(CurrentComboData->EffectiveFrameCount.IsValidIndex(ComboIndex));
    
    	// 현재 콤보 단계에서 다음 입력을 받을 수 있는 프레임 수 / 애니메이션의 초당 프레임수(FPS)
    	float ComboEffectiveTime = CurrentComboData->EffectiveFrameCount[ComboIndex] / CurrentComboData->FrameRate;
    	if (ComboEffectiveTime > 0.0f)
    	{
    		GetWorld()->GetTimerManager().SetTimer(ComboTimerHandle, this, &UABGA_Attack::CheckComboInput, ComboEffectiveTime, false);
    	}
    }
    
    // 타이머가 만료되면 CheckComboInput에서 입력 플래그를 확인
    // 입력이 있다면 다음 섹션으로 이동(MontageJumpToSection)하고, 새로운 타이머를 설정하여 다음 콤보 입력을 기다림
    // 입력이 없으면 onComplete 델리게이트가 호출되며 콜백함수 실행되고 종료
    void UABGA_Attack::CheckComboInput()
    {
    	ComboTimerHandle.Invalidate();
    	if (HasNextComboInput)
    	{
    		MontageJumpToSection(GetNextSection());
    		StartComboTimer();
    		HasNextComboInput = false;
    	}
    }

     

     

    어빌리티 태스크(AT)의 제작 규칙

    • AT는 UAbilityTask 클래스를 상속받아 제작
    • AT 인스턴스를 생성해 반환하는 static 함수를 선언해 구현
    • AT가 종료되면 GA에 알려줄 델리게이트를 선언
    • 시작과 종료 처리를 위해 Activate와 OnDestroy 함수를 재정의(Override)해 구현
    • 일정 시간이 지난 후 AT를 종료하고자 한다면, 활성화시 SetWaitingOnAvatar 함수를 호출해 Waiting 상태로 설정
    • 만일 Tick을 활성화하고 싶다면 bTickingTask 값을 true로 설정
    • AT가 종료되면 델리게이트를 브로드캐스팅
    // ABAT_JumpAndWaitForLanding.cpp
    
    #include "GA/AT/ABAT_JumpAndWaitForLanding.h"
    #include "GameFramework/Character.h"
    
    UABAT_JumpAndWaitForLanding::UABAT_JumpAndWaitForLanding()
    {
    }
    
    // UABAT_JumpAndWaitForLanding 작업 생성 및 반환
    UABAT_JumpAndWaitForLanding* UABAT_JumpAndWaitForLanding::CreateTask(UGameplayAbility* OwningAbility)
    {
    	UABAT_JumpAndWaitForLanding* NewTask = NewAbilityTask<UABAT_JumpAndWaitForLanding>(OwningAbility);
    	return NewTask;
    }
    
    // 작업 실행시 호출, 점프 동작과 착지 이벤트 바인딩 설정
    void UABAT_JumpAndWaitForLanding::Activate()
    {
    	Super::Activate();
    
    	ACharacter* Character = CastChecked<ACharacter>(GetAvatarActor());
    	Character->LandedDelegate.AddDynamic(this, &UABAT_JumpAndWaitForLanding::OnLandedCallback);
    	Character->Jump();
    
    	// 작업이 아바타의 동작에 의존하고 있음을 명시
    	SetWaitingOnAvatar();
    }
    
    // 작업이 파괴되거나 능력이 종료될 때 호출
    void UABAT_JumpAndWaitForLanding::OnDestroy(bool AbilityEnded)
    {
    	ACharacter* Character = CastChecked<ACharacter>(GetAvatarActor());
    	Character->LandedDelegate.RemoveDynamic(this, &UABAT_JumpAndWaitForLanding::OnLandedCallback);
    
    	Super::OnDestroy(AbilityEnded);
    }
    
    // 캐릭터가 착지했을 때 호출
    void UABAT_JumpAndWaitForLanding::OnLandedCallback(const FHitResult& Hit)
    {
    	// 이 작업이 여전히 활성 상태인지 확인
    	if (ShouldBroadcastAbilityTaskDelegates())
    	{
    		// OnComplete 델리게이트 호출
    		OnComplete.Broadcast();
    	}
    }
    // ABGA_Jump.cpp
    
    #include "GA/ABGA_Jump.h"
    #include "GameFramework/Character.h"
    #include "GA/AT/ABAT_JumpAndWaitForLanding.h"
    
    UABGA_Jump::UABGA_Jump()
    {
    	InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
    }
    
    // 특정 능력이 실행 가능한지 여부를 판단하는 데 사용 (점프 가능한 상태인지 조건 검사)
    bool UABGA_Jump::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, OUT FGameplayTagContainer* OptionalRelevantTags) const
    {
    	// 부모 클래스의 CanActivateAbility를 호출하여, 기본조건(쿨다운, 비용 등)이 충족되는지 확인
    	bool bResult = Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags);
    	if (!bResult)
    	{
    		return false;
    	}
    
    	// 캐릭터가 점프 가능한지 내부 상태(공중 여부, 착지 여부 등)를 기반으로 판단
    	const ACharacter* Character = Cast<ACharacter>(ActorInfo->AvatarActor.Get());
    	return (Character && Character->CanJump());
    }
    
    void UABGA_Jump::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
    {
    	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
    
    	// OnComplete 델리게이트를 구독하고, 착지에 성공해서 호출시 OnLandedCallback 함수 호출
    	UABAT_JumpAndWaitForLanding* JumpAndWaitingForLandingTask = UABAT_JumpAndWaitForLanding::CreateTask(this);
    	JumpAndWaitingForLandingTask->OnComplete.AddDynamic(this, &UABGA_Jump::OnLandedCallback);
    	JumpAndWaitingForLandingTask->ReadyForActivation();
    }
    
    // 점프키를 떼면 점프가 종료
    void UABGA_Jump::InputReleased(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
    {
    	ACharacter* Character = CastChecked<ACharacter>(ActorInfo->AvatarActor.Get());
    	Character->StopJumping();
    }
    
    void UABGA_Jump::OnLandedCallback()
    {
    	// true: Ability 종료가 서버와 클라이언트 간 동기화 됨, false: 로컬에서만 Ability를 종료
    	bool bReplicatedEndAbility = true;
    	// true: 능력이 취소된 경우, false: 능력이 정상적으로 종료된 경우
    	bool bWasCanceled = false;
    	// 활성화된 능력 종료
    	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCanceled);
    }

     

     

    공격 판정을 위한 신규 기능 기획

    • 애니메이션 몽타주의 노티파이를 활용해 원하는 타이밍에 공격을 판정하는 기능 추가
    • 애니메이션 노티파이가 발동되면 판정을 위한 GA를 트리거해 발동
    • 새로운 GA가 발동되면 공격 판정을 위한 AT를 실행
    • GAS에서 제공하는 타겟액터를 활용해 물리 공격 판정을 수행
    • 판정 결과를 시각적으로 확인할 수 있도록 드로우 디버그 기능 제공
    // AnimNotify_GASAttackHitCheck.cpp
    
    #include "Animation/AnimNotify_GASAttackHitCheck.h"
    #include "AbilitySystemBlueprintLibrary.h"
    
    // UAnimNotify를 상속 받아 구현된 애니메이션 알림 클래스
    // 애니메이션의 특정 프레임에서 호출되어 GAS와 연동하는 기능 제공
    UAnimNotify_GASAttackHitCheck::UAnimNotify_GASAttackHitCheck()
    {
    }
    
    FString UAnimNotify_GASAttackHitCheck::GetNotifyName_Implementation() const
    {
    	return TEXT("GASAttackHitCheck");
    }
    
    void UAnimNotify_GASAttackHitCheck::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
    {
    	Super::Notify(MeshComp, Animation, EventReference);
    
    	if (MeshComp)
    	{
    		AActor* OwnerActor = MeshComp->GetOwner();
    		if (OwnerActor)
    		{
    			FGameplayEventData PayloadData;
    			// 내가 지정한 어떤 특정한 액터에다가 태그를 넣어서 이벤트를 발동시킴
    			UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(OwnerActor, TriggerGameplayTag, PayloadData);
    		}
    	}
    }
    // ABGA_AttackHitCheck.cpp
    
    #include "GA/ABGA_AttackHitCheck.h"
    #include "ArenaBattleGAS.h"
    #include "AbilitySystemBlueprintLibrary.h"
    #include "GA/AT/ABAT_Trace.h"
    #include "GA/TA/ABTA_Trace.h"
    
    UABGA_AttackHitCheck::UABGA_AttackHitCheck()
    {
    	InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
    }
    
    void UABGA_AttackHitCheck::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
    {
    	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
    
    	// 히트 체크를 수행하는 AT 생성
    	UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, AABTA_Trace::StaticClass());
    
    	AttackTraceTask->OnComplete.AddDynamic(this, &UABGA_AttackHitCheck::OnTraceResultCallback);
    	AttackTraceTask->ReadyForActivation();
    }
    
    void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
    {
    	// UAbilitySystemBlueprintLibrary: 타겟 데이터에서 히트 결과를 추출하고 처리하기 위한 헬퍼 함수 제공
    	// TargetDataHasResult : 주어진 타겟 데이터에 히트 결과가 포함되어 있는지 확인
    	if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
    	{
    		// GetHitResultFromTargetData : 타겟 데이터에서 히트 결과 추출
    		FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0);
    		ABGAS_LOG(LogABGAS, Log, TEXT("Target %s Detected"), *(HitResult.GetActor()->GetName()));
    	}
    
    	bool bReplicatedEndAbility = true;
    	bool bWasCanceled = false;
    	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCanceled);
    }
    // UABAT_Trace.cpp
    
    #include "GA/AT/ABAT_Trace.h"
    #include "GA/TA/ABTA_Trace.h"
    #include "AbilitySystemComponent.h"
    
    UABAT_Trace::UABAT_Trace()
    {
    }
    
    UABAT_Trace* UABAT_Trace::CreateTask(UGameplayAbility* OwningAbility, TSubclassOf<class AABTA_Trace> TargetActorClass)
    {
    	UABAT_Trace* NewTask = NewAbilityTask<UABAT_Trace>(OwningAbility);
    	NewTask->TargetActorClass = TargetActorClass;
    	return NewTask;
    }
    
    void UABAT_Trace::Activate()
    {
    	Super::Activate();
    
    	// 타겟 액터(AABTA_Trace)를 스폰하고 초기 설정을 수행
    	SpawnAndInitializeTargetActor();
    
    	// 타겟 액터의 스폰 작업을 완료하고, 타겟팅을 시작
    	FinalizeTargetActor();
    
    	// 이 작업이 아바타의 동작에 의존하고 있음을 명시
    	SetWaitingOnAvatar();
    }
    
    void UABAT_Trace::OnDestroy(bool AbilityEnded)
    {
    	if (SpawnedTargetActor)
    	{
    		SpawnedTargetActor->Destroy();
    	}
    
    	Super::OnDestroy(AbilityEnded);
    }
    
    // 타겟 엑터(AABTA_Trace)를 스폰하고 초기 설정을 수행
    void UABAT_Trace::SpawnAndInitializeTargetActor()
    {
    	// 타겟 액터를 스폰(생성)하되, 초기화 작업이 완료되지 않은 상태로 유지
    	SpawnedTargetActor = Cast<AABTA_Trace>(Ability->GetWorld()->SpawnActorDeferred<AGameplayAbilityTargetActor>(TargetActorClass, FTransform::Identity, nullptr, nullptr, ESpawnActorCollisionHandlingMethod::AlwaysSpawn));
    	if (SpawnedTargetActor)
    	{
    		// 디버깅 정보를 표시
    		SpawnedTargetActor->SetShowDebug(true);
    
    		// 트레이스가 완료되었을 때 호출될 델리게이트를 바인딩
    		SpawnedTargetActor->TargetDataReadyDelegate.AddUObject(this, &UABAT_Trace::OnTargetDataReadyCallback);
    	}
    }
    
    // 스폰된 타겟 액터를 초기화하고 타겟팅을 시작
    void UABAT_Trace::FinalizeTargetActor()
    {
    	UAbilitySystemComponent* ASC = AbilitySystemComponent.Get();
    	if (ASC)
    	{
    		const FTransform SpawnTarnsform = ASC->GetAvatarActor()->GetTransform();
    		// 타겟 액터의 스폰 작업을 완료하고, 트랜스폼(위치/회전)을 설정
    		SpawnedTargetActor->FinishSpawning(SpawnTarnsform);
    
    		// ASC에 새로 생성된 타겟 액터를 추가
    		ASC->SpawnedTargetActors.Push(SpawnedTargetActor);
    
    		// 타겟팅 작업을 시작하고, 즉시 완료
    		SpawnedTargetActor->StartTargeting(Ability);
    		SpawnedTargetActor->ConfirmTargeting();
    	}
    }
    
    void UABAT_Trace::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& DataHandle)
    {
    	if (ShouldBroadcastAbilityTaskDelegates())
    	{
    		OnComplete.Broadcast(DataHandle);
    	}
    
    	EndTask();
    }

     

    게임플레이 어빌리티 타겟 액터

    • 게임플레이 어빌리티에서 대상에 대한 판정(주로 물리 판정)을 구현할 때 사용하는 특수한 액터. 줄여서 TA
    • AGameplayAbilityTargetActor 클래스를 상속받아서 구현
    • 왜 타겟 액터(TA)가 필요한가?
      • 타겟을 설정하는 다양한 방법이 있음
      • Trace를 사용해 즉각적으로 타겟을 검출하는 방법
      • 사용자의 최종 확인을 한번 더 거치는 방법이 있음 ( ex. 원거리 범위 공격 )
      • 공격 범위 확인을 위한 추가 시각화 ( 시각화를 수행하는 액터를 월드레티클(WorldReticle)이라고 함 )
    • 주요 함수
      • StartTargeting : 타겟팅을 시작
      • ConfirmTargetingAndContinue : 타겟팅을 확정하고 이후 남은 프로세스를 진행
      • ConfirmTargeting : 태스크 진해 없이 타겟팅만 확정
      • CancelTargeting : 타겟팅을 취소

     

    게임플레이 어빌리티 타겟 데이터

    • 타겟 액터에서 판정한 결과를 담은 데이터
    • 다음의 속성을 가짐
      • Trace 히트 결과 (HitResult)
      • 판정된 다수의 액터 포인터
      • 시작 지점
      • 끝 지점
    • 타겟 데이터를 여러 개 묶어 전송하는 것이 일반적인데 이를 타겟 데이터 핸들이라고 함
    // ABTA_Trace.cpp
    
    #include "GA/TA/ABTA_Trace.h"
    #include "Abilities//GameplayAbility.h"
    #include "GameFramework/Character.h"
    #include "Components/CapsuleComponent.h"
    #include "Physics/ABCollision.h"
    #include "DrawDebugHelpers.h"
    
    AABTA_Trace::AABTA_Trace()
    {
    }
    
    // 타겟팅 시작 시 호출되며, 공격의 시작점(소스 액터)을 설정
    void AABTA_Trace::StartTargeting(UGameplayAbility* Ability)
    {
    	Super::StartTargeting(Ability);
    
    	// 능력을 소유한 캐릭터를 저장
    	SourceActor = Ability->GetCurrentActorInfo()->AvatarActor.Get();
    }
    
    // 트레이스를 수행하고, 생성된 타겟 데이터를 델리게이트를 통해 호출자에게 전달
    void AABTA_Trace::ConfirmTargetingAndContinue()
    {
    	if (SourceActor)
    	{
    		FGameplayAbilityTargetDataHandle DataHandle = MakeTargetData();
    		TargetDataReadyDelegate.Broadcast(DataHandle);
    	}
    }
    
    // 캐릭터를 기준으로 트레이스 작업을 수행하고, 타겟 데이터를 생성
    FGameplayAbilityTargetDataHandle AABTA_Trace::MakeTargetData() const
    {
    	ACharacter* Character = CastChecked<ACharacter>(SourceActor);
    
    	FHitResult OutHitResult;
    	const float AttackRange = 100.0f;
    	const float AttackRadius = 50.0f;
    
    	FCollisionQueryParams Params(SCENE_QUERY_STAT(UABTA_Trace), false, Character);
    	const FVector Forward = Character->GetActorForwardVector();
    	const FVector Start = Character->GetActorLocation() + Forward * Character->GetCapsuleComponent()->GetScaledCapsuleRadius();
    	const FVector End = Start + Forward * AttackRange;
    
    	bool HitDetected = GetWorld()->SweepSingleByChannel(OutHitResult, Start, End, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);
    
    	FGameplayAbilityTargetDataHandle DataHandle;
    	if (HitDetected)
    	{
    		FGameplayAbilityTargetData_SingleTargetHit* TargetData = new FGameplayAbilityTargetData_SingleTargetHit(OutHitResult);
    		DataHandle.Add(TargetData);
    	}
    
    #if ENABLE_DRAW_DEBUG
    	if (bShowDebug)
    	{
    		FVector CapsuleOrigin = Start + (End - Start) * 0.5f;
    		float CapsuleHalfHeight = AttackRange * 0.5f;
    		FColor DrawColor = HitDetected ? FColor::Green : FColor::Red;
    		DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsuleHalfHeight, AttackRadius, FRotationMatrix::MakeFromZ(Forward).ToQuat(), DrawColor, false, 5.0f);
    	}
    #endif
    
    	return DataHandle;
    }
Designed by Tistory.