Unreal Engine - 데디케이티드 서버 5 (RPC, Replication)
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을 통해서 조정