프로퍼티 리플리케이션을 사용해 움직임 정보를 전송하고, 클라이언트는 받을 때마다 이를 적용함
물리가 활성화되어 있다면 물리 리플리케이션 틱에서 클라이언트와 서버의 물리상태를 동기화
캐릭터 무브먼트 컴포넌트의 틱마다 캐릭터의 추가적인 움직임 처리

서버에서 SimulatedProxy로 보내는 움직임 정보를 기록한 멤버 변수 (FRepMovement 타입)
값이 변경되면 리플리케이션을 통해 OnRep_ReplicatedMovement() 이벤트 함수 호출함
일반 움직임과 물리 움직임의 리플리케이션을 모두 처리하는 용도로 활용됨
액터의 물리 상태를 기록하는 구조체
FRepMovement::FillFrom()
FRigidBodyState를 받아와 FRepMovement 값을 세팅FRepMovement::CopyTo()
FillFrom()과 반대로 현재 프로퍼티의 상태를 FRigidBodyState로 옮겨줌
현재 액터의 움직임을 ReplicatedMovement 속성으로 변환해 설정하는 함수
액터의 PreReplication() 함수에서 호출됨
액터의 물리 움직임과 일반 움직임을 구분해 각각 처리함
일반 움직임은 단순히 액터의 현재 위치/회전/속도 값을 ReplicatedMovement에 저장함
물리 시뮬레이션의 경우 현재 월드에 설정된 물리 씬에서 해당 컴포넌트의 물리 상태를 저장함
현재 컴포넌트의 물리 상태 정보를 ReplicatedMovement로 옮김 - FRigidBodyState::FillFrom()
이를 통해 최종 ReplicatedMovement가 설정되어 접속한 클라이언트에 보내짐
액터의 움직임 리플리케이션 옵션을 활성화해주어야 올바로 동작함
// Actor.cpp
void AActor::PreReplication(IRepChangedPropertyTracker & ChangedPropertyTracker)
{
...
GatherCurrentMovement();
...
}
// ActorReplication.cpp
void AActor::GatherCurrentMovement()
{
// bReplicateMovement 옵션을 설정하지 않으면 동작하지 않음
if (IsReplicatingMovement() || (RootComponent && RootComponent->GetAttachParent()))
{
...
UPrimitiveComponent* RootPrimComp = Cast<UPrimitiveComponent>(GetRootComponent());
if (RootPrimComp && RootPrimComp->IsSimulatingPhysics())
{
// 물리 시뮬레이션이 활성화 된 상태
...
// World에는 물리적인 데이터를 관리하는 물리 씬이 항상 복제되어 두 개의 시스템이 돌아감
UWorld* World = GetWorld();
int ServerFrame = 0;
if (bShouldUsePhysicsReplicationCache)
{
// 물리 씬을 가져옴
if (FPhysScene_Chaos* Scene = static_cast<FPhysScene_Chaos*>(World->GetPhysicsScene()))
{
if (const FRigidBodyState* FoundState = Scene->GetStateFromReplicationCache(RootPrimComp, /*OUT*/ServerFrame))
{
if (ReplicatedMovement.ServerFrame != ServerFrame)
{
// FillFrom() 함수로 상태를 옮겨줌
ReplicatedMovement.FillFrom(*FoundState, this, ServerFrame);
bWasRepMovementModified = true;
}
bFoundInCache = true;
}
}
}
...
// Don't replicate movement if we're welded to another parent actor.
// Their replication will affect our position indirectly since we are attached.
ReplicatedMovement.bRepPhysics = !RootPrimComp->IsWelded();
...
}
else if (RootComponent != nullptr)
{
// If we are attached, don't replicate absolute position, use AttachmentReplication instead.
if (RootComponent->GetAttachParent() != nullptr)
{
...
}
else
{
ReplicatedMovement.Location = FRepMovement::RebaseOntoZeroOrigin(RootComponent->GetComponentLocation(), this);
ReplicatedMovement.Rotation = RootComponent->GetComponentRotation();
ReplicatedMovement.LinearVelocity = GetVelocity();
// 물리가 없으니 각속도는 0으로 설정
ReplicatedMovement.AngularVelocity = FVector::ZeroVector;
// Technically, the values might have stayed the same, but we'll just assume they've changed.
bWasRepMovementModified = true;
}
bWasRepMovementModified = (bWasRepMovementModified || ReplicatedMovement.bRepPhysics);
ReplicatedMovement.bRepPhysics = false; // 물리 리플리케이션 안함
}
...
}
}
ReplicatedMovement의 물리 시뮬레이션 속성(bRepPhysics) 여부에 따라 두 가지로 실행 됨
// GatherCurrentMovement()에서 진행한 사항을 거꾸로 진행
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;
}
// 리플리케이션 받은 ReplicatedMovement 값을 그대로 가져옴
const FRepMovement& LocalRepMovement = GetReplicatedMovement();
...
if (RootComponent)
{
...
if (LocalRepMovement.bRepPhysics)
{
// Sync physics state
...
// 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)
{
...
PostNetReceiveVelocity(LocalRepMovement.LinearVelocity); // 빈 함수 (필요 시 오버라이드)
PostNetReceiveLocationAndRotation();
}
}
}
}
}
void AActor::PostNetReceiveLocationAndRotation()
{
const FRepMovement& LocalRepMovement = GetReplicatedMovement();
FVector NewLocation = FRepMovement::RebaseOntoLocalOrigin(LocalRepMovement.Location, this);
if( RootComponent && RootComponent->IsRegistered() && (NewLocation != GetActorLocation() || LocalRepMovement.Rotation != GetActorRotation()) )
{
// 위치와 회전 값을 리플리케이트 받은 정보로 세팅
SetActorLocationAndRotation(NewLocation, LocalRepMovement.Rotation, /*bSweep=*/ false);
}
}
// ActorReplication.cpp
void AActor::PostNetReceivePhysicState()
{
UPrimitiveComponent* RootPrimComp = Cast<UPrimitiveComponent>(RootComponent);
if (RootPrimComp)
{
const FRepMovement& ThisReplicatedMovement = GetReplicatedMovement();
FRigidBodyState NewState;
// 리플리케이트 받은 움직임 정보를 NewState(FRigidBodyState)로 옮겨줌
ThisReplicatedMovement.CopyTo(NewState, this);
RootPrimComp->SetRigidBodyReplicatedTarget(NewState, NAME_None, ThisReplicatedMovement.ServerFrame, ThisReplicatedMovement.ServerPhysicsHandle);
}
}
// PrimitiveComponentPhysics.cpp
void UPrimitiveComponent::SetRigidBodyReplicatedTarget(FRigidBodyState& UpdatedState, FName BoneName, int32 ServerFrame, int32 ServerHandle)
{
...
if (UWorld* World = GetWorld())
{
if (FPhysScene* PhysScene = World->GetPhysicsScene())
{
if (IPhysicsReplication* PhysicsReplication = PhysScene->GetPhysicsReplication())
{
// If we are not allowed to replicate physics objects,
// don't set replicated target unless we have a BodyInstance.
if (PrimitiveComponentCVars::bReplicatePhysicsObject == false)
{
FBodyInstance* BI = GetBodyInstance(BoneName);
if (BI == nullptr || !BI->IsValidBodyInstance())
{
return;
}
}
// 물리 씬에 대한 정보에서 타겟 컴포넌트의 값에 목적값(UpdatedState)을 설정
PhysicsReplication->SetReplicatedTarget(this, BoneName, UpdatedState, ServerFrame);
}
}
}
}
// PhysicsReplication.cpp
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); // UPrimitiveComponent가 Key
FReplicatedPhysicsTarget* Target = ComponentToTargets_DEPRECATED.Find(TargetKey);
if (!Target)
{
// 타겟에 ReplicatedTarget(FRigidBodyState) 정보를 덮어씌움 (타겟 설정)
// 타겟에는 클라이언트가 앞으로 동기화해야할 서버의 물리 상태 값을 저장할 수 있게됨
// 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_DEPRECATED.Add(TargetKey);
Target->PrevPos = ReplicatedTarget.Position;
Target->PrevPosTarget = ReplicatedTarget.Position;
}
Target->ServerFrame = ServerFrame;
Target->TargetState = ReplicatedTarget; // TargetState 세팅
Target->BoneName = BoneName;
Target->ArrivedTimeSeconds = OwningWorld->GetTimeSeconds();
...
}
}
FReplicatedPhysicsTarget에는 동기화를 위해 필요한 여러 프로퍼티가 존재
타겟을 설정했다면 이에 맞춰 클라이언트의 물리 상태를 타겟에 맞게 수정하는 로직 필요
(클라이언트가 변경해야될 물리 상태의 목표값이 설정되었다면, 적절한 시점에 클라이언트 액터의 물리 상태를 변경해줘야함)
물리 리플리케이션의 틱에서 호출되는 함수
클라이언트의 물리 상태가 서버의 물리 상태의 오차 내에 있을 때까지 계속 호출됨
움직임 리플리케이션 옵션을 활성화해주어야 올바로 동작함
// PhysicsReplication.cpp
void FPhysicsReplication::OnTick(float DeltaSeconds, TMap<TWeakObjectPtr<UPrimitiveComponent>, FReplicatedPhysicsTarget>& ComponentsToTargets)
{
using namespace Chaos;
...
// 여러 조건을 만족하면 호출됨 (너무 많아서 if문 생략)
const bool bRestoredState = ApplyRigidBodyState(DeltaSeconds, BI, PhysicsTarget, PhysicErrorCorrection, PingSecondsOneWay, LocalFrame, 0);
...
}
bool FPhysicsReplication::ApplyRigidBodyState(float DeltaSeconds, FBodyInstance* BI, FReplicatedPhysicsTarget& PhysicsTarget, const FRigidBodyErrorCorrection& ErrorCorrection,
const float PingSecondsOneWay, bool* bDidHardSnap)
{
if (!BI->IsInstanceSimulatingPhysics())
{
return false;
}
//
// NOTES:
//
// The operation of this method has changed since 4.18.
//
// When a new remote physics state is received, this method will
// be called on tick until the local state is within an adequate
// tolerance of the new state.
//
// The received state is extrapolated based on ping, by some
// adjustable amount.
//
// A correction velocity is added new state's velocity, and assigned
// to the body. The correction velocity scales with the positional
// difference, so without the interference of external forces, this
// will result in an exponentially decaying correction.
//
// Generally it is not needed and will interrupt smoothness of
// the replication, but stronger corrections can be obtained by
// adjusting position lerping.
//
// If progress is not being made towards equilibrium, due to some
// divergence in physics states between the owning and local sims,
// an error value is accumulated, representing the amount of time
// spent in an unresolvable state.
//
// Once the error value has exceeded some threshold (0.5 seconds
// by default), a hard snap to the target physics state is applied.
//
bool bRestoredState = true;
// TargetState를 가져와 NewState에 지정
const FRigidBodyState NewState = PhysicsTarget.TargetState;
const float NewQuatSizeSqr = NewState.Quaternion.SizeSquared();
...
// Get Current state
FRigidBodyState CurrentState;
BI->GetRigidBodyState(CurrentState);
/////// EXTRAPOLATE APPROXIMATE TARGET VALUES ///////
// 외삽 방식으로 예측
...
/////// COMPUTE DIFFERENCES ///////
// 예측된 값과 타겟 값의 차이를 계산
...
/////// ACCUMULATE ERROR IF NOT APPROACHING SOLUTION ///////
// 유의미한 차이가 발생했는지 에러 측정
...
bRestoredState = Error < MaxRestoredStateError;
if (bRestoredState)
{
PhysicsTarget.AccumulatedErrorSeconds = 0.0f;
}
else
{
//
// The heuristic for error accumulation here is:
// 1. Did the physics tick from the previous step fail to
// move the body towards a resolved position?
// 2. Was the linear error in the same direction as the
// previous frame?
// 3. Is the linear error large enough to accumulate error?
//
// If these conditions are met, then "error" time will accumulate.
// Once error has accumulated for a certain number of seconds,
// a hard-snap to the target will be performed.
//
// TODO: Rotation while moving linearly can still mess up this
// heuristic. We need to account for it.
//
... // 현재 위치와 이전 위치를 내적해서 방향이 일치하는지 확인
// If the conditions from the heuristic outlined above are met, accumulate
// error. Otherwise, reduce it.
if (PrevProgress < ErrorAccumulationDistanceSq &&
PrevSimilarity > ErrorAccumulationSimilarity)
{
// 오류라고 판단되면 DeltaSeconds만큼 증가
PhysicsTarget.AccumulatedErrorSeconds += DeltaSeconds;
}
else
{
PhysicsTarget.AccumulatedErrorSeconds = FMath::Max(PhysicsTarget.AccumulatedErrorSeconds - DeltaSeconds, 0.0f);
}
// AccumulatedErrorSeconds가 지정한 값(ErrorAccumulationSeconds)보다 큰 경우 하드스냅
// Hard snap if error accumulation or linear error is big enough, and clear the error accumulator.
const bool bHardSnap =
LinDiffSize > MaxLinearHardSnapDistance ||
PhysicsTarget.AccumulatedErrorSeconds > ErrorAccumulationSeconds ||
CharacterMovementCVars::AlwaysHardSnap;
const FTransform IdealWorldTM(TargetQuat, TargetPos);
if (bHardSnap)
{
// 하드스냅 진행하며 위치와 속도 강제로 지정
...
BI->SetBodyTransform(IdealWorldTM, ETeleportType::ResetPhysics, bAutoWake);
// Set the new velocities
BI->SetLinearVelocity(NewState.LinVel, false, bAutoWake);
BI->SetAngularVelocityInRadians(FMath::DegreesToRadians(NewState.AngVel), false, bAutoWake);
}
else
{
// 내삽 진행
// Small enough error to interpolate
if (PhysicsReplicationAsync == nullptr) //sync case
{
const FVector NewLinVel = FVector(NewState.LinVel) + (LinDiff * LinearVelocityCoefficient * DeltaSeconds);
const FVector NewAngVel = FVector(NewState.AngVel) + (AngDiffAxis * AngDiff * AngularVelocityCoefficient * DeltaSeconds);
// 선형 보간
const FVector NewPos = FMath::Lerp(FVector(CurrentState.Position), FVector(TargetPos), PositionLerp);
const FQuat NewAng = FQuat::Slerp(CurrentState.Quaternion, TargetQuat, AngleLerp);
BI->SetBodyTransform(FTransform(NewAng, NewPos), ETeleportType::ResetPhysics);
BI->SetLinearVelocity(NewLinVel, false);
BI->SetAngularVelocityInRadians(FMath::DegreesToRadians(NewAngVel), false);
}
...
}
...
}
...
}
Simulated Proxy의 캐릭터가 처리하는 캐릭터의 움직임
캐릭터가 Simulated Proxy인 경우 캐릭터만의 추가적인 작업을 수행함 - SimulatedMovement() 함수에서 캐릭터가 고려할 다양한 상황에 대한 처리 진행 (캐릭터를 대표하는 캡슐 컴포넌트에서 진행)
시뮬레이션으로 캡슐을 이동시킨 후 메시의 움직임을 부드럽게 보간함 - SmoothClientPosition()
void UCharacterMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
...
if (CharacterOwner->GetLocalRole() > ROLE_SimulatedProxy)
{
...
}
else if (CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy)
{
...
SimulatedTick(DeltaTime);
}
...
}
void UCharacterMovementComponent::SimulatedTick(float DeltaSeconds)
{
... // 루트 모션과 관련된 처리 (일반적인 캐릭터와 크게 상관 없음)
// 캐릭터와 관련하여 추가적으로 동기화
SimulateMovement(DeltaSeconds);
...
// Smooth mesh location after moving the capsule above.
if (!bNetworkSmoothingComplete)
{
SCOPE_CYCLE_COUNTER(STAT_CharacterMovementSmoothClientPosition);
// 비주얼을 담당하는 메시 컴포넌트를 캡슐 컴포넌트의 위치로 부드럽게 이동
SmoothClientPosition(DeltaSeconds);
}
else
{
UE_LOG(LogCharacterNetSmoothing, Verbose, TEXT("Skipping network smoothing for %s."), *GetNameSafe(CharacterOwner));
}
}
void UCharacterMovementComponent::SimulateMovement(float DeltaSeconds)
{
... // 캐릭터 무브먼트가 가지는 특수한 상황에 대한 추가적인 처리
LastUpdateLocation = UpdatedComponent ? UpdatedComponent->GetComponentLocation() : FVector::ZeroVector;
LastUpdateRotation = UpdatedComponent ? UpdatedComponent->GetComponentQuat() : FQuat::Identity;
LastUpdateVelocity = Velocity;
}
void UCharacterMovementComponent::SmoothClientPosition(float DeltaSeconds)
{
...
// 프록시로 복제하는 액터에 대해서만 진행하도록 체크
// Only client proxies or remote clients on a listen server should run this code.
const bool bIsSimulatedProxy = (CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy);
const bool bIsRemoteAutoProxy = (CharacterOwner->GetRemoteRole() == ROLE_AutonomousProxy);
if (!ensure(bIsSimulatedProxy || bIsRemoteAutoProxy))
{
return;
}
// 내삽 방식으로 현재 틱에서 부드럽게 이동해야될 클라이언트 메시 데이터(ClientData)를 저장
SmoothClientPosition_Interpolate(DeltaSeconds);
// 측정된 ClientData의 위치를 MeshComponent에 적용하여 이동 처리
SmoothClientPosition_UpdateVisuals();
}
https://github.com/dnjfs/ArenaBattle_Network/commit/916ee933f49d38b676577a3710ec43bb7c897b4a