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 수집을 필터링하는 것은 매우 효과적이다.
사용법
Actor의 Constructor에서 NetDormancy를 초기화
보통은 DORM_DormantAll로 초기화
Actor가 Map에 배치된 경우, DORM_Initial로 초기화
NetDormancy가 DORM_DormantAll/DORM_Initial일 때에는 Dormant 상태가 되어 Replicate가 되지 않는다.
Dormant 상태의 Actor의 값을 변경하더라도 Awake/Flush를 하면 변경사항이 보존되지 않는다.
Replicated Property를 변경하기 전 FlushNetDormancy/ForceNetUpdate/SetNetDormancy 함수를 호출.
일반적으로는 값을 변경 후 함수 호출을 해도 Replicate 될 수 있다.
하지만 정식 스펙 상으로는 올바른 사용법도 아니니 이를 지양해야 한다.
대표적으로 Fast Array의 값을 변경한 후 함수를 호출하면 변경사항이 Replicate되지 않는다.
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 상태가 즉각 적용되지 않는다.
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;
}
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 된다.
정보를 수신할 때에는 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 부하를 크게 줄여준다.
일반 게임 접속을 통하지 않고 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와 통신해 관전 가능한 세션을 요청, 입장 승인 여부를 확인
일반적으로 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에서 생성, 추가한다.
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 추가
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을 하지 않는다.
/** 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 된다.
/**
* 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 한다.
/**
* 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에 전달한다.
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하다.
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;
}
}
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하게 받는다.
/*
* 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;
}
/*
* 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 과정에서 반환한 것들을 사용한다.
/**
* 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);
/** Wipes all 'given' abilities. This will be ignored if the actor is not authoritative. */
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="Gameplay Abilities")
void ClearAllAbilities();
/**
* 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);
/** 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 출력을 처리한다.
/** 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 활성화를 할 수 있다.
//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()가 호출 될 때 자동으로 호출된다.
/** 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 할 수 있다.
/**
* 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 외부의 데이터를 사용하는 옵션을 제공한다.
/**
* 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 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를 걸기 매우 적절한 함수다.
/** 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을 받게 된다.
/** Delegate for when an effect is applied */
DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnGameplayEffectAppliedDelegate, UAbilitySystemComponent*, const FGameplayEffectSpec&, FActiveGameplayEffectHandle);
/** Called on server whenever a GE is applied to self. This includes instant and duration based GEs. */
FOnGameplayEffectAppliedDelegate OnGameplayEffectAppliedDelegateToSelf;
/** Called on server whenever a GE is applied to someone else. This includes instant and duration based GEs. */
FOnGameplayEffectAppliedDelegate OnGameplayEffectAppliedDelegateToTarget;
/** Called on both client and server whenever a duration based GE is added (E.g., instant GEs do not trigger this). */
FOnGameplayEffectAppliedDelegate OnActiveGameplayEffectAddedDelegateToSelf;
/** Called on server whenever a periodic GE executes on self */
FOnGameplayEffectAppliedDelegate OnPeriodicGameplayEffectExecuteDelegateOnSelf;
/** Called on server whenever a periodic GE executes on target */
FOnGameplayEffectAppliedDelegate OnPeriodicGameplayEffectExecuteDelegateOnTarget;
/** This ASC has successfully applied a GE to something (potentially itself) */
void OnGameplayEffectAppliedToTarget(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);
void OnGameplayEffectAppliedToSelf(UAbilitySystemComponent* Source, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);
void OnPeriodicGameplayEffectExecuteOnTarget(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecExecuted, FActiveGameplayEffectHandle ActiveHandle);
void OnPeriodicGameplayEffectExecuteOnSelf(UAbilitySystemComponent* Source, const FGameplayEffectSpec& SpecExecuted, FActiveGameplayEffectHandle ActiveHandle);
위 함수들은 LocalRole에 따라 호출 여부가 다르다.
Authority: 언제나 항상 호출
Autonomous: 대상 GE의 Replication Mode가 Full이거나 Mixed인 경우에만 호출 됨
Simulated: 대상 GE의 Replication Mode가 Full일 때에만 호출 됨
Remove
GE는 GameplayAbilities와 ASC에서 RemoveActiveGameplayEffect가 포함된 함수들에 의해 제거된다.
또한 Duration, Infinite Ge는 제거될 때 Delegate를 받을 수 있다.
/** Contains all of the gameplay effects that are currently active on this component */
UPROPERTY(Replicated)
FActiveGameplayEffectsContainer ActiveGameplayEffects;
DECLARE_MULTICAST_DELEGATE_OneParam(FOnGivenActiveGameplayEffectRemoved, const FActiveGameplayEffect&);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnActiveGameplayEffectRemoved_Info, const FGameplayEffectRemovalInfo&);
/** Called when any gameplay effects are removed */
FOnGivenActiveGameplayEffectRemoved& OnAnyGameplayEffectRemovedDelegate();
/** Returns delegate structure that allows binding to several gameplay effect changes */
FOnActiveGameplayEffectRemoved_Info* OnGameplayEffectRemoved_InfoDelegate(FActiveGameplayEffectHandle Handle);
위 함수들은 LocalRole에 따라 호출 여부가 다르다.
Authority: 언제나 항상 호출
Autonomous: 대상 GE의 Replication Mode가 Full이거나 Mixed인 경우에만 호출 됨
Simulated: 대상 GE의 Replication Mode가 Full일 때에만 호출 됨
/** Gameplay effect duration policies */
UENUM()
enum class EGameplayEffectDurationType : uint8
{
/** This effect applies instantly */
Instant,
/** This effect lasts forever */
Infinite,
/** The duration of this effect will be specified by a magnitude */
HasDuration
};
/**
* Helper function to sum all of the mods in the specified array, using the specified modifier bias and evaluation parameters
*
* @param InMods Mods to sum
* @param Bias Bias to apply to modifier magnitudes
* @param Parameters Evaluation parameters
*
* @return Summed value of mods
*/
float FAggregatorModChannel::SumMods(const TArray<FAggregatorMod>& InMods, float Bias, const FAggregatorEvaluateParameters& Parameters)
{
float Sum = Bias;
for (const FAggregatorMod& Mod : InMods)
{
if (Mod.Qualifies())
{
Sum += (Mod.EvaluatedMagnitude - Bias);
}
}
return Sum;
}
/**
* Evaluates the channel's mods with the specified base value and evaluation parameters
*
* @param InlineBaseValue Base value to use for the evaluation
* @param Parameters Additional evaluation parameters to use
*
* @return Evaluated value based upon the channel's mods
*/
float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
for (const FAggregatorMod& Mod : Mods[EGameplayModOp::Override])
{
if (Mod.Qualifies())
{
return Mod.EvaluatedMagnitude;
}
}
float Additive = SumMods(Mods[EGameplayModOp::Additive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Additive), Parameters);
float Multiplicitive = SumMods(Mods[EGameplayModOp::Multiplicitive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Multiplicitive), Parameters);
float Division = SumMods(Mods[EGameplayModOp::Division], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Division), Parameters);
if (FMath::IsNearlyZero(Division))
{
ABILITY_LOG(Warning, TEXT("Division summation was 0.0f in FAggregatorModChannel."));
Division = 1.f;
}
return ((InlineBaseValue + Additive) * Multiplicitive) / Division;
}
Multiply/Divide Modifier는 둘 다 1의 Bias Value를 가지고 있다.
/** Notifies when GameplayEffectSpec is blocked by an ActiveGameplayEffect due to immunity */
DECLARE_MULTICAST_DELEGATE_TwoParams(FImmunityBlockGE, const FGameplayEffectSpec& /*BlockedSpec*/, const FActiveGameplayEffect* /*ImmunityGameplayEffect*/);
/** Immunity notification support */
FImmunityBlockGE OnImmunityBlockGameplayEffectDelegate;
GarantedApplicationImmunityTags
5.3 버전에서 Deprecated 됨
UImmunityGameplayEffectComponent를 대신 사용
GrantedApplicationImmunityQuery
5.3 버전에서 Deprecated 됨
UImmunityGameplayEffectComponent를 대신 사용
GameplayEffectSpec
GE의 인스턴스화로 취급할 수 있다.(이하 GESpec)
자신이 가르키는 GE의 Reference 만들어진 Level, 만든 주체에 대한 정보를 내포하고 있다.
이 정보들은 GE와 다르게 Runtime상에서 적용되기 전까지 자유롭게 생성/수정이 가능하다.
GE가 적용 될 때 GESpec이 GE로부터 생성되고 실질적으로 GESpec이 Target에 적용된다.
GESpec은 UASC::MakeOutgoingSpec()함수를 통해 GE로부터 생성된다.
/** Get an outgoing GameplayEffectSpec that is ready to be applied to other things. */
UFUNCTION(BlueprintCallable, Category = GameplayEffects)
virtual FGameplayEffectSpecHandle MakeOutgoingSpec(TSubclassOf<UGameplayEffect> GameplayEffectClass, float Level, FGameplayEffectContextHandle Context) const;
GESpec은 그 즉시 적용될 필요가 없다.
보통은 투사체에 GESpec을 전달하고 충돌 발생 시 연산에 사용하곤 한다.
GESpec이 성공적으로 적용되면 FActiveGameplayEffect를 반환한다.
GESpec에는 아래 내용들이 제공된다.
GESpec을 만드는데 사용된 GE 클래스
GESpec의 Level
보통 GESpec을 만드는데 사용한 Ability와 같지만 다를수도 있음
Duration
Default는 GE의 Duration이지만 값이 다를 수도 있다.
Period
Default는 GE의 Period이지만 다를 수도 있다.
현재 GESpec의 Stack Count
상한은 GE의 설정값을 따른다.
GameplayEffectContextHandle
이를 통해 누가 이 GeSpec을 만들었는지 인지할 수 있다.
GESpec이 생성 되었을 때 Snapshotting으로 인해 Capture되는 Attribute 들
DynamicGrantedTag
GESpec이 부여 될 때 GameplayTag에 GE가 추가로 부여해야 하는 GameplayEffectSpec
DynamicAssetTag
GESpec이 GE와 비교해 추가로 가져야 하는 Tag들
SetByCaller
SetByCallers
GESpec으로 하여금 GameplayTag나 FName과 관련된 float 값을 전달할 수 있도록 한다.
이들은 GESpec에 TMap 형태로 저장된다.
이 값들은 GE의 Modifier에 사용되거나, 일반적인 의미의 Ferring Floats로 사용될 수 있다.
Modifiers에서 사용
GE class보다 앞서 정의되어야만 한다.
GameTag 버전만 사용 가능하다.
GE class에 하나가 정의 되었는데 GESpec이 대응하는 Tag-float pair가 없는 경우, GESpec에서 Runtime Error가 발생하며 0을 반환한다.
Divide Operation에서 문제를 야기할 수 있다.
그 외에서 사용
어느 class보다도 앞서 정의 될 필요가 없다.
GESpec에 존재하지 않는 SetByCaller를 읽더라도 사용자로 하여금 Default 값과 warning을 반환할 수 있다.
관련 함수
보통 Ability 안에서 생성된 숫자 데이터들은 SetByCallers을 통해 GameplayEffectExecutionCalculations나 ModifierMagnitudeCalculations로 전달하는 것이 일반적이다.
SetByCaller를 BP에서 읽을 수 있으려면 BPLibrary에 Custom Node를 만들어 줘야 한다.
/** Sets the magnitude of a SetByCaller modifier */
void SetSetByCallerMagnitude(FName DataName, float Magnitude);
/** Sets the magnitude of a SetByCaller modifier */
void SetSetByCallerMagnitude(FGameplayTag DataTag, float Magnitude);
/** Returns the magnitude of a SetByCaller modifier. Will return 0.f and Warn if the magnitude has not been set. */
float GetSetByCallerMagnitude(FName DataName, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;
/** Returns the magnitude of a SetByCaller modifier. Will return 0.f and Warn if the magnitude has not been set. */
float GetSetByCallerMagnitude(FGameplayTag DataTag, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;
이 중 FName을 쓰는 것보다 GameplayTag를 사용하는 것을 권장한다.
GameplayEffectContext
GEContext는 GESpec의 Instigator와 TargetData를 가지고 있다.
이는 Subclass를 통해 임시 데이터를 다른 곳으로 전달하는 좋은 구조이다.
ModifierMagnitudeCalculations, GameplayEffectExecutionCalculations, AS, GameplayCues 등
/**
* Struct representing the definition of a custom execution for a gameplay effect.
* Custom executions run special logic from an outside class each time the gameplay effect executes.
*/
USTRUCT(BlueprintType)
struct GAMEPLAYABILITIES_API FGameplayEffectExecutionDefinition
{
GENERATED_USTRUCT_BODY()
/**
* Gathers and populates the specified array with the capture definitions that the execution would like in order
* to perform its custom calculation. Up to the individual execution calculation to handle if some of them are missing
* or not.
*
* @param OutCaptureDefs [OUT] Capture definitions requested by the execution
*/
void GetAttributeCaptureDefinitions(OUT TArray<FGameplayEffectAttributeCaptureDefinition>& OutCaptureDefs) const;
/** Custom execution calculation class to run when the gameplay effect executes */
UPROPERTY(EditDefaultsOnly, Category=Execution)
TSubclassOf<UGameplayEffectExecutionCalculation> CalculationClass;
/** These tags are passed into the execution as is, and may be used to do conditional logic */
UPROPERTY(EditDefaultsOnly, Category = Execution)
FGameplayTagContainer PassedInTags;
/** Modifiers that are applied "in place" during the execution calculation */
UPROPERTY(EditDefaultsOnly, Category = Execution)
TArray<FGameplayEffectExecutionScopedModifierInfo> CalculationModifiers;
/** Other Gameplay Effects that will be applied to the target of this execution if the execution is successful. Note if no execution class is selected, these will always apply. */
UPROPERTY(EditDefaultsOnly, Category = Execution)
TArray<FConditionalGameplayEffect> ConditionalGameplayEffects;
};
/** Array of executions that will affect the target of this effect */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = GameplayEffect)
TArray<FGameplayEffectExecutionDefinition> Executions;
Modifier에서 지원하는 정보 이상의 것이 필요할 때에는 사용
UGameplayEffectExecutionCalculation을 사용하면 GE의 Custom Behaviour를 정의
Modifier로 커버할 수 없는 복잡한 식을 정의할 때 유용
Application Requirements
GE를 적용하는 조건을 처리하는 부분
5.3 버전 기준으로 Deprecated
UCustomCanApplyGameplayEffectComponent를 대신 사용
Granted Abilities
GE를 통해 부여하는 Tag 혹은 Attribute
5.3 버전 기준으로 Deprecated
GetBlockedAbilityTags 함수를 통해 UTargetTagsGameplayEffectComponent를 대신 사용
Overflow Effects
/** Effects to apply when a stacking effect "overflows" its stack count through another attempted application. Added whether the overflow application succeeds or not. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Stacking|Overflow", meta = (EditConditionHides, EditCondition = "StackingType != EGameplayEffectStackingType::None"))
TArray<TSubclassOf<UGameplayEffect>> OverflowEffects;
/**
* FGameplayEffectCue
* This is a cosmetic cue that can be tied to a UGameplayEffect.
* This is essentially a GameplayTag + a Min/Max level range that is used to map the level of a GameplayEffect to a normalized value used by the GameplayCue system.
*/
USTRUCT(BlueprintType)
struct FGameplayEffectCue
{
GENERATED_USTRUCT_BODY()
FGameplayEffectCue()
: MinLevel(0.f)
, MaxLevel(0.f)
{
}
FGameplayEffectCue(const FGameplayTag& InTag, float InMinLevel, float InMaxLevel)
: MinLevel(InMinLevel)
, MaxLevel(InMaxLevel)
{
GameplayCueTags.AddTag(InTag);
}
/** The attribute to use as the source for cue magnitude. If none use level */
UPROPERTY(EditDefaultsOnly, Category = GameplayCue)
FGameplayAttribute MagnitudeAttribute;
/** The minimum level that this Cue supports */
UPROPERTY(EditDefaultsOnly, Category = GameplayCue)
float MinLevel;
/** The maximum level that this Cue supports */
UPROPERTY(EditDefaultsOnly, Category = GameplayCue)
float MaxLevel;
/** Tags passed to the gameplay cue handler when this cue is activated */
UPROPERTY(EditDefaultsOnly, Category = GameplayCue, meta = (Categories="GameplayCue"))
FGameplayTagContainer GameplayCueTags;
float NormalizeLevel(float InLevel)
{
float Range = MaxLevel - MinLevel;
if (Range <= KINDA_SMALL_NUMBER)
{
return 1.f;
}
return FMath::Clamp((InLevel - MinLevel) / Range, 0.f, 1.0f);
}
};
/** Cues to trigger non-simulated reactions in response to this GameplayEffect such as sounds, particle effects, etc */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "GameplayCues")
TArray<FGameplayEffectCue> GameplayCues;
Particle이나 Sound 같은 Decoration Effect를 관리하는 효율적인 Network 방식
GAS로 제어할 수 있다.
GA나 GE로 트리거 할 수 있다.
모든 Gameplay Cue에는 GameplayCue로 시작하는 Gameplay Tag를 붙여야 한다.
/** Called when a GameplayCue is executed, this is used for instant effects or periodic ticks */
UFUNCTION(BlueprintNativeEvent, Category = "GameplayCueNotify")
bool OnExecute(AActor* MyTarget, const FGameplayCueParameters& Parameters);
/** Called when a GameplayCue with duration is first activated, this will only be called if the client witnessed the activation */
UFUNCTION(BlueprintNativeEvent, Category = "GameplayCueNotify")
bool OnActive(AActor* MyTarget, const FGameplayCueParameters& Parameters);
/** Called when a GameplayCue with duration is first seen as active, even if it wasn't actually just applied (Join in progress, etc) */
UFUNCTION(BlueprintNativeEvent, Category = "GameplayCueNotify")
bool WhileActive(AActor* MyTarget, const FGameplayCueParameters& Parameters);
/** Called when a GameplayCue with duration is removed */
UFUNCTION(BlueprintNativeEvent, Category = "GameplayCueNotify")
bool OnRemove(AActor* MyTarget, const FGameplayCueParameters& Parameters);
GA 와의 Interaction
GE가 GA와 상호작용할 때에 AS에서 제공하는 함수를 이용한다.
하지만 이는 기본적인 기능만 제공하고, Attribute의 값 변경에 대한 추가 작업이나 범위 지정을 하려면 AS의 함수를 Override 해야 한다.
/**
* Called just before modifying the value of an attribute. AttributeSet can make additional modifications here. Return true to continue, or false to throw out the modification.
* Note this is only called during an 'execute'. E.g., a modification to the 'base value' of an attribute. It is not called during an application of a GameplayEffect, such as a 5 ssecond +10 movement speed buff.
*/
virtual bool PreGameplayEffectExecute(struct FGameplayEffectModCallbackData &Data) { return true; }
/**
* Called just after a GameplayEffect is executed to modify the base value of an attribute. No more changes can be made.
* Note this is only called during an 'execute'. E.g., a modification to the 'base value' of an attribute. It is not called during an application of a GameplayEffect, such as a 5 ssecond +10 movement speed buff.
*/
virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData &Data) { }
/**
* An "On Aggregator Change" type of event could go here, and that could be called when active gameplay effects are added or removed to an attribute aggregator.
* It is difficult to give all the information in these cases though - aggregators can change for many reasons: being added, being removed, being modified, having a modifier change, immunity, stacking rules, etc.
*/
/**
* Called just before any modification happens to an attribute. This is lower level than PreAttributeModify/PostAttribute modify.
* There is no additional context provided here since anything can trigger this. Executed effects, duration based effects, effects being removed, immunity being applied, stacking rules changing, etc.
* This function is meant to enforce things like "Health = Clamp(Health, 0, MaxHealth)" and NOT things like "trigger this extra thing if damage is applied, etc".
*
* NewValue is a mutable reference so you are able to clamp the newly applied value as well.
*/
virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) { }
/** Called just after any modification happens to an attribute. */
virtual void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) { }
/**
* This is called just before any modification happens to an attribute's base value when an attribute aggregator exists.
* This function should enforce clamping (presuming you wish to clamp the base value along with the final value in PreAttributeChange)
* This function should NOT invoke gameplay related events or callbacks. Do those in PreAttributeChange() which will be called prior to the
* final value of the attribute actually changing.
*/
virtual void PreAttributeBaseChange(const FGameplayAttribute& Attribute, float& NewValue) const { }
/** Called just after any modification happens to an attribute's base value when an attribute aggregator exists. */
virtual void PostAttributeBaseChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) const { }