참고 자료

Actor Owner and Owning Connection

  • Unreal Engine은 Server가 모든 권한을 가지는(server-authoritative) client-server model을 채용하고 있다.
    • 이 model에서 client는 중앙화 된 Server에 접속하게 된다.
  • Client가 Server에 접속을 하게 되면 그에 대응하는 NetConnection이 생성한다.
    • 이 때 NetConnection은 PlayerController의 Owner가 된다.
  • Client가 Server에서 Play를 시작하면, Player Controller은 Client가 Control 할 pawn을 possess한다.
    • 이 때 PlayerController는 Pawn의 Owner가 된다.
  • Actor의 Owning Connection은 Actor를 소유한 PlayerController의 Owning Connection과 연관되어 있다.
  • Owner와 Owning Connection은 어느 Client에서 Replicate와 RPC를 전달해야하는지 판단한다.

용처

Actor Replication

  • Actor의 변경사항을 어느 Connection에 Replicate하는지 판단이 필요.
  • PlayerController와 같이 Actor를 소유한 Connection에서만 Replicate를 받는 경우에는 
    bOnlyRelevantToOwner 트리거가 True로 설정되어 있다.
    • PlayerController의 경우에는 이 플래그가 기본적으로 설정되어 있다.

Property Replication

  • Owner에 따른 Property Replication 조건이 붙은 경우에 사용된다.

RPCs

  • Multicast RPC가 아니라면 Client RPC가 어느 Client에 전달되어야 하는지 판단이 필요

NetRole이 ROLE_AutonomousProxy인 Actor

  • 해당 Actor의 Owner가 아닌 NetConnection에 대한 Property Replication이 발생하면
    Role이 ROLE_SimulatedProxy로 변경된다.

Role

/** The network role of an actor on a local/remote network context */
UENUM(BlueprintType)
enum ENetRole : int
{
	/** No role at all. */
	ROLE_None UMETA(DisplayName = "None"),
	/** Locally simulated proxy of this actor. */
	ROLE_SimulatedProxy UMETA(DisplayName = "Simulated Proxy"),
	/** Locally autonomous proxy of this actor. */
	ROLE_AutonomousProxy UMETA(DisplayName = "Autonomous Proxy"),
	/** Authoritative control over the actor. */
	ROLE_Authority UMETA(DisplayName = "Authority"),
	ROLE_MAX UMETA(Hidden),
};

NetRole

ROLE_SimulatedProxy

  • 수동적인(Approximate) 프록시라는 의미
  • 클라이언트에서 모조(Simulated) 프록시로 동작한다.
    • 기본적인 물리 동작만 Simulation하고 자체적인 판단을 하지 않는다.

ROLE_AutonomousProxy

  • 능동적인(Autonomous) 프록시라는 의미
  • 클라이언트에서 Simulation이 아닌 Prediction 로직을 갖는다.

ROLE_Authority

  • 해당 Connection에서 Actor에 대한 완전하고 절대적인(Authoritative) 제어권을 갖는다는 뜻

Actor Role

Local Role

  • 현재 자신 PC에서의 NetRole을 지칭

Remote Role

  • 자신이 원격으로 연결되어 있는 PC에서의 NetRole을 지칭

Server/Client에서의 Role 값

Server

  • Unreal Engine은 Replicate Actor에 대해 모든 Authority를 Server가 갖는다.
    • 때문에 Server의 모든 Actor는 LocalRole이 ROLE_Authority가 된다.
  • 특정 PlayerController가 Actor에 대한 Ownership을 가진 경우,
    RemoteRole은 ROLE_AutonomousProxy가 된다.
  • 그 외의 Actor들에 대해서는 ROLE_SimulatedProxy가 된다.

Client

  • 해당 Client의 Connection이 Ownership을 갖는 Actor의 경우,
    Prediction이 가능하기 때문에 LocalRole이 ROLE_AutonomousProxy가 된다.
  • 반대로 자신이 OwnerShipe을 갖지 않는 Actor의 경우,
    Simulation만이 가능하기 때문에 LocalRole이 ROLE_SimulatedProxy가 된다.
  • 어느 Actor든, Replicate가 되면 Authority는 Server가 가지기 때문에 RemoteRole이 ROLE_Authority가 된다.
  • 단, Client에서만 존재하는 Local Actor의 경우에는 LocalRole이 ROLE_Authority,
    RemoteRole이 ROLE_None이다.

Dormancy

  • 가장 영향력이 강한 Network Optimize 중 하나
    • Project에서 자주 Replicate 되지 않는 Actor가 많을수록 효과적이다.
  • NetDriver는 Connection의 모든 Replicated Actor를 수집하고, 이를 Iterate하여 Replicate 대상을 결정한다.
    • Dormancy는 특정 Actor를 Dormant 상태로 두어 NetDriver로부터 Actor가 수집되지 않도록 한다.
    • Actor가 많을수록 Iterate 비용이 부담되기에 Actor 수집을 필터링하는 것은 매우 효과적이다.

사용법

  1. Actor의 Constructor에서 NetDormancy를 초기화
    • 보통은 DORM_DormantAll로 초기화
    • Actor가 Map에 배치된 경우, DORM_Initial로 초기화
  2. NetDormancy가 DORM_DormantAll/DORM_Initial일 때에는 Dormant 상태가 되어 Replicate가 되지 않는다.
    • Dormant 상태의 Actor의 값을 변경하더라도 Awake/Flush를 하면 변경사항이 보존되지 않는다.
  3. Replicated Property를 변경하기 전 FlushNetDormancy/ForceNetUpdate/SetNetDormancy 함수를 호출.
    • 일반적으로는 값을 변경 후 함수 호출을 해도 Replicate 될 수 있다.
    • 하지만 정식 스펙 상으로는 올바른 사용법도 아니니 이를 지양해야 한다.
    • 대표적으로 Fast Array의 값을 변경한 후 함수를 호출하면 변경사항이 Replicate되지 않는다.
  4. Replicated Property 값을 변경
  • 다시 한번 강조하지만 이는 값이 자주 변경되지 않는 Replicated Actor에 적합하다.
    • 너무 자주 값이 바뀌면 Dormant<->Awake/Flush 오버헤드가 발생.
  • Dormancy는 Relavancy Handling과 달리 Dormant 상태가 되더라도 Server/Client 양쪽에서 Actor가 존재한다.
    • Dormant Actor은 Relavancy 검증에서 제외된다.

ENetDormancy

/** Describes if an actor can enter a low network bandwidth dormant mode */
UENUM(BlueprintType)
enum ENetDormancy : int
{
	/** This actor can never go network dormant. */
	DORM_Never UMETA(DisplayName = "Never"),
	/** This actor can go dormant, but is not currently dormant. Game code will tell it when it go dormant. */
	DORM_Awake UMETA(DisplayName = "Awake"),
	/** This actor wants to go fully dormant for all connections. */
	DORM_DormantAll UMETA(DisplayName = "Dormant All"),
	/** This actor may want to go dormant for some connections, GetNetDormancy() will be called to find out which. */
	DORM_DormantPartial UMETA(DisplayName = "Dormant Partial"),
	/** This actor is initially dormant for all connection if it was placed in map. */
	DORM_Initial UMETA(DisplayName = "Initial"),

	DORM_MAX UMETA(Hidden),
};

Awake Method

  • NetDormancy 변수는 Public 접근자로 선언되어 있지만, 가급적 Awake Method 사용을 권장한다.
  • 변경사항을 NetDriver에 알리는 기능이 Awake Methond에 내장되어 있다.

SetNetDormancy

/** Puts actor in dormant networking state */
UFUNCTION(BlueprintAuthorityOnly, BlueprintCallable, Category = "Networking")
ENGINE_API void SetNetDormancy(ENetDormancy NewDormancy);

void AActor::SetNetDormancy(ENetDormancy NewDormancy)
{
	if (IsNetMode(NM_Client))
	{
		return;
	}

	if (IsPendingKillPending())
	{
		return;
	}

	ENetDormancy OldDormancy = NetDormancy;
	NetDormancy = NewDormancy;
	const bool bDormancyChanged = (OldDormancy != NewDormancy);

	if (UWorld* MyWorld = GetWorld())
	{
		if (FWorldContext* const Context = GEngine->GetWorldContextFromWorld(MyWorld))
		{
			// Tell driver about change
			if (bDormancyChanged)
			{
				for (FNamedNetDriver& Driver : Context->ActiveNetDrivers)
				{
					if (Driver.NetDriver != nullptr && Driver.NetDriver->ShouldReplicateActor(this))
					{
						Driver.NetDriver->NotifyActorDormancyChange(this, OldDormancy);
					}
				}
			}

			// If not dormant, flush actor from NetDriver's dormant list
			if (NewDormancy <= DORM_Awake)
			{
				// Since we are coming out of dormancy, make sure we are on the network actor list
				MyWorld->AddNetworkActor(this);

				for (FNamedNetDriver& Driver : Context->ActiveNetDrivers)
				{
					if (Driver.NetDriver != nullptr && Driver.NetDriver->ShouldReplicateActor(this))
					{
						Driver.NetDriver->FlushActorDormancy(this);
					}
				}
			}
		}
	}
}
  • Dormant Actor의 NetDormancy 값을 수정해 Awake/Dormant 상태를 변경할 수 있다.
    • Map에 배치된 Dormant Actor의 경우, Awake 이후 DORM_Initial 대신 DORM_DormantAll로 설정되어야 한다.
  • Dormant Actor가 매 Frame마다 움직일 때 사용하면 적합하다.

FlushNetDormancy

/** Forces dormant actor to replicate but doesn't change NetDormancy state (i.e., they will go dormant again if left dormant) */
UFUNCTION(BlueprintAuthorityOnly, BlueprintCallable, Category="Networking")
ENGINE_API void FlushNetDormancy();

/** Removes the actor from the NetDriver's dormancy list: forcing at least one more update. */
void AActor::FlushNetDormancy()
{
	if (IsNetMode(NM_Client) || NetDormancy <= DORM_Awake || IsPendingKillPending())
	{
		return;
	}

	QUICK_SCOPE_CYCLE_COUNTER(NET_AActor_FlushNetDormancy);

	bool bWasDormInitial = false;
	if (NetDormancy == DORM_Initial)
	{
		// No longer initially dormant
		NetDormancy = DORM_DormantAll;
		bWasDormInitial = true;
	}

	// Don't proceed with network operations if not actually set to replicate
	if (!bReplicates)
	{
		return;
	}

	if (UWorld* const MyWorld = GetWorld())
	{
		// Add to network actors list if needed
		MyWorld->AddNetworkActor(this);
	
		if (FWorldContext* const Context = GEngine->GetWorldContextFromWorld(MyWorld))
		{
			for (FNamedNetDriver& Driver : Context->ActiveNetDrivers)
			{
				if (Driver.NetDriver != nullptr && Driver.NetDriver->ShouldReplicateActor(this))
				{
					Driver.NetDriver->FlushActorDormancy(this, bWasDormInitial);
				}
			}
		}
	}
}
  • Dormant Actor의 변경사항을 Replicate
    • Actor의 NetDormancy를 변경하지 않고도 연결된 Updates를 강제로 Replicate한다.
    • 단, NetDormancy가 DORM_Initial인 Actor는 NetDormancy가 DORM_DormantAll로 변경된다.
  • BP에서는 Dormant Actor의 Replicated Property를 수정하면 자동으로 FlushNetDormancy가 호출된다.
    • 5.4 버전 기준, ActorComponent 한정으로 Replicated Property를 수정해도 FlushNetDormancy가 호출되지 않는다.

ForceNetUpdate

/** Force actor to be updated to clients/demo net drivers */
UFUNCTION( BlueprintCallable, Category="Networking")
ENGINE_API virtual void ForceNetUpdate();

void AActor::ForceNetUpdate()
{
	UNetDriver* NetDriver = GetNetDriver();

	if (GetLocalRole() == ROLE_Authority)
	{
		// ForceNetUpdate on the game net driver only if we are the authority...
		if (NetDriver && NetDriver->GetNetMode() < ENetMode::NM_Client) // ... and not a client
		{
			NetDriver->ForceNetUpdate(this);

			if (NetDormancy > DORM_Awake)
			{
				FlushNetDormancy();
			}
		}
	}

	// Even if not authority, other drivers (like the demo net driver) may need to ForceNetUpdate
	if (UWorld* MyWorld = GetWorld())
	{
		if (FWorldContext* const Context = GEngine->GetWorldContextFromWorld(MyWorld))
		{
			for (FNamedNetDriver& Driver : Context->ActiveNetDrivers)
			{
				if (Driver.NetDriver != nullptr && Driver.NetDriver != NetDriver && Driver.NetDriver->ShouldReplicateActor(this))
				{
					Driver.NetDriver->ForceNetUpdate(this);
				}
			}
		}
	}
}
  • FlushNetDormancy가 호출되고, 다음 NetUpdate에서 해당 Actor가 Replication 대상으로 고려된다.
    • Actor가 단일 Frame에서 간헐적으로 1회성 Update를 발생하는 경우에 유용
  • Actor가 Flush/Awake 된 후에 다시 Dormant 설정을 하더라도 즉시 Dormant 상태가 되지 않는다.
    • 때문에 여러 Update를 전송할 수 있다.
    • 해당 Actor와 SubObject에서 Replicate 되어야 할 Updates가 없을 때까지 Replicate 한다.
  • 또한 DormancyHysteresis가 활성화 된 경우에도 Dormant 상태가 즉각 적용되지 않는다.
  • UActorHannel::ReadyForDormancy
    • 더보기
      bool UActorChannel::ReadyForDormancy(bool suppressLogs)
      {
      	// We need to keep replicating the Actor and its subobjects until none of them have
      	// changes, and would otherwise go Dormant normally.
      	if (!bIsInDormancyHysteresis)
      	{
      		for (auto MapIt = ReplicationMap.CreateIterator(); MapIt; ++MapIt)
      		{
      			if (!MapIt.Value()->ReadyForDormancy(suppressLogs))
      			{
      				return false;
      			}
      		}
      	}
      
      	if (DormancyHysteresis > 0 && Connection && Connection->Driver)
      	{
      		bIsInDormancyHysteresis = true;
      		const double TimePassed = Connection->Driver->GetElapsedTime() - LastUpdateTime;
      		if (TimePassed < DormancyHysteresis)
      		{
      			return false;
      		}
      	}
      
      	return true;
      }
  • FObjectReplicator::ReadyForDormancy
    • 더보기
      bool FObjectReplicator::ReadyForDormancy(bool bSuppressLogs)
      {
      	if (GetObject() == nullptr)
      	{
      		UE_LOG(LogRep, Verbose, TEXT("ReadyForDormancy: Object == nullptr"));
      		return true;		// Technically, we don't want to hold up dormancy, but the owner needs to clean us up, so we warn
      	}
      
      	// Can't go dormant until last update produced no new property updates
      	if (!bLastUpdateEmpty)
      	{
      		if (!bSuppressLogs)
      		{
      			UE_LOG(LogRepTraffic, Verbose, TEXT("    [%d] Not ready for dormancy. bLastUpdateEmpty = false"), OwningChannel->ChIndex);
      		}
      
      		return false;
      	}
      
      	if (FSendingRepState* SendingRepState = RepState.IsValid() ? RepState->GetSendingRepState() : nullptr)
      	{
      		if (SendingRepState->HistoryStart != SendingRepState->HistoryEnd)
      		{
      			// We have change lists that haven't been acked
      			return false;
      		}
      
      		if (SendingRepState->NumNaks > 0)
      		{
      			return false;
      		}
      
      		if (!SendingRepState->bOpenAckedCalled)
      		{
      			return false;
      		}
      
      		if (SendingRepState->PreOpenAckHistory.Num() > 0)
      		{
      			return false;
      		}
      
      		// Can't go dormant if there are unAckd property updates
      		for (FPropertyRetirement& Retirement : SendingRepState->Retirement)
      		{
      			if (Retirement.Next != nullptr)
      			{
      				if (!bSuppressLogs)
      				{
      					UE_LOG(LogRepTraffic, Verbose, TEXT("    [%d] OutAckPacketId: %d First: %d Last: %d "), OwningChannel->ChIndex, OwningChannel->Connection->OutAckPacketId, Retirement.OutPacketIdRange.First, Retirement.OutPacketIdRange.Last);
      				}
      				return false;
      			}
      		}
      	}
      
      	return true;
      }

Awake Method를 사용해야 하는 경우

  • Actor가 Awake 되면 Replicated Property의 값을 Shadow State를 Reinitialize한다.
    • Shadow State는 변경된 Property와 Replicated Property를 비교하는데 사용한다.
    • 때문에 Domant Actor의 Replicated Property 값을 변경 하더라도 Awake 과정에서 변경사항이 탐지되지 않는다.

Dormancy with Replication Graph

  • ReplicationGraph를 사용하더라도 Dormancy는 Default NetDriver와 동일하게 동작해야 하기 때문에,
    Project에서는 Actor의 Dormant/Awake 세팅과 FlushNetDormancy 호출이 동일하게 이루어진다.
  • ReplicationGraphNode에서 Actor List를 수집할 때 Dormant Actor가 반환되더라도 아래 함수에서 건너뛴다.
void UReplicationGraph::ReplicateActorListsForConnections_Default(UNetReplicationGraphConnection* ConnectionManager, FGatheredReplicationActorLists& GatheredReplicationListsForConnection, FNetViewerArray& Viewers)
{				
//-------Skip-------//
    // Skip if dormant on this connection. We want this to always be the first/quickest check.
    if (ConnectionData.bDormantOnConnection)
    {
        DO_REPGRAPH_DETAILS(PrioritizedReplicationList.GetNextSkippedDebugDetails(Actor)->bWasDormant = true);
        if (bDoCulledOnConnectionCount)
        {
            DormancyClassAccumulator.Increment(Actor->GetClass());
        }
        continue;
    }
//-------Skip-------//
}
  • ReplicationGraphNode는 Dormant Actor에 대한 특별한 Handling이 포함될 수 있다.
    • 이를 통해 Node의 Dormant Actor 처리 시간, 메모리 뿐 아니라 Actor List의 크기도 줄일 수 있다.
    • 예를 들어 ReplicationGraphNode_GridSpatialization2D의 경우,
      Dormant Actor은 Static으로 취급하고 Awake Actor은 Dynamic으로 취급하는 Handling이 포함되어 있다.
    • 이러한 Handling은 보통은 정지 및 Dormant이지만 가끔 Grid를 통과하는 Actor에 유용하다.

Debug

Log

  • LogNetDormancy 로그 카테고리를 활성화하여 Dormant 정보를 가져올 수 있다.
    • 상세도를 높이면 Actor의 NetDormancy가 Flush될 때처럼 더 자세한 정보가 기록된다.

Console Command

NetPriority

  • Unreal Engine의 Network Update는 Bandwidth 제한으로 모든 Actor의 Replicate를 보장하지 않는다.
    • 만약 Update 용량이 Bandwidth를 초과하면 자체적인 Load Balancing을 통해 NetPriority를 할당한다.
  • NetPriority가 높을수록 더 중요한 Actor이므로 더 많은 Bandwidth가 할당된다.

Actor의 NetPriority 구하기

/** Priority for this actor when checking for replication in a low bandwidth or saturated situation, higher priority means it is more likely to replicate */
UPROPERTY(Category=Replication, EditDefaultsOnly, BlueprintReadWrite)
float NetPriority;
  • Actor의 Replicate 빈도 차이는 NetPriority의 비율과 일치하다.
    • NetPriority가 3.0인 Actor는 1,0인 Actor보다 3배의 빈도로 Update 된다.
  • 일반적으로 Actor는 1.0, Pawn과 PlayerController는 3.0의 초기값을 가진다.

Current NetPriority 구하기

/**
 * Function used to prioritize actors when deciding which to replicate
 * @param ViewPos		Position of the viewer
 * @param ViewDir		Vector direction of viewer
 * @param Viewer		"net object" owned by the client for whom net priority is being determined (typically player controller)
 * @param ViewTarget	The actor that is currently being viewed/controlled by Viewer, usually a pawn
 * @param InChannel		Channel on which this actor is being replicated.
 * @param Time			Time since actor was last replicated
 * @param bLowBandwidth True if low bandwidth of viewer
 * @return				Priority of this actor for replication, higher is more important
 */
ENGINE_API virtual float GetNetPriority(const FVector& ViewPos, const FVector& ViewDir, class AActor* Viewer, AActor* ViewTarget, UActorChannel* InChannel, float Time, bool bLowBandwidth);

/**
 * Defines in NetworkDistanceConstants.h
 * CLOSEPROXIMITY: 500
 * NEARSIGHTTHRESHOLD: 2000
 * MEDSIGHTTHREHOLD: 3162
 * FARSIGHTTHRESHOLD: 8000
 */
float AActor::GetNetPriority(const FVector& ViewPos, const FVector& ViewDir, AActor* Viewer, AActor* ViewTarget, UActorChannel* InChannel, float Time, bool bLowBandwidth)
{
	if (bNetUseOwnerRelevancy && Owner)
	{
		// If we should use our owner's priority, pass it through
		return Owner->GetNetPriority(ViewPos, ViewDir, Viewer, ViewTarget, InChannel, Time, bLowBandwidth);
	}

	if (ViewTarget && (this == ViewTarget || GetInstigator() == ViewTarget))
	{
		// If we're the view target or owned by the view target, use a high priority
		Time *= 4.f;
	}
	else if (!IsHidden() && GetRootComponent() != NULL)
	{
		// If this actor has a location, adjust priority based on location
		FVector Dir = GetActorLocation() - ViewPos;
		float DistSq = Dir.SizeSquared();

		// Adjust priority based on distance and whether actor is in front of viewer
		if ((ViewDir | Dir) < 0.f)
		{
			if (DistSq > NEARSIGHTTHRESHOLDSQUARED)
			{
				Time *= 0.2f;
			}
			else if (DistSq > CLOSEPROXIMITYSQUARED)
			{
				Time *= 0.4f;
			}
		}
		else if ((DistSq < FARSIGHTTHRESHOLDSQUARED) && (FMath::Square(ViewDir | Dir) > 0.5f * DistSq))
		{
			// Compute the amount of distance along the ViewDir vector. Dir is not normalized
			// Increase priority if we're being looked directly at
			Time *= 2.f;
		}
		else if (DistSq > MEDSIGHTTHRESHOLDSQUARED)
		{
			Time *= 0.4f;
		}
	}

	return NetPriority * Time;
}
  • Base NetPriority에 Viewer와의 거리, 마지막 Replicate 이후 시간 등을 복합적으로 판단해 결정된다.
  • 만약 NetPriority를 Customize하고 싶다면 이 함수를 Override해야 한다.
    • 단, 이는 매우 높은 숙련도와 이해도를 요구한다.

Reference

NetRelevancy

  • Level 상의 Actor들 중 Server상에서 시야 안에 들어오는 Actor들이나 Client에 영향을 Actor들만 Replicate하는 방식
    • Runtime 중에 Spawn/Replicate 되는 Actor들은 Relevant 하지 않으면 Client에서 제거된다.
    • 제거된 Actor의 경우에는 Client에서 더이상 보이지 않는다.

Actor의 현재 Relevancy

/** 
 * Checks to see if this actor is relevant for a specific network connection
 *
 * @param RealViewer - is the "controlling net object" associated with the client for which network relevancy is being checked (typically player controller)
 * @param ViewTarget - is the Actor being used as the point of view for the RealViewer
 * @param SrcLocation - is the viewing location
 *
 * @return bool - true if this actor is network relevant to the client associated with RealViewer 
 */
ENGINE_API virtual bool IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const;

bool AActor::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
	if (bAlwaysRelevant || IsOwnedBy(ViewTarget) || IsOwnedBy(RealViewer) || this == ViewTarget || ViewTarget == GetInstigator())
	{
		return true;
	}
	else if (bNetUseOwnerRelevancy && Owner)
	{
		return Owner->IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
	}
	else if (bOnlyRelevantToOwner)
	{
		return false;
	}
	else if (RootComponent && RootComponent->GetAttachParent() && RootComponent->GetAttachParent()->GetOwner() && (Cast<USkeletalMeshComponent>(RootComponent->GetAttachParent()) || (RootComponent->GetAttachParent()->GetOwner() == Owner)))
	{
		return RootComponent->GetAttachParent()->GetOwner()->IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
	}
	else if(IsHidden() && (!RootComponent || !RootComponent->IsCollisionEnabled()))
	{
		return false;
	}

	if (!RootComponent)
	{
		UE_LOG(LogNet, Warning, TEXT("Actor %s / %s has no root component in AActor::IsNetRelevantFor. (Make bAlwaysRelevant=true?)"), *GetClass()->GetName(), *GetName() );
		return false;
	}

	return !GetDefault<AGameNetworkManager>()->bUseDistanceBasedRelevancy ||
			IsWithinNetRelevancyDistance(SrcLocation);
}
  • Network Driver는 IsNetRelevantFor를 통해 Actor가 각 Connection과 Relevant한지를 판단한다.
    • Relevancy를 Customize하고 싶다면 이 함수를 Override해야 한다.
    • 다만 Override 작업은 높은 이해도와 난이도를 요구한다.
  • 참고로 Actor를 상속받은 Class 중 Pawn과 PlayerController는 위 함수를 Override하여 다른 로직으로 동작한다.

Actor Relevant 생성

/** Forces this actor to be net relevant if it is not already by default	 */
ENGINE_API virtual void ForceNetRelevant();

void AActor::ForceNetRelevant()
{
	if ( !NeedsLoadForClient() )
	{
		UE_LOG(LogSpawn, Warning, TEXT("ForceNetRelevant called for actor that doesn't load on client: %s" ), *GetFullName() );
		return;
	}

	if (RemoteRole == ROLE_None)
	{
		SetReplicates(true);
		bAlwaysRelevant = true;
		if (NetUpdateFrequency == 0.f)
		{
			NetUpdateFrequency = 0.1f;
		}
	}
	ForceNetUpdate();
}
  • Actor에서 ForceNetRelevant를 호출해 강제로 해당 Actor에 Relevancy를 부여할 수 있다.

Customize Relevancy Settings

  • Actor를 상속받은 Class는 사진의 옵션 또는 C++에서 Relevancy Setting을 Customize 할 수 있다.

bAlwaysRelevant

  • 모든 Client에서 조건 없이 Replicate 된다.

bNetUseOwnerRelevancy

  • 해당 Actor의 Owner에게 Relevant 할 때에 Replicate 된다.
  • 모든 Client에게 Replicate 되지 않고, Owner와 Relevant한 Client들에게만 Replicate 된다.

bOnlyRelevantToOwner

  • 해당 Actor의 Owner에게 Relevant 할 때에 Owner에게만 Replicate 된다.

Reference

'UE5 > Network' 카테고리의 다른 글

[Network] Remote Procedure Calls(RPCs)  (0) 2024.06.28
[Network] Property Replication  (1) 2024.06.28
[Network] Network Driver  (0) 2024.06.18
[Network] DemoNetDriver 및 Streamer  (0) 2024.06.17
[Network] Beacon  (0) 2024.06.17

참고 링크

더보기

기본 개념

Packet

  • UDP에서 사용하는 Packet과 동한 개념
  • 여러 개의 Bunch로 구성되어 있음.

Bunch

  • 보통은 하나의 RPC나 Property Replication와 대응
    • Packet 최대 크기를 초과하면 여러 개로 쪼개진다.
    • 반대로 크기가 충분히 작으면 하나로 합쳐지기도 한다.
  • 정보를 수신할 때에는 FInBunch로 수신하고, 송신할 때에는 FOutBunch로 송신한다.

ACK/NAK

ACK

  • 일반적인 Network에서 사용하는 ACK와 동일한 개념
    • 상대방이 Packet을 성공적으로 수신 했을 때 수신 됨.
  • Ack 역시 Bunch의 형태를 띈다. 
    • 한 Packet에 여러 Bunch가 존재할 수 있듯, 한 Packet에 ACK Bunch 또한 여러 개 존재할 수 있다.

NAK

  • ACK의 반대되는 개념
    • 상대방이 Packet을 성공적으로 수신하지 못했을 때 수신 됨.
  • 실제로 구현 상 특정 Packet이 전달되지 못함을 정확하게 판단하는 것은 불가능하다.
    • 때문에 송신한 쪽에서 다른 Packet의 ACK를 통해 자체적으로 NAK 판단을 내린다.
    • 모든 Packet(ACK 포함)에는 PacketId가 입력되기 때문에 가능한 동작.
      • 예를 들어 PacketId 1, 2, 4가 수신되면 3에 해당하는 Packet은 NAK판정을 낸다.
      • 이 3번에 해당하는 Packet은 뒤늦게 수신하더라도 처리하지 않는다.
  • NAK에 대한 처리는 다음 코드를 통해 대략적으로 확인이 가능하다.
void UNetConnection::ReceivedPacket( FBitReader& Reader, bool bIsReinjectedPacket, bool bDispatchPacket )
{
/*-------Skip------*/
    // Process acks
    // Lambda to dispatch delivery notifications, 
    auto HandlePacketNotification = [&Header, &ChannelsToClose, this](FNetPacketNotify::SequenceNumberT AckedSequence, bool bDelivered)
    {
        // Increase LastNotifiedPacketId, this is a full packet Id
        ++LastNotifiedPacketId;
        ++OutTotalNotifiedPackets;
        Driver->IncreaseOutTotalNotifiedPackets();

        // Sanity check
        if (FNetPacketNotify::SequenceNumberT(LastNotifiedPacketId) != AckedSequence)
        {
            UE_LOG(LogNet, Warning, TEXT("LastNotifiedPacketId != AckedSequence"));

            Close(ENetCloseResult::AckSequenceMismatch);

            return;
        }

        if (bDelivered)
        {
            ReceivedAck(LastNotifiedPacketId, ChannelsToClose);
        }
        else
        {
            ReceivedNak(LastNotifiedPacketId);
        };
    };

    // Update incoming sequence data and deliver packet notifications
    // Packet is only accepted if both the incoming sequence number and incoming ack data are valid		
    const int32 UpdatedPacketSequenceDelta = PacketNotify.Update(Header, HandlePacketNotification);
/*-------Skip------*/
}

template<class Functor>
FNetPacketNotify::SequenceNumberT::DifferenceT FNetPacketNotify::Update(const FNotificationHeader& NotificationData, Functor&& InFunc)
{
	const SequenceNumberT::DifferenceT InSeqDelta = GetSequenceDelta(NotificationData);

	if (InSeqDelta > 0)
	{
		UE_LOG_PACKET_NOTIFY(TEXT("FNetPacketNotify::Update - Seq %u, InSeq %u"), NotificationData.Seq.Get(), InSeq.Get());
	
		ProcessReceivedAcks(NotificationData, InFunc);

		return InternalUpdate(NotificationData, InSeqDelta);
	}
	else
	{
		return 0;
	}
}

template<class Functor>
void FNetPacketNotify::ProcessReceivedAcks(const FNotificationHeader& NotificationData, Functor&& InFunc)
{
	if (NotificationData.AckedSeq > OutAckSeq)
	{
		SequenceNumberT::DifferenceT AckCount = SequenceNumberT::Diff(NotificationData.AckedSeq, OutAckSeq);

		UE_LOG_PACKET_NOTIFY(TEXT("Notification::ProcessReceivedAcks - AckedSeq: %u, OutAckSeq: %u AckCount: %u"), NotificationData.AckedSeq.Get(), OutAckSeq.Get(), AckCount);

		// Update InAckSeqAck used to track the needed number of bits to transmit our ack history
		// Note: As we might reset sequence history we need to check if we already have advanced the InAckSeqAck
		const SequenceNumberT NewInAckSeqAck = UpdateInAckSeqAck(AckCount, NotificationData.AckedSeq);
		if (NewInAckSeqAck > InAckSeqAck)
		{
			InAckSeqAck = NewInAckSeqAck;
		}
		
		// ExpectedAck = OutAckSeq + 1
		SequenceNumberT CurrentAck(OutAckSeq);
		++CurrentAck;

		// Make sure that we only look at the sequence history bit included in the notification data as the sequence history might have been reset, 
		// in which case we might not receive the max size history even though the ack-count is bigger than the history
		const SequenceNumberT::DifferenceT HistoryBits = NotificationData.HistoryWordCount * SequenceHistoryT::BitsPerWord;

		// Warn if the received sequence number is greater than our history buffer, since if that is the case we have to treat the data as lost
		// Note: This should normally not be a problem since we try to flush the sequence history before allowing an overshoot of the sequence history window on the sending side.
		// If this occurs with no hitches on server or client, there might be reason to investigate if too much data is being sent in which case the the size sequence history might have to be increased.
		if (AckCount > (SequenceNumberT::DifferenceT)(SequenceHistoryT::Size))
		{
			UE_LOG_PACKET_NOTIFY_WARNING(TEXT("FNetPacketNotify::ProcessReceivedAcks - Missed Acks: AckedSeq: %u, OutAckSeq: %u, FirstMissingSeq: %u Count: %u"), NotificationData.AckedSeq.Get(), OutAckSeq.Get(), CurrentAck.Get(), AckCount - (SequenceNumberT::DifferenceT)(SequenceHistoryT::Size));
		}

		// Everything not found in the history buffer is treated as lost
		while (AckCount > HistoryBits)
		{
			--AckCount;
			InFunc(CurrentAck, false);
			++CurrentAck;
		}

		// For sequence numbers contained in the history we lookup the delivery status from the history
		while (AckCount > 0)
		{
			--AckCount;
			UE_LOG_PACKET_NOTIFY(TEXT("Notification::ProcessReceivedAcks Seq: %u - IsAck: %u HistoryIndex: %u"), CurrentAck.Get(), NotificationData.History.IsDelivered(AckCount) ? 1u : 0u, AckCount);
			InFunc(CurrentAck, NotificationData.History.IsDelivered(AckCount));
			++CurrentAck;
		}
		OutAckSeq = NotificationData.AckedSeq;

		// Are we done waiting for an reset of the ack history?
		if (OutAckSeq > WaitingForFlushSeqAck)
		{
			WaitingForFlushSeqAck = OutAckSeq;
		}
	}
}

 

NetGUID

  • Object Pointer를 Network로 Replicate하기 위해 발급하는 ID
    • 여기서 Object는 Actor 뿐 아니라 Component도 포함된다.
    • 정확히는 UObject 단위로 구분하는 편이 적절하다.
  • Network에서 Object 식별이나 Replicate 여부를 판단하는 기준으로 사용된다.

대략적인 Unreal Network Architecture를 표현 한 그림

NetDriver

  • Game에서 Network를 총괄하는 객체
  • 크게 2가지 정보를 관리.
    • NetConnection
      • Server에서는 ClientConnection으로, Client에서는 ServerConnection으로 관리한다.
    • Connection별로 관리가 필요 없는 정보들

특징

연결 지향

  • NetConnection을 통해 연결이 되어 있어야 통신 가능.
  • 만약 비연결 프로토콜을 사용하면 Timeout 등을 처리하는 로직이 구현되어 있어야 한다.

Packet 형식

  • Stream 형식이 아닌 Packet 형식
  • 모든 데이터가 MAX_PACKET_SIZE 크기로 쪼개져서 독립된 Packet으로 저장된다.

비신뢰성

  • 신뢰성을 보장하는 패킷 송수신은 NteDriver에서 알 수 없는 고수준 계층에서 동작한다.

무결성

  • Packet의 데이터가 변조되지 않는다.

Connection별 관리가 필요 없는 정보들

FNetworkObjectInfo

  • Network에서 Replicate되는 Object에 대한 정보 및 포인터를 포함한 구조체
  • Replicated Actor의 현재 상태를 추적, 관리한다.

FReplicationChangelistMgr

  • 각각의 Replicated Object가 Replicate될 때까지의 변경사항들을 관리한다.
  • List가 가득 차게 된다면, 가장 먼저 들어온 것이 그 다음 것과 Merge 된다.
  • 이 구조체는 모든 Connection에서 사용된다.
    • 각 Connection에 어떤 정보를 전송해야 할지 결정하기 위한 비교 작업을 공유한다.
  • Connection은 마지막으로 Connection이 확인된 이후의 모든 변경사항을 전송한다.

FRepChangedPropertyTracker

  • Connection들이 공유하는 Property에 대한 메타 데이터를 저장.
    • 조건, 활성화 여부, Replay에 필요할 수 있는 외부 데이터와 무관하게 모든 메타데이터가 저장된다.
  • Object가 Replicate 될 때 어느 Property가 변경 되었는지 파악해 최적의 Network Data Packet을 구성한다.

NetConnection

Client가 Server에 접속을 요청할 때 NetConnection의 상호작용 구조

  • Game이 실질적으로 네트워크 통신을 하는 Host의 주체
  • Replicated Actor의 연결을 담당하는 (Actor)Channel을 관리한다.
    • PlayerController, GameMode 등도 이 Actor Channel을 통해 Replicate된다.
  • 때문에 하나의 Connection은 하나의 Server/Client로 취급된다.
    • Client의 경우 접속한 Server에 대응하는 NetConnection을 가진다.
    • Server의 경우 자신에게 접속한 Client에 대응하는 NetConnection들을 가진다.
      • 즉, NetConnection의 수가 Client의 수와 일치한다.

Channel

  • 실질적으로 Packet을 송/수신하는 주체
  • 각 Channel은 타겟팅하는 Object의 정보를 소유하고 있다.
  • 각 Connection들이 서로 다른 Channel을 가진다.
    • 예를 들어 4개의 Client가 연결되어 있고 Replicated Object가 200개라면,
      Client에 대응하는 NetConnection들은 각각 고유한 Actor Channel을 200개씩 가지게 된다.

ControlChannel

  • Client가 Server에 연결될 때 생성되는 Handshake용 Channel
  • 연결 설정, 인증, 상태 업데이트 등의 Control Message를 처리한다.

ActorChannel

  • Actor의 상태, Event를 Replicate하는 Channel
  • Actor 내 RepNotify, RPC이 발생하면 그 Actor의 ActorChannel을 통해 Client로 전달된다.

VoiceChannel

  • 음성 데이터를 송/수신 하는 Channel
  • Game 내 음성채팅 기능 구현에 사용된다.

Replication 판단 순서

Replicated Actor의 변경사항이 발생했을 때 Client로 Replicate되는 구조

  • Replicate 판단 여부는 UNetDriver::ServerReplicateActors에서 이루어진다.
  • Replicate가 결정된 Actor들은 UActorChannel::ReplicateActor 함수에서 세부 사항을 처리해 Replicte 된다.

NetUpdateFrequency

  • NetPriority를 통해 Replicate 여부를 판단

PreReplication

  • Replication 조건을 검사하거나 변수를 Update

bOnlyRelevantToOwner

  • NetRelevancy를 통해 Replicate 여부 판단
  • Owner에게만 Replicate 되어야 하는지 여부 결정
  • IsRelevancyOwnerFor, IsNetRelevantFor의 호출 조건을 결정

IsRelevancyOwnerFor

  • bOnlyRelevantToOwner가 True일 때에만 호출
  • bOnlyRelevantToOwner가 true일 때, 대상 Client와 Relevant한지 판단

IsNetRelevantFor

  • bOnlyRelevantToOwner가 false일 때, 어느 Client에 Relevant한지 판단

NetDormancy

  • Actor의 Dormancy(휴면) 여부를 관리
    • Dormant인 경우 Actor가 List에 등록되지 않아 Iterate 부하를 크게 줄여준다.
  • Network Traffic과 상태 변화의 빈도에 따라 조정된다.

'UE5 > Network' 카테고리의 다른 글

[Network] Property Replication  (1) 2024.06.28
[Network] Network Property  (0) 2024.06.25
[Network] DemoNetDriver 및 Streamer  (0) 2024.06.17
[Network] Beacon  (0) 2024.06.17
[Network] Replication Graph  (1) 2024.06.13

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/demonetdriver-and-streamers-in-unreal-engine?application_version=5.3

DemoNetDeriver

  • Streamer를 사용해 Replay 재생에 필요한 정보를 추출/기록
  • 시청 방식에 따라 Engine에 포함된 다양한 Streamer를 DemoNetDriver에 붙일 수 있음.

Streamer 변경

  • ReplayStreamerOverride 옵션에서 다른 Streamer Factory Module을 변경해 덮어쓸 수 있다.
    • InMemoryNetworkReplayStreaming
    • HttpNetworkReplayStreaming
  • 기본값인 LocalFileNetworkReplayStreaming은
    Default.ini의 NetworkReplayStreaming 섹션의 DefaultFactoryName 변수를 설정해 변경 가능
  • InitBase를 호출하고 적당한 URL 인수를 파라미터로 제공하는 것으로도 변경 가능
  • GameMode는 Replay를 보기 위해 ReplaySpectatorPlayerControllerClass에 지정된 Class를 사용한다.

시간 비용 분산

  • 콘솔 변수 demo.CheckpointSaveMaxMSPerFrame 값을 양수로 설정해
    리플레이 데이터 녹화에 필요한 시간 비용 분산할 수 있다.
  • Frame당 시간 제한 만료 전 Replay에 녹화되지 않은 Actor는 대기열에 등록 되었다가 다음 Frame에 녹화된다.
  • Checkpoint 녹화 및 Take에 드는 시간에 상한선을 두어 게임이 멈추는 현상을 방지하는데 도움이 된다.
  • Actor에 대한 Data에 들어있는 Checkpoint를 여러 Frame걸쳐 찍기 때문에
    재생 도중 약간의 시각적 오류가 발생할 수 있다.
  • 이 기능은 주로 게임에서 Checkpoint 녹화를 위해 지정된 시간 제한보다 오래 걸렸을 때에만 발동한다.
    • 저사양 머신 또는 퍼포먼스를 많이 요하는 게임에서 주로 적용된다는 의미이다.

녹화 우선순위 지정

  • 다음 조건을 만족하면 Replay에 저장되는 Actor의 녹화 순서를 지정 할 수 있다.
    • bPriotizeActors 옵션을 활성화
    • GetReplayPriority 함수를 Implement
  • MaxDesiredRecordTimeMS를 통한 분산 녹화와 함께 사용하면 더 효과적이다.

Checkpoint 녹화 빈도

  • 콘솔 변수 demo.CVarCheckpointUploadDelayInSeconds로 조절
    • 기본값: 30초
  • Checkpoint 사이 기간을 늘리면 Replay를 앞뒤로 문질러 이동하는 작업이 느려진다.
  • 대신 Replay 크기는 줄어든다.

녹화 중 임시 정지

  • bPauseRecording 변수를 true로 설정해 Demo 녹화 중 녹화를 임시 정지할 수 있다.
    • false로 설정하면 녹화가 재게된다.

대체 PlayerController 생성

  • SerViewerOverride를 사용하면 DemoNetDriver가 녹화 목적으로 사용되는 대체 PlayerController를 생성한다.
    • 이 Playercontroller는 Actor의 Network Relevancy, Curling, Priority 결정 방식을 변경할 수 있다.
  • 이 기능은 GamePlay에서 멀리서 일어나는 일을 알 수 없지만, Replay에서는 모든 것을 볼 수 있도록 할 때 좋다.

Slate와의 병렬 처리

  • Slate와 병렬 처리를 위해 아래 콘솔 변수가 0이 아닌 값으로 적용되어야 한다.
    • tick.DoAsyncEndOfFrameTasks
    • demo.ClientRecordAsyncEndOfFrame

주의점

  • Replay 생성 Actor는 Live Gameplay Actor와 마찬가지로 함수 호출을 한다.
  • 이는 Live Actor처럼 행동해 Replay Data를 최소화할 수도 있다.
  • 하지만 공유 Object에 영향을 주는 함수 호출이 Replay Actor에서도
    사용 가능해 원하지 않는 방식으로 Game의 상태에 영향을 줄 수 있다.
    • GameInstance, GameState, GameMode
  • 특히 Memory Streamer가 그러한데, 현재 Play 중인 Live GamePlay를 바로 시청하는 경우이다.
  • Actor에 영향을 주지 않도록 하는 보호 장치로,
    Actor가 Live인지 Replay Level의 일부인지 확인한 후 작업을 처리하는 것을 권장한다.
  • 이러한 문제는 거의 Game에서만 발생할 것이라서 각 Project에서 건 by 건으로 처리해줘야 한다.
  • 이러한 문제의 대표적인 예시로 Replay 도중 Player의 상태가 변하지만 점수가 변하지 않는 것이다.

Replay Data Format

  • Data 측면에서 Replay에는 3가지 게임 상태 정보와 더불어 약간의 Text Data가 추가로 저장된다.

기준 Data

  • Game World의 시작 상태를 설명하는 시작 부분

Checkpoint

  • World에 가해지는 전체 변경 사항의 Snapshot 역할을 한다.
  • 정기적인 사용지 지정 간격으로 나타난다.

점증적 변화량

  • Checkpoint 사이를 채워주는 요소
    • World의 개별 Object들이 각자의 점증적 변화량을 가진다.
  • 이를 통해 Game 내 어느 순간도 Engine에서 빠르고 정확하게 재구성 할 수 있다.

Text Data

  • 다음 2가지로 구성되어 있다.
    • Player가 보는 목록을 만드는데 사용할 수 있는 표시명
    • Game 목록의 검색 또는 필터링에 사용할 수 있는 Custom Text Tag
      • HTTP Streamer 전용

Replay 방식

  • World를 기준 Data로 초기화
  • 선택한 시간 이전의 Checkpoint가 구현된 상태로 변경
  • 가장 최근 Checkpoint 이후 지정된 시간 직전까지의 점증적 변화량 적용

Local File Streamer

  • DemoNetDriver가 가진 기본 Streamer 타입
  • Host Machine의 Replay Data를 로컬 저장장치의 단일 파일에 비동기 기록한다.
    • 때문에 Single Player Game 및 Replay를 로컬에서 재생하는 게임에 적합하다.
  • 저장된 Replay 배포와 관리가 쉽다.
  • 비동기 기록 기능이 있어 Console처럼 HDD 속도가 낮은 System의 Performance가 향상된다.
  • Replay Data File은 Project/Saved/Demos/ 폴더에 저장된다.
  • 확장자는 ".replay"이다.

SaveGame Replay Streamer

  • Replay를 SaveGame Slot을 선택해 저장하는 기능이 추가된 특수한 Local File Streamer
  • Client에 Replay를 저장한 뒤, 다음 Platform의 SaveGame 로드 용 Interface를 통해 읽을 수 있다. 
    •  SaveGame System 없이 Replay를 저장/보관/로드가 가능하다.
  • 주 목적은 SaveGame Slot에 복사되지 않은 Replay 식별/복사/재생/삭제 등의 작업을 Local File과 SaveGame Slot 양쪽에서 할 수 있는 보조 API 활용이다.
  • Console 게임에 적합함.

Null Streamer

  • Replay Data를 Local 디스크에 직접 작성한다.
    • Local 녹화 제작, Single Player Game에 좋다.
  • GamePlay Trailer나 Cut-Scene 영상 제작, Tutorial, TimeAttack Video 시청 및 공유를 할 수 있게 해준다.
  • 4.20 버전 이후로 Deprecated 되었지만 예전 Replay를 재생할 수 있도록 하위 호환성을 유지하고 있다.

Memory Streamer

  • 사용자가 지정할 수 있는 실행 길이의 Replay Data를 Client Machine의 Memory에 저장
  • 최근의 극적인 순간에 대한 즉석 Replay에 최적화 되어 있다. 
    • Sports Game의 즉석 Replay
    • Shooting Game의 Kill Cam
    • Action Game에서의 최종 보스 처치 장면

사용법 세부사항

  • Memory Streamer는 하나의 Session 도중 녹화와 재생, Gameplay가 재개되도록 되어 있다는 점에서 특별하다.
    • Player가 Replay를 시청하는 도중 LiveGame은 모습도 소리도 업싱 계속 진행시키고,
      Replay가 끝나면 매끄럽게 Game이 이어지도록 할 수 있다.
  • 로드 시간에 Engine은 Level을 Static, Dynamic Source, Dynamic Duplicate으로 모은다.
    • 이를 그룹을 통해 Live Gameplay 및 Replya System과의 Level 상호작용 방식이 다음과 같이 정해진다.

Static Level

  • Persistant Level이 아니면서 IsStatic이 마킹 된 Level
  • 작동 시 Gameplay 영향을 받지 않으며, Live Play와 Replay 양쪽에 모두 표시된다.

Dynamic Source Level

  • Persistant Level이면서 IsStatic이 마킹되지 않은 Sublevel
  • Live Gameplay의 영향을 받는다.
  • Replay 도중에는 숨겨지지만, Gameplay에는 여전히 정상 실행된다.

Dynamic Duplicate Level

  • 로드 시간에 Dynamic Source Level에서 복제한 사본
    • Dedicated Server나 Editor Mode에는 존재하지 않음
  • Live Gameplay 도중에는 숨겨진다.
  • Replay는 이 Level에서 발생한 뒤 비워진다.

효율적인 사용법

  • Dynamic Source Level에 대해 DemoNetDriver를 하나, Dynamic Duplicate Level에 대해 또 하나 만들 수 있다.
    • 이 경우 Dynamic Source Level의 Live Gameplay를 녹화한 뒤, Dynamic Duplicate Level에서 재생할 수 있다.
    • Replay 도중 Dynamic Source Level은 숨기고 Dynamic Duplicate Level을 표시하면
      Gameplay는 계속하면서 REplay에 영향받지 않는 Network Update를 받을 수 있다.
  • Static Level 그룹은 언제든 활성화시켜 보이도록 할 수 있다.
  • Live Gameplay에 영향을 받지 않은 것들이 저장 되므로 Replay Process에는 포함시킬 필요가 없다.
    • Static World Geometry
    • BGM
    • Particle
    • Animation
  • Dynamic Source Level은 소멸이나 정지된 적 없이 숨기기만 했을 뿐이다.
    • 그렇기에 Gameplay Replay 시청 도중에도 자연스럽게 진행되고,
      Dynamic Source Level을 다시 시청할 수 있도록 만들 수 있다.
  • 더불어 이 System에는 개발자가 Level을 Static 마킹하여 REplay 녹화 및 재생에서 제외도도록 하는 기능이 있다.
    • 결과적으로 Memory와 시간을 절약할 수 있다.

HTTP Streamer

  • Replay Data를 LAN이나 인터넷을 통해 다른 Machine에 전송할 때 사용
  • 다음 경우에 유용함
    • Dedicated Server Game
      • Server가 항상 Game 내 모든 곳에서 벌어지는 일을 알고 있음.
      • Replay Data 처리 작업에 들어가는 작업을 분산시켜
        단일 서버에서 동시 Hosting 할 수 있는 Game 수를 늘리려는 경우에 좋음.
    • 많은 수의 시청자에게 Live Streaming 방송을 하면서도 반응 속도는 좋게 유지해야 하는 경우
    • Live Streaming 경기나 경기 녹화 기능을 유지하여 언제든지 볼 수 있도록 할 때
  • Game을 실행하는 쪽이 완벽히 제어하는 Computer에서 Data를 캡쳐할 수 있어 치트 감지 툴 역할을 하기 좋다.

사용법 세부사항

  • HTTP Streamer REST API를 통해 Custom Replay Server와 통신한다.
    • GET 및 POST Method를 사용해 Binary나 JSON Format String으로 Data 전송
    • Server를 구성하기 위해서는 URL을 구성해야 한다.

Custom Replay Server URL 구성법

  • Project의 DefaultEngine.ini 파일의 [HttpNetworkReplayStreaming] 섹션 아래 ServerURL 변수
  • http://replay/yourgame.com/ << 이와같은 포맷
  • 마지막 /는 있으나 없으나 문제 없다

HTTP Streamer REST API

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/http-streamer-rest-api-for-unreal-engine?application_version=5.3

'UE5 > Network' 카테고리의 다른 글

[Network] Property Replication  (1) 2024.06.28
[Network] Network Property  (0) 2024.06.25
[Network] Network Driver  (0) 2024.06.18
[Network] Beacon  (0) 2024.06.17
[Network] Replication Graph  (1) 2024.06.13

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/using-online-beacons-in-unreal-engine?application_version=5.3

  • 일반 게임 접속을 통하지 않고 RPC를 통해 서버와 접촉해 가벼운 상호작용을 제공하는 Actor
  • Project에 맞는 상호작용, 로직, 정보 요청을 위한 Custom Class 확장을 권장
    • 특수한 경우에 내장 Class를 그대로 사용 하기도 함.

 

OnlineBeacon

  • 대표적인 사용 예시는 다음과 같다.
    • 서비스 품질 정보 요청
    • Client가 참여하고자 하는 Game의 Slot 예약
    • Player 이름 목록
    • 진행 중인 Game의 진행 시간 및 점수

AOnlineBeacon

  • AOnlineBeaconHost, AOnlineBeaconClient의 BaseClass

AOnlineBeaconHost

  • 별도의 NetDriver를 사용해 Remote Client에서 들어오는 OnlineBeacon Access를 Listen.
  • Access를 받으면 등록된 OnlineBeaconHostObject 인스턴스를 대상으로 일치하는 Client를 탐색,
    해당 Object에 Access를 넘겨준다.
  • 이 Class는 파생 Class를 만들 필요가 없다.
    • Client와 등록된 OnlineBeaconHostObject 사이의 초기 접속만 관리하기 때문이다.

AOnlineBeaconClient

  • Host에 접속하여 실제 RPC를 생성
    • 하나는 Client에서 생성
    • 다른 하나는 Server의 OnlineBeaconHost에 등록된 적합한 OnlineBeaconHostObject에 의해 생성
  • GetBeaconType 함수를 사용해 적합한 Host Object Class의 Register Instance에 일치시킨다.
    • 이 방식은 일반적으로 Server가 Spawn하고 Client에 Replicate하는 Actor Spawning 방식과 다르다.
    • 하지만 Client Object와 Server Object 사이에 접속이 이루어진 후,
      어느 한 쪽이 다른 쪽에 RPC를 할 수 있게 되면서 Object Replicate가 정상적으로 이루어지게 되며,
      Server Actor는 Property Replicate와 관련해 Authority를 가지게 된다.
  • OnConnected와 OnFailure 함수를 통해 접속 시 RPC를 호출하거나, 접속 실패 처리를 할 수 있다.
  • Beacon에서 요구하는 Client쪽 작업이 필요할 시 이 Class에서 구현되어야 한다.

AOnlineBeaconHostObject

  • OnlineBeaconClient Class와 짝을 이루도록 만들어져야 하는 Class
    •  Client의 GetBeaconType() 함수의 반환값과, BeaconTypeName에 저장된 값을 일치 시켜 짝을 이룬다.
  • OnlineBeaconHost에 Access를 요구하는 OnlineBeaconClient에 대응하는 OnlineBeaconHostObject가 탐지 되면,
    OnlineBeaconHostObject::SpawnBeaconActor를 통해 OnlineBeaconClient의 사본을 Spawn
    • SpawnBeaconActor로 ClientBeaconActorClass 변수를 사용해 Sapwn 할 Actor Class를 결정
    • 이 CleintBeaconActorClass가 짝을 이룬 OnlineBeaconClient Class로 설정 되어야 함.
  • Spawn 된 OnlineBeaconClient에서 SetBeaconOwner도 호출해야 Client와 통신을 할 수 있다.
  • 위의 기능들은 대부분 BaseClass에서 이루어지기 때문에 Override 할 필요가 없다.

PartyBeacon

  • Party 기반 매치메이킹을 위해 특별히 설계된 클래스
  • Multiplay Game에서 파티 형성 및 GameSession 접속을 관리

PartyBeaconClient

  • Server에 있는 PartyBeaconHost와 통신해 Party 매치메이킹을 요청
    • Server에 Party 정보 전송
    • 성공 여부를 받아 처리하는 Client Interface

PartyBeaconHost

  • Server에서 각 Party의 요청에 따라 GameSession 할당

PartyBeaconState

  • 현재 Party의 상태와 Session의 예약 정보를 관리
    • Party 크기
    • Session에 접속된 Party 수
    • 가능한 최대 Party 수
  • Server로부터 예약 요청 처리에 필요한 정보들을 제공

SpectatorBeacon

  • 관전자 모드를 위한 특수 구성 요소
  • Multiplayer Game에서 관전자가 게임을 관찰할수 있도록 한다.
    • 관전자가 Server에 효율적으로 접속하고, 게임 진행을 관찰할 수 있게하는 과정을 관리

SpectatorBeaconClient

  • Client에서 관전자가 GameServer에 접속 요청을 할 때 사용
  • Server의 SpectatorBeaconHost와 통신해 관전 가능한 세션을 요청, 입장 승인 여부를 확인

SpectatorBeaconHost

  • Server에서 실질적인 관전자의 접속 요청을 수신, 관리
  • 관전자에게 제공할 GameSession을 관리
  • 관전자의 접속을 허가하거나 거부할 수 있는 권한이 있음

SpectatorBeaconState

  • 관전자 접속 상태와 관려 정보를 관리
    • 관전 가능한 GameSession의 상태
    • 현재 관전자 수
    • 최대 관전자 수
  • SpectatorBeaconHost가 이 정보를 기반으로 판단을 내린다.

VoiceSynthSystem

  • 음성 통신 기능을 담당하는 구성 요소
  • Game 내에서 Player간의 실시간 음성 커뮤니케이션이 가능하도록 한다.

VoiceEnvineImpl

  • Unreal Engine의 음성 엔진 구현 Class
    • Hardware와 OS에 특화된 음성 처리를 담당
  • 음성 데이터의 Capture, 처리, 재생을 관리

VoiceInterfaceImpl

  • 음성 데이터 인터페이스의 구현을 제공
  • 다양한 Network 환경에서 음성 데이터의 Reliable 한 전송을 보장
  • 음성 데이터 Pakcet의 생성과 송수신 Interface를 정의

VoicePacketBuffer

  • 음성 데이터 Packet을 임시로 저장하는 버퍼 역할
    • Network 지연, Data Packet 손실을 관리하는데 유용

VoicePacketImpl

  • 실제 음성 데이터 Packet의 구현체
  • 음성 데이터를 압축해 Packet 형태로 만드는 역할

VoiceListenerSynthComponent

  • Unreal Engine의 합성 음성 Component
  • 게임 내 음성 데이터를 재생하는 기능 제공

'UE5 > Network' 카테고리의 다른 글

[Network] Property Replication  (1) 2024.06.28
[Network] Network Property  (0) 2024.06.25
[Network] Network Driver  (0) 2024.06.18
[Network] DemoNetDriver 및 Streamer  (0) 2024.06.17
[Network] Replication Graph  (1) 2024.06.13

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/replication-graph-in-unreal-engine?application_version=5.3

https://www.unrealengine.com/ko/tech-blog/replication-graph-overview-and-proper-replication-methods

 

  • 일반적으로 Replicate되는 Actor들은 매 Tick마다 해당 Actor가 Replicate될지 말지 판단을 한다.
    • 게임이 클수록, 동시접속자가 많을수록 CPU 병목현상이 발생하기 쉬워짐
  • Replication Graph는 Actor 단위의 Replicate 판단 여부를 Node 단위로 묶어서 비용을 감소시킨다.

동작 방식

  • 기본적인 동작 방식은 이전의 Actor Replicate와 크게 다르지 않다.
    • 특정 공간 안의 Actor를 별도로 관리(Spatial Partitioning)
    • 대상 클라이언트로부터 먼 거리의 Actor를 제외 (Relevancy Culling)
    • Replicate 빈도 관리 (Frequency Control)

ReplicationGraphNode

  • 동일한 조건으로 Replicate 되어야 할 Actor를 하나로 묶어주는 단위
  • BaseClass인 UReplicationGraphNode는 Pure Virtual Function이 있어서 반드시 이를 상속 받은 Class를 써야 한다.
    • 최소 아래 함수들은 반드시 Implement 되어야 한다.
/** Override this function to initialize the per-class data for replication */
//Initialize UReplicationGraph::GlobalActorReplicationInfoMap.
virtual void InitGlobalActorClassSettings();

/** Override this function to init/configure your project's Global Graph */
//Instantiate new UGraphNodes via ::CreateNewNode. Use ::AddGlobalGraphNode if they are global (for all connections).
virtual void InitGlobalGraphNodes();

//Route actor spawning/despawning to the right node. (Or your nodes can gather the actors themselves)
virtual void RouteAddNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo, FGlobalActorReplicationInfo& GlobalInfo);
virtual void RouteRemoveNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo);

/** Override this function to init/configure graph for a specific connection. Note they do not all have to be unique: connections can share nodes (e.g, 2 nodes for 2 teams) */
//Initialize per-connection nodes (or associate shared nodes with them via ::AddConnectionGraphNode)
virtual void InitConnectionGraphNodes(UNetReplicationGraphConnection* ConnectionManager);
  • Node 안에 별도의 Child Node를 가질 수 있다.

ReplicationGraphNode_ActorList

  • 간단한 Actor 목록을 관리
  • 복잡한 Frequency나 Relevancy 로직 없이 관리 된다.
  • 소규모 목록에서 사용할 때 유용

ReplicationGraphNode_ActorListFrequencyBuckets

  • Replicate 빈도에 따라 Actor들을 Bucket 단위로 관리
  • 다양한 간격으로 업데이트가 필요한 Actor를 관리할 때 유용

ReplicationGraphNode_AlwaysRelevant

  • 위치, 상태와 관계 없이 모든 Client에게 항상 Replicate 되어야 하는 경우
  • GameMode, Controller, GameState와 같은 GamePlay에 주요한 Actor에게 적합

ReplicationGraphNode_AlwaysRelevant_ForConnection

  • AlwaysRelevant와 유사하지만 그 대상을 특정 Client에게만 제한할 수 있다.
  • 특정 Player에게 필수적인 Actor를 Replicate할 때 적합

ReplicationGraphNode_ConnectionDormancyNode

  • 비활성화 될 여지가 있는 Actor들에 적합
  • 또는 배경의 Actor와 같이 업데이트를 제한하여 자원을 최적화 하는데 적합

ReplicationGraphNode_DormancyNode

  • Actor가 활성화 될 때 모든 Client에 Replicate되어야 하는 경우
  • 간헐적으로 활성화 되는 Actor들에게 유용

ReplicationGraphNode_DynamicSpatialFrequency

  • 공간적 조건에 따라 Replicate 빈도를 동적으로 조절
  • Player의 이동이나 게임 내 Event로 인해 Relevancy가 자주 변경되는 환경에 이상적

ReplicationGraphNode_GridSpatialization2D

  • World를 Grid로 나누고, Grid 위치에 따라 Replicate를 관리
  • 넓은 오픈월드 게임에서 플레이어 주변 Actor만 업데이트하여 네트워크 트래픽 최적화

ReplicationGraphNode_TearOff_ForConnnection

  • Tear Off(서버에서 어느정도 분리된 상태)된 Actor들을 관리
  • 해당 Actor들을 새로운 Client에 적절하게 Replicate 되도록 보장
  • Actor가 급격하게 변경되는 빠른 페이스의 액션이나 파괴 시나리오에서 사용됨.

ReplicationGraph

  • Replication Driver의 기능을 확장한 Class
  • 다수의 ReplicationGraphNode를 관리한다.

작업 방식

Node 추가

  • 기존에 만들었던 ReplicationGraphNode를 InitGlobalGraphNodes에서 생성, 추가한다.
void UBasicReplicationGraph::InitGlobalGraphNodes()
{
	// -----------------------------------------------
	//	Spatial Actors
	// -----------------------------------------------

	GridNode = CreateNewNode<UReplicationGraphNode_GridSpatialization2D>();
	GridNode->CellSize = 10000.f;
	GridNode->SpatialBias = FVector2D(-UE_OLD_WORLD_MAX, -UE_OLD_WORLD_MAX);

	AddGlobalGraphNode(GridNode);

	// -----------------------------------------------
	//	Always Relevant (to everyone) Actors
	// -----------------------------------------------
	AlwaysRelevantNode = CreateNewNode<UReplicationGraphNode_ActorList>();
	AddGlobalGraphNode(AlwaysRelevantNode);
}

Actor 추가

  • AddNetworkActor에서 RouteAddNetworkActorToNodes를 호출
  • RouteAddNetworkActorToNodes에서 등록된 Node를 통해 NotifyAddNetworkActor로 Actor 등록
void UReplicationGraph::AddNetworkActor(AActor* Actor)
{
	LLM_SCOPE_BYTAG(NetRepGraph);
	QUICK_SCOPE_CYCLE_COUNTER(UReplicationGraph_AddNetworkActor);

	if (IsActorValidForReplicationGather(Actor) == false)
	{
		return;
	}

	if (NetDriver && !NetDriver->ShouldReplicateActor(Actor))
	{
		return;
	}

	bool bWasAlreadyThere = false;
	ActiveNetworkActors.Add(Actor, &bWasAlreadyThere);
	if (bWasAlreadyThere)
	{
		// Guarding against double adds
		return;
	}

	ensureMsgf(!Actor->bNetTemporary, TEXT("ReplicationGraph does not support bNetTemporary. Actor: %s has bNetTemporary set."), *Actor->GetPathName());

	// Create global rep info	
	FGlobalActorReplicationInfo& GlobalInfo = GlobalActorReplicationInfoMap.Get(Actor);
	GlobalInfo.bWantsToBeDormant = Actor->NetDormancy > DORM_Awake;

	RouteAddNetworkActorToNodes(FNewReplicatedActorInfo(Actor), GlobalInfo);
}

 

void UReplicationGraph::RouteAddNetworkActorToNodes(const FNewReplicatedActorInfo& ActorInfo, FGlobalActorReplicationInfo& GlobalInfo)
{
	// The base implementation just routes to every global node. Subclasses will want a more direct routing function where possible.
	for (UReplicationGraphNode* Node : GlobalGraphNodes)
	{
		Node->NotifyAddNetworkActor(ActorInfo);
	}
}

Node 탐색

  • 매 Tick마다 ServerReplicateActors를 호출
  • 함수 내부에서 조건에 맞는 Node를  FNewReplicatedActorInfo로 검색
/** This is the struct we use to push new replication actors into the graph. "New" doesn't mean "newly spawned" it means "new to the graph". FIXME: Please suggest a better name! */
struct FNewReplicatedActorInfo
{
	explicit FNewReplicatedActorInfo(const FActorRepListType& InActor) : Actor(InActor), Class(InActor->GetClass())
	{
		StreamingLevelName = GetStreamingLevelNameOfActor(Actor);
	}

	explicit FNewReplicatedActorInfo(const FActorRepListType& InActor, FName OverrideLevelName)
		: Actor(InActor)
		, StreamingLevelName(OverrideLevelName)
		, Class(InActor->GetClass())
	{
	}

	AActor* GetActor() const { return Actor; }

	REPLICATIONGRAPH_API static FName GetStreamingLevelNameOfActor(const AActor* Actor);

	FActorRepListType Actor;
	FName StreamingLevelName;
	UClass* Class;
};

설정 방법

ini 파일 설정

  • Project의 Default.ini에서 아래와 같이 ReplicationDriverClassName 추가
[/Script/OnlineSubsystemUtils.IpNetDriver]
ReplicationDriverClassName="/Script/ProjectName.ClassName"

Instance 반환 함수 Bind

UReplicationDriver::CreateReplicationDriverDelegate().BindLambda([](UNetDriver* ForNetDriver, const FURL& URL, UWorld* World) -> UReplicationDriver*
{
	return NewObject<UMyReplicationDriverClass>(GetTransientPackage());
});

NetReplicationGraphConnection

  • 각 Client 연결에 대한 Replicate Data를 관리
    • ReplicationGraph가 ReplicationDriver를 상속받아 NetDriver의 역할을 한다면, 
      NetReplicationGraphConnection은 ReplicationConnectionDriver를 상속받아 NetConnection의 역할을 한다.
  • ReplcationGraph에서 Replication이 결정 된다면,
    NetReplcationgraphConnection은 해당 Client에게 최적화 된 데이터를 전달해준다.

일반적인 사용 기준

  • Actor의 위치에 따라 Group을 나눈다.
  • 비활성화된 Actor를 식별하여 별도의 목록으로 관리한다.
  • Character가 주워서 들고 다닐 수 있다면, 소유자와 같이 업데이트 한다.
  • 모든 Client가 Replicate 받는 특수 목록을 만든다.
  • 특정 Client에 Relevancy를 갖는 특수 목록을 만든다.

'UE5 > Network' 카테고리의 다른 글

[Network] Property Replication  (1) 2024.06.28
[Network] Network Property  (0) 2024.06.25
[Network] Network Driver  (0) 2024.06.18
[Network] DemoNetDriver 및 Streamer  (0) 2024.06.17
[Network] Beacon  (0) 2024.06.17

https://velog.io/@doorbals_512/언리얼-엔진-멀티-플레이어-시스템-Part1

 

[Unreal]언리얼 엔진 멀티 플레이어 시스템 Part1

언리얼 엔진 멀티 플레이어 시스템 이해하기 Part1

velog.io

https://velog.io/@doorbals_512/Unreal언리얼-엔진-멀티-플레이어-시스템-Part2

 

[Unreal]언리얼 엔진 멀티 플레이어 시스템 Part2

언리얼 엔진 멀티 플레이어 시스템 이해하기 Part2

velog.io

 

실제 코드나 함수 호출을 알 수는 없지만, 전반적인 구조를 잘 설명해주는 블로그.

특히 도식화가 알아보기 쉽게 잘 정리되어 있어서 개괄 구조만을 확인하기에는 이보다 좋은 예시를 본 적이 없다.

'메모장 > 참고링크' 카테고리의 다른 글

Unreal Engine Network Architecture  (0) 2024.06.12

https://sites.google.com/site/techaht/trans/unreal-net-arch

 

techaht - 언리얼 네트워킹 아키텍처

원문: http://unreal.epicgames.com/Network.htm 저자: 팀 스위니(Tim Sweeny) 기록: 2007년 5월 - 최초 번역 2011년 2월 - 갱신 (사소한 수정) 2011년 4월 - 최종 정리 Tim Sweeney Epic MegaGames, Inc. http://www.epicgames.com/ Audience

sites.google.com

17년 전에 작성되었고 지금은 원본조차 없는 원시 고대 Unreal Engine의 네트워크 아키텍쳐에 대한 내용.

코드 설명도 없고, 문장으로 풀어내고 있으며 현재 구조나 선언과 비교해서 다른 점도 많다.

하지만 작성자가 Tim Sweeny이기도 하고, 한국어로 번역이 잘 되어 있어서 언리얼 엔진의 네트워크.

특히 RPC쪽을 공부하기 앞서 약간의 흥미 유발이나 워밍업 차원으로 읽어보는 것도 나쁘지 않을 것 같다.

무엇보다 정식 문서에서 설명하지 않고 코드로 확인 할 때에도 놓치기 쉬운 사소한 부분이 담겨져 있다.

'메모장 > 참고링크' 카테고리의 다른 글

Unreal Engine Multiplayer System  (0) 2024.06.12

https://github.com/tranek/GASDocumentation?tab=readme-ov-file#concepts-p

 

GitHub - tranek/GASDocumentation: My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer s

My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer sample project. - tranek/GASDocumentation

github.com

Definition

  • GAS는 Client 쪽에 Prediction을 기본적으로 제공한다.
    • 하지만 모두 다 Predict 되는 것은 아니다.
  • Client에서 Predict 된다는 의미는 Server에서 Gameplay Ability 활성화나 Gameplay Effect 등록에 대한
    허가를 기다릴 필요가 없다는 것을 의미한다.
  • 이는 작업을 수행할 권한을 부여해준 Server와 Gameplay Effect가 적용 될 Target을 Predict 하는 것이다.
  • Server는 Client가 활동하고 나서 네트워크 지연 시간이 지난 후 GameplayAbility를 실행한 후,
    Client에 Predict로 시행한 동작이 맞는지 틀린지 답해준다.
    • 만약 Client가 틀렸다면, Client에서 잘못 Predict 한 것을 Server의 결과로 복구한다.
  • GAS에서의 Predict 관련된 코드들은 GameplayPrediction.h 파일에 있다.
  • Epic의 Predict에 대한 철학은 "해도 될 것 같은 것"에 사용하는 것이다.
    • 예를 들어 Paragon이나 Fortnite는 Damage를 Predict 하지 않는다.
    • 대부분 ExecutionCalculations를 통해 Predict 할 수 없는 Damage를 처리한다.
... we are also not all in on a "predict everything: seamlessly and automatically" solution. We still feel player prediction is best kept to a minimum (meaning: predict the minimum amount of stuff you can get away with).
  • Dave Ratti from Epic's comment from the new Network Prediction Plugin

Predict 가능한 것

  • Ability Activation
  • Triggered Events
  • GameplayEffect Application
    • Attribute Modification(Not Executions, only Attribute Modification)
    • GameplayTag Modification
  • Gameplay Cue Events
  • Montages
  • Movement(in UCharacterMovement)

Predict 불가능한 것

  • GameplayEffect Removal
  • GameplayEffect periodic Effect

추가 설명

  • GameplayEffect 등록은 Predict 할 수 있지만, 제거는 Predict 할 수 없다.
    • 이 제약을 피하는 한가지 방법은 제거 대신 Inverse Effect를 Predict하는 것이다.
  • GameplayEffect 제거를 Predict 할 수 없기 때문에, 우리는 GameplayEffect Cooldown을 Predict 할 수 없다.
    • 심지어 이건 Inverse도 없다.
    • Server에서 Replicate 된 Cooldown GE는 Client에 존재 할 것이고
      이를 제거하려는 모든 시도는 Server에 의해 거부될 것이다.
    • 이는 지연시간이 긴 유저가 서버로부터 Cooldown을 받는데 걸리는 시간이 길어 더 불리하다는 얘기이다.
      • Fortnite는 이를 피하기 위해 Cooldown GE 대신 Custom Bookkeeping을 사용하고 있다.
  • Instant GE를 자신에게 사용할 때에는 문제 없지만, 타인에게 사용할 때에는 값이 잠깐 튈 수 있다.
    • Instant GE를 Predict 하는 것은 Infinite GE처럼 취급되어 복구가 될 수 있다.
    • 만약 Server의 GE가 적용되면, 아주 잠깐동안 동시에 2개의 GE가 존재하여 Modifier가 2번 적용될 수 있다.
    • 이는 곧 정상화가 되겠지만, 일부 유저들은 이상현상을 감지할 수 있다.

PredictionKey

  • GA의 Prediction은 Client가 Gameplay Ability를 활성화 할 때 생성되는 Integer Identifier를 기반으로 동작한다.
    • Gameplay Ability가 활성화 될 때 Prediction Key가 생성된다.
      • 이를 Activation Prediction Key라 지칭한다.
    • Client는 이 Key를 CallServerTryActivateAbility() 함수를 통해 Server에 전달한다.
    • Client는 이 Key가 Valid한 동안 등록된 모든 Gameplay Effect에 추가한다.
    • Client의 Prediction Key가 Scope를 벗어난다.
      • 같은 Gameplay Ability에 Predict 한 Effect는 새 Scoped Prediction Window가 필요하다.
    • Serverrk Prediction Key를 Client로부터 전달 받는다.
    • Server가 Prediction Key를 등록된 모든 Gameplay Effect에 전달한다.
    • Server가 Prediction Key를 Client로 다시 Replicate 한다.
    • Client는 GE를 등록하는데 사용한 Prediction Key와 함께 GE를 Replicate 받는다.
      • 만약 Client에서 등록한 GE 중 Replicate 받은 GE와 동일한 것이 있다면, Predict가 성공한 것이다.
      • Client가 Predict한 GE를 제거하기 전까지 한 Target에 대해 일시적으로 2개의 GE가 존재하게 된다.
    • Client가 Server로부터 Prediction Key를 받는다.
      • 이것은 Replicated Prediction Key라 부른다.
      • 이 Prediction Key는 이제 Stale 상태로 둔다.
    • Client는 Stale 상태의 Replicated Prediction Key을 이용해 모든 GE를 지운다.
      • Server로부터 Replicate 받은 GE는 유지가 될 것이다.
      • Client가 생성했지만 Server로부터 Replicated Prediction Key를 받지 못한 GE는 모두 Predict 실패 처리된다.
  • Prediction Key는 GameAbility가 Activation Prediction Key로 인해 활성화 되고 나서
    Window라는 Atomic Group 명령이 수행되는 동안 Valid를 보장 받는다.
    • 단순하게 한 Frame동안 Valid를 보장 받는다 생각하면 된다.
    • 새로운 Scoped Prediction Window를 생성하는 Synch Point를 가지고 있지 않다면
      예정되어 있는 Ability Task의 모든 Callback은 Valid한 Prediction Key를 가질 수 없다.

Creating New Prediction Windows in Ability

  • Ability Task에서 발생한 Callback에 대해 더 많은 Predict를 하기 위해, 
    새 Scoped Prediction Key를 이용해 새 Scoped Prediction Window를 만들 필요가 있다.
    • 이는 Client와 Server 사이의 Synch Point로 여겨지기도 한다.
  • Input과 같은 일부 Ability Task에는 새 Scoped Prediction Window를 생성하는 기능이 내장되어 있다.
    • 이는 Ability Task의 Callback의 단위 코드는 유효한 Scoped Prediction Key를 가지고 있다는 뜻이다.
  • 만약 Ability Task 이후의 행동을 Predict 한다면,
    WaitDelay와 같이 Scoped Prediction Window를 만드는 코드가 없을 것이다.
    • 때문에 OnlyServerWait 옵션과 함께 AbilityTask관련하여 WaitNetSync을 사용해야만 한다.
  • Client가 OnlyServerWait 옵션인 채로 WaitNetSync를 호출하면 다음 동작을 시행한다.
    • Gameplay Ability의 Activation Prediction Key를 기반으로 한 새 Scoped Prediction Key를 생성
    • 새 Scoped Prediction Key를 Server에 RPC로 전달하고
    • 등록된 GE에 새 Scoped Prediction Key 추가
  • Server가 OnlyServerWait 옵션인채로 WaitNetSync를 호출하면 다음 동작을 시행한다.
    • 작업하기 전 Client로부터 새 Scoped Prediction Key를 받을 때까지 대기
  • 이 Scoped Prediction Key는 GE에 등록된 Activation Prediction Keys와 일치하고,
    Client에 Stale 한 채로 Replicate 된다.
  • Scoped Prediction Key는 Scope를 벗어나기 전까지 유효하다.
    • 이는 Scoped Prediction Window는 닫혀 있다는 것이다.
    • 그렇기에 미루어지지 않는 Atomic 연산만이 새 Scoped Prediction Key를 사용할 수 있다.
  • WaitNetSync를 사용하면, Server의 Gameplay Ability가 Client로부터 패킷을 받을 때까지 대기상태에 빠질 수 있다.
    • 이는 해커에게 취약점을 제공할 수 있다.

Predictively Spawning Actor

  • Client에서 Actor Spawn을 Predict 하는 것은 좀 더 복잡한 이슈이다.
    • 적어도 GAS는 이러한 기능을 제공하지는 않는다.
    • 중요한건 Replicate 되는 Actor을 Client와 Server 양쪽에 모드 Spawn 해야 한다는 것이다.
  • 만약 Actor가 장식이거나 Gameplay 목적이 전혀 없다면, IsNetRelavantFor() 함수를 Override 하여
    Server가 소유한 Client에 Replicate하지 않도록 하는 것도 방법이다.
    • Owner Client는 Local 상으로 Spawn을 하고, Server나 다른 Client는 Spawn을 하지 않는다.
더보기
bool APAReplicatedActorExceptOwner::IsNetRelevantFor(const AActor * RealViewer, const AActor * ViewTarget, const FVector & SrcLocation) const
{
	return !IsOwnedBy(ViewTarget);
}
  • 만약 Projectile처럼 Gameplay에 영향을 주는 Actor인 경우, 좀 더 복잡한 추가 작업이 필요하다.
    • Unreal Tournament에서는 Client에 Dummy Projectile을 Spawn하고
      Server의 Replicated Projecile와 Synch up을 하여 Actor Spawn Predict를 수행한다.

Future of Prediction in GAS

  • GameplayPrediction.h는 차후에 다음 항목에 대한 기능을 제공할 예정이라 밝혔다.
    • GameplayEffect Removal
    • Periodic Gameplay Effect
  • 또한 Dave Ratti는 Cooldown Prediction에서의 Latency로 인한 불균형 문제 수정에 관심을 보이기도 했다.
  • Epic의 새 Network Prediction Plugin은 예전 CharacterMovementComponent와 같이
    GAS와 상호운영이 될 것으로 예상된다.

Network Prediction Plugin

https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Plugins/NetworkPrediction?application_version=5.3

 

Network Prediction | Unreal Engine 5.4 Documentation | Epic Developer Community

 

dev.epicgames.com

 

  • Epic은 CharacterMovementComponent를 새로운 Network Prediction Plugin으로 대체하는 작업을 시작했다.
  • 이 Plugin은 아직 매우 초창기이지만 Unreal Engine Github에서 쉽게 접근할 수 있다.
  • 그럼에도 도입 시기를 논하기에는 매우 이른 감이 없지않아 있다.

'UE5 > GAS' 카테고리의 다른 글

[GAS] Gameplay Cue  (0) 2024.05.21
[GAS] Gameplay Ability  (0) 2024.05.17
[GAS] Ability Task  (0) 2024.05.16
[GAS] Gameplay Effect  (0) 2024.05.15
[GAS] Attribute Set  (1) 2024.05.14

https://github.com/tranek/GASDocumentation?tab=readme-ov-file#concepts-gc

 

GitHub - tranek/GASDocumentation: My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer s

My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer sample project. - tranek/GASDocumentation

github.com

Definition

  • Gameplay와 직접적인 관계가 없는 것들을 실행
    • Sound, Particle, Camer Shake 등
  • 보통은 Replicate되고, predict 가능하다.
  • Gameplay Cue의 필수 상위 이름이 포함된 Gameplay Tag와 EventType을
    ASC를 통해 GameplayCueManager에 전송해 실행한다.
  • 다음 조건을 만족하는 것들은 GameplayCue의 GameplayTag를 기반으로 한 이벤트를 구독할 수 있다.
    • GameplayCueNotify 객체
    • IGameplayCueInterface를 Implement 한 Actor

GameplayCueNotify Class

  • GameplayCueNotify는 어느 것이든 어떤 Event에도 반응하도록 작업이 되어 있다.
    • 다만 특정 Event에 더 적합한 GameplayCueNotify를 제공하고 있다.

GameplayCueNotify_Static

  • GameplayCue와 관련된 정보를 저장하지 않고 즉시 실행 하는 경량 Notify
    • ClassDefaultObject에서 작업이 이루어짐
  • Instant나 Periodic GE와 같이 한번만 즉발로 효과가 나타나야 하는 케이스에 적합함

GameplayCueNotify_Actor

  • GameplayCue와 관련된 정보를 저장할 수 있음
    • Spawn 될 때 새 Instance를 생성
    • 생성된 Instance를 통해 Remove 시점에 필요한 정보를 확인
  • 또한 동시에 얼마나 많은 GameplayCue가 존재할 수 있는지에 대한 정보를 지정할 수도 있다.
  • Duration이나 Infinite와 같이 종료 시점이 정해져 있거나, 그 시점에 어떤 행동을 요구할 때 적합함.
  • AutoDestoryOnRemove 옵션 값을 유의해서 사용해야 한다.
    • 활성화 되어 있을 시 Add에서 발생하는 후속 작업이 호출되지 않을 수 있다.
  • ASC의 Replication Mode가 Full이 아닌 경우,
    Listen Server 환경에서 Add와 Remove 이벤트가 Server에 2번 발생한다.
    • 한 번은 Gameplay Event가 등록될 때
    • 다른 한번은 Client에 Minimal NetMultiCast가 발동할 때.
  • 하지만 WhileActive 이벤트는 여전히 한번만 발생한다.
  • Client에서는 모든 Event가 1번씩만 발생한다.

Trigger Gameplay Cue

  • 내부에서 GE가 성공적으로 등록되면, 실행되어야만 하는 GameplayCue의 모든 GmeplayTag가 채워진다.

  • UGameplayAbility에서는 GameplayCue에 대한 Execute/Add/Remove BP Node를 제공한다.
  • C++에서는 아래 함수들을 통해 사용할 수 있다.
더보기
/** GameplayCues can also come on their own. These take an optional effect context to pass through hit result, etc */
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);

/** Add a persistent gameplay cue */
void AddGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void AddGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);

/** Add gameplaycue for minimal replication mode. Should only be called in paths that would replicate gameplaycues in other ways (through GE for example) if not in minimal replication mode */
void AddGameplayCue_MinimalReplication(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());

/** Remove a persistent gameplay cue */
void RemoveGameplayCue(const FGameplayTag GameplayCueTag);

/** Remove gameplaycue for minimal replication mode. Should only be called in paths that would replicate gameplaycues in other ways (through GE for example) if not in minimal replication mode */
void RemoveGameplayCue_MinimalReplication(const FGameplayTag GameplayCueTag);

/** Removes any GameplayCue added on its own, i.e. not as part of a GameplayEffect. */
void RemoveAllGameplayCues();

/** Handles gameplay cue events from external sources */
void InvokeGameplayCueEvent(const FGameplayEffectSpecForRPC& Spec, EGameplayCueEvent::Type EventType);
void InvokeGameplayCueEvent(const FGameplayTag GameplayCueTag, EGameplayCueEvent::Type EventType, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void InvokeGameplayCueEvent(const FGameplayTag GameplayCueTag, EGameplayCueEvent::Type EventType, const FGameplayCueParameters& GameplayCueParameters);

Local Gameplay Cues

  • ASC와 Gameplay Ability에서 제공하는 GameplayCue 실행 함수는 기본적으로 replicate 된다.
    • 각 GameplayCue Event는 multicast RPC이다.
더보기
// Do not call these functions directly, call the wrappers on GameplayCueManager instead
UFUNCTION(NetMulticast, unreliable)
void NetMulticast_InvokeGameplayCueExecuted_FromSpec(const FGameplayEffectSpecForRPC Spec, FPredictionKey PredictionKey) override;

UFUNCTION(NetMulticast, unreliable)
void NetMulticast_InvokeGameplayCueExecuted(const FGameplayTag GameplayCueTag, FPredictionKey PredictionKey, FGameplayEffectContextHandle EffectContext) override;

UFUNCTION(NetMulticast, unreliable)
void NetMulticast_InvokeGameplayCuesExecuted(const FGameplayTagContainer GameplayCueTags, FPredictionKey PredictionKey, FGameplayEffectContextHandle EffectContext) override;

UFUNCTION(NetMulticast, unreliable)
void NetMulticast_InvokeGameplayCueExecuted_WithParams(const FGameplayTag GameplayCueTag, FPredictionKey PredictionKey, FGameplayCueParameters GameplayCueParameters) override;

UFUNCTION(NetMulticast, unreliable)
void NetMulticast_InvokeGameplayCuesExecuted_WithParams(const FGameplayTagContainer GameplayCueTags, FPredictionKey PredictionKey, FGameplayCueParameters GameplayCueParameters) override;

UFUNCTION(NetMulticast, unreliable)
void NetMulticast_InvokeGameplayCueAdded(const FGameplayTag GameplayCueTag, FPredictionKey PredictionKey, FGameplayEffectContextHandle EffectContext) override;

UFUNCTION(NetMulticast, unreliable)
void NetMulticast_InvokeGameplayCueAdded_WithParams(const FGameplayTag GameplayCueTag, FPredictionKey PredictionKey, FGameplayCueParameters Parameters) override;

UFUNCTION(NetMulticast, unreliable)
void NetMulticast_InvokeGameplayCueAddedAndWhileActive_FromSpec(const FGameplayEffectSpecForRPC& Spec, FPredictionKey PredictionKey) override;

UFUNCTION(NetMulticast, unreliable)
void NetMulticast_InvokeGameplayCueAddedAndWhileActive_WithParams(const FGameplayTag GameplayCueTag, FPredictionKey PredictionKey, FGameplayCueParameters GameplayCueParameters) override;

UFUNCTION(NetMulticast, unreliable)
void NetMulticast_InvokeGameplayCuesAddedAndWhileActive_WithParams(const FGameplayTagContainer GameplayCueTags, FPredictionKey PredictionKey, FGameplayCueParameters GameplayCueParameters) override;
  • 이는 많은 RPC를 야기하는데,
    GAS는 동일한 Gameplay Cue RPC가 net update 당 최대 2개만 발생하도록 제한하고 있다.
    • 우리는 이 문제를 Local Gameplay Cue를 사용함으로 해결할 수 있다.
      • Local Gameplay Cue는 GAS에서 제공하는 기능이 아니라 Gameplay Cue 사용 권장 사항이다.
    • Local Gameplay Cue는 각 Client에서 오직 Execute, Add, Remove를 시행한다.
  • Local GameplayCue를 호출하기 적절한 케이스는 다음과 같다.
    • 투사체 
    • 물리 공격 
    • Animation Montage에서 발생하는 GameplayCue
  • 만약 GameplayCue가 Local에서 Add 되었다면, Remove 역시 Local에서 수행 되어야만 한다.

Gameplay Cue Parameter

  • GameplayCue는 외부 정보를 FGameplayCueParameters라는 구조체에 담아 파라미터로 전달받는다.
  • GameplayAbility나 ASC의 함수에서 GameplayCue를 실행하고 싶다면,
    반드시 GameplayCueparameters를 채워서 GameplayCue에 전달해야만 한다.
  • 만약 GameplayCue가 GE로부터 발생했다면, FGameplayCueParameters에서 아래 변수들이 자동으로 채워진다.
    • AggregatedSourceTags
    • AggregatedTargetTags
    • GameplaEffectLevel
    • AbilityLevel
    • EffectContext
    • Magnitude
  • SourceObject 항목은 GameplayCue를 실행시킬 때 임시 데이터를 전달하기 적절한 변수이다.
  • Instigator와 같은 FGameplayCueParameters 안의 일부 변수들은 EffectContext에 이미 존재할 수 있다.
    • 또한 EffectContext는 GameplayCue가 World에서 Spawn 된 Location을 FHitResult로 가지고 있기도 하다.
  • 하지만 EffectContext를 확장할 때에는 GameplayCue에 더 많은 정보를 가지게 하는 것이 적절할 것이다.
    • 특히 GameplayCue가 GE에서 발생을 한다면 더더욱이.
  • UAbilitySystemGlobals에서는 FGameplayCueParameters를 채우는 함수들을 제공한다.
    • 이들은 virtual로 선언되어 있어 override를 통해 더 많은 정보를 자동으로 채워주도록 확장할 수 있다.
더보기
/** Initialize GameplayCue Parameters */
virtual void InitGameplayCueParameters(FGameplayCueParameters& CueParameters, const FGameplayEffectSpecForRPC &Spec);
virtual void InitGameplayCueParameters_GESpec(FGameplayCueParameters& CueParameters, const FGameplayEffectSpec &Spec);
virtual void InitGameplayCueParameters(FGameplayCueParameters& CueParameters, const FGameplayEffectContextHandle& EffectContext);

Gameplay Cue Manager

  • GameplayCueManager는 Default로 다음 기능이 동작한다.
    • GameplayCueNotifier에 대한 전체 Game Directory를 Scan
    • Scan 한 Directory들을 Play 할 때 Memory에 Load
  • 또한 defaultGame.ini에서 Scan 할 Game Directory를 변경할 수 있다.
더보기
[/Script/GameplayAbilities.AbilitySystemGlobals]
GameplayCueNotifyPaths="/Game/GASDocumentation/Characters"
  • 우리는 GameplayCueManager가 모든 GameplayCueNotifier를 Scan하기를 원한다.
    • 하지만 이들을 하나하나 Async Load 하는 것은 원하지 않는다.
    • GameplayCueManager는 Level에서 사용되는지 여부와 무관하게 모든 GameplayCueNotifier와
      관련 Sound, Particle을 Memory에 올려둘 것이다.
  • GameplayCue가 게임 시작 할 때 Async Load 하는 것에 대한 대안은 게임 중 실행 될 때 Async Load 하는 것이다.
    • 이는 Play 중 GameplayCue가 처음 호출 될 때 해당 Effect에 대해 약간의 Delay를 안고 가는 대신,
      불필요한 메모리 사용과 Async Load를 하는 동안 발생할 수 있는 Hard Freeze를 줄여준다.
    • Editor에서는 Particle System을 컴파일 해야 하는 경우 GameplayCue를 처음 Load 하는 동안
      약간의 히칭이나 Freeze가 발생할 수 있다.
  • 이러한 기능을 수행하기 위해, 가장 먼저 UGameplayCueManager를 확장해 아래와 같이 함수를 override 해야 한다.
더보기
virtual bool ShouldAsyncLoadRuntimeObjectLibraries() const override
{
	return false;
}
  • 그리고 확장한 Class Type을 DefaultGame.ini를 통해 AbilitySystemGlobals에 전달해야 한다.
더보기
[/Script/GameplayAbilities.AbilitySystemGlobals]
GlobalGameplayCueManagerClass="/Script/ParagonAssets.PBGameplayCueManager"

Prevent Gameplay Cue from Firing

  • 때로는 GameplayCue가 발생하는 것을 막아야 하는 경우가 있다.
    • 공격이 막혔다던가.
    • Damage GE에 연결된 Hit Impact를 재생하고 싶지 않다던가.
    • 별개의 Hit Impact를 재생하고 싶다던가.
  • 그럴 때에는 아래 함수를 호출하고, 명시적으로 GameplayCue Event를 Targer/Source ASC에 전달하면 된다.
더보기
void FGameplayEffectCustomExecutionOutput::MarkGameplayCuesHandledManually()
{
	bHandledGameplayCuesManually = true;
}
  • 만약 특정 ASC에서 영구적으로 GameplayCue가 발생하지 않기 원한다면, 아래 변수 값을 수정해주면 된다.
더보기
/** Suppress all GameplayCues on this component */
UPROPERTY()
bool bSuppressGameplayCues;

Batch Gameplay Cue

  • 각 Gameplay Cue는 unreliable NetMulticast RPC로 호출된다.
  • 만약 동시에 여러개의 Gameplay Cue가 발생 한다면, 몇 가지 최적화를 적용할 수 있다.
    • RPC를 하나로 합치던가
    • Data를 압축하여 더 적은 정보를 패킷으로 전송하던가.

Manual RPC

  • 예를 들어 산탄을 사격한다고 가정하자.
    • 산탄이 발사하면 pellet의 수 만큼 TargetData에 대한 EffectContext가 발생을 한다.
    • 이걸 1개로 합친다 하더라도 Data 용량은 1회 RPC 용량(500 byte)를 초과할 것이다.
  • 이를 좀 더 최적화 하는 방법은 별도의 구조체로 정보를 보내는 것이다.
    • 이 구조체는 Hit Location은 효과적으로 encode 하였거나,
      Client에서 Impact Location을 재생성/근사 할 수 있도록 Random Seed Number를 가지고 있을 것이다.
    • Client는 이 정보를 통해 Local Gameplay Cue를 발생시켜 작업을 복구할 것이다.
  • 이 방식은 보통 다음 조건에서 사용된다.
    • GameplayCue에서 필요로 하는 일부 parameter가 GameplayCueParameter에서 제공한 것과 맞지 않은 경우
    • GameplayCue에서 필요로 하는 일부 parameter를 EffectContext에 추가하고 싶지 않은 경우
      • DamageID, 치명타 계수, 실드 파괴 지수, 결정타 지수 등

사용 방법

  • FScopedGameplayCueSendContext를 선언한다.
    • 이 구조체는 Scope를 벗어날 때까지 UGameplayCueManager::FlushPendingCues() 함수를 억제한다.
    • 즉, 모든 GameplayCue는 이 구조체 Scope를 벗어날 때까지 queue에 쌓이게 된다.
더보기
/**
 *	FScopedGameplayCueSendContext
 *	Add this around code that sends multiple gameplay cues to allow grouping them into a smalkler number of cues for more efficient networking
 */
struct GAMEPLAYABILITIES_API FScopedGameplayCueSendContext
{
	FScopedGameplayCueSendContext();
	~FScopedGameplayCueSendContext();
};
  • FlushPendingCues() 함수를 override하여 Custom Struct에서
    몇몇 Custom Gamplay Tag로 batch 할수 있는 GameplayCue를 Merge하고, Client로 RPC 한다.
더보기
void UGameplayCueManager::FlushPendingCues()
{
	OnFlushPendingCues.Broadcast();

	TArray<FGameplayCuePendingExecute> LocalPendingExecuteCues = PendingExecuteCues;
	PendingExecuteCues.Empty();
	for (int32 i = 0; i < LocalPendingExecuteCues.Num(); i++)
	{
		FGameplayCuePendingExecute& PendingCue = LocalPendingExecuteCues[i];

		// Our component may have gone away
		if (PendingCue.OwningComponent)
		{
			bool bHasAuthority = PendingCue.OwningComponent->IsOwnerActorAuthoritative();
			bool bLocalPredictionKey = PendingCue.PredictionKey.IsLocalClientKey();

			IAbilitySystemReplicationProxyInterface* RepInterface = PendingCue.OwningComponent->GetReplicationInterface();
			if (RepInterface == nullptr)
			{
				// If this returns null, it means "we are replicating througha proxy and have no avatar". Which in this case, we should skip
				continue;
			}

			// TODO: Could implement non-rpc method for replicating if desired
			if (PendingCue.PayloadType == EGameplayCuePayloadType::CueParameters)
			{
				if (ensure(PendingCue.GameplayCueTags.Num() >= 1))
				{
					if (bHasAuthority)
					{
						RepInterface->ForceReplication();
						if (PendingCue.GameplayCueTags.Num() > 1)
						{
							RepInterface->Call_InvokeGameplayCuesExecuted_WithParams(FGameplayTagContainer::CreateFromArray(PendingCue.GameplayCueTags), PendingCue.PredictionKey, PendingCue.CueParameters);
						}
						else
						{
							RepInterface->Call_InvokeGameplayCueExecuted_WithParams(PendingCue.GameplayCueTags[0], PendingCue.PredictionKey, PendingCue.CueParameters);

							static FName NetMulticast_InvokeGameplayCueExecuted_WithParamsName = TEXT("NetMulticast_InvokeGameplayCueExecuted_WithParams");
							CheckForTooManyRPCs(NetMulticast_InvokeGameplayCueExecuted_WithParamsName, PendingCue, PendingCue.GameplayCueTags[0].ToString(), nullptr);
						}
					}
					else if (bLocalPredictionKey)
					{
						for (const FGameplayTag& Tag : PendingCue.GameplayCueTags)
						{
							PendingCue.OwningComponent->InvokeGameplayCueEvent(Tag, EGameplayCueEvent::Executed, PendingCue.CueParameters);
						}
					}
				}
			}
			else if (PendingCue.PayloadType == EGameplayCuePayloadType::FromSpec)
			{
				if (bHasAuthority)
				{
					RepInterface->ForceReplication();
					RepInterface->Call_InvokeGameplayCueExecuted_FromSpec(PendingCue.FromSpec, PendingCue.PredictionKey);

					static FName NetMulticast_InvokeGameplayCueExecuted_FromSpecName = TEXT("NetMulticast_InvokeGameplayCueExecuted_FromSpec");
					CheckForTooManyRPCs(NetMulticast_InvokeGameplayCueExecuted_FromSpecName, PendingCue, PendingCue.FromSpec.Def ? PendingCue.FromSpec.ToSimpleString() : TEXT("FromSpecWithNoDef"), PendingCue.FromSpec.EffectContext.Get());
				}
				else if (bLocalPredictionKey)
				{
					PendingCue.OwningComponent->InvokeGameplayCueEvent(PendingCue.FromSpec, EGameplayCueEvent::Executed);
				}
			}
		}
	}
}
  • Client는 Custom Struct를 받아 Local GameplayCue로 실행한다.

Multiple Gameplay Cue on One Gameplay Effect

  • GameplayEffect에서의 Gameplay Cue는 이미 하나의 RPC로 전달된다.
  • 아래 함수에서 모든 GameplayEffectSpec를 FGameplayEffectSpecForRPC로 변환하여
    ASC의 Replication Mode와 무관하게 unreliable NetMulticast로 전달한다.
더보기
virtual void InvokeGameplayCueAddedAndWhileActive_FromSpec(UAbilitySystemComponent* OwningComponent, const FGameplayEffectSpec& Spec, FPredictionKey PredictionKey);

 

  • 하지만 이 방식은 GameplayEffectSpec에 어떤 정보가 있는지에 따라 매우 큰 Bandwidth를 가질 수 있다.
    • 여기에 대해 아래 변수를 통해 좀 더 기능을 최적화 할 수 있다.
더보기
/**
 *	Enabling AbilitySystemAlwaysConvertGESpecToGCParams will mean that all calls to gameplay cues with GameplayEffectSpecs will be converted into GameplayCue Parameters server side and then replicated.
 *	This potentially saved bandwidth but also has less information, depending on how the GESpec is converted to GC Parameters and what your GC's need to know.
 */

int32 AbilitySystemAlwaysConvertGESpecToGCParams = 0;
static FAutoConsoleVariableRef CVarAbilitySystemAlwaysConvertGESpecToGCParams(TEXT("AbilitySystem.AlwaysConvertGESpecToGCParams"), AbilitySystemAlwaysConvertGESpecToGCParams, TEXT("Always convert a GameplayCue from GE Spec to GC from GC Parameters on the server"), ECVF_Default );
  • 이 변수는 FGameplayEffectSpec를 FGameplayCueParameter로 변환하여 RPC에 전달한다.
  • Bandwidth를 절약할 수 있는 대신 더 적은 정보를 담을 수 밖에 없다.
    • 때문에 GESpec을 GameplayCueParameter로 어떻게 변환할지가 중요하다.

Gameplay Cue Event

더보기
UENUM(BlueprintType)
namespace EGameplayCueEvent
{
	/** Indicates what type of action happened to a specific gameplay cue tag. Sometimes you will get multiple events at once */
	enum Type : int
	{
		/** Called when a GameplayCue with duration is first activated, this will only be called if the client witnessed the activation */
		OnActive,

		/** Called when a GameplayCue with duration is first seen as active, even if it wasn't actually just applied (Join in progress, etc) */
		WhileActive,

		/** Called when a GameplayCue is executed, this is used for instant effects or periodic ticks */
		Executed,

		/** Called when a GameplayCue with duration is removed */
		Removed
	};
}

OnActive

  • GameplayCue가 활성화(Add) 될 때 호출
  • GameplayCue가 시작할 때 발생하는 모든 것에서 사용
    • 단, Joiner가 늦어서 이 Event를 놓치더라도 WhileActive에서 보강해줄 수 있다.

WhileActive

  • GameplayCue가 동작 할 때 호출된다.
    • 심지어 아직 등록이 완료되지 않더라도 동작만 하면 호출된다.
  • GameplayCueNotify_Actor가 Add 되거나 관련 작업이 생기면 OnActive와 같이 한번만 호출된다.
    • 따라서 Tick이 아니다.
    • Actor가 GameplayCueNotify_Actor 범위 안에 들어올 때마다 한번씩 발생한다.
  • 만약 Tick이 필요하다면, GameplayCueNotify_Actor::Tick()을 사용해야 한다.

Removed

  • GameplayCue가 제거 될 때 호출된다.
    • BP에서는 OnRemove Event가 이 기능과 관련 있다.
  • OnActive/WhileActive에서 추가한 것들을 반드시지워야 한다.
  • Actor가 GameplayCueNotify_Actor 범위 밖을 나갈 때마다 한번씩 발생한다.

Executed

  • Instant Effect나 Periodic Tick에서 GameplayCue가 실해오딜 때 호출된다.
    • BP에서는 OnExecute가 관련있다.

Gameplay Cue Reliability

  • 일반적으로 GameplayCue는 Unreliable 함을 염두해야만 한다.
    • 때문에 Gameplay에 직접적으로 영향을 주는 모든 겨우에 대해서는 적합하지 않다.
  • 만약 GameplayCue를 Relaible하게 사용하고 싶다면, 
    GameplayEffect으로부터 등록하여 WhileActive에서 FX를 추가하고, OnRevmoe에서 제거해야 한다.

Executed GameplayCues

  • 이 경우에는 Unreliable Multicast로 등록이 되기 때문에 항상 Unreliable하다.

GameplayCues applied from GameplayEffect

  • Autonomous Proxy의 경우 OnActive, WhileActive, OnRemove에 대해 Reliable하다.
    • OnActive, WhileActive는 다음 함수에서 호출된다.
    • 더보기
      void FActiveGameplayEffectsContainer::PostReplicatedReceive(const FFastArraySerializer::FPostReplicatedReceiveParameters& Parameters)
      {
      	// After the array has been replicated, invoke GC events ONLY if the effect is not inhibited
      	// We postpone this check because in the same net update we could receive multiple GEs that affect if one another is inhibited	
      	if (Owner != nullptr)
      	{
      		QUICK_SCOPE_CYCLE_COUNTER(STAT_ActiveGameplayEffectsContainer_NetDeltaSerialize_CheckRepGameplayCues);
      
      		if ( LIKELY(UE::GameplayEffect::bSkipUnmappedReferencesCheckForGameplayCues) )
      		{
      			if (Owner->IsReadyForGameplayCues())
      			{
      				Owner->HandleDeferredGameplayCues(this);
      			}
      		}
      		else
      		{
      // Keep this until we have actually deprecated the parameter just in case.
      PRAGMA_DISABLE_DEPRECATION_WARNINGS
      			if (!Parameters.bHasMoreUnmappedReferences) // Do not invoke GCs when we have missing information (like AActor*s in EffectContext)
      			{
      				NumConsecutiveUnmappedReferencesDebug = 0;
      				if (Owner->IsReadyForGameplayCues())
      				{
      					Owner->HandleDeferredGameplayCues(this);
      				}
      			}
      			else
      			{
      				++NumConsecutiveUnmappedReferencesDebug;
      
      				constexpr uint32 HighNumberOfConsecutiveUnmappedRefs = 30;
      				ensureMsgf(NumConsecutiveUnmappedReferencesDebug < HighNumberOfConsecutiveUnmappedRefs, TEXT("%hs: bHasMoreUnmappedReferences is preventing GameplayCues from firing"), __func__);
      				UE_CLOG((NumConsecutiveUnmappedReferencesDebug % HighNumberOfConsecutiveUnmappedRefs) == 0, LogAbilitySystem, Error, TEXT("%hs: bHasMoreUnmappedReferences is preventing GameplayCues from firing (%u consecutive misses)"), __func__, NumConsecutiveUnmappedReferencesDebug);
      			}
      PRAGMA_ENABLE_DEPRECATION_WARNINGS
      		}
      	}
      }

       

      void UAbilitySystemComponent::HandleDeferredGameplayCues(const FActiveGameplayEffectsContainer* GameplayEffectsContainer)
      {
      	for (const FActiveGameplayEffect& Effect : GameplayEffectsContainer)
      	{
      		if (Effect.bIsInhibited == false)
      		{
      			if (Effect.bPendingRepOnActiveGC)
      			{
      				InvokeGameplayCueEvent(Effect.Spec, EGameplayCueEvent::OnActive);
      			}
      			if (Effect.bPendingRepWhileActiveGC)
      			{
      				InvokeGameplayCueEvent(Effect.Spec, EGameplayCueEvent::WhileActive);
      			}
      		}
      
      		Effect.bPendingRepOnActiveGC = false;
      		Effect.bPendingRepWhileActiveGC = false;
      	}
      }


    • OnRemoved의 경우 다음 함수에서 호출된다.
    • 더보기
      void FActiveGameplayEffectsContainer::RemoveActiveGameplayEffectGrantedTagsAndModifiers(const FActiveGameplayEffect& Effect, bool bInvokeGameplayCueEvents)
      {
      	// Update AttributeAggregators: remove mods from this ActiveGE Handle
      	if (Effect.Spec.GetPeriod() <= UGameplayEffect::NO_PERIOD)
      	{
      		for (const FGameplayModifierInfo& Mod : Effect.Spec.Def->Modifiers)
      		{
      			if (Mod.Attribute.IsValid())
      			{
      				if (const FAggregatorRef* RefPtr = AttributeAggregatorMap.Find(Mod.Attribute))
      				{
      					RefPtr->Get()->RemoveAggregatorMod(Effect.Handle);
      				}
      			}
      		}
      	}
      
      	// Update gameplaytag count and broadcast delegate if we are at 0
      	Owner->UpdateTagMap(Effect.Spec.Def->GetGrantedTags(), -1);
      	Owner->UpdateTagMap(Effect.Spec.DynamicGrantedTags, -1);
      
      	// Update our owner with the blocked ability tags this GameplayEffect adds to them
      	Owner->UnBlockAbilitiesWithTags(Effect.Spec.Def->GetBlockedAbilityTags());
      
      	// Update minimal replication if needed.
      	if (ShouldUseMinimalReplication())
      	{
      		Owner->RemoveMinimalReplicationGameplayTags(Effect.Spec.Def->GetGrantedTags());
      		Owner->RemoveMinimalReplicationGameplayTags(Effect.Spec.DynamicGrantedTags);
      	}
      
      PRAGMA_DISABLE_DEPRECATION_WARNINGS
      	// Cancel/remove granted abilities
      	if (IsNetAuthority())
      	{
      		for (const FGameplayAbilitySpecDef& AbilitySpecDef : Effect.Spec.GrantedAbilitySpecs)
      		{
      			if (AbilitySpecDef.AssignedHandle.IsValid())
      			{
      				switch(AbilitySpecDef.RemovalPolicy)
      				{
      				case EGameplayEffectGrantedAbilityRemovePolicy::CancelAbilityImmediately:
      					{
      						Owner->ClearAbility(AbilitySpecDef.AssignedHandle);
      						break;
      					}
      				case EGameplayEffectGrantedAbilityRemovePolicy::RemoveAbilityOnEnd:
      					{
      						Owner->SetRemoveAbilityOnEnd(AbilitySpecDef.AssignedHandle);
      						break;
      					}
      				default:
      					{
      						// Do nothing to granted ability
      						break;
      					}
      				}
      			}
      		}
      	}
      PRAGMA_ENABLE_DEPRECATION_WARNINGS
      
      	// Update GameplayCue tags and events
      	if (!Owner->bSuppressGameplayCues)
      	{
      		for (const FGameplayEffectCue& Cue : Effect.Spec.Def->GameplayCues)
      		{
      			// If we use Minimal/Mixed Replication, then this will cause EGameplayCueEvent::Removed
      			if (ShouldUseMinimalReplication())
      			{
      				for (const FGameplayTag& CueTag : Cue.GameplayCueTags)
      				{
      					Owner->RemoveGameplayCue_MinimalReplication(CueTag);
      				}
      			}
      			else
      			{
      				// Perform pseudo-RemoveCue (without affecting ActiveGameplayCues, as we were not inserted there - see AddActiveGameplayEffectGrantedTagsAndModifiers)
      				Owner->UpdateTagMap(Cue.GameplayCueTags, -1);
      
      				if (bInvokeGameplayCueEvents)
      				{
      					Owner->InvokeGameplayCueEvent(Effect.Spec, EGameplayCueEvent::Removed);
      				}
      			}
      		}
      	}
      }
  • Simulated Proxy의 경우 WhileActive와 OnRemove가 Reliable하다.
    • 아래 함수에서의 Replication이 WhileActive, OnRemove Event를 호출한다.
    • 더보기
      /** Replicated gameplaycues when in minimal replication mode. These are cues that would come normally come from ActiveGameplayEffects (but since we do not replicate AGE in minimal mode, they must be replicated through here) */
      UPROPERTY(Replicated)
      FActiveGameplayCueContainer MinimalReplicationGameplayCues;

       

    • OnActive Event는 Unreliable Multicast로 전달된다.

GameplayCues applied withou a GameplayEffect 

  • Autonomous Proxy의 경우 OnRemove Event를 Reliable하게 받는다.
    • OnActive, WhileActive Event는 Unreliable Multicast로 전달된다.
  • Simulated Proxy의 경우 WhileActive, OnRemove Event를 Reliable하게 받는다.
    • UASC::MinimalReplicationGameplayCue의 Replication이 WhileActive, OnRemove를 호출한다.
    • OnActive Event는 Unreliable Multicast로 전달된다.

'UE5 > GAS' 카테고리의 다른 글

[GAS] Prediction  (0) 2024.05.21
[GAS] Gameplay Ability  (0) 2024.05.17
[GAS] Ability Task  (0) 2024.05.16
[GAS] Gameplay Effect  (0) 2024.05.15
[GAS] Attribute Set  (1) 2024.05.14

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/using-gameplay-abilities-in-unreal-engine?application_version=5.3

 

언리얼 엔진의 게임플레이 어빌리티 사용하기 | 언리얼 엔진 5.4 문서 | Epic Developer Community

게임플레이 어빌리티 클래스의 개요입니다.

dev.epicgames.com

 

https://github.com/tranek/GASDocumentation?tab=readme-ov-file#concepts-ga

 

GitHub - tranek/GASDocumentation: My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer s

My understanding of Unreal Engine 5's GameplayAbilitySystem plugin with a simple multiplayer sample project. - tranek/GASDocumentation

github.com

 

  • Game 내 Actor가 하는 모든 행동이나 Skill, Ability가 하는 일, Cost, 조건 및 시점을 정의
  • UGameplayAbility를 상속
  • GA는 비동기로 실행되는 Instance화 된 Object로 존재할 수 있다.
    • 다양한 캐릭터 상호작용에 따른 분기처럼 전문화 된 다단계 Task를 실행할 수 있다.
    • Character Animation, Particle/Sound Effect, Player Input 이나 실행 등
  • Net Execution Policy를 통한 자체 Replicate, Client/Server에서의 실행, 변수 동기화나 RPC 호출도 가능
    • 단, Simulated Proxy에는 호출 불가
  • Game Session 중에 Engine이 GA를 구현하는 방식에 있어 유연성을 제공합니다.
    • Cooldown, Cost, Player Input, Anim Montage Animation, Actor에 부여되는
      Ability 자체에 대한 반응을 구현하는 확장 함수
  • 여러 시간에 걸쳐서 발생하는 Gameplay Ability를 실행하는 데에 Ability Task를 사용
    • Attribute 변경 대기
    • Target 선택 대기
    • Root Motion Source의 캐릭터 움직임 등
  • Simulated client는 Gameplay Ability를 호출하지 않는다.
    • 대신 서버에서 Ability가 동작하고, 시각적으로 재생이 필요한 것들은 모두
      Ability Task나 GameplayCue를 통해 Replicate 된다.
  • BP, C++에서 모두 작업 가능

Bind Input to ASC

  • ASC는 직접적인 Input Bind를 제공하고 이에 대해 GameAbility를 Grant 할 수 있도록 기능을 제공한다.
  • GameAbility에 등록된 Input Action은 Press가 발생할 때 Gameplay Tag 조건을 만족하면
    자동으로 Gameplay Activity로 동작한다.
  • Input Action이 등록된 Input에 반응하려면 내장된 Ability Task를 요구한다.
  • 활성화 된 Gameplay Ability에 등록된 Input Action에 더불어, ASC도 범용적인 Confirm, Cancel Input을 받을 수 있다.
    • 이런 특별한 Input은 Ability Task에서 Target Actor나 Ability 취소와 같은 것들을 등록할 때 사용된다.
  • ASC에 Input을 Bind하려면 가장 먼저 Inpuat Action Name을 byte로 변환하는 Enum을 선언해야 한다.
    • 이 Enum의 이름은 Project Setting에서의 Input Action 이름과 완벽하게 일치해야 한다.
    • DisplayName은 아무 상관 없다.
더보기
UENUM(BlueprintType)
enum class EGDAbilityInputID : uint8
{
	// 0 None
	None			UMETA(DisplayName = "None"),
	// 1 Confirm
	Confirm			UMETA(DisplayName = "Confirm"),
	// 2 Cancel
	Cancel			UMETA(DisplayName = "Cancel"),
	// 3 LMB
	Ability1		UMETA(DisplayName = "Ability1"),
	// 4 RMB
	Ability2		UMETA(DisplayName = "Ability2"),
	// 5 Q
	Ability3		UMETA(DisplayName = "Ability3"),
	// 6 E
	Ability4		UMETA(DisplayName = "Ability4"),
	// 7 R
	Ability5		UMETA(DisplayName = "Ability5"),
	// 8 Sprint
	Sprint			UMETA(DisplayName = "Sprint"),
	// 9 Jump
	Jump			UMETA(DisplayName = "Jump")
};

ASC in Character

  • ACharacter::SetupPlayerInputcomponent() 함수안에서 ASC에 Bind 하는 작업을 추가
더보기
/** Bind to an input component with customized bindings */
virtual void BindAbilityActivationToInputComponent(UInputComponent* InputComponent, FGameplayAbilityInputBinds BindInfo);

void ACharacter::SetupPlayerInputComponent
{
    // Bind to AbilitySystemComponent
    FTopLevelAssetPath AbilityEnumAssetPath = FTopLevelAssetPath(FName("/Script/GASDocumentation"), FName("EGDAbilityInputID"));
    AbilitySystemComponent->BindAbilityActivationToInputComponent(PlayerInputComponent, FGameplayAbilityInputBinds(FString("ConfirmTarget"),
	FString("CancelTarget"), AbilityEnumAssetPath, static_cast<int32>(EGDAbilityInputID::Confirm), static_cast<int32>(EGDAbilityInputID::Cancel)));
}

ASC in PlayerState

  • PlayerState가 아직 Client에 Replicate 되지 않을 수 있어 ACharacter::SetupPlayerInputComponent 안에서
    Race Condition이 발생할 위험이 있다.
  • 때문에 ACharacter::SetupPlayerInputcomponent 뿐 아니라 OnRep_PlayerState() 안에서도 Bind 작업이 필요하다.
  • OnRep_PlayerState() 자체만으로는 Bind 작업에 충분치 않다.
    • PlayerController가 Cliient에서 ClientRestart()를 호출해 InputComponent를 생성하기 전에
      PlayerState가 Replicate 되면 Actor의 InputComponent가 null이 될 수 있기 때문이다.

Bind to Input Without Actiavting Ability

  • Input이 발생할 때 Gameplay Ability가 동작하지는 않지만, Ability Task를 통한 Bind는 계속 유지하고 싶을 때 사용
  • UAbilitySystemComponent::AbilityLocalInputPressed()를 override 하여 Gameplay Ability 작업을 Customize.

Basic Usage

Activation Sequence

Local pretiction

  • TryActivateAbility()
  • InternalTryActivateAbility()
  • CanActivateAbility()
    • Gameplay Tag 조건이 부합한디
    • ASC에서 Cost를 지불할 수 있는지
    • Gameplay Ability가 Cooldown 상태가 아닌지
    • 이미 활성화된 Instance가 없는지
  • CallServerTryActivateAbility()
    • 이 함수를 통하 생성된 Pretiction Key를 전달한다.
  • CallActivateAbility()
  • PreActivate()
    • Epic은 이걸 Boilerplate Init Stuff라 부른다.
  • ActivateAbility()
    • 이 함수에서 비로소 Ability가 동작한다.

Server

  • CallServerTryActivateAbility()에서 RPC를 받으면서 시작
  • ServerTryActivateAbility()
  • InterServerTryActivateAbility()
  • InternalTryActivateAbility()
  • CanActivateAbility()
    • Local Pretiction과 동일한 조건을 체크, 반환
  • ClientActivateAbilitySucced()
    • 다음 두 조건을 만족 하면 호출 됨
      • Server에서 활성화 되는 ActivationInfo가 성공적으로 수정 된 경우.
      • OnConfirmDelegate가 Broadcast 된 경우
  • CallActivateAbility()
  • PreActivate()
  • ActivateAbility()
    • 여기서 Ability가 동작한다.
  • ClientActivateAbilityFailed()
    • Server가 Ability 활성화에 실패한 경우에 호출 된다.
    • 즉시 Client의 Gameplay Ability를 제거하고 적용된 것들을 복구한다.

Grant Ability

  • ASC의 ActivatableAbility에 Gameplay Ability를 추가
    • ActivatableAbility에서 Gameplay Tag 조건이 맞으면 활성화가 된다.
  • Gameplay Ability를 서버에서 등록하면 GameplayAbilitySpec을 소유한 Client에 자동으로 Replicate 한다.
    • 다른 Client나 Simulated Proxy는 GameplayAbilitySpec을 받지 않는다.
    • GameplayAbilitySpec은 Gameplay Ability가 Grant 될 때 생성
    • Ability Level, Bind 된 input, 해당 Gameplay Ability를 ASC에 전달한 Source Object 정보를 담고 있다.

GiveAbility

더보기
/*
 * Grants an Ability.
 * This will be ignored if the actor is not authoritative.
 * Returns handle that can be used in TryActivateAbility, etc.
 * 
 * @param AbilitySpec FGameplayAbilitySpec containing information about the ability class, level and input ID to bind it to.
 */
FGameplayAbilitySpecHandle GiveAbility(const FGameplayAbilitySpec& AbilitySpec);

 

void AGDCharacterBase::AddCharacterAbilities()
{
	// Grant abilities, but only on the server	
	if (Role != ROLE_Authority || !AbilitySystemComponent.IsValid() || AbilitySystemComponent->bCharacterAbilitiesGiven)
	{
		return;
	}

	for (TSubclassOf<UGDGameplayAbility>& StartupAbility : CharacterAbilities)
	{
		AbilitySystemComponent->GiveAbility(
			FGameplayAbilitySpec(StartupAbility, GetAbilityLevel(StartupAbility.GetDefaultObject()->AbilityID), static_cast<int32>(StartupAbility.GetDefaultObject()->AbilityInputID), this));
	}

	AbilitySystemComponent->bCharacterAbilitiesGiven = true;
}
  • FGameplayAbilitySpec으로 추가할 Ability를 나타냄
  • FGameplayAbilitySpecHandle을 반환

GiveAbilityAndActivateOnce

더보기
/*
 * Grants an ability and attempts to activate it exactly one time, which will cause it to be removed.
 * Only valid on the server, and the ability's Net Execution Policy cannot be set to Local or Local Predicted
 * 
 * @param AbilitySpec FGameplayAbilitySpec containing information about the ability class, level and input ID to bind it to.
 * @param GameplayEventData Optional activation event data. If provided, Activate Ability From Event will be called instead of ActivateAbility, passing the Event Data
 */
FGameplayAbilitySpecHandle GiveAbilityAndActivateOnce(FGameplayAbilitySpec& AbilitySpec, const FGameplayEventData* GameplayEventData = nullptr);
  • FGameplayAbilitySpec으로 추가할 Ability를 나타냄
  • FGameplayAbilitySpecHandle을 반환
  • Ability는 반드시 Instance화되고 Server에서 실행할 수 있어야 한다.
    • Server에서 실행을 시도한 후에는 FGameplayAbilitySpecHandle을 반환한다.
  • Ability가 필수 조건을 충족하지 못하거나 실행 할 수 없는 경우,
    Handle이 유효하지 않아 ASC가 Ability를 부여받지 못한다.

Revoke Ability

  • 입력받은 FGameplayAbilitySpecHandle은 모두 Grant 과정에서 반환한 것들을 사용한다.

ClearAbility

더보기
/** 
 * Removes the specified ability.
 * This will be ignored if the actor is not authoritative.
 * 
 * @param Handle Ability Spec Handle of the ability we want to remove
 */
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Gameplay Abilities")
void ClearAbility(const FGameplayAbilitySpecHandle& Handle);
  • 지정한 Ability를 ASC에서 제거한다.

ClearAllAbilities

더보기
/** Wipes all 'given' abilities. This will be ignored if the actor is not authoritative. */
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="Gameplay Abilities")
void ClearAllAbilities();
  • ASC의 모든 Ability를 제거한다.

ClearAllAbilitiesWithInputID

더보기
/**
 * Clears all abilities bound to a given Input ID
 * This will be ignored if the actor is not authoritative
 *
 * @param InputID The numeric Input ID of the abilities to remove
 */
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Gameplay Abilities")
void ClearAllAbilitiesWithInputID(int32 InputID = 0);
  • Input ID에 Bind 된 모든 Ability를 ASC에서 제거한다.

SetRemoveAbilityOnEnd

더보기
/** Sets an ability spec to remove when its finished. If the spec is not currently active, it terminates it immediately. Also clears InputID of the Spec. */
void SetRemoveAbilityOnEnd(FGameplayAbilitySpecHandle AbilitySpecHandle);
  • 지정한 Ability 실행이 완료되면 ASC에서 제거한다.
    • 실행중이지 않는 경우 즉시 제거한다.
    • 실행중인 경우 그 입력을 즉시 지워 Player가 더이상 재활성화/상호작용 못하도록 막는다.

CanActivateAbility

  • 호출자가 Ability 실행을 시도하지 않아도 사용 가능 여부를 알려주는 함수
    • UI에서 Player가 사용할 수 없는 Icon을 Dimd 처리해야 하는 경우
    • Character에 특정 Particle/Sound Effect를 재생해 특정 Ability가 사용 가능한지 Notify해야 하는 경우
더보기
/** Returns true if this ability can be activated right now. Has no side effects */
virtual bool CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags = nullptr, const FGameplayTagContainer* TargetTags = nullptr, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr) const;

CallActivateAbility

  • Ability에 관련된 Game Code를 실행
    • 하지만 사용 가능 여부를 검사하지 않는다.
  • CanActivateAbility 검사와 Ability 실행 사이에 약간의 로직이 필요한 경우에 실행
  • Actor/Component와 달리 주 작업이 Tick을 수행하지 않는다.
    • 대신 활성화 중 비동기 작업을 시행할 수 있는 Ability Task를 지원 함.
    • C++에서는 Delegate로, BP에서는 실행 핀 노드로 해당 Task 출력을 처리한다.
더보기
/** Executes PreActivate and ActivateAbility */
void CallActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate = nullptr, const FGameplayEventData* TriggerEventData = nullptr);

ActivateAbility

  • 사용자가 Ability의 Custom Function 기능으로 Override해야 하는 Main Code
더보기
/** Actually activate ability, do not call this directly */
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData);

CommitAbility

  • Activate 내에서 호출 한 경우 Ability 실행 비용을 적용하는 함수
    • Attribute에서 Game System에 맞는 Resource를 감산하고 쿨다운 적용
더보기
virtual bool CommitAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr);
virtual bool CommitAbilityCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const bool ForceCooldown, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr);
virtual bool CommitAbilityCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr);

CancelAbility

  • Ability 취소 매커니즘을 제공한다.
    • Ability는 CanBeCanceled 함수가 요청을 거부할 수 있다.
  • CommitAbility와 달리 Ability 자체 외부 호출자에서 사용할 수 있다.
  • 취소가 성공하면 다음 작업이 이어진다.
    • OnGameplayAbilityCancelled로 Broadcast
    • 해당 Ability를 종료하기 위한 표준 Code 경로로 이동
    • Ability에 특수한 Cleanup Code를 실행할 기회를 주거나,
      정상적으로 종료했을 때와 다른 동작을 하도록 함.
  • RemoteEndOrCancelAbility()에서 WasCancelled 파라미터를 true로 설정하면 내부에서 CalcelAbility가 호출된다.
더보기
/** Cancels the specified ability CDO. */
void CancelAbility(UGameplayAbility* Ability);	

/** Cancels the ability indicated by passed in spec handle. If handle is not found among reactivated abilities nothing happens. */
void CancelAbilityHandle(const FGameplayAbilitySpecHandle& AbilityHandle);

/** Cancel all abilities with the specified tags. Will not cancel the Ignore instance */
void CancelAbilities(const FGameplayTagContainer* WithTags=nullptr, const FGameplayTagContainer* WithoutTags=nullptr, UGameplayAbility* Ignore=nullptr);

/** Cancels all abilities regardless of tags. Will not cancel the ignore instance */
void CancelAllAbilities(UGameplayAbility* Ignore=nullptr);

/** Cancels all abilities and kills any remaining instanced abilities */
virtual void DestroyActiveState();

TryActivateAbility

  • Ability를 실행하는 전형적인 방식
  • CanActivateAbility를 호출해 실행 여부를 판단한 뒤, 가능하면 CallActivateAbility를 호출
더보기
/** 
 * Attempts to activate every gameplay ability that matches the given tag and DoesAbilitySatisfyTagRequirements().
 * Returns true if anything attempts to activate. Can activate more than one ability and the ability may fail later.
 * If bAllowRemoteActivation is true, it will remotely activate local/server abilities, if false it will only try to locally activate abilities.
 */
UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilitiesByTag(const FGameplayTagContainer& GameplayTagContainer, bool bAllowRemoteActivation = true);

/**
 * Attempts to activate the ability that is passed in. This will check costs and requirements before doing so.
 * Returns true if it thinks it activated, but it may return false positives due to failure later in activation.
 * If bAllowRemoteActivation is true, it will remotely activate local/server abilities, if false it will only try to locally activate the ability
 */
UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilityByClass(TSubclassOf<UGameplayAbility> InAbilityToActivate, bool bAllowRemoteActivation = true);

/** 
 * Attempts to activate the given ability, will check costs and requirements before doing so.
 * Returns true if it thinks it activated, but it may return false positives due to failure later in activation.
 * If bAllowRemoteActivation is true, it will remotely activate local/server abilities, if false it will only try to locally activate the ability
 */
UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool bAllowRemoteActivation = true);

/** Triggers an ability from a gameplay event, will only trigger on local/server depending on execution flags */
bool TriggerAbilityFromGameplayEvent(FGameplayAbilitySpecHandle AbilityToTrigger, FGameplayAbilityActorInfo* ActorInfo, FGameplayTag Tag, const FGameplayEventData* Payload, UAbilitySystemComponent& Component);
  • TriggetAbilityFromGameplayEvent를 사용하려면 해당 Gameplay Ability에 Trigger가 세팅되어 있어야 한다.
    • Gameplay Tag를 할당하고 Gameplay Event 옵션을 활성화 해야 한다.
    • Event를 사용하면 Data를 포함한 Payload에 전달할 수 있다.
  • GameplayTag가 추가/제거 되었을 때 GameplayAbility 활성화를 할 수 있다.
  • Event를 발생하려면 다음 함수를 사용해야 한다.
더보기
//UAbilitySystemBlueprintLibrary
/**
 * This function can be used to trigger an ability on the actor in question with useful payload data.
 * NOTE: GetAbilitySystemComponent is called on the actor to find a good component, and if the component isn't
 * found, the event will not be sent.
 */
UFUNCTION(BlueprintCallable, Category = Ability, Meta = (Tooltip = "This function can be used to trigger an ability on the actor in question with useful payload data."))
static void SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload);

Passive Ability

  • 자동으로 활성화 되고 계속해서 동작해야 하는 Gameplay Ability
  • UGameplayAbility::OnAvatarSet() 함수를 Override
    • Gameplay Ability가 등록되고 AvatarActor가 세팅되어 TryActivateAbility()가 호출 될 때 자동으로 호출된다.
  • Passive Ability는 기본적으로 Server Only로 동작한다.
더보기
void UGDGameplayAbility::OnAvatarSet(const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilitySpec & Spec)
{
	Super::OnAvatarSet(ActorInfo, Spec);

	if (bActivateAbilityOnGranted)
	{
		ActorInfo->AbilitySystemComponent->TryActivateAbility(Spec.Handle, false);
	}
}

Activation Failed Tag

  • Ability는 왜 활성화에 실패했는지 알려주는 기능을 제공하고 있다.
  • 이 기능을 사용하려면 기본적인 실패 케이스에 해당하는 GameplayTag를 세팅하고,
    [Project]/Config/DefaultGame.ini 파일에 등록해야 한다,
  • Project
더보기
+GameplayTagList=(Tag="Activation.Fail.BlockedByTags",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.CantAffordCost",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.IsDead",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.MissingTags",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.Networking",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.OnCooldown",DevComment="")
  • DefaultGame.ini
더보기
[/Script/GameplayAbilities.AbilitySystemGlobals]
ActivateFailIsDeadName=Activation.Fail.IsDead
ActivateFailCooldownName=Activation.Fail.OnCooldown
ActivateFailCostName=Activation.Fail.CantAffordCost
ActivateFailTagsBlockedName=Activation.Fail.BlockedByTags
ActivateFailTagsMissingName=Activation.Fail.MissingTags
ActivateFailNetworkingName=Activation.Fail.Networking

EndAbility

  • Ability 실행을 마치면 Ability를 종료
  • Ability가 취소 된 경우 UGameplayAbility 클래스에서 취소 Process의 일부로 자동 처리를 한다.
    • 하지만 그 외 경우에는 개발자가 C+++ 함수를 호출하거나 BPNode를 추가해야 한다.
  • Ability를 정상적으로 종료하지 못하면 GAS는 아직 실행중으로 판단,
    이후 해당 Ability는 이를 차단하는 다른 Ability를 사용하지 못하게 할 수 있다.
더보기
/** Called by ServerEndAbility and ClientEndAbility; avoids code duplication. */
void	RemoteEndOrCancelAbility(FGameplayAbilitySpecHandle AbilityToEnd, FGameplayAbilityActivationInfo ActivationInfo, bool bWasCanceled);

UFUNCTION(Server, reliable, WithValidation)
void	ServerEndAbility(FGameplayAbilitySpecHandle AbilityToEnd, FGameplayAbilityActivationInfo ActivationInfo, FPredictionKey PredictionKey);

UFUNCTION(Client, reliable)
void	ClientEndAbility(FGameplayAbilitySpecHandle AbilityToEnd, FGameplayAbilityActivationInfo ActivationInfo);

Get ActiveAbilities

  • Active Ability란 존재할 수 없다.
    • 한번에 여러개의 Gameplay Ability가 활성화 될 수 있기 때문이다.
  • 대신 ASC::ActivatableAbilities에서 Asset/Granted/GameplayTag가 일치한 Ability를 검색해야만 한다.
    • GetActivatableAbilities() 함수를 통해 ActivatableAbilities의 TArray<FGameplayAbilitySpec>를 제공한다.
더보기
/** Returns the list of all activatable abilities. Read-only. */
const TArray<FGameplayAbilitySpec>& GetActivatableAbilities() const
{
	return ActivatableAbilities.Items;
}

/** Returns the list of all activatable abilities. */
TArray<FGameplayAbilitySpec>& GetActivatableAbilities()
{
	return ActivatableAbilities.Items;
}
  • 또한 GameplayAbilitySpecs를 수동으로 순회하는 대신 편의 함수를 제공한다.
더보기
/** 
 * Gets all Activatable Gameplay Abilities that match all tags in GameplayTagContainer AND for which
 * DoesAbilitySatisfyTagRequirements() is true.  The latter requirement allows this function to find the correct
 * ability without requiring advanced knowledge.  For example, if there are two "Melee" abilities, one of which
 * requires a weapon and one of which requires being unarmed, then those abilities can use Blocking and Required
 * tags to determine when they can fire.  Using the Satisfying Tags requirements simplifies a lot of usage cases.
 * For example, Behavior Trees can use various decorators to test an ability fetched using this mechanism as well
 * as the Task to execute the ability without needing to know that there even is more than one such ability.
 */
void GetActivatableGameplayAbilitySpecsByAllMatchingTags(const FGameplayTagContainer& GameplayTagContainer, TArray < struct FGameplayAbilitySpec* >& MatchingGameplayAbilities, bool bOnlyAbilitiesThatSatisfyTagRequirements = true) const;
  • bOnlyAbilitiesThatSatisfyTagRequirements
    • 주어진 Gameplay Tag 조건을 만족하고 현재 활성화 되어 있는 GameplayAbilitySpec만 반환할지 여부
  • 한번 FGameplayAbilitySpec을 탐색하면 IsActive()를 호출할 수 있다.

Tags

  • 여러 Gameplay Ability의 상호작용 방식을 결정

Tag Set

  • Ability의 Behaviour에 영향을 미칠수 있는 방식
  • Ability를 식별 및 분류

Gamplay Tag Container

  • 다른 Ability와의 Interaction을 지원

Cancel Abilities With Tag

  • 이 Ability가 실행되는 동안 이미 실행 중인 Ability의 Tag가 제공된 목록과 일치하면 그 Ability를 취소한다.

Block Abilities With Tag

  • 이 Ability가 실행되는 동안 일치하는 Tag가 있는 다른 Ability의 실행을 방지한다.

Activation Owned Tags

  • 이 Ability가 실행되는 동안 해당 Ability의 Owner에 이 Tag Set가 부여된다.

Activation Required Tags

  • 이 Ability는 Active 상태인 Actor/Component에 모든 Tag가 있을 때에만 Activate 할 수 있다.

Activation Blocked Tags

  • 이 Ability는 Active 상태인 Actor/Component에 모든 Tag가 없을 때에만 Activate 할 수 있다.

Target Required Tags

  • 이 Ability는 Target Actor/Component에 이 Tag가 모두 있을 때에만 Activate 할 수 있다.

Target Blocked Tags

  • 이 Ability는 Target Actor/Component에 이 Tag가 모두 없을 때에만 Actiavte 할 수 있다.

Gameplay Tag Query

  • 다른 Ability와의 Interaction을 지원

Gameplay Ability Spec

더보기
/**
 * An activatable ability spec, hosted on the ability system component. This defines both what the ability is (what class, what level, input binding etc)
 * and also holds runtime state that must be kept outside of the ability being instanced/activated.
 */
USTRUCT(BlueprintType)
struct GAMEPLAYABILITIES_API FGameplayAbilitySpec : public FFastArraySerializerItem
{
	GENERATED_USTRUCT_BODY()

PRAGMA_DISABLE_DEPRECATION_WARNINGS
	FGameplayAbilitySpec(const FGameplayAbilitySpec&) = default;
	FGameplayAbilitySpec(FGameplayAbilitySpec&&) = default;
	FGameplayAbilitySpec& operator=(const FGameplayAbilitySpec&) = default;
	FGameplayAbilitySpec& operator=(FGameplayAbilitySpec&&) = default;
	~FGameplayAbilitySpec() = default;
PRAGMA_ENABLE_DEPRECATION_WARNINGS

	FGameplayAbilitySpec()
		: Ability(nullptr), Level(1), InputID(INDEX_NONE), SourceObject(nullptr), ActiveCount(0), InputPressed(false), RemoveAfterActivation(false), PendingRemove(false), bActivateOnce(false)
	{ }

	/** Version that takes an ability class */
	FGameplayAbilitySpec(TSubclassOf<UGameplayAbility> InAbilityClass, int32 InLevel = 1, int32 InInputID = INDEX_NONE, UObject* InSourceObject = nullptr);

	/** Version that takes an ability CDO, this exists for backward compatibility */
	FGameplayAbilitySpec(UGameplayAbility* InAbility, int32 InLevel = 1, int32 InInputID = INDEX_NONE, UObject* InSourceObject = nullptr);

	/** Version that takes an existing spec def */
	FGameplayAbilitySpec(FGameplayAbilitySpecDef& InDef, int32 InGameplayEffectLevel, FActiveGameplayEffectHandle InGameplayEffectHandle = FActiveGameplayEffectHandle());

	/** Handle for outside sources to refer to this spec by */
	UPROPERTY()
	FGameplayAbilitySpecHandle Handle;
	
	/** Ability of the spec (Always the CDO. This should be const but too many things modify it currently) */
	UPROPERTY()
	TObjectPtr<UGameplayAbility> Ability;
	
	/** Level of Ability */
	UPROPERTY()
	int32	Level;

	/** InputID, if bound */
	UPROPERTY()
	int32	InputID;

	/** Object this ability was created from, can be an actor or static object. Useful to bind an ability to a gameplay object */
	UPROPERTY()
	TWeakObjectPtr<UObject> SourceObject;

	/** A count of the number of times this ability has been activated minus the number of times it has been ended. For instanced abilities this will be the number of currently active instances. Can't replicate until prediction accurately handles this.*/
	UPROPERTY(NotReplicated)
	uint8 ActiveCount;

	/** Is input currently pressed. Set to false when input is released */
	UPROPERTY(NotReplicated)
	uint8 InputPressed:1;

	/** If true, this ability should be removed as soon as it finishes executing */
	UPROPERTY(NotReplicated)
	uint8 RemoveAfterActivation:1;

	/** Pending removal due to scope lock */
	UPROPERTY(NotReplicated)
	uint8 PendingRemove:1;

	/** This ability should be activated once when it is granted. */
	UPROPERTY(NotReplicated)
	uint8 bActivateOnce : 1;

	/** Cached GameplayEventData if this ability was pending for add and activate due to scope lock */
	TSharedPtr<FGameplayEventData> GameplayEventData = nullptr;

	/** Activation state of this ability. This is not replicated since it needs to be overwritten locally on clients during prediction. */
	UPROPERTY(NotReplicated)
	FGameplayAbilityActivationInfo	ActivationInfo;

	/** Optional ability tags that are replicated.  These tags are also captured as source tags by applied gameplay effects. */
	UPROPERTY()
	FGameplayTagContainer DynamicAbilityTags;

	/** Non replicating instances of this ability. */
	UPROPERTY(NotReplicated)
	TArray<TObjectPtr<UGameplayAbility>> NonReplicatedInstances;

	/** Replicated instances of this ability.. */
	UPROPERTY()
	TArray<TObjectPtr<UGameplayAbility>> ReplicatedInstances;

	/**
	 * Handle to GE that granted us (usually invalid). FActiveGameplayEffectHandles are not synced across the network and this is valid only on Authority.
	 * If you need FGameplayAbilitySpec -> FActiveGameplayEffectHandle, then use AbilitySystemComponent::FindActiveGameplayEffectHandle.
	 */
	UPROPERTY(NotReplicated)
	FActiveGameplayEffectHandle	GameplayEffectHandle;

	/** Passed on SetByCaller magnitudes if this ability was granted by a GE */
	TMap<FGameplayTag, float> SetByCallerTagMagnitudes;

	/** Returns the primary instance, used for instance once abilities */
	UGameplayAbility* GetPrimaryInstance() const;

	/** interface function to see if the ability should replicated the ability spec or not */
	bool ShouldReplicateAbilitySpec() const;

	/** Returns all instances, which can include instance per execution abilities */
	TArray<UGameplayAbility*> GetAbilityInstances() const
	{
		TArray<UGameplayAbility*> Abilities;
		Abilities.Append(ReplicatedInstances);
		Abilities.Append(NonReplicatedInstances);
		return Abilities;
	}

	/** Returns true if this ability is active in any way */
	bool IsActive() const;

	void PreReplicatedRemove(const struct FGameplayAbilitySpecContainer& InArraySerializer);
	void PostReplicatedAdd(const struct FGameplayAbilitySpecContainer& InArraySerializer);

	FString GetDebugString();
};
  • GameplayAbility가 활성화 가능한 Ability가 정의된 후에 ASC에 존재하게 된다.
    • GameAbility Class
    • GameAbility Level
    • Input Binding
    • GameplayAbility Class로부터 분리되어야만 하는 runtime state
  • GameAbility가 Server에서 부여되면,
    Server는 해당 Ability의 GameplayAbilitySpec을 활성화 되어야 할 대상 Client에 Replicate 한다.
  • GameplayAbilitySpec을 활성화 하면 Instance Policy에 따라 GameplayAbility Instance가 생성된다.

Passing Data to Abilities

  • GameplayAbility의 범용적인 패러다임은 Activate -> General Data -> Aply -> End이다.
    • 하지만 때로는 이미 존재하는 Data를 기반으로 동작해야 하기도 하다.
  • GAS는 몇 가지 Gameplay Ability 외부의 데이터를 사용하는 옵션을 제공한다.

Activate GameplayAbility by Event

더보기
/**
 * This function can be used to trigger an ability on the actor in question with useful payload data.
 * NOTE: GetAbilitySystemComponent is called on the actor to find a good component, and if the component isn't
 * found, the event will not be sent.
 */
UFUNCTION(BlueprintCallable, Category = Ability, Meta = (Tooltip = "This function can be used to trigger an ability on the actor in question with useful payload data."))
static void SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload);
  • Payload가 있는 Data를 포함한 Event로 Gameplay Ability를 활성화
  • Event의 Payload는 Client에서 Server로 local predict 된 Gameplay Ability를 Replicate한다.
  • 다만 Ability를 Input Bind로 활성화 하는 것을 방지한다는 단점이 있다.

AbilityTask_WaitGameplayEvent 

  • Gameplay Ability가 활성화 된 후 Payload Data를 Event로 받도록 설정
  • Event Payload를 보내는 과정은 Gameplay Ability가 Event로 활성화 되는 것과 같다.
  • Event가 Ability Task로 REplicate 되지 않고, 오직 LocalOnly나 ServerOnly만 가능하다는 단점이 있다.

Use TargetData

  • Custom Target Data를 선언하여 Client와 Server 사이에 임시 Data 전달

Store Data on the OwnerActor or AvatarActor

  • OwnerActor나 AvatarActor, 혹은 Reference를 갖을 수 있는 Object의 Replicated Varaiable에 저장
  • 이 방법은 Input Bind로 활성화되는 Gameplay Ability로 작업하기에 가장 유연한 방법이다.
  • 하지만 Data를 사용하는 때에 Replication으로 동기화가 되었음을 보장할 수 없다.
    • Replicated Variable을 설정 한 직후 Gameplay Ability를 활성화 하는 경우에
      Packet loss로 인한 동기화 오류가 발생할 여지가 있다.

Ability Cost and Cooldown

  • Gameplay Ability는 선택적으로 Cost나 Cooldown 기능을 제공한다.
  • Cost
    • Instant GE로 실행할 때 반드시 충족해야 하는 ASC에서 사전에 선언된 Gameplay Ability가 Attribute의 요구량
  • Cooldown
    • Duration GE 가 만료된 이후 재실행 될 때까지 Gameplay Ability의 재실행을 방지하는 Duration
  • CanActivateAbility() 함수에서 해당 Gameplay Ability를 소유한 ASC에서 Cost와 Cooldown을 체크한다.
더보기
/** Checks cost. returns true if we can pay for the ability. False if not */
virtual bool CheckCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr) const;

/** Checks cooldown. returns true if we can be used again. False if not */
virtual bool CheckCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr) const;
  • Activate()가 호출된 후, 선택적으로 Cost와 Cooldown 관련 함수가 호출된다.
더보기
virtual bool CommitAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr);
virtual bool CommitAbilityCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const bool ForceCooldown, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr);
virtual bool CommitAbilityCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr);

/** Applies CooldownGameplayEffect to the target */
virtual void ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const;
/** Applies the ability's cost to the target */
virtual void ApplyCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const;
  • Designer는 Cost와 Cooldown이 동시에 적용되지 않아야 하면 이를 분리할 수 있다.
  • 마지막으로 GameplayAbility는 Cost나 Cooldown으로 실행이 실패 했을 때 관련해서 한번 더 체크한다.
  • ASC의 Attribute는 GameplayAbility가 활성화 되면서 값이 변경되어 Cost 조건을 만족하지 못할 수도 있다.

Leveling Up Abilities

기존 Ability 부여를 해제하고 재부여

  • ASC에서 GameplayAbility를 제거
  • 다음 Level의 GameplayAbility를 Server에서 부여
  • 이 제거 동작은 활성화 될 때 발생한다.

GameplayAbilitySpec의 Level을 증가

  • Server에서 Level이 오르는 GameplayAbilitySpec을 탐색
  • 해당 GameplayAbilitySpec을 dirty 처리해서 Client에 Replicate 되도록 처리
  • GameplayAbility가 활성화 될 때 제거되지 않음

비교

  • Level이 상승할 때 해당 Gameplay Ability를 취소할 지 말지 정도의 차이가 있다.
  • 두가지 방법 모두 Gameplay Ability에 따라 매우 빈번하게 사용할 것이다.

Gameplay Ability Set

  • Input Bind와 초기 Gameplay Ability 정보를 담고 있는 편의성 Data Asset
    • Gameplay Ability를 부여하는 로직이 있는 캐릭터에게 유용
    • Subclass에서 외부 로직이나 Property를 포함시킬 수 있다.
    • Paragon에서는 영웅들이 소유하고 있는 고유한 Gameplay Ability를 들고 있는 Gameplay Ability Set을
      각각 가지고 있다.

Ability Batching

  • 전통적인 Gameplay Ability는 LifeCycle동안 Client에서 Server로 최소 2, 3개의 RPC를 보낸다.
    • CallServerTryActivateAbility()
    • ServerSetReplicatedTargetData()
      • 선택적
    • ServerEndAbility
  • 만약 위 모든 동작을 Frame에서 하나의 Atomic Group으로 수행할 수 있다면,
    모든 RPC를 하나의 RPC로 최적화 할 수 있다.
  • GAS는 이런 경우에 대해 Ability Batching 기능을 통해 RPC 최적화를 제공한다.
    • 대표적인 예시는 Hitscan 총기류이다.
    • Hitscan 총기류 활성화, Line Trace, Target Data를 서버로 전달을 한번에 처리한다.
  • 다만 Ability Batching은 ASC에서 Default로는 Disable 처리되어 있다.
    • 이를 사용하려면 아래 함수를 Override 해야 한다.
더보기
virtual bool ShouldDoServerAbilityRPCBatch() const { return false; }
  • Ability Batching을 활성화 하였다면, Batch 하고 싶은 Ability를 활성화 하기 전에
    FScopedServerAbilityRPCBatcher 구조체를 미리 만들어야 한다.
더보기
/** Helper struct for defining ServerRPC batch windows. If null ASC is passed in, this becomes a noop. */
struct GAMEPLAYABILITIES_API FScopedServerAbilityRPCBatcher
{
	FScopedServerAbilityRPCBatcher(UAbilitySystemComponent* InASC, FGameplayAbilitySpecHandle InAbilityHandle);
	~FScopedServerAbilityRPCBatcher();

private:

	UAbilitySystemComponent* ASC;
	FGameplayAbilitySpecHandle AbilityHandle;
	FScopedPredictionWindow ScopedPredictionWindow;
};
  • 이 구조체는 Scope 안의 Ability들을 모두 Batch한다.
    • 한 번 FScopedServerAbilityRPCBatcher의 Scope를 벗어나면, 활성화 된 어떠한 Ability들도 Batch 되지 않는다.
  • FScopedServerAbilityRPCBatcher는 RPC 호출을 가로채는 대신 구조체에 Batch 할 수 있는 message를 Batch 한다.
  • FScopedServerAbilityRPCBatcher의 Scope를 벗어날 때,
    UASC::EndServerAbilityRPCBatch() 함수를 통해 Batch 구조체를 Server로 RPC한다.
    • Server는 UASC::ServerAbilityRPCBatch_Internal 함수를 통해 Batch RPC를 받는다.
더보기
virtual void EndServerAbilityRPCBatch(FGameplayAbilitySpecHandle AbilityHandle);

// Overridable function for sub classes
virtual void ServerAbilityRPCBatch_Internal(FServerAbilityRPCBatch& BatchInfo);
  • 이 때 BatchInfo는 다음 Flag 정보를 포함한다.
    • Ability가 반드시 끝나야 하는지 여부
    • Activity가 활성화 되었을 때 Press Input이 발생했는지 여부
    • TargetData가 포함되었는지 여부
  • ServerAbilityRPCBatch_Internal은 Batch가 정상 동작하는지 확인하기 위해 Break Point를 걸기 매우 적절한 함수다.
  • 혹은 이를 대신 해, 아래 커맨드를 입력해 로그를 남길 수 있다.
더보기
AbilitySystem.ServerRPCBatching.Log 1
  • 다음은 Ability Batch에 대한 간단한 예시코드이다.
더보기
bool UGSAbilitySystemComponent::BatchRPCTryActivateAbility(FGameplayAbilitySpecHandle InAbilityHandle, bool EndAbilityImmediately)
{
	bool AbilityActivated = false;
	if (InAbilityHandle.IsValid())
	{
		FScopedServerAbilityRPCBatcher GSAbilityRPCBatcher(this, InAbilityHandle);
		AbilityActivated = TryActivateAbility(InAbilityHandle, true);

		if (EndAbilityImmediately)
		{
			FGameplayAbilitySpec* AbilitySpec = FindAbilitySpecFromHandle(InAbilityHandle);
			if (AbilitySpec)
			{
				UGSGameplayAbility* GSAbility = Cast<UGSGameplayAbility>(AbilitySpec->GetPrimaryInstance());
				GSAbility->ExternalEndAbility();
			}
		}

		return AbilityActivated;
	}

	return AbilityActivated;
}

Replication

  • 내부 상태 및 GE Replication을 지원
    • 이를 비활성화 하면 네트워크 대역폭과 CPU 사이클 절약이 가능

Gameplay Ability Replication Policy

  • Ability가 Network를 통해 자체 Instance를 Replicate 할지, 상태를 업데이트 할지, GE를 전송할지 제어하는 수단
  • 이름이 기능을 명확히 알려주지 않으며 사용하지 않는 것을 권장하는 옵션
    • GameplayAbilitySpec이 기본적으로 Server에서 Client로 Replicate된다.
    • 또한 GameplayAbility는 AbilityTask나 GameplayCue를 통해 Replicate 되거나 RPC를 호출한다.
    • 근시일에 이 기능을 제거할 것이라고 Epic의 Dave Ratti가 업급하기도 하였다.

Gameplay Net Execution Policy

  • Gameplay Ability가 로컬에서 predict 되는지 여부를 결정
  • GE의 추가 cost, cooldown과 관련된 기본적인 동작을 포함함.

Local Predicted

  • 반응성과 정확성 사이에 균현이 잘 잡힌 옵션
  • Client가 명령을 내리면 Local Client에서 Ability가 즉시 실행된다.
    • 하지만 실제 영향이 어땠는지에 대한 최종 결정은 Server가 내린다.
    • 그리고 이 값을 Client에 Override 할 수 있다.
  • 실제로 Client는 Server에 Ability 실행 권한을 요청하지만, Server가 동의할 것을 예상하고 Local로 진행한다.
  • Client는 Local에서 Ability의 Behaviour를 예측하므로
    서버와의 Behaviour가 다르지 않으면 지연 없이 매끄럽게 느껴진다.

Local Only

  • Client가 단순히 Local에서 Ability를 실행
  • Server에 Replicate하지 않는다.
    • Listen Server Host, Single Player Game에서는 서버에서도 실행되기도 한다.
    • Dedicate Server에서는 적용되지 않는다.
  • 이 Ability로 Client가 영향을 주는 모든 것은 일반 Replicate Protocol을 따른다.
    • 여기에 서버에서 보정을 받을 가능성도 포함된다.

Server Initiated

  • Server에서 시작하는 Abilit가 Client에 전파된다.
    • 종종 Client 관점에서 Server의 실제 상황과 더욱 정확하게 동작하기도 하지만,
      Ability를 사용하는 Client에서는 Delay가 관측된다.
    • Delay가 매우 짧더라도 긴박한 상황에서는 매끄럽지 않은 느낌을 받는다.

Server Only

  • Server에서만 Ability가 실행되고 Client로 Replicate 되지 않는다.
  • 이 Ability가 변경하는 모든 변수는 평소처럼 Replicate 된다.
    • Ability가 Server Authorized Data에 영향을 미칠 수 있고, 그 뒤에 Client에 Replicate 된다는 의미이다.
    • 이 방식을 통해 Ability를 Server에서만 실행 하더라도 Client가 관측하는 Effect를 계속 보유할 수 있다.

Net Security Policy

  • 대상 Ability가 Network 환경에서 어디서 실행되어야만 하는지 정의
  • Client에서 제한된 기능을 동작하는 것을 방지한다.

ClientOrServer

  • 아무런 보안이 필요 없음.
  • Server, Client 모두 자유롭게 실행과 제거를 할 수 있다.

ServerOnlyExecution

  • Client에서 이 Ability에 대한 실행 요청을 Server가 거부한다.
  • Client는 여전히 Server에 Ability의 취소나 종료 요청을 보낼 수는 있다.

ServerOnlyTermination

  • Client에서 이 Ability에 대한 취소/종료 요청을 Server가 거부한다.
  • Client는 여전히 Ability의 실행 요청을 보낼 수는 있다.

ServerOnly

  • 오직 Server에서만 Ability에 대한 실행과 제거를 조절한다.
  • Client에서 보내는 어떠한 요청도 모두 무시된다.

Server Respects Remote Ability Cancellation

  • Client의 GameplayAbility가 취소 혹은 정상적인 동작 종료로 끝난다면,
    서버에서 작업이 완수가 되었는지와 무관하게 강제로 끝낸다는 의미의 옵션
  • 특히 정상적인 종료의 케이스의 경우, Latency가 높은 유저의 경우 매우 중요한 문제를 야기할 수 있다.
  • 때문에 이 옵션도 사용하지 않는 것을 권장한다.

Replicate Input Directly

  • Client에서 발생하는 모든 Input Press/Release를 Server로 Replicate 하는 옵션
    • Epic에서는 이 옵션 사용을 지양하는 것을 권장한다.
    • 대신 ASC에 Input이 Bind 되어 있다면 기존 입력 관련해 Ability Task에서 제공하는
      Generic Replicated Event 사용을 권장한다.
더보기
/** Direct Input state replication. These will be called if bReplicateInputDirectly is true on the ability and is generally not a good thing to use. (Instead, prefer to use Generic Replicated Events). */
UFUNCTION(Server, reliable, WithValidation)
void ServerSetInputPressed(FGameplayAbilitySpecHandle AbilityHandle);

UFUNCTION(Server, reliable, WithValidation)
void ServerSetInputReleased(FGameplayAbilitySpecHandle AbilityHandle);

Instancing Policy

  • Gamplay Ability를 실행하면 보통 해당 Ability Type의 새 Object가 Spawn 되어 상황을 추적한다.
    • BR, MOBA, MMO, RTS 게임처럼 수백의 Player나 AI가 전투를 벌이는 경우,
      Ability 실행 빈도가 매우 높아져 Object 생성이 많아져 성능에 부정적인 영향을 줄 수 있다.
  • 이를 해결하기 위해 Ability에 3가지 Instancing Policy를 제공해 성능과 기능 사이 균형을 맞출 수 있다.

Instanced per Execution

  • Ability를 실행할 때마다 Ability Object의 사본을 Spawn

장점

  • BP Graph와 멤버 변수를 자유롭게 사용 할 수 있음
  • 실행 시작 시 모든 값이 Default로 초기화 된다.
  • 가장 간단하게 구현할 수 있는 Instancing Polity

단점

  • 대규모 Overhad가 수반되어 자주 실행되지 않는 Ability에 이상적
    • ex) MOBA의 궁극기

Instanced per Actor

  • Ability를 처음 실행하면 Actor마다 하나의 Ability Instance를 Spawn
    • 향후 재실행 시 해당 Instance를 재사용
  • Ability를 실행할 때마다 멤버 변수를 지워야 하지만, 여러번 실행에서 정보를 절약할 수 있다.
  • 각 Instance는 Ability에 Replicated Object를 통해 변수 변화와 RPC를 처리 할 수 있다.
    • 하지만 실행할 때마다 새 Object를 Spawn하느라 네트워크 대역폭과 CPU 시간이 낭비되지 않는다.
    • 때문에 Replication에 이상적이다.
  • 규모가 큰 상황에서 Performance가 뛰어난 정책
    • 많은 수의 Actor가 처음 Ability를 사용할 때에만 Object를 Spawn하기 때문

Non-Instanced

  • 전체 Category 중 가장 효율적인 Instancing Policy
  • Ability를 실행할 때마다 Object Spawn 대신 Class Default Object를 사용
  • 하지만 이런 효용성으로 인해 몇 가지 제약사항이 발생한다.
    • C++로만 작성이 가능하다.
    • Instance가 없는 Ability의 BP Class를 생성할 수는 있지만,
      노출된 Property의 Default 값을 변경할 때에만 사용 가능하다.
    • Ability는 실행 중에 멤버 변수를 변경하거나 Delegate Bind를 하면 안된다.
    • 변수를 Replicate 하거나 RPC를 처리할 수 없다.
  • 내부적인 변수 저장과 Data Replication이 필요 없는 Ability에만 사용
    • 다만 Ability의 사용자가 Attribute를 설정하는 것은 가능
  • ex) RTS나 MOBA 게임에서 유닛이 사용하는 기본 공격

Trigger with Gameplay Event

  • 일반 Channel을 통하지않고도 어떤 Context의 Data Payload를 전송해
    Ability를 직접 Trigger 하도록 전달될 수 있는 Struct
  • 일반적으로 다음 기능을 제공
    • Actor에 SendGameplayEventToActor을 호출
    • IAbilitySystmInterface와 Gameplay Event에 필요한 Context 정보를 구현하는 Actor 제공
  • 하지만 ASC에서 HandleGameplayEvent를 바로 호출해도 문제 없다.
  • Gameplay Ability를 호출하는 정상적인 경로가 아니기에
    필요한 Context 정보는 FGameplayEventData 구조체로 전달한다.
  • 이 구조체는 범용이기 때문에 특정 Gameplay Event나 Ability용으로 확장되지 않는다.
    • 하지만 ContextHandle 관련 필드에 부가 정보를 제공하면 Ability나 Gameplay Event용으로 사용할 수 있다.
  • Gameplay Event가 Gameplay Ability를 Trigger하면, Activate Ability 대신 Activate Ability From Event를 사용한다.
    • Activate Ability From Event는 부가 Context Data를 parameter로 제공할 수 있다.
  • Ability가 Gameplay Event에 반응하려면 반드시 Activate Ability from Event로 처리되어야 한다.
    • 하지만 BP에서 구현되고 나면 Activate Ability를 대신해 모든 Acitivate Traffic을 받게 된다.

'UE5 > GAS' 카테고리의 다른 글

[GAS] Prediction  (0) 2024.05.21
[GAS] Gameplay Cue  (0) 2024.05.21
[GAS] Ability Task  (0) 2024.05.16
[GAS] Gameplay Effect  (0) 2024.05.15
[GAS] Attribute Set  (1) 2024.05.14

+ Recent posts