Unreal Engine - 데디케이티드 서버 5 (RPC, Replication)

2025. 4. 10. 20:00·Unreal Engine

https://gbleem.tistory.com/152 

 

Unreal Engine - 데디케이티드 서버 4 (RPC)

1. RPC 개념https://gbleem.tistory.com/140#6.%20RPC-1-5 Unreal Engine - 데디케이티드 서버 개념 및 실습1. 서버의 종류P2P각 컴퓨터가 서버랑 클라이언트를 모두 수행하는 방식리슨 서버HOST 역할을 하는 서버용

gbleem.tistory.com

이어지는 글 입니다.

 

1. NetMulticast RPC vs Replication


1 -1. overview

게임에서 리플리케이션 된 액터가 있을때, 이 액터의 속성이 서버에서 수정된 경우를 생각해보자

  • 넷 멀티캐스트 RPC를 통해서 서버 + 모든 클라이언트에게 변경된 값을 알려주거나
  • 프로퍼티 리플리케이션을 통해서 값을 알려줄 수 있다.

결론적으로 두 방식 모두 원하는 데이터의 동기화를 보장해줄 수 있다.

그러면 어떤 경우에 어떤 방식을 쓰는 것이 좋을까?

 

각 방식의 특징을 살펴보면, 

  • 프로퍼티 리플리케이션 된 속성은 무조건 클라이언트에게 동기화를 하게된다 (늦은 시점이더라도)
  • 넷 멀티캐스트의 경우는 RPC를 실행한 시점에 클라이언트가 없으면, 그 동기화 정보는 사라지게 된다.

그렇기 때문에 "게임에 중요한 영향을 끼치는 동기화의 경우 프로퍼티 리플리케이션을 써야 한다."

 

1 - 2. 실습 (변수의 프로퍼티 리플리케이션)

  • 이전에 Mine 액터를 만들어서 터지는 시스템을 구현하였다.
  • 터진 Mine인지 아닌지 체크하는것은 게임 내 중요한 시스템이기 때문에, 프로퍼티 리플리케이션 해주는 방식으로 수정해야 한다.
//mine.h
UPROPERTY(Replicated)
uint8 bIsExploded : 1;


//mine.cpp
void ADXLandMine::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutlifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutlifetimeProps);

	DOREPLIFETIME(ThisClass, bIsExploded);
}

void ADXLandMine::MulticastRPCSpawnEffect_Implementation()
{
	if (HasAuthority())
	{
		bIsExploded = true;
		return;
	}
	//already exploded
	if (bIsExploded)
	{
		return;
	}
	Particle->Activate(true);
	//bIsExploded = true;
}

 

위의 코드처럼 실행하면, 클라이언트로 프로퍼티 리플리케이션 되는 시점과, 넷 멀티케스트 실행 시점이 맞지 않는 문제가 생겨서, 폭발 이팩트가 발생하지 않는다.

 

해결

  • 멀티케스트 함수가 아니라, 충돌되는 순간에 해당 값을 체크하도록 로직을 수정했다.
//mine.cpp

void ADXLandMine::OnLandMineBeginOverlap(AActor* OverlappedActor, AActor* OtherActor)
{
	//if server
	if (HasAuthority())
	{
		UKismetSystemLibrary::PrintString(this, FString::Printf(TEXT("run on server")), true, true, FLinearColor::Green, 5.f);
		MulticastRPCSpawnEffect();

		if (!bIsExploded)
		{
			bIsExploded = true;
		}
	}
	else
	{
		APawn* OwnerPawn = Cast<APawn>(GetOwner());
		if (IsValid(OwnerPawn))
		{
			if (OwnerPawn->IsLocallyControlled())
			{
				UKismetSystemLibrary::PrintString(this, FString::Printf(TEXT("Run on owning client")), true, true, FLinearColor::Green, 5.f);
			}
			else
			{
				UKismetSystemLibrary::PrintString(this, FString::Printf(TEXT("Run on other client")), true, true, FLinearColor::Green, 5.f);
			}
		}
		if (!bIsExploded)
		{
			Particle->Activate(true);
		}
	}
}

void ADXLandMine::MulticastRPCSpawnEffect_Implementation()
{
	//if (HasAuthority())
	//{
	//	bIsExploded = true;
	//	return;
	//}
	////already exploded
	//if (bIsExploded)
	//{
	//	return;
	//}
	//Particle->Activate(true);
	////bIsExploded = true;
}

 

1 - 3. 실습 (넷 멀티캐스트 함수)

(SetNetCullDistanceSquared를 적용해둔 상태)

 

터지는 순간 머티리얼이 변동되도록 로직 추가

void ADXLandMine::MulticastRPCSpawnEffect_Implementation()
{
	if (IsValid(ExplodedMaterial))
	{
		Mesh->SetMaterial(0, ExplodedMaterial);
	}
}

 

위의 코드를 적용후 실행하면, 아래 영상과 같은 문제가 발생한다.

또한 

  • 폭발 이팩트의 경우 프로퍼티 리플리케이션을 통해서 값이 동기화가 되지만, 
  • 머티리얼의 경우는 멀티캐스트를 통해서 값을 알려주기 때문에, 클라이언트1이 색을 바꾼 순간 다른 클라이언트2가 범위안에 없었기 때문에 클라이언트2는 해당 머티리얼이 변경된 것을 알지 못한다.

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

이러한 문제를 해결하기 위해서는 OnRep함수를 통해서 즉각적으로 값을 연동하는 방법이 있다.

//mine.h
UPROPERTY(ReplicatedUsing = OnRep_IsExploded)
uint8 bIsExploded : 1;

//mine.cpp
void ADXLandMine::OnRep_IsExploded()
{
	if (bIsExploded && IsValid(ExplodedMaterial))
	{
		Mesh->SetMaterial(0, ExplodedMaterial);
	}
}

void ADXLandMine::MulticastRPCSpawnEffect_Implementation()
{
	/*if (IsValid(ExplodedMaterial))
	{
		Mesh->SetMaterial(0, ExplodedMaterial);
	}*/
}

 

아래와 같이 정상적으로 동작하는 것을 확인할 수 있다.

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

1 - 4. 결론

  • 게임 내 중요한 로직에 있어서는 프로퍼티 리플리케이션을 사용해야 한다.
  • 또한 프로퍼티 리플리케이션도 연동되는 시간은 보장할 수 없기 때문에, OnRep 함수를 사용하여 즉각적으로 값의 연동을 해줄 수 있다.

 

 

2. Actor Movement Replication


액터의 움직임 관련 리플리케이션은 "프로퍼티 리플리케이션"을 사용한다.

  • 서버는 움직임 정보를 전송하고
  • 클라이언트는 이 정보를 받아서 받을 때마다 적용하는 방식으로 동작한다.

ReplicatedMovement 의 속성

  • 서버에서 클라이언트로의 움직임 정보를 저장
  • 일반적인 움직임 + 물리적인 움직임의 리플리케이션을 모두 처리한다.
// Actor.h 

...

UCLASS(BlueprintType, Blueprintable, config=Engine, meta=(ShortTooltip="An Actor is an object that can be placed or spawned in the world."), MinimalAPI)
class AActor : public UObject
{
	...

private:
	/** Used for replication of our RootComponent's position and velocity */
	UPROPERTY(EditDefaultsOnly, ReplicatedUsing=OnRep_ReplicatedMovement, Category=Replication, AdvancedDisplay)
	struct FRepMovement ReplicatedMovement;

	...	
}

 

 

FRepMovement 구조체가 가지는 정보

  • 위치와 회전
  • 물리 시뮬레이션 여부
  • 이동속도
  • 각속도
  • 서버 프레임
  • 등

 

FRigidBodyState 구조체

  • 액터의 물리 상태를 기록하는 구조

 

GatherCurrentMovement() 함수

  • 액터의 움직임을 RepicatedMovement 속성을 통해서 클라이언트로 전달한다.
  • 액터의 물리 움직임과 일반 움직임을 구별하여 처리한다.
    • 일반 움직임
      • 단순한 액터의 위치, 회전 속도값을 ReplicatedMovement 에 저장
    • 물리 움직임
      • FRigidBodyState::FillFrom() 함수로 현재 컴포넌트의 물리 상태정보를 ReplicatedMovement  에 저장
  • 최종적으로 모든 움직임들을 저장한 후 ReplicateMovement를 설정하여 클라이언트로 보낸다.
  • 아래와 같이 default는 false이기 때문에 설정이 필요한 액터는 true로 바꿔주어야 한다.

 

OnRep_ReplicatedMovement() 함수 & SetReplicatedTarget() 함수

  • 서버에서 작성된 액터 움직임 정보를 클라에서 수신
  • bRepPhysics 여부에 따라
    • 일반 움직임
      • Simulated Proxy 처리
      • 컴포넌트의 위치와 회전 정보 갱신
      • 속도 처리는 x
    • 물리 움직임
      • FRigidBodyState::CopyTo() 함수를 통해 ReplicatedMovement 정보를 현재 컴포넌트 물리 상태로 옮긴다.
      • 물리 리플리케이션 씬에서 컴포넌트와 일치하는 타겟을 찾아서 업데이
더보기
void AActor::OnRep_ReplicatedMovement()
{
	// Since ReplicatedMovement and AttachmentReplication are REPNOTIFY_Always (and OnRep_AttachmentReplication may call OnRep_ReplicatedMovement directly),
	// this check is needed since this can still be called on actors for which bReplicateMovement is false - for example, during fast-forward in replay playback.
	// When this happens, the values in ReplicatedMovement aren't valid, and must be ignored.
	if (!IsReplicatingMovement())
	{
		return;
	}

	const FRepMovement& LocalRepMovement = GetReplicatedMovement();

#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
		if (CVarDrawDebugRepMovement->GetInt() > 0)
		{
			FColor DebugColor = FColor::Green;
			if (LocalRepMovement.bRepPhysics)
			{
				switch (GetPhysicsReplicationMode())
				{
				case EPhysicsReplicationMode::PredictiveInterpolation:
					DebugColor = FColor::Yellow;
					break;
				case EPhysicsReplicationMode::Resimulation:
					DebugColor = FColor::Red;
					break;
				case EPhysicsReplicationMode::Default:
				default:
					DebugColor = FColor::Cyan;
					break;
				}
			}
			DrawDebugCapsule(GetWorld(), LocalRepMovement.Location, FMath::Max(GetSimpleCollisionHalfHeight(), 25.0f), FMath::Max(GetSimpleCollisionRadius(), 25.0f), LocalRepMovement.Rotation.Quaternion(), DebugColor, false, 1.f);
		}
#endif

	if (RootComponent)
	{
		if (ActorReplication::SavedbRepPhysics != LocalRepMovement.bRepPhysics)
		{
			// Turn on/off physics sim to match server.
			SyncReplicatedPhysicsSimulation();
		}

		// NOTE: This is only needed because ClusterUnion has a flag bHasReceivedTransform
		// which does not get updated until the component's transform is directly set.
		// Until that flag is set, its root particle will be in a disabled state and not
		// have any children, therefore replication will be dead in the water.
		RootComponent->OnReceiveReplicatedState(LocalRepMovement.Location, LocalRepMovement.Rotation.Quaternion(), LocalRepMovement.LinearVelocity, LocalRepMovement.AngularVelocity);

		if (LocalRepMovement.bRepPhysics)
		{
			// Sync physics state
#if DO_GUARD_SLOW
			if (!RootComponent->IsSimulatingPhysics())
			{
				UE_LOG(LogNet, Warning, TEXT("IsSimulatingPhysics() returned false during physics replication for %s"), *GetName());
			}
#endif
			// If we are welded we just want the parent's update to move us.
			UPrimitiveComponent* RootPrimComp = Cast<UPrimitiveComponent>(RootComponent);
			if (!RootPrimComp || !RootPrimComp->IsWelded())
			{
				PostNetReceivePhysicState();
			}
		}
		else
		{
			// Attachment trumps global position updates, see GatherCurrentMovement().
			if (!RootComponent->GetAttachParent())
			{
				if (GetLocalRole() == ROLE_SimulatedProxy)
				{
#if ENABLE_NAN_DIAGNOSTIC
					if (LocalRepMovement.Location.ContainsNaN())
					{
						logOrEnsureNanError(TEXT("AActor::OnRep_ReplicatedMovement found NaN in ReplicatedMovement.Location"));
					}
					if (LocalRepMovement.Rotation.ContainsNaN())
					{
						logOrEnsureNanError(TEXT("AActor::OnRep_ReplicatedMovement found NaN in ReplicatedMovement.Rotation"));
					}
#endif

					PostNetReceiveVelocity(LocalRepMovement.LinearVelocity);
					PostNetReceiveLocationAndRotation();
				}
			}
		}
	}
}

 

void AActor::PostNetReceivePhysicState()
{
	UPrimitiveComponent* RootPrimComp = Cast<UPrimitiveComponent>(RootComponent);
	if (RootPrimComp)
	{
		const FRepMovement& ThisReplicatedMovement = GetReplicatedMovement();
		FRigidBodyState NewState;
		ThisReplicatedMovement.CopyTo(NewState, this);
		RootPrimComp->SetRigidBodyReplicatedTarget(NewState, NAME_None, ThisReplicatedMovement.ServerFrame, ThisReplicatedMovement.ServerPhysicsHandle);
	}
}

...

void UPrimitiveComponent::SetRigidBodyReplicatedTarget(FRigidBodyState& UpdatedState, FName BoneName, int32 ServerFrame, int32 ServerHandle)
{
	if (UWorld* World = GetWorld())
	{
		if (FPhysScene* PhysScene = World->GetPhysicsScene())
		{
			if (FPhysicsReplication* PhysicsReplication = PhysScene->GetPhysicsReplication())
			{
				FBodyInstance* BI = GetBodyInstance(BoneName);
				if (BI && BI->IsValidBodyInstance())
				{
					PhysicsReplication->SetReplicatedTarget(this, BoneName, UpdatedState, ServerFrame);
					BI->GetPhysicsActorHandle();// ->GetGameThreadAPI().SetParticleID(Chaos::FParticleID{ ServerPhysicsHandle, INDEX_NONE });
				}
			}
		}
	}
}

...

void FPhysicsReplication::SetReplicatedTarget(UPrimitiveComponent* Component, FName BoneName, const FRigidBodyState& ReplicatedTarget, int32 ServerFrame)
{
	if (UWorld* OwningWorld = GetOwningWorld())
	{
		//TODO: there's a faster way to compare this
		TWeakObjectPtr<UPrimitiveComponent> TargetKey(Component);
		FReplicatedPhysicsTarget* Target = ComponentToTargets.Find(TargetKey);
		if (!Target)
		{
			// First time we add a target, set it's previous and correction
			// positions to the target position to avoid math with uninitialized
			// memory.
			Target = &ComponentToTargets.Add(TargetKey);
			Target->PrevPos = ReplicatedTarget.Position;
			Target->PrevPosTarget = ReplicatedTarget.Position;
		}

		Target->ServerFrame = ServerFrame;
		Target->TargetState = ReplicatedTarget;
		Target->BoneName = BoneName;
		Target->ArrivedTimeSeconds = OwningWorld->GetTimeSeconds();

		ensure(!Target->PrevPos.ContainsNaN());
		ensure(!Target->PrevPosTarget.ContainsNaN());
		ensure(!Target->TargetState.Position.ContainsNaN());
	}
}

 

ApplyRigidBodyState() 함수

  • 물리 움직임의 동기화, 물리 리플리케이션의 틱에서 호출
  • 로직
    • 서버에서 받은 최종 속도와 핑을 기반으로 클라이언트의 물리 상태를 예측
    • 예측한 위치와 방향이 서버와 올바른지 체크하고 문제가 있다면 에러 시간을 누적, 누적된 에러 시간이 설정값을 넘어가는 경우 강제조정을 진행
    • 차이가 크지 않으면 interpolation을 통해서 조정

'Unreal Engine' 카테고리의 다른 글

Unreal Engine - 데디케이티드 서버 7 (동기화 2)  (0) 2025.04.12
Unreal Engine - 데디케이티드 서버 6 (동기화)  (0) 2025.04.11
Unreal Engine - 데디케이티드 서버 4 (RPC)  (0) 2025.04.07
Unreal Engine - 데디케이티드 서버 3 (Property Replication)  (0) 2025.04.06
Unreal Engine - 데디케이티드 서버 2  (1) 2025.04.03
'Unreal Engine' 카테고리의 다른 글
  • Unreal Engine - 데디케이티드 서버 7 (동기화 2)
  • Unreal Engine - 데디케이티드 서버 6 (동기화)
  • Unreal Engine - 데디케이티드 서버 4 (RPC)
  • Unreal Engine - 데디케이티드 서버 3 (Property Replication)
gbleem
gbleem
gbleem 님의 블로그 입니다.
  • gbleem
    gbleem 님의 블로그
    gbleem
  • 전체
    오늘
    어제
    • 분류 전체보기 (189)
      • Unreal Engine (73)
      • C++ (19)
      • 알고리즘(코딩테스트) (32)
      • TIL (60)
      • CS (4)
      • 툴 (1)
  • 블로그 메뉴

    • 홈
    • 카테고리
  • 링크

    • 깃허브
    • velog
  • 공지사항

  • 인기 글

  • 태그

    DP
    상속
    actor 클래스
    enhanced input system
    매크로 지정자
    map을 vector로 복사
    gamestate
    BFS
    템플릿
    const
    applydamage
    cin함수
    C++
    싱글턴
    motion matching
    additive animation
    Vector
    character animation
    addonscreendebugmessage
    blend pose
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
gbleem
Unreal Engine - 데디케이티드 서버 5 (RPC, Replication)
상단으로

티스토리툴바