리플리케이트
- 서버와 클라이언트 사이에서 데이터와 명령을 주고받는 프로세스
액터 업데이트 방식은 크게 두 가지입니다.
- 프로퍼티 업데이트
- RPC (Remote Procedure Call).
프로퍼티 리플리케이션과 RPC 사이의 큰 차이점은, 프로퍼티는 변경될 때마다 자동으로 리플리케이트되는 반면, RPC 는 실행될 때만 리플리케이트된다는 점입니다.
컴포넌트 리플리케이트
- 액터의 그 게임플레이 로직이 리플리케이트되는 것이고, 그 결과가 가끔은 컴포넌트에 대한 호출/변화로 나타납니다. 그러나 가끔은 컴포넌트의 프로퍼티나 이벤트 자체적으로 직접 리플리케이트해야 할 때도 있습니다.
- 액터가 리플리케이트되면, 그 컴포넌트도 리플리케이트시킬 수 있습니다.
- 이 컴포넌트는 액터와 같은 방식으로 프로퍼티와 RPC 리플리케이션이 가능합니다. 컴포넌트는 반드시 액터와 같은 방식으로 ::GetLifetimeReplicatedProps (...) 함수를 구현해야 합니다.
- 스태틱 컴포넌트는 액터 생성시 생성되는 컴포넌트입니다. 즉 소유중인 액터가 클라이언트나 서버에 스폰되면, 컴포넌트의 리플리케이션 여부와 상관없이 컴포넌트 역시도 스폰됩니다.
- 이러한 컨텍스트에서 스태틱 컴포넌트는 C++ 생성자에서 Default Subobject 로 생성되는 컴포넌트 또는 블루프린트 에디터의 컴포넌트 모드에서 생성되는 컴포넌트입니다.
- 스태틱 컴포넌트는 클라이언트에 리플리케이트시킬 필요가 없이 그냥 기본적으로 존재합니다. 서버와 클라이언트 사이에 프로퍼티나 이벤트를 자동 동기화시킬 필요가 있을 때만 리플리케이트 해주면 됩니다.
- 다이내믹 컴포넌트는 실행시간에 서버에서 스폰되는 컴포넌트로, 그 생성 및 소멸이 클라이언트에 리플리케이트되는 것을 말합니다.
- 다이내믹 컴포넌트는 모든 클라이언트에 존재하기 위해 리플리케이션이 필요합니다.
- 리플리케이션이 등장하는 일은, 서버상에서 발동되는 프로퍼티 또는 이벤트를 클라이언트에 자동으로 동기화시킬 필요가 있을 경우만입니다.
- 컴포넌트를 리플리케이트시키려면 단순히 AActorComponent::SetIsReplicated(true) 호출을 해 주면 됩니다. 컴포넌트가 디폴트 서브오브젝트인 경우, 컴포넌트 생성 이후 클래스 생성자에서 처리될 것입니다.
타임라인
- 타임라인은 반드시 그 프로퍼티의 Replicated 옵션을 통해 리플리케이션을 켜 줘야 합니다.
- 리플리케이트되는 타임라인은 서버에서만 직접 조작해야 합니다 (시작/중지 등).
- 클라이언트에서는 타임라인 자체에 대한 변경을 시도해서는 안될 것입니다.
액터와 그 접속의 소유
- 예시: Pawn 액터가 PlayerController 에 빙의(possess)될 때입니다. 그 오너는 빙의된 PlayerController 가 될 것입니다. 이 동안 액터는 PlayerController 의 접속에 소유됩니다. Pawn 은 PlayerController 에 의해 소유/빙의된 기간동안 이 접속에 소유됩니다. 그리고 PlayerController 가 더이상 Pawn 에 빙의하지 않게 되면, Pawn 은 더이상 접속에 소유되지 않습니다.
- bOnlyRelevantToOwner 가 True 설정된 액터의 경우, 액터를 소유하는 접속만 해당 액터에 대한 프로퍼티 업데이트를 받습니다.
RPC(Remote Procedure Call)
- 로컬에서 호출되지만 (호출하는 머신과는) 다른 머신에서 원격 실행되는 함수를 말합니다.
- RPC 함수는 매우 유용하게 사용될 수 있으며, 네트워크 연결을 통해 클라이언트와 서버 사이에 메시지를 전송할 수 있습니다.
- 이러한 기능의 주요 사용 사례는 본질적으로 일시적이거나 외관적인 신뢰할 수 없는 게임 플레이 이벤트를 수행하는 것이다. 여기에는 연기 소리, 입자 생성 또는 액터 기능에 중요하지 않은 기타 임시 효과를 수행하는 이벤트가 포함될 수 있습니다.
- 함수를 RPC 로 선언하려면 UFUNCTION 선언에 Server, Client, NetMulticast 키워드를 붙여주기만 하면 됩니다.
- 실행될 때만 리플리케이트 된다.
예를 들어 함수를 서버에서 호출되지만 클라이언트에서 실행되는 RPC 로 선언하려면, 이렇게 합니다.
UFUNCTION( Client )
void ClientRPCFunction();
함수를 클라이언트에서 호출되지만 서버에서 실행되는 RPC 로 선언하는 것은 Server 키워드를 사용한다는 것 빼고는 매우 비슷합니다.
UFUNCTION( Server )
void ServerRPCFunction();
Multicast 라 불리는 특수 유형 RPC 함수가 하나 더 있습니다. Multicast RPC 는 서버에서 호출된 다음 서버는 물론 현재 연결된 모든 클라이언트에서도 실행되도록 고안된 것입니다. 멀티캐스트 함수를 선언하려면 그냥 NetMulticast 키워드를 사용하면 됩니다.
UFUNCTION( NetMulticast )
void MulticastRPCFunction();
멀티캐스트 RPC 는 클라이언트에서도 호출 가능하지만, 이 경우 로컬에서만 실행됩니다.
신뢰성
기본적으로 RPC 는 비신뢰성입니다. RPC 호출이 원격 머신에서 확실히 실행되도록 하기 위해서는 Reliable 키워드를 붙이면 됩니다:
UFUNCTION( Client, Reliable )
void ClientRPCFunction();
RPC 의 정상 작동을 위해 충족시켜야 하는 요건
- Actor 에서 호출되어야 합니다.
- Actor 는 반드시 replicated 여야 합니다.
- 서버에서 호출되고 클라이언트에서 실행되는 RPC 의 경우, 해당 Actor 를 실제 소유하고 있는 클라이언트에서만 함수가 실행됩니다.
- 클라이언트에서 호출되고 서버에서 실행되는 RPC 의 경우, 클라이언트는 RPC 가 호출되는 Actor 를 소유해야 합니다.
- Multicast RPC 는 예외입니다:
- 서버에서 호출되는 경우, 서버에서는 로컬에서 실행될 뿐만 아니라 현재 연결된 모든 클라이언트에서도 실행됩니다.
- 클라이언트에서 호출되는 경우, 로컬에서만 실행되며, 서버에서는 실행되지 않습니다.
- 현재 멀티캐스트 이벤트에 대해 단순한 스로틀 조절 메카니즘이 있습니다. 멀티캐스트 함수는 주어진 액터의 네트워크 업데이트 기간동안 두 번 이상 리플리케이트되지 않습니다.
서버에서 호출된 RPC
액터 소유권 | 리플리케이트 안됨 | NetMulticast | Server | Client |
클라이언트 소유 액터 | 서버에서 실행 | 서버와 모든 클라이언트에서 실행 |
서버에서 실행 | 액터의 소유 클라이언트에서 실행 |
서버 소유 액터 | 서버에서 실행 | 서버와 클라이언트에서 실행 |
서버에서 실행 | 서버에서 실행 |
미소유 액터 | 서버에서 실행 | 서버와 모든 클라이언트에서 실행 |
서버에서 실행 | 서버에서 실행 |
클라이언트에서 호출된 RPC
액터 소유권 | 리플리케이트 안됨 | NetMulticast | Server | Client |
호출하는 클라이언트에 소유 | 호출하는 클라이언트에서 실행 | 호출하는 클라이언트에서 실행 | 서버에서 실행 | 호출하는 클라이언트에서 실행 |
다른 클라이언트에 소유 | 호출하는 클라이언트에서 실행 | 호출하는 클라이언트에서 실행 | 드롭됨 | 호출하는 클라이언트에서 실행 |
서버 소유 액터 | 호출하는 클라이언트에서 실행 | 호출하는 클라이언트에서 실행 | 드롭됨 | 호출하는 클라이언트에서 실행 |
미소유 액터 | 호출하는 클라이언트에서 실행 | 호출하는 클라이언트에서 실행 | 드롭됨 | 호출하는 클라이언트에서 실행 |
테스트 결과
호출 : 함수가 불려져온 곳 ( 코드를 만났을 때 )
소유자 : 함수가 적용될 대상
실행 : 함수가 실행되는 곳
예시)
로컬플레이어가 장전을 했는데 client함수가 코드에 있다. (소유권이 클라이언트인 상태)
그 해당 함수는 클라이언트(로컬플레이어)에서 실행된다.
UFUNCTION(reliable, client)
void ServerTest();
로컬플레이어가 장전을 했는데 server함수가 코드에 있다. (소유권이 클라이언트인 상태)
그 해당 함수는 서버에서 실행된다.
UFUNCTION(reliable, server)
void ServerTest();
로컬플레이어가 장전을 했는데 netMulticast함수가 코드에 있다. (소유권이 클라이언트인 상태)
그 해당 함수는 클라이언트(로컬플레이어)에서 실행된다.
UFUNCTION(reliable, netMulticast)
void NetMulticastTest();
서버인 로컬플레이어가 장전을 했는데(소유권이 클라이언트인 상태이며 서버에서 호출) netMulticast함수가 코드에 있다.
그 해당 함수는 서버와 나머지 모든 클라이언트에서 실행된다.
서버 접속프로세스
- 클라이언트가 접속 요청을 보냅니다.
- 서버가 수락하면, 현재 맵을 전송합니다.
- 서버는 클라이언트가 이 맵을 로드할 때까지 기다립니다.
- 로드가 되면, 서버 로컬에서 AGameModeBase::PreLogin 를 호출합니다.
- GameMode 에 접속을 거부할 수 있는 기회를 줍니다.
- 수락되면, 서버는 AGameModeBase::Login 을 호출합니다.
- 이 함수의 역할은 PlayerController 를 만든 다음 새로 접속된 클라이언트에 리플리케이트 시킵니다. 수신되면 이 PlayerController 가 클라이언트의 접속 과정에서 견본으로 사용되던 임시 PlayerController 를 대체합니다.
- 여기서 APlayerController::BeginPlay 가 호출되는 것을 눈여겨 봅시다. 이 액터에서 RPC 함수를 호출하는 것은 아직 안전하지 않다는 점 참고하시구요. AGameModeBase::PostLogin 호출시까지 기다려야 합니다.
- 모든 것이 잘 돌아갔다는 가정하에 AGameModeBase::PostLogin 이 호출됩니다.
- 이제 서버가 이 PlayerController 에서 RPC 함수 호출을 시작해도 안전합니다.
액터 리플리케이트
- 리플리케이션의 중심은 액터이다.
- 액터 리플리케이션 대부분은 UNetDriver::ServerReplicateActors 안에서 일어납니다. 서버가 각 클라이언트에 연관성이 있다고 결정내린 액터 전부를 수집하고, 접속된 각 클라이언트가 지난 번 업데이트된 이후 변경된 프로퍼티가 있으면 전송하는 곳입니다.
- 각 액터에는 Replicated 지정자 를 포함하는 모든 프로퍼티 목록이 유지됩니다. 서버는 리플리케이트된 프로퍼티의 값이 변할 때마다 각 클라이언트에 업데이트를 전송하며, 클라이언트는 액터의 로컬 버전에 적용합니다. 이 업데이트는 서버에서만 받으며, 클라이언트는 프로퍼티 업데이트를 서버나 다른 클라이언트로 절대 전송하지 않습니다.
- 프로퍼티는 변경될 때마다 자동으로 리플리케이트 된다.
- 예를 들어 액터의 생명력과 같은 것은 변할 때마다 클라이언트에서 확인할 수 있어야한다.
프로퍼티를 리플리케이트하려면 몇 가지 작업이 필요합니다.
1. 프로퍼티가 정의되는 액터 클래스의 헤더에서, UPROPERTY 선언에 파라미터의 하나로 replicated 키워드를 넣어줘야 합니다.
class ENGINE_API AActor : public UObject
{
UPROPERTY( replicated )
AActor * Owner;
};
2. 액터 클래스의 구현에서 GetLifetimeReplicatedProps 함수를 구현해 줘야 합니다:
void AActor::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const
{
DOREPLIFETIME( AActor, Owner );
}
3. 액터의 생성자에서, bReplicates 플래그가 true 로 설정되었는지 확인합니다:
AActor::AActor( const class FPostConstructInitializeProperties & PCIP ) : Super( PCIP )
{
bReplicates = true;
}
4. Owner 멤버 변수는 이제 현재 인스턴싱된 이 액터 유형 (이 경우, 베이스 액터 클래스) 모든 사본에 대해 접속된 모든 클라이언트에 동기화될 것입니다.
조건식 프로퍼티 리플리케이션
- 프로퍼티가 일단 리플리케이션 등록되면 해제시킬 수 없습니다
- 기본적으로 리플리케이트되는 각 프로퍼티에는 조건이 내장되어 있는데, 변경되지 않은 경우 리플리케이트하지 않는다는 것입니다.
- 프로퍼티 리플리케이션에 대한 세밀한 제어를 위해, 부차적인 조건을 추가시킬 수 있는 특수 매크로가 있습니다.
이 매크로는 DOREPLIFETIME_CONDITION 라 합니다. 그 사용법 예제는 다음과 같습니다:
void AActor::GetLifetimeReplicatedProps( TArray< FLifetimeProperty > & OutLifetimeProps ) const
{
DOREPLIFETIME_CONDITION( AActor, ReplicatedMovement, COND_SimulatedOnly );
}
COND_InitialOnly | 이 프로퍼티는 초기 번치에만 전송을 시도합니다. |
COND_OwnerOnly | 이 프로퍼티는 액터의 오너에만 전송합니다. |
COND_SkipOwner | 이 프로퍼티는 오너를 제외한 모든 접속에 전송합니다. |
COND_SimulatedOnly | 이 프로퍼티는 시뮬레이션되는 액터에만 전송합니다. |
COND_AutonomousOnly | 이 프로퍼티는 자율 액터에만 전송합니다. |
COND_SimulatedOrPhysics | 이 프로퍼티는 시뮬레이션되는 또는 bRepPhysics 액터에 전송합니다. |
COND_InitialOrOwner | 이 프로퍼티는 초기 패킷시, 또는 액터의 오너에 전송합니다. |
COND_Custom | 이 프로퍼티에는 별다른 조건이 없지만, SetCustomIsActiveOverride 를 통해 껐다 켰다 토글 기능을 원합니다. |
Actor, Role과 RemoteRole
리플리케이션 관련해서 액터에 중요한 프로퍼티가 두 가지 있습니다. Role (롤)과 Remote Role (리모트 롤)입니다.
Role과 RemoteRole은 다음을 알려줍니다.
- Actor에 대한 Authority 소유자
- Actor의 리플리케이션여부
- 리플리케이션의 모드
첫번째로 결정해야 할 중요한 것은 누가 특정 Actor에 대한 Authority을 가졌는가입니다.
현재 인스턴스에 대한 Authority가 있는지 확인은 Role == ROLE_Authority입니다.
이것이 true였을 때 실행중인 엔진의 인스턴스는 replicated의 여부와는 관계없이 Actor의 권한을 가집니다.
Role이 ROLE_Authority이고, RemoteRole이 ROLE_SimulatedProxy나 ROLE_AutonomousProxy인 경우
이 엔진의 인스턴스가 이 Actor를 원격접속으로 다시 리플리케이트하는 역할을 담당합니다.
- 서버만 Role == ROLE_Authority 이고 RemoteRole == ROLE_SimulatedProxy 또는 ROLE_AutonomousProxy 로 보일 것입니다.
- ROLE_AutonomousProxy : 일반적으로 플레이어 컨트롤러가 소유한 액터에만 사용됩니다.
ROLE_Authority : 서버
ROLE_AutonomousProxy : 소유 클라이언트
ROLE_SimulatedProxy : 다른 클라이언트
Role/RemoteRole 반전
롤과 리모트 롤은 이 값을 누가 조사하는가에 따라 반대가 될 수 있습니다. 예를 들어 서버에서 환경설정이 이렇게 되어있다면:
- Role == ROLE_Authority
- RemoteRole == ROLE_SimulatedProxy
클라이언트에서는 이렇게 보일 것입니다:
- Role == ROLE_SimulatedProxy
- RemoteRole == ROLE_Authority
이는 정상인데, 액터를 담당하는 것은 서버이며, 이 액터를 클라이언트에 리플리케이트하기 때문입니다. 클라이언트는 그저 업데이트를 받아 그 업데이트 사이 액터에 시뮬레이션을 적용할 뿐입니다.
테스트 결과
GetLocalRole() == ROLE_Authority 일 때, 해당 캐릭터의 소유주와 서버에서 호출되었습니다.
void AShooterCharacter::EquipWeapon(AShooterWeapon* Weapon)
{
if (Weapon)
{
if (GetLocalRole() == ROLE_Authority)
{
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Blue, "EquipWeapon : " + this->GetOwner()->GetName());
SetCurrentWeapon(Weapon, CurrentWeapon);
}
else
{
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Red, "EquipWeapon : " + this->GetOwner()->GetName());
ServerEquipWeapon(Weapon);
}
}
}
- 클라이언트에서 무기 교환시 클라이언트(로컬플레이어)와 서버에서 작동이 되었습니다.
리플리케이션 모드
서버는 업데이트마다 액터를 리플리케이트하지 않습니다. 그러면 대역폭과 CPU 리소스를 너무 많이 잡아먹을 것입니다. 그래서 서버는 AActor::NetUpdateFrequency 프로퍼티에 정의된 빈도로 액터를 리플리케이트합니다.
클라이언트에서 액터 업데이트 사이에 약간의 시간이 흐른다는 뜻입니다. 이때문에 액터의 동작이 간헐적으로 끊어져 보일 수가 있습니다. 그에 대한 보정을 위해, 클라이언트는 업데이트 사이에 액터 시뮬레이션을 적용합니다.
현재 발생하는 시뮬레이션 유형은 두 가지입니다.
`ROLE_SimulatedProxy`
SimulatedProxy는 표준 시뮬레이션 방법이며, 일반적으로 마지막 알려진 속도에 따라 움직임을 외삽하는 것을 기반으로 합니다. 서버가 특정 액터에 대한 업데이트를 전송할 때, 클라이언트는 그 위치를 새로운 위치쪽으로 조정한 다음, 업데이트 사이마다 클라이언트는 서버에서 전송된 최근 속도에 따라 액터를 계속해서 움직입니다.
마지막 알려진 속도를 사용한 시뮬레이션은 일반적인 시뮬레이션 작동방식의 한 예제일 뿐입니다. 얼마든지 서버 업데이트 사이에 다른 정보를 사용한 외삽법 커스텀 코드를 작성해도 됩니다.
`ROLE_AutonomousProxy`
Autonomous Proxy는 보통 플레이어 컨트롤러에 빙의된 액터에만 사용됩니다. 이 액터는 사람 컨트롤러에서 입력을 받으므로, 외삽을 할 때 약간의 정보가 더 있으며, (마지막 알려진 속도를 기반으로 외삽하기 보다는) 실제 사람 입력을 사용하여 빠진 정보를 채울 수 있다는 뜻입니다.
본문 링크
https://tayke.tistory.com/9
https://docs.unrealengine.com/4.26/ko/InteractiveExperiences/Networking/Actors/Components/
https://docs.unrealengine.com/4.26/ko/InteractiveExperiences/Networking/Actors/Properties/
https://docs.unrealengine.com/4.26/en-US/InteractiveExperiences/Networking/Actors/RPCs/
https://docs.unrealengine.com/4.26/en-US/InteractiveExperiences/Networking/Actors/Roles/
'Game Programming > Unreal' 카테고리의 다른 글
[Unreal] TSet (0) | 2022.12.28 |
---|---|
[Unreal] UFUNCTION 함수 지정자 (0) | 2022.06.28 |
[Unreal] 유용한 언리얼 함수 (0) | 2022.06.07 |
[Unreal] 언리얼 아키텍처 (0) | 2022.06.04 |
Unreal - Zombie FPS Game Portfolio (0) | 2022.05.06 |