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 |