본문 바로가기
언리얼

[UE4/네트워크] 네트워킹과 멀티플레이어

by 노오오오오옹 2020. 11. 26.

1. 액터 리플리케이션

리플리케이션의 주역은 Actor이다.

서버는 엑터 목록을 유지하고, 클라이언트(리플리케이트되도록 마킹된) 각 에터에 대한 근접 추정치를 유지할 수 있또록 클라이언트를 주기적으로 업데이트한다.


1-1. 업데이트 주요 방식

1) 프로퍼티 업데이트

2) RPC (Remote Procedure Call)

방식 특징
프로퍼티 업데이트 변경될 때마다 자동으로 리플리케이트 된다.

ex : 액터의 생명력(Hp)이 변화할 때만 전송된다.
RPC 실행할 때만 리플리케이트를 한다.

ex : 위치와 반경을 파라미터로 하는 RPC를 선언하고, 폭팔 발생시마다 호출하면 된다.
폭팔물을 프로퍼티로 하면 안되는 이유 : 폭팔이 자주 일어나지 않을 수 있기 때문이다.

1-2. 컴포넌트 리플리케이션

컴포넌트는 그 소유중인 액터의 일부로 리플리케이트된다. 

액터가 리플리케이트되면, 그 컴포넌트도 리플리케이트시킬 수 있다.

이 컴포넌트는 액터와 같은 방식으로 프로퍼티와 RPC 리플리케이션이 가능하다. 컴포넌트는 반드시 액터와 같은 방식으로 ::GetLifetimeReplicatedProps (...) 함수를 구현해야 한다.


1) 스태틱 컴포넌트

1-1) 액터 생성시 생성되는 컴포넌트소유중인 액터가 클라이언트나 서버에 스폰되면, 컴포넌트의 리플리케이션 여부와 상관없이 컴포넌트 역시도 스폰된다.

1-2) 서버가 클라이언트에 이 컴포넌트를 명시적으로 스폰시킬지 알려주지 않는다.

1-3) C++ 생성자에서 Default Subobject로 생성되는 컴포넌트, BP 에디터의 컴포넌트 모드에서 생성된다.

1-4) 스태틱 컴포넌트는 클라이언트에 리플리케이트시킬 필요가 없이 그냥 기본적으로 존재한다.

1-5) 서버와 클라이언트 사이에 프로퍼티나 이벤트를 자동 동기화시킬 필요가 있을 때만 리플리케이트 해주면 됩니다.


2) 다이내믹 컴포넌트

2-1) 실행시간에 서버에서 스폰되는 컴포넌트로, 생성 및 소멸이 클라이언트에 리플리케이트되는 것을 말합니다.

2-2) 액터의 작동방식과 매우 비슷하고, 다이내믹 컴포넌트는 모든 클라이언트에 존재하기 위해 리플리케이션이 필요함.

2-3) 클라이언트 자체적으로 리플리케이트되지 않는 로컬 컴포넌트를 스폰시킬 수도 있다.

2-4) 리플리케이션이 등장하는 일은, 서버상에서 발동되는 프로퍼티 또는 이벤트를 클라이언트에 자동으로 동기화시킬 필요가 있을 경우다.


3) 사용 방법

컴포넌트에서의 프로퍼티 및 RPC 셋업은 액터에서와 마찬가지로 이루어집니다. 클래스에 리플리케이트되는 것이 있게끔 셋업되면, 이 컴포넌트의 실제 인스턴스 역시도 리플리케이트되도록 설정되어야 한다.

// 컴포넌트 리플리케이트 : 단순히 AActorComponent::SetIsReplicated(true) 호출 하면 된다. 
// 컴포넌트가 디폴트 서브오브젝트인 경우 : 컴포넌트 생성 이후 클래스 생성자에서 처리된다.

ACharacter::ACharacter()
{
    // 기타...

    CharacterMovement = CreateDefaultSubobject<UMovementComp_Character>(TEXT("CharMoveComp"));
    
    if (CharacterMovement)
    {
        CharacterMovement->UpdatedComponent = CapsuleComponent;

        CharacterMovement->GetNavAgentProperties()->bCanJump = true;
        CharacterMovement->GetNavAgentProperties()->bCanWalk = true;
        CharacterMovement->SetJumpAllowed(true);
        CharacterMovement->SetNetAddressable(); // Make DSO components net addressable
        CharacterMovement->SetIsReplicated(true); // Enable replication by default
    }
}

1-3. 액터의 그 접속 소유

궁극적으로 각 접속에는 PlayerController 가 있으며, 해당 접속 전용으로 만들어진다.

액터가 접속에 소유되었는지 알아보려면, 액터의 가장 바깥쪽(outer) 오너에 질의를 하여, 그 오너가 PlayerController 인 경우 그 액터 역시도 그 PlayerController 를 소유하는 동일 접속에 소유된 것이다.

 

예제 1 : Pawn 액터가 PlayerController 에 빙의(possess)될 때입니다. 그 오너는 빙의된 PlayerController 가 될 것입니다. 이 동안 액터는 PlayerController 의 접속에 소유됩니다. Pawn 은 PlayerController 에 의해 소유/빙의된 기간동안 이 접속에 소유됩니다. 그리고 PlayerController 가 더이상 Pawn 에 빙의하지 않게 되면, Pawn 은 더이상 접속에 소유되지 않습니다.

 

예제 2 : Pawn 에 소유된 인벤토리 아이템입니다. 이 인벤토리 아이템은 Pawn 을 소유한 (것이 있다면) 동일 접속에 소유된다.


1-4 엑터 연관성 및 우선권

서버가 보이는 것이나 클라이언트에 영향을 끼칠 수 있는 액터 세트는, 그 클라이언트에 대해 연관성이 있는 액터 세트라 한다.


1) 연관성 있는 액터 세트를 결정하는 규칙

1-1) 액터가 bAlwaysRelevant (항상 연관성이 있)거나, Pawn 이거나, 폰이 노이즈나 대미지같은 동작의 Instigator (유발자)인 경우, 연관성이 있다.

1-2) 액터가 bNetUseOwnerRelevancy (네트 오너 연관성 사용)하고 Owner가 있는 경우, 오너의 연관성을 사용한다.

1-3) 액터가 bOnlyRelevantToOwner (오너에게만 연관성이 있)고 첫 번째 검사를 통과하지 못한 경우, 연관성이 없다.

1-4) 액터가 다른 액터의 스켈레톤에 붙어있으면, 그 연관성은 조상의 연관성에 의해 결정된다.

1-5) 액터가 숨겨져있고 (bHidden == true) 루트 컴포넌트가 충돌하지 않으면 액터는 연관성이 없다.

만약 루트 컴포넌트가 없다면 경고를 출력하고 액터에 bAlwaysRelevant=true 설정을 할지 물어본다.

6)  AGameNetworkManager가 거리 기반 연관성을 사용하도록 설정되어 있고, 액터가 네트 컬 디스턴스보다 가깝다면 연관성이 있다.


2) 우선 순위 부여

2-1) 각 액터의 게임플레이 중요도에 따라 대역폭을 적절히 분배한다.

2-2) 각 액터에는 NetPriority (우선권)이라는 변수가 있고, 수치가 클수록 다른 액터에 비해 더욱 많은 대역폭을 받는다.

2-3) 우선권이 2.0인 액터는 1.0인 것보다 정확히 두 배의 빈도로 업데이트된다. 이때 비율이니 무작정 늘어나진 않는다.


1-5. 자세한 엑터 리플리케이션 흐름

액터 리플리케이션 대부분은 UNetDriver::ServerReplicateActors 안에서 일어난다.

서버가 각 클라이언트에 연관성이 있다고 결정내린 액터 전부를 수집하고, 접속된 각 클라이언트가 지난 번 업데이트된 이후 변경된 프로퍼티가 있으면 전송하는 곳이다.

명칭 설명
AActor::NetUpdateFrequency 액터가 얼마나 자주 리플리케이트되는지 결정
AActor::PreReplication
리플리케이션 발생 전 호출
AActor::bOnlyRelevantToOwner
이 액터가 오너에게만 리플리케이트되는 경우 true 
AActor::IsRelevancyOwnerFor
bOnlyRelevantToOwner = true 일 때 연관성 결정을 위해 호출
AActor::IsNetRelevantFor
bOnlyRelevantToOwner = false 일 때 연관성 결정을 위해 호출

1) 하이 레블 흐름도


2) 접속에 액터 리플리케이트 하기


액터와 그 모든 컴포넌트를 접속에 리플리케이트하는 주역은 UChannel::ReplicateActor이다.

2-1) 액터 채널이 열린 이후 첫 번째 업데이트인지 알아낸다.

그렇다면, 필요한 구체적인 정보를 (초기 위치, 회전 등) serialize 합니다.

2-2) 이 접속이 이 액터를 소유하는지 알아낸다.

소유하지 않고, 이 액터의 롤이 ROLE_AutonomousProxy 라면, ROLE_SimulatedProxy 로 다운그레이드합니다.

2-3) 이 액터의 변경된 프로퍼티를 리플리케이트

2-4) 각 컴포넌트의 변경된 프로퍼티를 리플리케이트

2-5) 삭제된 컴포넌트에 대해, 특수한 삭제 명령을 전송

 

공식 주소 : docs.unrealengine.com/ko/Gameplay/Networking/Actors/ReplicationFlow/index.html

 


1-6. 퍼포먼스 및 대역폭 팁

액터 리플리케이션은 시간이 걸리기 때문에, 검사를 미세조정하여 퍼포먼스를 향상시 킬 수 있다.

1) 리플리케이션을 끈다.  (AActor::SetReplicates( false ))

2) NetUpdateFrequency (네트 업데이트 빈도) 값을 낮춘다.

3) 휴면 여부 (Dormancy)

4) 연관성 (Relevancy)

5) NetClientTicksPerSecond (네트 클라이언트 초당 틱 수)


1-7. 액터 롤 및 리모트 롤

리플리케이션 관련해서 액터에 중요한 프로퍼티가 Role (롤)과 Remote Role (리모트 롤) 2가지가 있다.

2개의 프로퍼티로 액터에 대한 오소리티 소유자, 액터의 리플리케이션 여부, 리플리케이션 모드를 알 수 있다.

알아내야할 가장 중요한 것 한 가지는, 특정 액터에 대한 오소리티를 누가 갖고 있느냐 입니다.

현재 실행중인 엔진 인스턴스가 오소리티를 갖고 있는지 알아내기 위해서는, Role 프로퍼티가 ROLE_Authority 인지 검사한다. 그렇다면 현재 실행중인 엔진 인스턴스가 (리플리케이션 여부와 무관하게) 이 액터를 담당합니다.

Role : ROLE_Authority

RemoteRole : ROLE_SimulatedProxy 또는 ROLE_AutonomousProxy 

롤이 오소리티고, 리모트 롤은 시뮬레이션 또는 자율 프록시인 경우, 이 엔진 인스턴스는 이 액터를 원격 접속으로 다시 리플리케이트하는 것을 담당한다.

서버만 액터를 접속된 클라이언트로 리플리케이트하고, 클라는 절대 액터를 서버에 리플리케이트하지 않는다.

오직 서버 : Role == ROLE_Authority이고, RemoteRole == ROLE_SimulatedProxy or ROLE_AutonomousProxy

1) 롤/리모트 롤 반전

롤과 리모트 롤은 이 값을 누가 조사하는가에 따라 반대가 될 수있다.

서버 : Role == ROLE_Authority, RemoteRole == ROLE_SimulatedProxy로 설정되어있다면

클라이언트 : Role == ROLE_SimulatedProxy, RemoteRole == ROLE_Authority로 보인다.

액터를 담당하는 것은 서버이며, 이 액터를 클라이언트에 리플리케이트하기 때문이다. 클라는 단지 업뎃을 받아 엡뎃 사이 액터에 시뮬레이션만 적용할 뿐이다.


2) 리플리케이션 모드

서버는 대역폭과 CPU 리소스 자원 때문에, 업데이트마다 액터를 리플리케이트하지 않는다. 

서버는 AActor::NetUpdateFrequency 프로퍼티에 정의된 빈도가 있끼 때문에, 클라이언트에서 액터 업데이트 사이에는 약간의 시간이 흐른다. 이를 보정하기 위해 업데이트 사이에 액터 시뮬레이션을 적용한다.

2-1) ROLE_SimulatedProxy

일반적으로 마지막 알려진 속도에 따라 움직임을 외삽하는 것을 기반으로 둔다.

서버가 특정 액터에 대한 업데이트를 전송할 때, 클라이언트는 그 위치를 새로운 위치쪽으로 조정한 다음, 업데이트 사이마다 클라이언트는 서버에서 전송된 최근 속도에 따라 액터를 계속해서 움직인다.

2-2) ROLE_AutonomousProxy

자율 프록시는 보통 플레이어 컨트롤러에 빙의된 액터에만 사용된다.

이 액터는 사람 컨트롤러에서 입력을 받으므로, 외삽을 할 때 약간의 정보가 더 있으며, (마지막 알려진 속도를 기반으로 외삽하기 보다는) 실제 사람 입력을 사용하여 빠진 정보를 채울 수 있다.


1-8. RPC

로컬에서 호출되지만 (호출하는 머신과는) 다른 머신에서 원격 실행되는 함수다.

주요 용도는 속성상 장식이나 휘발성인 비신뢰성 게임플레이 이벤트(사운드 재생, 파티클 스폰, 액터의 핵심적인 기능과는 무관한 일시적 효과와 같은 작업을 하는 이벤트)를 위한 것이다.


1) 사용법

// UFUNCTION 선언에 Server, Client, NetMulticast 키워드를 붙여주자.

{
    /* 함수를 서버에서 호출되지만 클라이언트에서 실행되는 RPC로 선언 */
    UFUNCTION(Client)
    void ClientRPCFunction();
    
    /* 함수를 클라이언트에서 호출되지만 서버에서 실행되는 RPC로 선언 */
    UFUNCTION(Server)
    void ServerRPCFunction();
    
    /* 서버에서 호출된 다음 서버는 물론 현재 연결된 모든 클라이언트에서도 실행 */
    UFUNCTION(NetMulticast)
    void MulticastRPCFunction();
}
멀티캐스트 PRC는 클라이언트에서 호출 가능하다. 다만 로컬에서만 실행된다!

2) 요건 및 주의 사항

2-1) Actor에서 호출.

2-2) Actor는 반드시 replicated 

2-3) 서버에서 호출 클라이언트에서 실행되는 RPC : 해당 Actor를 실제 소유하고 있는 클라이언트에서만 함수가 실행

2-4) 클라이언트에서 호출 서버에서 실행되는 RPC : 클라이언트는 RPC가 호출되는 Actor를 소유해야 한다.

2-5) Multicast RPC는 예외

서버 호출 : 서버에서는 로컬에서 실행될 뿐만 아니라, 현재 연결된 모든 클라이언트에서도 실행된다.

클라 호출 : 로컬에서만 실행되며, 서버에서는 실행되지 않음.
 
멀티캐스트 이벤트 : 주어진 액터의 네트워크 업데이트 기간동안 두 번 이상 리플리케이트되지 않는다.


3) 신뢰성

기본적으로 RPC 는 비신뢰성로, RPC 호출이 원격 머신에서 확실히 실행되도록 하기 위해서는 Reliable 키워드를 붙인다.

{
    UFUNCTION(Client, Reliable)
    void ClientRPCFunction();
}

1-9. 프로퍼티 리플리케이션 

각 액터에는 Replicated 지정자를 포함하는 모든 프로퍼티 목록이 유지된다.

서버는 리플리케이트된 프로퍼티의 값이 변할 때마다 각 클라이언트에 업데이트를 전송하며, 클라이언트는 액터의 로컬 버전에 적용한다.

이 업데이트는 서버에서만 받으며, 클라이언트는 프로퍼티 업데이트를 서버나 다른 클라이언트로 절대 전송하지 않음.

// OOOO.h 파일

class ENGINE_API AActor : public UObject
{
    UPROPERTY(replicated) // 키워드는 필수다.
    AActor* Owner;
};
// OOOO.cpp 파일

AActor::AActor(const class FPostConstructInitializeProperties& PCIP ) : Super( PCIP )
{ 
    bReplicates = true; // 생성자에서 bReplicates를 true로 설정한지 확인
}

void AActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    DOREPLIFETIME(AActor, Owner);
}

Owner 멤버 변수는 이제 현재 인스턴싱된 이 액터 유형(이 경우, 베이스 액터 클래스) 모든 사본에 대해 접속된 모든 클라이언트에 동기화된다.


1) 조건식 프로퍼티 리플리케이션

프로퍼티가 일단 리플리케이션 등록되면 해제시킬 수 없다. 

많은 정보를 구워넣어, 같은 프로퍼티 세트에 대해 다수의 접속에서 작업물 공유의 이점을 활용하기 위함.

그렇게 계산 시간이 많이 절약된다.

리플리케이트 여부를 미세 조정하기 위해 조건형 프로퍼티가 등장한다.

기본적으로 리플리케이트되는 각 프로퍼티에는 조건이 내장되어 있고, 변경되지 않은 경우 리플리케이트하지 않는다.

프로퍼티 리플리케이션에 대한 세밀한 제어를 위해, 부차적인 조건을 추가시킬 수 있는 특수 매크로(DOREPLIFETIME_CONDITION)이 있다.

void AActor::GetLifetimeReplicatedProps(TArray< FLifetimeProperty>& OutLifetimeProps) const
{
    DOREPLIFETIME_CONDITION(AActor, ReplicatedMovement, COND_SimulatedOnly);
    
    /* 플래그 COND_SimulatedOnly */
    // 리플리케이션 여부 고려전에 검사(시뮬레이션 사본이 있는 클라이언트에만 리플리케이트한다)
}

장점 1 : 대역폭이 저장되어, 이 액터의 자율 프록시 버전이 있는 클라이언트는 이 프로퍼티에 대해 알 필요가 없다. (ex : 이 클라이언트는 이 프로퍼티를 예측 목적으로 직접 설정하고 있습니다).

장점 2 : 이 프로퍼티를 받지 않는 클라이언트의 경우, 서버가 클라이언트의 로컬 사본을 짓밟지 않는다는 점

조건 설명
COND_InitialOnly 이 프로퍼티는 초기 번치에만 전송을 시도합니다.
COND_OwnerOnly 이 프로퍼티는 액터의 오너에만 전송합니다.
COND_SkipOwner 이 프로퍼티는 오너를 제외한 모든 접속에 전송합니다.
COND_SimulatedOnly 이 프로퍼티는 시뮬레이션되는 액터에만 전송합니다.
COND_AutonomousOnly 이 프로퍼티는 자율 액터에만 전송합니다.
COND_SimulatedOrPhysics 이 프로퍼티는 시뮬레이션되는 또는 bRepPhysics 액터에 전송합니다.
COND_InitialOrOwner 이 프로퍼티는 초기 패킷시, 또는 액터의 오너에 전송합니다.
COND_Custom
이 프로퍼티에는 별다른 조건이 없지만, SetCustomIsActiveOverride 를 통해 껐다 켰다 토글 기능을 원합니다.

DOREPLIFETIME_ACTIVE_OVERRIDE라는 매크로를 이용해, 프로퍼티 제어 시기를 완벽히 제어할 수 도 있다.

접속 단위가 아니라 매크로 단위로, 접속별로 바뀔 수 있는 상태를 사용하는 것은 안전하지 않다.

void AActor::PreReplication( IRepChangedPropertyTracker & ChangedPropertyTracker )
{
	// bReplicateMovement가 True일 경우에만 리플리케이트 된다.

    DOREPLIFETIME_ACTIVE_OVERRIDE( AActor, ReplicatedMovement, bReplicateMovement );
    
    /* 단점 */
    // 1) 맞춤형 조건값이 많이 바뀌면, 느려질 수 있다.
    // 2) 접속별로 변경 가능한 조건은 사용할 수 없다. (이때 RemoteRole 검사 XXXXX)
}

문서 주소 : docs.unrealengine.com/ko/Gameplay/Networking/Actors/Properties/Conditions/index.html


2) 오브젝트 레퍼런스 리플리케이션

오브젝트 레퍼런스는 UE4 멀티플레이 프레임워크에서 자동으로 처리한다.

리플리케이트되는 UObject 프로퍼티가 있다면, 그 오브젝트에 대한 레퍼런스는 서버에 의해 할당된 특수 ID(FNetworkGUID) 로 네트워크를 통해 전송된다

서버는 이 ID 할당을 담당한 뒤, 그에 대해 접속된 모든 클라이언트에 알린다.

// OOOO.h 파일

class ENGINE_API AActor : public UObject
{
    UPROPERTY(replicated)
    AActor* Owner; 
    
    // Owner 프로퍼티는 이이 프로퍼티가 가르키는 액터에 대해 리플리케이트 되는 레퍼런스가 된다.
};
오브젝트가 네트워크를 통해 제대로 참조되도록 하려면, 반드시 네트워킹을 지원해야한다.

검사방법은 UObject::IsSupportedForNetworking() 를 호출하면 된다.

이는 일반적으로 로우 레벨 함수로 간주되므로, 게임 코드에서 이 검사를 할 일은 별로 없다.

 

3) 네트워크를 통해 한 오브젝트를 참조할 수 있는지 결정하는 기준

3-1) 리플리케이티드 액터는 레퍼런스로 리플리케이션 가능합니다.

3-2) 리플리케이티드 액터가 아닌 경우 반드시 안정된 이름을 사용해야 합니다 (패키지에서 직접 로드).

3-3) 리플리케이티드 컴포넌트는 레퍼런스로 리플리케이션 가능합니다.

3-4) 리플리케이티드 컴포넌트가 아닌 경우 반드시 안정된 이름을 사용해야 합니다.

3-5) 다른 모든 (액터나 컴포넌트가 아닌) UObject 는 로드된 패키지에서 직접 와야 합니다.

 

4) 안정된 이름의 오브젝트

4-1) 단순히 서버와 클라이언트 양쪽에 존재하는 오브젝트로, 똑같은 이름을 갖는다.

4-2) 패키지에서 직접 로드된 액터(게임플레이 도중 스폰된 것이 아니)라면 안정된 이름을 갖는다.

4-3) 컴포넌트가 안정된 이름을 갖는 경우는 ①패키지에서 직접 로드된 경우, 간단한 컨스트럭션 스크립트를 통해 추가된 경우, (UActorComponent::SetNetAddressable 를 통해) 수동 마킹된 경우다. 

서버와 클라이언트에 이름이 똑같도록 컴포넌트 이름을 수동으로 지어줘야겠다 알고있는 경우에만 사용해야 합니다 (AActor C++ 생성자에 추가된 컴포넌트가 좋은 예입니다).


 

예제 내용
Actor Replication 액터는 클라이언트/서버 양쪽에 표시된다.
No Actor Replication 서버에만 나타난다.
Detecting Network Authority and Remote Clients in Blueprints 네트워크 오소리티에 따라 다른 로직을 실행한다. 

즉 서버에서 보느냐, 클라이언트에서 보느냐에 따라 다르게 표시함.
Variable Replication
 
Variable Replication (RepNotify)  
Function Replication (RPCs)  
   

 

댓글