Actor Replication의 동작 원리를 이해하고 Property Replication을 구현하기 위한 방법을 학습하자. Property Replication은특정 플레이어에 속한 Actor의 정보를 Network 내 다른 플레이어에게 복제하는 작업이다. Client-Server 모델에서는 대부분 Server에서 Client로 데이터를 전달하고 액터를 Client에 전달하기 위해 사용하는 Replication의 방법에는 크게 2가지가 있다.

 

 

인프런 이득우님의 '언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해' 강의를 참고하였습니다. 
😎 [이득우의 언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해] 강의 들으러 가기!

 

 

목차

     

     


     

     

     

    Actor Replication


    Actor Replication의 동작 원리를 이해하자. 

    Property Replication을 구현하기 위한 방법을 학습하자.


     

    Actor Replication 기초

     

    특정 플레이어에 속한 Actor의 정보를 Network 내 다른 플레이어에게 복제하는 작업. 

    Client-Server 모델에서는 대부분 Server에서 Client로 전달함.

    Replication의 방법에는 크게 2가지가 있음 (속성: Property Replication, 함수: RPC)

    • Property Replication
    • RPC (Remote Procedure Call) (예전에는 Function Replication이라 불렸다.)


     

    기본 Actor의 로딩

     

    -  Client가 초기화 될 때 모든 Actor 정보를 서버로부터 받는 것은 비효율적

     

    -  따라서 기본 배경에 관련된 Actor는 맵을 통해 스스로 로딩하도록 설계되어 있음.

     

    -  고정으로 제공하는 Actor와 동적으로 생성하는 Actor

    •  고정으로 제공하는 Actor의 예:  레벨을 구성하는 배경 Actor
    •  동적으로 제공하는 Actor의 예:  PlayerControllerPawn 

     

    -  고정 Actor에 대해 NetLoadOnClient 속성을 체크해야 함 (기본값) 

    •  언리얼 엔진은 모든 Actor에 NetLoadOnClient라는 속성을 제공한다.
    •  이 속성들은 레벨에 배치된 Actor라면 true가 기본값이다. 
      • true로 설정된 Actor들은 Server와 통신없이 Client가 초기화될 때 자체적으로 로딩함으로 Server에서 콘텐츠를 배포한 것과 동일한 효과를 효과적으로 얻어낼 수 있다.
      • 클라이언트가 네트워크를 사용하지 않고 서버에서 제공하는 아주 많은 양의 데이터를 효과적으로 복제하는 효율적인 방법이다.

    만약 이 옵션을 체크 해제하면 해당 액터는 서버에서만 보이고 클라이언트에서는 보이지 않는다.

     


     

     

     

    프로퍼티 리플리케이션의 구현 


     

     

     

    액터의 리플리케이션 설정 Actor Replication 

     

    • 고정으로 보여주는 Actor 중, 게임 중 변경 사항이 발생하는 Actor는 그 값을 전달해야 함.
    • Network 데이터를 최소화하기 위해 변경 사항을 보내기보다, 변경을 유발한 속성 값을 전달함.
    • 이를 위해 Actor의 Replicates 옵션을 체크해야 함.    

     


     

    Replication Property 리플리케이션 프로퍼티의 지정

     

    #1.  Actor의 Replication Property를 true로 지정

    • bReplicates 속성을 true로 설정.

     

    #2. Network으로 복제할 Actor의 Property를 키워드로 지정 

    •  UPROPERTY에 Replicated 키워드 설정
      • 매 Frame 마다 실행되는 Tick의 코드가 방대해지는 단점이 있다. 상대방의 네트워크 전송 주기가 만일 Tick 보다 느리다면 데이터를 받기 전에 Tick이 여러번 호출돼 있어서 비효율적으로 동작할 수 있다.
    •  UPROPERTY에 ReplicatedUsing 키워드 설정 + ReplicatedUsing에 호출될 콜백 함수를 지정
      • 호출될 콜백 함수는 UFUNCTION으로 선언해야 함.
      • 일반적으로 OnRep_의 접두사를 가지는 이름 규칙을 가짐.
      • 콜백 함수는 Server가 아닌 Client에서만 호출됨. 
        • OnRep_함수는 항상 클라이언트에서만 호출된다. 서버에서는 자동으로 호출되지 않기 때문에 서버 로직의 경우, 이 함수가 필요한 경우 명시적으로 호출해야 한다.
      • 필요한 타이밍에만 해당 로직을 처리할 수 있어 효율적인 구현이 가능함. 
      • C++ ReplicatedUsing + OnRep_ vs. Blueprint RepNotify (유사하지만 동작 방식에 미세한 차이가 있음).
        • C++: Client에서만 호출됨.명시적 호출 가능. 값이 변경된 때만 호출됨.
        • BP: Server, Client에서 호출됨. 명시적 호출 불가능. Server는 항시 호출됨. Client는 값이 변경된 때만 호출됨.

     

     

    #3. GetLifetimeReplicatedProps 함수에 Network으로 복제할 속성을 추가

    •  #include "Net/UnrealNetwork.h" 헤더 파일 지정
    • DOREPLIFETIME 매크로를 사용해 복제할 속성을 명시 

     

    Lifetime은 Actor Channel의 Lifetime을 의미한다.

    즉, 활성화된 Actor Channel로 전송할 복제될 속성을 의미한다.


     

     

    리플리케이션 콜백 함수 호출

     

    1.  클라이언트에 속성이 복제될 때 콜백 함수가 호출되도록 구현

    • UPROPERTY의 Replicated 키워드를 ReplicatedUsing 키워드로 변경
    • ReplicatedUsing에 호출될 콜백함수를 지정
    • 호출될 콜백함수는 UFUNCTION으로 선언해야 함.

    2.  콜백 함수의 구현

    • 일반적으로 OnRep_의 접두사를 가지는 이름 규칙을 가짐.
    • 콜백 함수는 서버가 아닌 클라이언트에서만 호출됨.

     

    필요한 타이밍에만 해당 로직을 처리할 수 있어 효율적인 구현이 가능함


     

     

    C++ OnRep  vs.  블루프린트 RepNotify

     

    • 필요한 타이밍에만 해당 로직을 처리할 수 있어 효율적인 구현이 가능함. 
    • C++ ReplicatedUsing + OnRep_ vs. Blueprint RepNotify (유사하지만 동작 방식에 미세한 차이가 있음).
      • C++: Client에서만 호출됨. 명시적 호출 가능. 값이 변경된 때만 호출됨.
      • BP: Server, Client에서 호출됨. 명시적 호출 불가능. Server는 항시 호출됨. Client는 값이 변경된 때만 호출됨.


     

     

    예시 코드

     

    Fountain.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "GameFramework/Actor.h"
    #include "ABFountain.generated.h"
    
    UCLASS()
    class ARENABATTLE_API AABFountain : public AActor
    {
    	GENERATED_BODY()
    	
    public:	
    	AABFountain();
    
    protected:
    	virtual void BeginPlay() override;
    	virtual void Tick(float DeltaTime) override;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Body;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
    	TObjectPtr<class UStaticMeshComponent> Water;
    
    public:
    	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
    	virtual void OnActorChannelOpen(class FInBunch& InBunch, class UNetConnection* Connection) override;
    
    	UPROPERTY(ReplicatedUsing = OnRep_ServerRotationYaw)
    	float ServerRotationYaw;
    
    	UFUNCTION()
    	void OnRep_ServerRotationYaw();
    
    	float RotationRate = 30.0f;
    };

     

     

    Fountain.cpp

    더보기
    #include "Prop/ABFountain.h"
    #include "Components/StaticMeshComponent.h"
    #include "Net/UnrealNetwork.h"
    #include "ArenaBattle.h"
    
    AABFountain::AABFountain()
    {
     	PrimaryActorTick.bCanEverTick = true;
    
    	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Body"));
    	Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Water"));
    
    	RootComponent = Body;
    	Water->SetupAttachment(Body);
    	Water->SetRelativeLocation(FVector(0.0f, 0.0f, 132.0f));
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> BodyMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01'"));
    	if (BodyMeshRef.Object)
    	{
    		Body->SetStaticMesh(BodyMeshRef.Object);
    	}
    
    	static ConstructorHelpers::FObjectFinder<UStaticMesh> WaterMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Fountain_02.SM_Plains_Fountain_02'"));
    	if (WaterMeshRef.Object)
    	{
    		Water->SetStaticMesh(WaterMeshRef.Object);
    	}
    
    	bReplicates = true;
    }
    
    // Called when the game starts or when spawned
    void AABFountain::BeginPlay()
    {
    	Super::BeginPlay();
    	
    	if (HasAuthority())
    	{
    		FTimerHandle Handle;
    		GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
    			{
    				ServerRotationYaw += 1.0f;
    		}
    		), 1.0f, true, 0.0f);
    	}
    }
    
    void AABFountain::Tick(float DeltaTime)
    {
    	Super::Tick(DeltaTime);
    
    	if (HasAuthority())
    	{
    		AddActorLocalRotation(FRotator(0.0f, RotationRate * DeltaTime, 0.0f));
    		ServerRotationYaw = RootComponent->GetComponentRotation().Yaw;
    	}
    }
    
    void AABFountain::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
    {
    	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    	DOREPLIFETIME(AABFountain, ServerRotationYaw);
    }
    
    void AABFountain::OnActorChannelOpen(FInBunch& InBunch, UNetConnection* Connection)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	Super::OnActorChannelOpen(InBunch, Connection);
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    void AABFountain::OnRep_ServerRotationYaw()
    {
    	AB_LOG(LogABNetwork, Log, TEXT("Yaw : %f"), ServerRotationYaw);
    
    	FRotator NewRotator = RootComponent->GetComponentRotation();
    	NewRotator.Yaw = ServerRotationYaw;
    	RootComponent->SetWorldRotation(NewRotator);
    }