Replicated

** [Drag Down] GAS Local Prediction ** with 밀기 애니메이션 본문

언리얼 엔진/Drag Down (캡스톤 디자인)

** [Drag Down] GAS Local Prediction ** with 밀기 애니메이션

라구넹 2025. 3. 26. 15:03

* 애니메이션 설정 말고 GAS 구조 설계는 좀 더 밑에 있음

 

애니메이션 인스턴스, 블루프린트 다 만들었는데, 새로 받은 애셋이 구버전 스켈레톤 쓴다

새로 받은 거에 블루프린트 만들어두긴 했는데, 블루프린트 성능은 좀 떨어지고 이미 애니메이션 인스턴스 클래스 만들기도 해서 아깝고, 직접 만들어서 아는게 컨트롤하기 편하니 새로 만들자

저번 글이랑 똑같은 방식으로 쭉 다시 만든다

 

몽타주 생성

우 스트레이트, 좌 훅, 하이킥, 돌려차기

 

이제 이걸 어빌리티에서 실행하도록 할 것..

 

 

자 이제 문제는 구조이다

입력 -> 어빌리티 발동 -> 애니메이션 실행 -> 애니메이션 특정 타이밍에 Notify를 통해 밀기 어빌리티 발동 -> 밀기 태스크 발동 -> 밀기 타겟 액터 발동 -> 어빌리티로 돌아와서 밀기

이런 구조로 해보자

 

AnimNotify에서 어빌리티 발동 시 GAS 꼬일 수도 있는 듯

근데 일단 어빌리티 두 개를 쓰는 방식이 마음에 안드는데? 너무 복잡하지 않나

 

그래서 찾은 방법!

기존 어빌리티에서 WaitGameplayEvent 태스크 사용

Anim Notify에서 Gameplay Event 발생시키면 되는 거 아닌가??

 

그리고 태스크 하나 더 써서 타겟 액터 소환하면 되는 거다!

일단 태그를 날려야 하니 게임플레이 태그를 키자

 

저 이벤트를 AnimNotify에서 날리자

 

몽타주 재생용 슬롯 생성

 

그리고 이제 밀기 어빌리티를 만들어준다


구조

DDGA_PushingCharacter

In ActivateAbility,

- UAbilityTask_PlayMontageAndWait -> 애니메이션 실행

- UAbilityTask_WaitGameplayEvent -> Event.PushTrigger 발생 시까지 대기

 

Event.PushTrigger 발생 시,

OnPushingEventReceived 발동

-- UDDAT_MultiTrace -> 타겟팅 태스크 실행

--- ADDTA_MultiTrace -> 멀티 타겟팅 (SweepMultiByChannel 이용)

-- UDDAT_MultiTrace -> OnTargetDataReadyCallback에서 받고 태스크 종료

- DDGA_PushingCharacter에서 데이터 수신 및 처리

 

DDGA_PushingCharacter는 그리고 애니메이션이 콤보로 나오도록 처리

 

void UDDGA_PushingCharacter::ActivateAbility(const FGameplayAbilitySpecHandle Handle, 
	const FGameplayAbilityActorInfo* ActorInfo, 
	const FGameplayAbilityActivationInfo ActivationInfo, 
	const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);

	// Montage task
	UAbilityTask_PlayMontageAndWait* MontageTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
		this,
		NAME_None,
		PushingMontage,     // UAnimMontage* 타입
		1.0f,               // 플레이 속도
		NAME_None,          // Start Section
		false               // Stop when ability ends
	);

	MontageTask->OnCompleted.AddDynamic(this, &UDDGA_PushingCharacter::OnMontageCompleted);
	MontageTask->ReadyForActivation();

	// Wait task
	UAbilityTask_WaitGameplayEvent* EventTask = UAbilityTask_WaitGameplayEvent::WaitGameplayEvent( 
		this,
		FGameplayTag::RequestGameplayTag(FName("Event.PushTrigger"))
	);
	EventTask->EventReceived.AddDynamic(this, &UDDGA_PushingCharacter::OnPushingEventReceived);
	EventTask->ReadyForActivation();

	// Target & Damage

}

일단 애니메이션 실행 및 태그 대기 코드까지 작성

 

void UDDGA_PushingCharacter::OnMontageCompleted()
{
	bool bReplicatedEndAbility = true;
	bool bWasCancelled = false;
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}

void UDDGA_PushingCharacter::OnPushingEventReceived(FGameplayEventData Payload)
{
	UDDAT_MultiTrace* TraceTask = UDDAT_MultiTrace::CreateTask(this, ADDTA_MultiTrace::StaticClass());
	TraceTask->OnComplete.AddDynamic(this, &UDDGA_PushingCharacter::OnTraceResultCallback);
	TraceTask->ReadyForActivation();
}

각각 처리는 일단 어빌리티 종료되도록 해줌

 

이제 이걸 Local Prediction이 되도록 설계

UDDGA_PushingCharacter::UDDGA_PushingCharacter()
{
	NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
	InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;

	static ConstructorHelpers::FObjectFinder<UAnimMontage> PushingMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/Animation/Montage/AM_Manny_Pushing.AM_Manny_Pushing'"));
	if ( PushingMontageRef.Succeeded() )
	{
		PushingMontage = PushingMontageRef.Object;
	}
}

NetExecutionPolicy를 LocalPredicted로 설정하고

 

void ADDCharacterPlayer::GASInputPressed(int32 InputId)
{
	HandleGASInputPressed(InputId);

	if( !HasAuthority() )
	{
		ServerGASInputPressed(InputId);
	}
}

void ADDCharacterPlayer::GASInputReleased(int32 InputId)
{
	HandleGASInputReleased(InputId);

	if (!HasAuthority())
	{
		ServerGASInputReleased(InputId);
	}
}

클라이언트에서 미리 실행할 수 있도록 함수 콜 구조 변경 (원래 클라 측에선 발동을 아예 못하게 해놓음)

 

void ADDCharacterPlayer::HandleGASInputPressed(int32 InputId)
{
	if (!ASC)
	{
		return;
	}

	FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromInputID(InputId);
	if (Spec)
	{
		if (Spec->InputPressed) return;
		Spec->InputPressed = true;
		if (Spec->IsActive())
		{
			ASC->AbilitySpecInputPressed(*Spec);
		}
		else
		{
			ASC->TryActivateAbility(Spec->Handle, true);
		}
	}
}

또한 TryActivateAbility에서 두번째 인자를 true로 둠으로써 로컬 실행을 허용

 

이렇게 해두고 로그를 찍어보자

클라이언트에서 어빌리티 발동 시 프레딕션이 두 개가 뜬다?

뭔가 이상한데

 

로그를 더 자세히 찍어보자

일단 넷모드2는 서버고, 

 

if (ActivationInfo.GetActivationPredictionKey().IsValidKey())
{
	UE_LOG(LogDD, Warning, TEXT("[NetMode %d] Client-side(%s) prediction running"), 
		GetWorld()->GetNetMode(), *ActorInfo->AvatarActor.Get()->GetName());
}

if (HasAuthority(&ActivationInfo))
{
	UE_LOG(LogDD, Warning, TEXT("[NetMode %d] Server-side(%s) authority running"), 
		GetWorld()->GetNetMode(), *ActorInfo->AvatarActor.Get()->GetName()); 
}

일단 로그를 이렇게 찍었는데

 

3명 만들어보자

 

마지막 두 줄이 서버에서 어빌리티 실행한 거다

모든 클라이언트가 아니라, 실행한 클라이언트만 서버에서 보인다

 

근데 로그 조건은?

키가 서버에 없을 수가 있나?

클라이언트 -> 서버로 프레딕션 키를 보내는데 당연히 있을 것이다.

 

정상이다

이제 성능 테스트를 해보자

 

테스트 결과, 로컬에서 작동하여 레이턴시가 별로 없을 것임에도

클라이언트의 입력을 반영하여 서버 작동보다 0.02초 가량 미리 발동된다

 

프레딕션 설정은 됐고, 다음은 콤보 액션을 구현해야 한다

그런데 GAS 프레딕션 이용하려면 콤보 하나 하나 어빌리티로 만들어야 하지 않나?

구조는 좀 더 생각해보고, 다음 글에서 작업하자