Unreal GAS (26) - Cursor Data Replication

wnsduf0000·2025년 12월 1일

Unreal_GAS

목록 보기
27/34
  • Send Target Data
    • 클라이언트에서 커서 클릭 시, 디버그 구체가 서버 측에서는 (0, 0, 0) 위치에 생성되던 문제를 수정하기 위해 서버 측에 데이터를 전송하기 위한 함수를 생성함.
      • AbilityTask의 Activate()에서, 서버와 클라이언트의 처리를 다르게 해야 함.
        이를 위한 함수 SendCursorData()를 생성해서 클라이언트인 경우 호출하도록 함.
        ```cpp
        void UTargetDataUnderCursor::Activate()
        {
        	if (Ability == nullptr) return;
        	
        	const bool bIsLocallyControlled = Ability->GetCurrentActorInfo()->IsLocallyControlled();
        	if (bIsLocallyControlled)
        	{
        		// 클라이언트에서 서버 측으로 정보를 전송하기 위한 함수
        		SendCursorData();
        	}
        	else
        	{
        		// On Server, listen for TargetData
        	}
        }
        ```
    • SendCursorData()의 구현을 위해 핵심적으로 알아야 할 함수는 AbilitySystemComponent의 ServerSetReplicatedTargetData(), AbilityTask의 ShouldBroadcastAbilityTaskDelegates(),
      그리고 FGameplayAbilityTargetTypes.h와 FScopedPredictionWindow이다.
       // TargetDataUnderCursor.cpp
       void UTargetDataUnderCursor::SendCursorData()
       {
       	// FScopedPredictionWindow 생성
       	FScopedPredictionWindow ScopedPrediction(AbilitySystemComponent.Get());
       
       	// 기존에 PlayerController의 GetHitResultUnderCursor()를 호출하던 부분
       	FHitResult HitResult;
       	APlayerController* PC = Ability->GetCurrentActorInfo()->PlayerController.Get();
       	PC->GetHitResultUnderCursor(ECC_Visibility, false, HitResult);
       
       	// FGameplayAbilityTargetTypes.h를 참조
       	FGameplayAbilityTargetData_SingleTargetHit* Data = new FGameplayAbilityTargetData_SingleTargetHit();
       	Data->HitResult = HitResult;
       
       	// FGameplayAbilityTargetData 상속 구조체들을 래핑하는 핸들을 생성하여 추가
       	FGameplayAbilityTargetDataHandle DataHandle;
       	DataHandle.Add(Data);
       
       	AbilitySystemComponent->ServerSetReplicatedTargetData(
       		GetAbilitySpecHandle(), 
       		GetActivationPredictionKey(), 
       		DataHandle, 
       		FGameplayTag(), 
       		AbilitySystemComponent->ScopedPredictionKey);
       
       	if (ShouldBroadcastAbilityTaskDelegates())
       	{
       		OnCursorLocation.Broadcast(DataHandle);
       	}
       }
       ```
       
    • GameplayAbilityTargetTypes.h 내에는 여러 종류의 TargetData가 미리 정의되어 있음.
      FGameplayAbilityTargetData_LocationInfo, FGameplayAbilityTargetData_ActorArray, FGameplayAbilityTargetData_SingleTargetHit가 있는데,
      이 중에서 SingleTargetHit가 FHitResult를 전송하기 위한 것으로 목적과 일치함.
      • 따라서 FGameplayAbilityTargetData_SingleTargetHit* 변수를 선언하고, 이 구조체의 멤버변수인 HitResult에 PlayerController의 GetHitResultUnderCursor()로 얻은 HitResult를 대입해줌.
      • TargetData를 서버로 전송하는 작업은 AbilitySystemComponent에서 제공하는 함수를 사용하면 된다.
        • AbilitySystemComponent의 ServerSetReplicatedTargetData()를 사용하며, 매개변수를 하나씩 살펴보면 아래와 같다.
          • FGameplayAbilitySpecHandle AbilityHandle
            • AbilityTask들은 모두 자신이 소속된 GameplayAbility에 대한 AbilitySpecHandle을 GetAbilitySpecHandle()을 통해 얻어올 수 있다.
          • FPredictionKey AbilityOriginalPredictionKey
            • AbilityTask가 소속된 GameplayAbility에 대한 PredictionKey로, 이는 GetActivationPredictionKey()를 통해 얻어올 수 있다.
          • const FGameplayAbilityTargetDataHandle& ReplicatedTargetDataHandle
            • FGameplayAbilityTargetData_SingleTargetHit가 FGameplayAbilityTargetData를 상속하는데, 이를 래핑하는 FGameplayAbilityTargetDataHandle을 만들어서 TargetData를 추가해 준 후 넘겨주면 된다.
          • FGameplayTag ApplicationTag
            • 임시로 빈 FGameplayTag를 만들어서 전달한다.
          • FPredictionKey CurrentPredictionKey
            • AbilityOriginalPredictionKey는 GameplayAbility에 대한 PredictionKey이며, CurrentPredictionKey는 현재 컨텍스트에 대한 PredictionKey를 의미한다고 한다.
            • AbilitySystemComponent를 통해 ScopedPredictionKey를 얻어올 수 있다.
              다만 이는 변수일 뿐으로, 해당 변수를 AbilitySystemComponent.h에서 살펴보면 아래와 같이 주석이 달려 있다.
               /** Current prediction key, set with FScopedPredictionWindow */
               FPredictionKey	ScopedPredictionKey;
            • 따라서, SendCursorData()의 시작 위치에 FScopedPredictionWindow 변수를 선언하고, 생성자 매개변수로 AbilitySystemComponent를 넘겨준다.
            • Prediction 범위를 지정해주는 것으로, 로컬에서 실행할 수 있도록 서버에게 요청하는 개념으로 보면 된다고 한다.
      • GameplayAbility가 활성화 되어 있지 않거나 하여 Broadcast()를 하면 안되는 경우를 감지하기 위해서 ShouldBroadcastAbilityTaskDelegates()를 호출한다.
        이 함수는 어빌리티가 아직 존재하고, Active 상태인지를 확인하는 기능을 한다.
  • Receive Target Data
    • Activate()에서 클라이언트쪽의 구현이 되었으니, 서버 측에서 TargetData를 수신받은 경우를 구현해야 함.
      void UTargetDataUnderCursor::Activate()
      {
      	if (Ability == nullptr) return;
      	
      	const bool bIsLocallyControlled = Ability->GetCurrentActorInfo()->IsLocallyControlled();
      	if (bIsLocallyControlled)
      	{
      		SendCursorData();
      	}
      	else
      	{
      		// On Server, listen for TargetData
      		const FGameplayAbilitySpecHandle SpecHandle = GetAbilitySpecHandle();
      		const FPredictionKey ActivationPredictionKey = GetActivationPredictionKey();
      
      		AbilitySystemComponent->AbilityTargetDataSetDelegate(SpecHandle, ActivationPredictionKey)
      			.AddUObject(this, &UTargetDataUnderCursor::OnTargetDataReplicatedCallback);
      		const bool bCalledDelegate = AbilitySystemComponent->CallReplicatedTargetDataDelegatesIfSet(SpecHandle, ActivationPredictionKey);
      
      		if (!bCalledDelegate)
      		{
      			SetWaitingOnRemotePlayerData();
      		}
      	}
      }
      • TargetData가 서버에 도달하면 델리게이트 Broadcast()가 발생하며, 해당 델리게이트는 AbilitySystemComponent의 함수 AbilityTargetDataSetDelegate()를 통해 가져올 수 있다.
        • AbilityTargetDataSetDelegate()의 작동에 대한 설명
          • AbilityTargetDataSetDelegate()는 FAbilityTargetDataSetDelegate라는 이름의 델리게이트를 반환한다. 해당 함수의 구현을 살펴보면 아래와 같다.
            // AbilitySystemComponent_Abilities.cpp
            FAbilityTargetDataSetDelegate& UAbilitySystemComponent::AbilityTargetDataSetDelegate(FGameplayAbilitySpecHandle AbilityHandle, FPredictionKey AbilityOriginalPredictionKey)
            {
            	return AbilityTargetDataMap.FindOrAdd(FGameplayAbilitySpecHandleAndPredictionKey(AbilityHandle, AbilityOriginalPredictionKey))->TargetSetDelegate;
            }
            // GameplayAbilityTargetTypes.h
            /** Generic callback for returning when target data is available */
            DECLARE_MULTICAST_DELEGATE_TwoParams(FAbilityTargetDataSetDelegate, const FGameplayAbilityTargetDataHandle&, FGameplayTag);
            • AbilityTargetDataMap은 AbilitySystemComponent에 존재하는 FGameplayAbilityReplicatedDataContainer형의 변수이다.
              /** 
               *	Associative container of GameplayAbilitySpecs + PredictionKeys --> FAbilityReplicatedDataCache. Basically, it holds replicated data on the ability system component that abilities access in their scripting.
               *	This was refactored from a normal TMap. This mainly servers to:
               *		1. Return shared ptrs to the cached data so that callsites are not vulnerable to the underlying map shifting around (E.g invoking a replicated event ends the ability or activates a new one and causes memory to move, invalidating the pointer).
               *		2. Data is cleared on ability end via ::Remove.
               *		3. The FAbilityReplicatedDataCache instances are recycled rather than allocated each time via ::FreeData.
               * 
               **/
              struct FGameplayAbilityReplicatedDataContainer
              {
              	GAMEPLAYABILITIES_API TSharedPtr<FAbilityReplicatedDataCache> Find(const FGameplayAbilitySpecHandleAndPredictionKey& Key) const;
              	GAMEPLAYABILITIES_API TSharedRef<FAbilityReplicatedDataCache> FindOrAdd(const FGameplayAbilitySpecHandleAndPredictionKey& Key);
              
              	void Remove(const FGameplayAbilitySpecHandleAndPredictionKey& Key);
              	void PrintDebug();
              
              private:
              
              	typedef TPair<FGameplayAbilitySpecHandleAndPredictionKey, TSharedRef<FAbilityReplicatedDataCache>> FKeyDataPair;
              
              	TArray<FKeyDataPair> InUseData;
              	TArray<TSharedRef<FAbilityReplicatedDataCache>> FreeData;
              };
              • FGameplayAbilityReplicatedDataContainer는 GameplayAbilityTypes에 있는 구조체로, FindOrAdd()는 FAbilityReplicatedDataCache의 TSharedRef를 반환한다.
                • TPair의 형태로 FGameplayAbilitySpecHandleAndPredictionKey와 FAbilityReplicatedDataCache의 TSharedRef로 짝지어져 있는 private 변수를 볼 수 있는데, FindOrAdd는 바로 이 TPair에서 값을 찾는 것이다.
                  • 즉, TMap처럼 키를 통해서 값을 찾는 것이고, 그렇기 때문에 FGameplayAbilitySpecHandleAndPredictionKey에 해당하는 FGameplayAbilitySpecHandle과 FPredictionKey를 매개변수로 요구한 것이다.
              • FAbilityReplicatedDataCache는 특정 GameplayAbility에 대한 캐싱된 데이터를 정의하기 위한 구조체로, 클라이언트→서버 간의 동기화가 이뤄진다 한다.
                • 이 구조체의 FAbilityTargetDataSetDelegate 변수인 TargetSetDelegate를 얻어와 거기에 바인딩하기 위해서 AbilityTargetDataSetDelegate()를 호출했던 것이다.

                  /** Struct defining the cached data for a specific gameplay ability. This data is generally synchronized client->server in a network game. */
                  struct GAMEPLAYABILITIES_API FAbilityReplicatedDataCache
                  {
                  	/** What elements this activation is targeting */
                  	FGameplayAbilityTargetDataHandle TargetData;
                  
                  	/** What tag to pass through when doing an application */
                  	FGameplayTag ApplicationTag;
                  
                  	/** True if we've been positively confirmed our targeting, false if we don't know */
                  	bool bTargetConfirmed;
                  
                  	/** True if we've been positively cancelled our targeting, false if we don't know */
                  	bool bTargetCancelled;
                  
                  	/** Delegate to call whenever this is modified */
                  	FAbilityTargetDataSetDelegate TargetSetDelegate;
                  
                  	/** Delegate to call whenever this is confirmed (without target data) */
                  	FSimpleMulticastDelegate TargetCancelledDelegate;
                  
                  	/** Generic events that contain no payload data */
                  	FAbilityReplicatedData	GenericEvents[EAbilityGenericReplicatedEvent::MAX];
                  
                  	/** Prediction Key when this data was set */
                  	FPredictionKey PredictionKey;
                  
                  	FAbilityReplicatedDataCache() : bTargetConfirmed(false), bTargetCancelled(false) {}
                  	virtual ~FAbilityReplicatedDataCache() { }
                  
                  	/** Resets any cached data, leaves delegates up */
                  	void Reset()
                  	{
                  		bTargetConfirmed = bTargetCancelled = false;
                  		TargetData = FGameplayAbilityTargetDataHandle();
                  		ApplicationTag = FGameplayTag();
                  		PredictionKey = FPredictionKey();
                  		for (int32 i=0; i < (int32) EAbilityGenericReplicatedEvent::MAX; ++i)
                  		{
                  			GenericEvents[i].bTriggered = false;
                  			GenericEvents[i].VectorPayload = FVector::ZeroVector;
                  		}
                  	}
                  
                  	/** Resets cached data and clears delegates. */
                  	void ResetAll()
                  	{
                  		Reset();
                  		TargetSetDelegate.Clear();
                  		TargetCancelledDelegate.Clear();
                  		for (int32 i=0; i < (int32) EAbilityGenericReplicatedEvent::MAX; ++i)
                  		{
                  			GenericEvents[i].Delegate.Clear();
                  		}
                  	}
                  };
          • 결과적으로 GetAbilitySpecHandle()과 GetActivateionPredictionKey()를 통해서 FAbilityTargetDataSetDelegate를 반환받는 것이다.
      • 서버에서 Activate()가 호출되는 즉시, 서버는 FAbilityTargetDataSetDelegate에 바인딩을 실시하려 한다. 하지만 이 처리가 늦어서 이미 클라이언트 측에서 TargetData가 전송되고, 델리게이트가 Broadcast()된 경우를 처리하기 위해, AbilitySystemComponent에서 제공하는 함수인 CallReplicatedTargetDataDelegatesIfSet()이 있다.
        • CallReplicatedTargetDataDelegatesIfSet()가 false인 경우, 클라이언트의 데이터를 기다려야 하므로 SetWaitingOnRemotePlayerData()를 호출해준다.
          • 그런데 이러한 처리는 클라이언트 측의 데이터가 먼저 RPC 되버리고, 그 이후에 뒤늦게야 서버 측에서 Activate()가 실행되어서 문제가 되는것이 아닌가?
            그렇다면 서버가 클라이언트 측의 데이터를 더 기다리는 것이 무슨 의미가 있나? 혹은 대기하면 클라이언트 측의 데이터를 업데이트 해주는 처리가 되어있나?
      • FAbilityTargetDataSetDelegate는 const FGameplayAbilityTargetDataHandle&, FGameplayTag를 매개변수로 한다.
        이와 일치하는 형태의 함수 OnTargetReplicatedCallback()을 생성해준다.
        ```cpp
        void UTargetDataUnderCursor::OnTargetDataReplicatedCallback(const FGameplayAbilityTargetDataHandle& DataHandle, FGameplayTag ActivationTag)
        {
        	AbilitySystemComponent->ConsumeClientReplicatedTargetData(GetAbilitySpecHandle(), GetActivationPredictionKey());
        	if (ShouldBroadcastAbilityTaskDelegates())
        	{
        		OnCursorLocation.Broadcast(DataHandle);
        	}
        }
        ```
        
        - 서버에서 제때 Activate()가 실행되고 FAbilityTargetDataSetDelegate에 함수를 바인딩하면, 서버에서 TargetData를 제때 활용할 수 있고, 이 TargetData를 활용했다고 처리해주어야 한다. 이를 위한 함수가 AbilitySystemComponent의 ConsumeClientReplicatedTargetData()이다.
        - 그 후 SendCursorData()에서 한 것처럼, ShouldBroadcastAbilityTaskDelegates()를 검사하고 델리게이트 Broadcast()를 실시함.
    • AssetManager 상속 클래스의 StartInitialLoading()에서 반드시 UAbilitySystemGlobals::Get().InitGlobalData()를 호출해주어야 한다.
      (InitTargetDataScriptStructCache()를 호출하기 위함)
       void UAuraAssetManager::StartInitialLoading()
       {
       	Super::StartInitialLoading();
       
       	FAuraGameplayTags::InitializeNativeGameplayTags();
       
       	UAbilitySystemGlobals::Get().InitGlobalData();
       }
       
profile
저는 게임 개발자로 일하고 싶어요

0개의 댓글