TIL_074 : 네트워크 데이터 관련 용어, 클라 액터 BeginPlay flow, OnPossess/PossessedBy, NetLoadOnClient, OnRep함수 vs RepNotify 함수, NetUpdateFrequency

김펭귄·2025년 12월 4일

Today What I Learned (TIL)

목록 보기
74/111

오늘 학습 키워드

  • 네트워크 데이터 관련 용어

  • 클라 액터 BeginPlay flow

  • OnPossess

  • NetLocalOnClient

  • OnRep함수 vs RepNotify 함수

  • NetUpdateFrequency

1. 네트워크 데이터 관련 용어

1.1. NetConnection

  • 모든 멀티플레이 관련 데이터들이 드나드는 통로

  • 서버는 접속한 클라개수만큼 ClientConnection가지고, 클라는 하나의 ServerConnection만 가짐

1.2. 채널(Channel)

  • NetConnection 클래스는 Channels라는 멤버변수를 가짐

  • 현재 연결된 네트워크에서 열려있는 모든 네트워크 채널(UChannel*)들을 배열(TArray<UChannel*>) 형태로 관리

  • Channels를 통해 액터 복제, RPC, 네트워크 동기화 등 네트워크 작업울 험

  • 채널에는 ActorChannel, ControlChannel, VoiceChannel 등이 있으며 이 중에 ActorChannel이 반드시 열려있어야 Actor Replication이 가능해짐

  • PlayerController 액터의 네트워크 채널이 열려 초기화가 이루어지면, 클라이언트 쪽에서 APlayerController::OnActorChannelOpen() 함수가 호출됨. 이 이후에야 Replication이 가능

1.3. 패킷(Packet)

  • 네트워크에서 사용되는 통상적인 데이터 단위

1.4. 번치(Bunch)

  • 패킷 안에서의 데이터 묶음들

  • NetConnection 안에 Channel들이 관리되고 있고, 이 채널들 통해 Packet이 왔다갔다 하는데, 패킷은 번치로 이루어짐

  • 번치로는 RPC, 동기화될 속성값 이런것들

2. 클라 액터 BeginPlay flow

  • 서버의 게임모드에서 StartPlay가 호출되면 AGameStateBase::HandleBeginPlay()가 호출됨
void AGameModeBase::StartPlay()
{
	GameState->HandleBeginPlay();
}

void AGameStateBase::HandleBeginPlay()
{
	bReplicatedHasBegunPlay = true;

	GetWorldSettings()->NotifyBeginPlay();	// 서버의 모든 액터도 BeginPlay 실행
	GetWorldSettings()->NotifyMatchStarted();
}
// GameStateBase.h
class AGameStateBase : public AInfo
{
	UPROPERTY(Transient, ReplicatedUsing = OnRep_ReplicatedHasBegunPlay)
	bool bReplicatedHasBegunPlay;
    
    virtual void OnRep_ReplicatedHasBegunPlay();
}
  • AGameStateBase::HandleBeginPlay()함수는 게임 스테이트에 있는 bReplicatedHasBegunPlay변수를 true로 설정하게 되고, 값이 변경됨에 따라 클라로 동기화 됨

  • 클라로 동기화되면서 AGameStateBase::OnRep_ReplicatedHasBegunPlay()가 클라에서만 호출되게 되고, 이 함수 역시 BeginPlay()를 알리며 다른 액터들의 BeginPlay가 시작됨

void AGameStateBase::OnRep_ReplicatedHasBegunPlay()
{
	if (bReplicatedHasBegunPlay && GetLocalRole() != ROLE_Authority)
	{
		GetWorldSettings()->NotifyBeginPlay();	// 액터들 BeginPlay시작
		GetWorldSettings()->NotifyMatchStarted();
	}
}

void AWorldSettings::NotifyBeginPlay()
{
	if (!World->bBegunPlay)	{
		for (FActorIterator It(World); It; ++It) {	// 액터 순회하면서
			// ... //
			It->DispatchBeginPlay(bFromLevelLoad);	// 함수 호출
            // ... //
}

void AActor::DispatchBeginPlay(bool bFromLevelStreaming)
{
	if (World) {
		BeginPlay();	// BeginPlay 시작
		// ... //
}
  • 그래서 게임모드의 StartPlay는 게임을 시작하라는 지시 함수이고, 이 함수통해서 다른 모든 액터들의 BeginPlay가 호출되게 된다

3. OnPossess

  • 컨트롤러가 폰을 소유(Possess)할 때, 폰의 오너로 컨트롤러가 등록되면서 NetConnection 패밀리에 들어온다고 공부하였음

  • APlayerController::OnPossess()에서 해당 폰의 PossessedBy()를 호출해 컨트롤러 자신을 오너로 등록

void AController::OnPossess(APawn* InPawn)
{
    // ... // 
	InPawn->PossessedBy(this);
	SetPawn(InPawn);
}

void APawn::PossessedBy(AController* NewController)
{
	SetOwner(NewController);
	AController* const OldController = Controller;
	Controller = NewController;
	ForceNetUpdate();	// 네트워크 업데이트
}
  • 그리고 이렇게 오너가 바뀌면, 클라이언트의 폰에서 OnRep_Owner()가 호출됨

  • 오너로 등록되면 Simulated Proxy에서 Autonomous Proxy로 바뀐다

  • 소유 해제할 때는 OnUnPossess호출함

void APlayerController::OnUnPossess()
{
	if (GetPawn() != NULL)
	{
		// ... //
		GetPawn()->UnPossessed();
	}
	SetPawn(NULL);
}
  • 오너 설정 과정은 다 서버에서만 일어남

  • OnPossess는 서버에서만 호출, PossessedBy는 서버/클라 둘 다 호출됨

4. NetLoadOnClient

  • 언리얼 에디터에서 레벨에 직접 배치하여 무조건 스폰되는 액터들은 NetLoadOnClient 속성을 true로 지정하여 클라가 스스로 스폰하게 함

  • 이 속성값을 거짓으로하면 서버에서 생성한 후 액터를 클라로 복제해줌


  • 따라서 이 속성값이 참인 액터들은 각 클라가 레벨스트리밍하면서 직접 소환하게 됨

  • 알고보니 이 속성 말고도 bReplicates, Replicate Movement 등 레플리케이션 관련 속성들이 더 있어서, 이것만 킨다고 동기화 끊기진 않음

  • 특히 bReplicates은 우리가 true로 했기에 서버랑 동기화 됨

5. OnRep함수 vs RepNotify 함수

  • 전자는 이전에 공부했던 C++ 함수. 즉, 서버에서 멤버변수를 수정하면 클라이언트로 동기화되고, 이 복제됨에 따라 클라에서만 해당 함수가 호출됨

  • 후자는 블루프린트 함수. 얘는 서버와 클라 둘 다 호출됨.

    • 서버의 경우에는 값이 변경되면 바로 호출됨
    • 클라의 우에는 값이 변경되어 클라로 복제되었을 때 호출됨
  • C++에선 복제될 때뿐만이 아니라 그냥 호출하고 싶으면 명시적으로 호출도 가능했음.
    하지만, 블루프린트 함수는 명시적으로 따로 호출은 못 함. 조건 맞으면 알아서 호출되는 함수

6. NetUpdateFrequency

  • 이전 프로젝트에서 사용해봤듯이, 각 객체들은 서버에서 클라로의 복제빈도수 최대치가 정해져있음

  • 이 속성은 NetUpdateFrequency로 1초에 최대 몇 번 동기화할건지 정함

  • 물론 서버의 성능에 따라 최대치를 만족 못 시킬 수도 있다

SetNetUpdateFrequency(1);   // 1초에 1번만 레플리케이션 시도
NetUpdatePeriod = 1 / GetNetUpdateFrequency();	// Getter로 값 받기
  • 그래서 프레임마다 서버에서 동기화시키는 것은 부하가 좀 크니까 서버는 1초 간격으로 동기화 시키고 클라에서 그 사이를 알아서 보간시키는 방법으로 최적화 가능
void ADXBox::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	if (HasAuthority())	// 서버에선 틱마다 박스 회전
	{
		AddActorLocalRotation(FRotator(0.f, RotationSpeed * DeltaSeconds, 0.f));
		ServerRotationYaw = RootComponent->GetComponentRotation().Yaw;
	}
	else		// 클라에선 동기화 받은 값으로 다음 동기화값을 예측해 보간
	{
		DeltaSecFromRep += DeltaSeconds;
		float LerpR = FMath::Clamp(DeltaSecFromRep / NetUpdatePeriod, 0.f, 1.f);

		// 다음에 동기화 되었을 때의 Rotation 예측
		float NextRotationYaw = ServerRotationYaw + RotationSpeed * NetUpdatePeriod;

		// 다음 동기화 회전값으로 틱당 회전
		float NextTickRotationYaw = FMath::Lerp(ServerRotationYaw,
        										NextRotationYaw,
                                                LerpR);
		SetActorRotation(FRotator(0.f, NextTickRotationYaw, 0.f));
	}
}

// 1초마다 ServerRotationYaw이 동기화되면 불리는 함수
void ADXBox::OnRep_ServerRotationYaw()
{
	SetActorRotation(FRotator(0.f, ServerRotationYaw, 0.f));
	AccDeltaSecondSinceReplicated = 0.f;
}
  • 그냥 AddActorWorldRotation같은 함수로 틱당 더해주는게 더 쉬운 코드인데 Set이용해서 해봄
profile
반갑습니다

0개의 댓글