개요
Attack GA를 네트워크 지원을 위해 개선을 하면서 공격판정 GA(Attack Hit GA)도
기존 싱글플레이만 지원하던 로직을 네트워크 지원을 위해 변경했다
변경 과정에서 기존에 직접 구현하여 사용하던 어빌리티 테스크를
언리얼에서 제공하는 UAbilityTask_WaitTargetData 를 사용하여 공격판정 GA를 개선했다
UAbilityTask_WaitTargetData 에 대한 자세한 글은 아래 링크의 글에 자세히 작성해놓았다
https://k99812.tistory.com/195
Unreal Engine UAbilityTask_WaitTargetData 살펴보기
개요 최근 프로젝트를 네트워크(멀티플레이) 지원할 수 있도록 리팩토링하면서 기존 구현했던 어빌리티 테스크를 UAbilityTask_WaitTargetData 로 바꾸는 작업을 하였다 기존 구현한 어빌리티 테스크는
k99812.tistory.com
전체적인 흐름

- 콤보공격 GA로 인해 공격 몽타주 재생
- 특정 프레임에 AnimNotify로 AttackHitCheck GA 실행
- 클라이언트에서 GA가 실행 후 AT 생성 및 델리게이트 연결
- AT 마무리 후 델리게이트 브로드캐스트 및 서버에 결과 전송
- 클라이언트 델리게이트 콜백로직 실행 후 GA 종료
- 서버에서 결과 수신후 서버의 AT에서 델리게이트 브로드캐스트
- 서버 델리게이트 콜백 로직 실행 후 GA 종료
위와 같은 과정을 아래 로그 스크린샷으로 볼 수 있다


리슨서버에서 실행시 서버로 데이터를 전송하지않고 바로 서버로직을 실행하는것을 볼 수 있다
AttackHitCheck GA의 경우 GAS 기반의 클라이언트 예측을 사용하여
클라이언트-서버 순으로 GA가 실행된다
그래서 UAbilityTask_WaitTargetData 는 클라이언트와 서버 두 곳에서 생성되며
클라이언트에서는 타겟액터를 생성하여 서버에 전송하고 결과를 알려주는 델리게이트를 호출한다
서버에서는 클라이언트에서 타겟액터를 전송하면 실행되는 델리게이트에 콜백함수를 연결하여
클라이언트가 결과를 전송하면 그에 따른 로직을 실행한다
리슨서버 클라이언트에서 GA를 실행할 경우 UAbilityTask_WaitTargetData 내부 로직에 따라
클라이언트에서 전송되는 데이터를 기다리지 않고 직접 타겟액터를 생성하여 판정하게 된다
UAbilityTask_WaitTargetData 에 대한 글은 위에 링크를 클릭하면 볼 수 있다
즉 UAbilityTask_WaitTargetData 는 클라, 서버 두 곳에서 생성되어
클라이언트에선 판정후 결과전송, 서버에선 클라이언트의 결과를 대기한다
이 동작은 ShouldProduceTargetDataOnServer 가 false일때이다
만약 ShouldProduceTargetDataOnServer 가 true이면
클라이언트에서 판정을 진행하지만 결과는 버려지고
서버는 클라이언트의 결과를 기다리지않고 자체적으로 판정한다

구현 코드 살펴보기
생성자
UPPGA_AttackHitCheck::UPPGA_AttackHitCheck()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
ReplicationPolicy = EGameplayAbilityReplicationPolicy::ReplicateYes;
}
클라이언트 예측을 위해 LocalPredicted 로 설정하였다
ActivateAbility
void UPPGA_AttackHitCheck::ActivateAbility(생략)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
PPNET_SUBLOG(LogGAS, Log, TEXT("Begin"));
CurrentLevel = TriggerEventData->EventMagnitude;
bool bIsMonster = false;
IGameplayTagAssetInterface* Owner = Cast<IGameplayTagAssetInterface>(ActorInfo->AvatarActor.Get());
if (Owner && Owner->HasMatchingGameplayTag(PPTAG_CHARACTER_MONSTER))
{
bIsMonster = true;
}
AGameplayAbilityTargetActor* SpawnedActor = nullptr;
UAbilityTask_WaitTargetData* WaitTargetData = UAbilityTask_WaitTargetData::WaitTargetData(
this, FName("WaitTargetData"), EGameplayTargetingConfirmation::Instant, APPTA_Trace::StaticClass());
WaitTargetData->ValidData.AddDynamic(this, &UPPGA_AttackHitCheck::TraceResultCallback);
TargetActor = SpawnedActor;
if (WaitTargetData->BeginSpawningActor(this, APPTA_Trace::StaticClass(), SpawnedActor))
{
APPTA_Trace* MyTrace = Cast<APPTA_Trace>(SpawnedActor);
if (MyTrace)
{
MyTrace->SetShowDebug(true);
MyTrace->ShouldProduceTargetDataOnServer = bIsMonster;
}
WaitTargetData->FinishSpawningActor(this, SpawnedActor);
}
WaitTargetData->ReadyForActivation();
}
AttackHitCheck GA는 본 프로젝트에서 몬스터, 플레이어 캐릭터가 같이 사용하고 있다
그래서 AIController와 PlayerController의 차이점에 따른 로직이 필요하다
- AIController
- 서버에만 존재
- PlayerController
- 서버, 클라이언트 두 곳에 존재
위와 같은 특징으로 GA를 발동한 캐릭터가 AI인지 판단하여
ShouldProduceTargetDataOnServer 설정을 바꿔줘야 한다
- if (Owner && Owner->HasMatchingGameplayTag(PPTAG_CHARACTER_MONSTER))
- 언리얼에서 제공하는 인터페이스와 GameplayTag를 통해 몬스터인지 판별한다
UAbilityTask_WaitTargetData 테스크를 사용하려면 타겟액터를 먼저 생성해줘야한다
- AGameplayAbilityTargetActor* SpawnedActor = nullptr;
그 후 UAbilityTask_WaitTargetData::WaitTargetData를 사용하여 테스크를 생성한다
테스크 생성중 EGameplayTargetingConfirmation으로 Confirmation 타입을 바꿀수있다
- Instant: 타겟액터 생성즉시 판정진행
- UserConfirmed: ASC->LocalInputComfirm 이벤트를 호출하면 판정진행
- Custom
- CustomMulti
위와 같은 타입이 존재한다
타겟액터와, 어빌리티 테스크관련 설정은 FinishSpawningActor 호출전 마무리해야 한다
- WaitTargetData->ValidData.AddDynamic
- 해당 GA에선 테스크 생성 직후 델리게이트를 바인드 했다


만약 FinishSpawningActor 호출 후 델리게이트에 바인드하면 위 스크린샷과 같이 정상적으로 동작하지 않는다
타겟액터는 FinishSpawningActor 가 호출되면 로직이 실행되고 마무리 되기 때문에 델리게이트 콜백을 못받는다
BeginSpawningActor를 실행하여 타겟액터가 정상적으로 생성되면 true를 반환한다
- if (WaitTargetData->BeginSpawningActor(this, APPTA_Trace::StaticClass(), SpawnedActor))
타겟액터를 생성 후 캐스팅하여 설정을 마무리 한뒤 FinishSpawningActor를 호출한다
- APPTA_Trace* MyTrace = Cast<APPTA_Trace>(SpawnedActor);
- MyTrace->SetShowDebug(true);
- MyTrace->ShouldProduceTargetDataOnServer = bIsMonster;
해당 시점에 ShouldProduceTargetDataOnServer 를 몬스터이면 true로 설정해
타겟액터가 서버에서 실행되도록 설정한다
TraceResultCallback
void UPPGA_AttackHitCheck::TraceResultCallback(const FGameplayAbilityTargetDataHandle& DataHandle)
{
PPNET_SUBLOG(LogGAS, Log, TEXT("Begin"));
if (HasAuthority(&CurrentActivationInfo))
{
ServerApplyHitLogic(DataHandle);
}
if(IsLocallyControlled())
{
ClientPlayHitCue(DataHandle);
}
bool bReplicateEndAbility = true;
bool bWasCancelled = false;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled);
}
어빌리티 테스크 결과가 나오면 호출되는 콜백 함수이다.
클라이언트, 서버 두 곳에서 호출된다
- if (HasAuthority(&CurrentActivationInfo))
- ServerApplyHitLogic(DataHandle);
- if(IsLocallyControlled())
- ClientPlayHitCue(DataHandle);
특이점은 if-else로 구현하지 않고 별도의 if문을 통해
로컬컨트롤일때 로직을 실행하고 있다
해당 이유는 if-else로 묶으면 리슨서버 클라이언트에서
클라이언트 로직이 실행되지 않기 때문에 별도의 if문을 사용하였다
ServerApplyHitLogic
void UPPGA_AttackHitCheck::ServerApplyHitLogic(const FGameplayAbilityTargetDataHandle& DataHandle)
{
PPNET_SUBLOG(LogGAS, Log, TEXT("Begin"));
//#include "AbilitySystemBlueprintLibrary.h" 추가
if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(DataHandle, 0))
{
생략
float AttackRange = OwnerAttributeSet->GetAttackRange();
float Tolerance = 50.0f;
float Distance = OwnerActor->GetSquaredDistanceTo(TargetActor);
if (Distance > (AttackRange + Tolerance) * (AttackRange + Tolerance))
{
PPGAS_LOG(LogGAS, Warning, TEXT("Validation Failed: Too Far (Dist: %.2f, Max: %.2f)"), FMath::Sqrt(Distance), AttackRange + Tolerance);
return;
}
FVector ForwardVec = OwnerActor->GetActorForwardVector();
FVector TargetVec = (TargetActor->GetActorLocation() - OwnerActor->GetActorLocation()).GetSafeNormal();
float DotResult = FVector::DotProduct(ForwardVec, TargetVec);
if (DotResult < 0.0f)
{
PPGAS_LOG(LogGAS, Warning, TEXT("Validation Failed: Target is Behind"));
return;
}
if (TargetASC->HasMatchingGameplayTag(PPTAG_CHARACTER_ISDEAD))
{
PPGAS_LOG(LogGAS, Log, TEXT("Validation Failed: Target Already Dead"));
return;
}
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect, CurrentLevel);
if (SpecHandle.IsValid())
{
ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, SpecHandle, DataHandle);
}
//타겟이 몬스터일 경우 AI 데미지 센스 발동
IGameplayTagAssetInterface* Monster = Cast<IGameplayTagAssetInterface>(HitResult.GetActor());
if (Monster && Monster->HasMatchingGameplayTag(PPTAG_CHARACTER_MONSTER))
{
UAISense_Damage::ReportDamageEvent(this, HitResult.GetActor(), OwnerASC->GetAvatarActor(),
OwnerAttributeSet->GetAttackRate(), HitResult.GetActor()->GetActorLocation(), HitResult.Location);
}
}
}
서버 로직에선 클라이언트가 보낸 히트데이터를 검증 후
ApplyGameplayEffectSpecToTarget 를 통해 데미지를 적용한다
또 UAISense_Damage::ReportDamageEvent 이벤트를 발생시켜
AI한테 피격사실을 알려준다
ClientPlayHitCue
void UPPGA_AttackHitCheck::ClientPlayHitCue(const FGameplayAbilityTargetDataHandle& DataHandle)
{
PPNET_SUBLOG(LogGAS, Log, TEXT("Begin"));
if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(DataHandle, 0))
{
생략
//Hit 이벤트 발생
FGameplayEventData PayLoadData;
PayLoadData.Instigator = OwnerASC->GetAvatarActor();
PayLoadData.Target = TargetASC->GetAvatarActor();
OwnerASC->HandleGameplayEvent(PPTAG_ABILITY_HIT, &PayLoadData);
}
}
클라이언트 로직에선 추가적인 이펙트나 UI를 생성할 수 있다
현재는 피격에 성공하면 추가적인 GA 발동만 구현했다
'포트폴리오 제작 > Project_P' 카테고리의 다른 글
| Project_P 몬스터 HP Bar 네트워크 동기화 (0) | 2025.12.22 |
|---|---|
| Project_P 플레이어 HP Bar 개선 (어트리뷰트셋 동기화) (0) | 2025.12.19 |
| Project_P Attack GA 로직 변경 (네트워크 지원) (0) | 2025.11.21 |
| Project_P 몬스터 AI 개선(행동트리 테스크를 활용하여 몬스터 회전) (0) | 2025.06.04 |
| Project_P AI Perception Damage Sense 적용 (0) | 2025.05.30 |