넷커넥션(NetConnection)
채널(Channel)
패킷(Packet)
번치(Bunch)
GameMode의 StartPlay가 실행되지 않으면 다른 모든 액터의 BeginPlay가 실행되지 않습니다.
GameMode는 서버에만 존재한는데 멀티 환경에서 다른 Clinet들의 BeginPlaye는 어떻게 실행되는 걸까요?
GameMode는 본인이 직접 게임의 시작을 하는 것이 아니라 GameState에 명령을 내립니다.
그리고 GameState는 모든 Client에 복사됩니다. 때문에 게임의 시작을 알릴 수 있습니다.
출력 로그 - server
LogDXNet: [Server] ADXGameModeBase::ADXGameModeBase
LogDXNet: [Server] ADXGameStateBase::ADXGameStateBase
LogDXNet: [Server] ADXGameModeBase::StartPlay Begin
LogDXNet: [Server] ADXGameStateBase::HandleBeginPlay Begin
LogDXNet: [Server] ADXGameStateBase::HandleBeginPlay End
LogDXNet: [Server] ADXGameModeBase::StartPlay End
LogDXNet: [Server] ADXGameModeBase::PreLogin Begin
LogDXNet: [Server] ADXGameModeBase::PreLogin End
LogDXNet: [Server] ADXGameModeBase::PreLogin End
LogDXNet: [Server] ADXGameModeBase::Login Begin
LogDXNet: [Server] ADXPlayerController::ADXPlayerController
LogDXNet: [Server] ADXPlayerController::PostInitializeComponents Begin
LogDXNet: [Server] ADXPlayerController::PostInitializeComponents End
LogDXNet: [Server] ADXPlayerController::BeginPlay Begin
LogDXNet: [Server] ADXPlayerController::BeginPlay End
LogDXNet: [Server] ADXGameModeBase::Login End
LogDXNet: [Server] ADXGameModeBase::PostLogin Begin
LogDXNet: [Server] ADXPlayerCharacter::ADXPlayerCharacter
LogDXNet: [Server] ADXGameModeBase::PostLogin Client Connection: IpConnection_2
LogDXNet: [Server] ADXGameModeBase::PostLogin End
LogDXNet: [Server] ADXGameModeBase::PreLogin Begin
LogDXNet: [Server] ADXGameModeBase::PreLogin End
LogDXNet: [Server] ADXGameModeBase::PreLogin End
LogDXNet: [Server] ADXGameModeBase::Login Begin
LogDXNet: [Server] ADXPlayerController::ADXPlayerController
LogDXNet: [Server] ADXPlayerController::PostInitializeComponents Begin
LogDXNet: [Server] ADXPlayerController::PostInitializeComponents End
LogDXNet: [Server] ADXPlayerController::BeginPlay Begin
LogDXNet: [Server] ADXPlayerController::BeginPlay End
LogDXNet: [Server] ADXGameModeBase::Login End
LogDXNet: [Server] ADXGameModeBase::PostLogin Begin
LogDXNet: [Server] ADXPlayerCharacter::ADXPlayerCharacter
LogDXNet: [Server] ADXGameModeBase::PostLogin Client Connection: IpConnection_2
LogDXNet: [Server] ADXGameModeBase::PostLogin Client Connection: IpConnection_3
LogDXNet: [Server] ADXGameModeBase::PostLogin End
출력 로그 - client
LogDXNet: [Client01] ADXPlayerController::ADXPlayerController
LogDXNet: [Client01] ADXPlayerController::PostInitializeComponents Begin
LogDXNet: [Client01] ADXPlayerController::PostInitializeComponents End
LogDXNet: [Client01] ADXPlayerController::OnActorChannelOpen Begin
LogDXNet: [Client01] ADXPlayerController::OnActorChannelOpen End
LogDXNet: [Client01] ADXPlayerController::PostNetInit Begin
LogDXNet: [Client01] ADXPlayerController::PostNetInit Server Connection: IpConnection_0
LogDXNet: [Client01] ADXPlayerController::PostNetInit End
LogDXNet: [Client01] ADXPlayerCharacter::ADXPlayerCharacter
LogDXNet: [Client01] ADXGameStateBase::ADXGameStateBase
LogDXNet: [Client01] ADXGameStateBase::OnRep_ReplicatedHasBegunPlay Begin
LogDXNet: [Client01] ADXPlayerController::BeginPlay Begin
LogDXNet: [Client01] ADXPlayerController::BeginPlay End
LogDXNet: [Client01] ADXGameStateBase::OnRep_ReplicatedHasBegunPlay End
LogDXNet: [Client01] ADXPlayerCharacter::ADXPlayerCharacter
LogDXNet: [Client02] ADXPlayerController::ADXPlayerController
LogDXNet: [Client02] ADXPlayerController::PostInitializeComponents Begin
LogDXNet: [Client02] ADXPlayerController::PostInitializeComponents End
LogDXNet: [Client02] ADXPlayerController::OnActorChannelOpen Begin
LogDXNet: [Client02] ADXPlayerController::OnActorChannelOpen End
LogDXNet: [Client02] ADXPlayerController::PostNetInit Begin
LogDXNet: [Client02] ADXPlayerController::PostNetInit Server Connection: IpConnection_1
LogDXNet: [Client02] ADXPlayerController::PostNetInit End
LogDXNet: [Client02] ADXPlayerCharacter::ADXPlayerCharacter
LogDXNet: [Client02] ADXGameStateBase::ADXGameStateBase
LogDXNet: [Client02] ADXGameStateBase::OnRep_ReplicatedHasBegunPlay Begin
LogDXNet: [Client02] ADXPlayerController::BeginPlay Begin
LogDXNet: [Client02] ADXPlayerController::BeginPlay End
LogDXNet: [Client02] ADXGameStateBase::OnRep_ReplicatedHasBegunPlay End
LogDXNet: [Client02] ADXPlayerCharacter::ADXPlayerCharacter
AGameStateBase 클래스에 bReplicatedHasBegunPlay 속성이 존재합니다.
// GameStateBase.h
class AGameStateBase : public AInfo
{
...
/** Replicated when GameModeBase->StartPlay has been called so the client will also start play */
UPROPERTY(Transient, ReplicatedUsing = OnRep_ReplicatedHasBegunPlay)
bool bReplicatedHasBegunPlay;
...
}
ReplicatedUsing
HandleBeginPlay()에서 bReplicatedHasBegunPlay를 수정해서 OnRep_ReplicatedHasBegunPlay 호출 유도
동시에 서버에서 NotifyBeginPlay() 호출
...
// GameStateBase.cpp
...
void AGameStateBase::HandleBeginPlay()
{
bReplicatedHasBegunPlay = true; // 클라이언트 모든 액터의 BeginPlay() 함수가 호출되게끔 함
GetWorldSettings()->NotifyBeginPlay(); // 서버 모든 액터의 BeginPlay() 함수가 호출되게끔 함
...
}
// WorldSettings.cpp
void AWorldSettings::NotifyBeginPlay()
{
...
if (!World->bBegunPlay)
{
for (FActorIterator It(World); It; ++It)
{
...
It->DispatchBeginPlay(bFromLevelLoad);
}
...
}
}
PostNetInit()
PostInitializeComponents()
StartPlay()
BeginPlay()
Owner 설정 로직을 담당하는 부분입니다.
Owner 설정 로직은 액터가 어떤 클라이언트이 넷커넥션에 속하게 되는 지를 결정하는 중요한 로직입니다.
void APawn::PossessedBy(AController* NewController)
{
SetOwner(NewController);
...
}
void APawn::UnPossessed()
{
AController* const OldController = Controller;
ForceNetUpdate();
SetPlayerState(nullptr);
SetOwner(nullptr);
Controller = nullptr;
...
}
클라이언트에서는 OnPossess()가 호출되지 않지만 Actor::Owner는 ReplicatedUsing 키워드가 달린 속성이기에 Owner가 초기화 되면 OnRep_Owner() 함수가 클라이언트에서 호출된다고 합니다.
매크로를 통한 로컬 룰과 리모트 룰 출력
// DedicatedX.h
...
#pragma region NetLogging
...
#define LOCAL_ROLE_TCHAR *(UEnum::GetValueAsString(TEXT("Engine.ENetRole"), GetLocalRole()))
#define REMOTE_ROLE_TCHAR *(UEnum::GetValueAsString(TEXT("Engine.ENetRole"), GetRemoteRole()))
#define DX_LOG_ROLE(LogCat, Verbosity, Format, ...) UE_LOG(LogCat, Verbosity, TEXT("[%s][%s/%s] %s %s"), NETMODE_TCHAR, LOCAL_ROLE_TCHAR, REMOTE_ROLE_TCHAR, FUNCTION_TCHAR, *FString::Printf(Format, ##__VA_ARGS__))
#pragma endregion
빙의 전후 캐릭터의 리모트 룰 비교
LogDXNet: [Server] ADXPlayerCharacter::ADXPlayerCharacter
LogDXNet: [Server] ADXPlayerController::OnPossess Begin
LogDXNet: [Server][ROLE_Authority/ROLE_SimulatedProxy] <- 여기 ADXPlayerCharacter::PossessedBy Begin
LogDXNet: [Server] ADXPlayerCharacter::PossessedBy There is no OwnerActor.
LogDXNet: [Server] ADXPlayerCharacter::PossessedBy OwnerActor Name: BP_PlayerController_C_1
LogDXNet: [Server][ROLE_Authority/ROLE_AutonomousProxy] <- 여기 ADXPlayerCharacter::PossessedBy End
LogDXNet: [Server] ADXPlayerController::OnPossess End
LogDXNet: [Server] ADXGameModeBase::PostLogin Client Connection: IpConnection_2
LogDXNet: [Server] ADXGameModeBase::PostLogin Client Connection: IpConnection_3
레벨에 존재하는 모든 액터의 정보를 서버에서 클라이언트로 복제하는 것은 매우 비효율적입니다.
레벨 디자인을 통해 레벨에 고정적으로 배치되는 액터는 NetLoadOnClient 속성을 true로 지정해서 모든 클라이언트가 스스로 스폰하도록 해야합니다.
각각 알아서 생성하게 둔다.
플레이어의 로그인에 따라 스폰되는 액터(PlayerController, Pawn 등)와 혼동하면 안 됩니다.
지형 같은 것들이 이에 해당할 것 같습니다.
Replication Notify의 특징
Replication Notify 함수라고 합니다. C++에서는 OnRep_, 블루프린트에선 RepNotify| C++ OnRep_ 함수 | Blueprint RepNotify 함수 |
|---|---|
| 클라이언트에서만 호출 | 서버와 클라이언트 모두에서 호출 |
| 명시적 호출 가능 | 명시적 호출 불가능 |
| 속성 값이 변경될 때만 호출 | 서버는 항시 호출되고,클라이언트는 속성 값 변경될 때만 호출 |
UE_DISABLE_OPTIMIZATION
~
UE_ENABLE_OPTIMIZATION
위 매크로 사이의 함수는 Development Editor 빌드에 따른 최적화를 막아 Debug Game Editor를 돌리는 것처럼 돌아갑니다.
NetUpdateFrequency
주의 보장되지 않습니다.
NetUpdateFrequency는 최대치일 뿐 이를 보장하진 않습니다.
서버의 Tick Rate에 따라 레플리케이션이 발생하지만, 서버의 성능에 따라 달라집니다.
서버의 성능이 NetUpdateFrequency보다 낮은 경우, 서버의 성능에 맞춰서 복제됩니다.
그래서 그래픽 기능이 없는 데이케이티드 서버가 더 좋은 성능을 발휘 가능하다고 합니다.
| Type | Frequency |
|---|---|
| Actor | 100 |
| Pawn | 100 |
| PlayerController | 100 |
| GameState | 10 |
| PlayerState | 1 |
어떤 액터의 서버의 위치와 클라이언트의 위치가 서로 동기화되어야 한다고 가정해 봅니다.
동기화하는 방법은 두 가지가 있습니다.
1. 매 틱마다 액터의 서버 위치를 클라이언트 측으로 레플리케이션해서 동기화
2. 액터의 처음 서버 위치와 속도 값을 안다면, 이를 이용해서 예측을 통해 동기화
레벨에 있는 모든 액터의 정보를 모든 클라이언에게 실시간으로 전송하기엔 꽤 큰 부하가 걸립니다.(N*N)
연관성을 판단하는 여러 기준으로 토대로 서버 측에서 해당 클라이언트 커넥션과 연관성이 있는 액터만 레플리케이션하는 최적화가 필요합니다.
Owner
Instigator
AlwaysRelevant
NetUseOwnerRelevancy
OnlyRelevantToOwner
NetCullDistance
폰 클래스의 연관성을 판단하는 기준
플레이어 컨트롤러에 빙의된 폰이 움직이면서 어떤 액터가 레플리케이션될지 결정됩니다.
Viewer
ViewTarget
bool IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const;
Relevant 관계를 확인하는 함수입니다.
서버에서만 호출됩니다.
RealViewer: 데이터를 받을 대상, 관찰자 본인. 일반적으로는 클라이언트의 PlayerController
ViewTarget: 관찰자가 현재 보고 있는 대상. PlayerController의 경우 빙의 중인 Pawn인 경우가 많습니다.
SrcLocation: 판단의 기준이 되는 위치입니다.
// APlayerController.cpp
bool APlayerController::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
return ( this==RealViewer );
}
// APawn.cpp
bool APawn::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
CA_SUPPRESS(6011);
if (bAlwaysRelevant || RealViewer == Controller || IsOwnedBy(ViewTarget) || IsOwnedBy(RealViewer) || this == ViewTarget || ViewTarget == GetInstigator()
|| IsBasedOnActor(ViewTarget) || (ViewTarget && ViewTarget->IsBasedOnActor(this)))
{
return true;
}
else if ((IsHidden() || bOnlyRelevantToOwner) && (!GetRootComponent() || !GetRootComponent()->IsCollisionEnabled()))
{
return false;
}
else
{
UPrimitiveComponent* MovementBase = GetMovementBase();
AActor* BaseActor = MovementBase ? MovementBase->GetOwner() : nullptr;
if ( MovementBase && BaseActor && GetMovementComponent() && ((Cast<const USkeletalMeshComponent>(MovementBase)) || (BaseActor == GetOwner())) )
{
return BaseActor->IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
}
}
return !GetDefault<AGameNetworkManager>()->bUseDistanceBasedRelevancy ||
IsWithinNetRelevancyDistance(SrcLocation);
}
// ActorReplication.cpp
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);
}
클라이언트에 보낼 수 있는 대역폭(NetBandwidth)은 한정되어 있습니다.
클라이언트에 보낼 액터 중, NetPriority 값이 높은 액터의 데이터를 우선 전달하도록 설계되어 있습니다.
액터에 설정된 NetPriority 값을 활용해 전송 순서를 결정합니다.
| NetPriority | |
|---|---|
| PlayerController | 3 |
| Pawn | 2 |
| Actor | 1 |
가중치
여러 가지 기준에 따라 가중치를 결정합니다.
1. 해당 액터가 ViewTarget인 경우 -> 4.f
2. 해당 액터가 ViewTarget 액터에게 영향을 받은 경우 -> 4.f
3. ViewTarget이 바라보는 방향에 해당 액터가 있고 가까운 경우 → 2.f
4. ViewTarget이 바라보는 방향에 해당 액터가 있지만 적당히 먼 경우 → 0.4f
5. ViewTarget이 바라보는 방향에 해당 액터가 없고 적당히 먼 경우 → 0.2f
휴면 상태인 액터는 더 이상 프로퍼티 레플리케이션이나 RPC가 동작하지 않습니다.
액터가 레플리케이트 되었지만, 자주 수정되진 않을 때 활용합니다.
플레이어 캐릭터처럼 자주 수정되는 액터를 휴면 상태로 두면 오히려 오버헤드가 발생합니다.
네트워크 휴면 상태들

프로퍼티는 한 번 레플리케이션에 등록되면 다시 해제시킬 수가 없습니다.
그래서 DOREPLIFETIME라고 합니다.
Conditional Property Replication은 레플리케이션에 등록된 프로퍼티를
조건식을 통해 좀 더 세밀하게 조정할 수 있게끔 해줍니다.
단점은 조건식 값이 너무 자주 바뀐다면 오버헤드 발생할 수 있습니다.
Dormancy를 해제? 초기화시켜주는 함수
Server에서 호출