-
[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; }
'UE5' 카테고리의 다른 글
[UE5] 게임플레이 이펙트의 활용 (0) 2025.01.07 [UE5] 캐릭터 어트리뷰트 설정 (0) 2025.01.06 [UE5] GAS를 활용한 캐릭터의 입력 처리 (0) 2025.01.02 [UE5] GAS(GamePlay Ability System)의 시작 (1) 2024.12.30 [UE5] 네트워크 멀티플레이어 최적화 (0) 2024.12.26