[이득우 네트워크 멀티플레이] 01. 언리얼 네트워크 프레임워크 개요
에디터 설정
Late Join 버튼 활성화
Editor Preferences - Allow late joining 체크
Late Join 버튼을 이용하면 특정 시점 클라 접속을 확인할 수 있어서 좋을 것 같다.
언리얼에서 네트워크 상태로 게임에 접속하는 또 다른 방법은 Net Mode - Play as Listen or Client인데
Net Mode 기능을 이용하면 모든 클라이언트가 동시에 서버에 접속하기 때문에
추가적인 클라이언트 난입 테스트를 체크하기 어렵다
만약 자신이 만드는 게임이 MMORPG류면 Late Join은 유용한 기능일 듯
네트워크 오버뷰
언리얼 네트워크 도큐먼트 : Networking Overview for Unreal Engine | Unreal Engine 5.4 Documentation | Epic Developer Community (epicgames.com)
언리얼 네트워크 게임도큐먼트 내용 중 중요하다고 생각한 것을 요약해 보면
서버 : 멀티플레이 게임이 실제로 진행됨. 정보 연산을 한 후 서버에 연결된 클라이언트에 정보를 복제함
클라이언트 : 서버에 정보 처리에 관한 신호를 보낼 수 있고 서버로부터 받은 정보를 기반으로 시뮬레이션을 진행함
'정보'라는 말에 유의해야 한다.
서버는 비주얼을 클라이언트 모니터에 직접 복제하지 않는다.
왜냐하면 비주얼을 복제하면 서버 대역폭의 부담이 크기 때문이다.
따라서 모든 정보를 복제하지 않고 필요한 정보만 복제해서 대역폭을 최소화해야 한다.
관련된 예를 들어보면 클라이언트 간 애니메이션 동기화가 있다
언리얼에서 애니메이션을 컨트롤하는 AnimInstance는 리플리케이트 관련 옵션이 없다.
때문에 AnimInstance의 상태 머신과 애님 그래프는 동기화되지 않는데 이건 언리얼이 의도한 것이다.
보통 AnimInstance에서 사용하는 변수는 폰의 정보를 참조한다.
예를 들어 W 버튼을 누르면 Pawn의 FrontDir = 1이 되고, AnimInstance의 FrontDir이 Pawn의 FrontDir을 추적해 값을 업데이트한다.
그리고 FrontDir이 1일 경우 Walk 애니메이션 , 0일 경우 Idle 애니메이션을 출력하는 게 일반적이다.
이럴 경우 Pawn의 FrontDir 정보를 복제하면 된다.
Pawn의 FrontDir 데이터가 복제되면 AnimInstance의 FrontDir도 업데이트되고
업데이된 FrontDir로 Walk, Idle 중 무슨 애니메이션을 출력할지 결정한다.
이와 관련된 코드를 적어봤다
// --------------- AnimInstance ------------------------------
void UCPP_A_PGCharacter::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
//이 AnimInstance를 사용하고 있는 Pawn의 정보를 가져온다
ACPP_PlayableCharacter* Character = Cast<ACPP_PlayableCharacter>(TryGetPawnOwner());
if (!IsValid(Character))
{
return;
}
// 캐릭터의 MoveForwardAxis로 AnimInstance의 MoveForwardAxis를 업데이트
MoveForwardAxis = Character->MoveForwardAxis;
MoveRightAxis = Character->MoveRightAxis;
}
// --------------- Pawn ------------------------------
// 변수 리플리케이트를 설정하는 가상 함수
// 즉 어떤 정보를 복제할지 결정하는 함수
void ACPP_PGCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 변수 MoveForwardAxis를 리플리케이트 한다. 즉 서버에서 MoveForwardAxis 정보가 변경되면,
// Server의 MoveForwardAxis 정보가 서버에 접속된 Client에 복제된다
DOREPLIFETIME(ACPP_PGCharacter, MoveForwardAxis);
}
//인풋 콜백에 바인드되는 함수
void ACPP_PGCharacter::SetMoveForwardAxis(float _Axis)
{
// 이 Pawn이 내 클라이언트가 제어하는 폰이라면
if (GetLocalRole() == ROLE_AutonomousProxy)
{
//움직임값 동일하면 서버에 전송하지 않는다
//무의미한 정보를 서버에 전송하지 않고 정보를 최소화하기 위해
if (MoveForwardAxis != _Axis)
{
//서버에 _Axis로 MoveForwardAxis를 변경하도록 신호를 보낸다
SetMoveForwardAxis_Server(_Axis);
}
}
MoveForwardAxis = _Axis;
}
// 서버에서'만'실행되는 함수
// 서버 MoveForwardAxis 의 변경하는 함수
void ACPP_PGCharacter::SetMoveForwardAxis_Server_Implementation(float _Axis)
{
MoveForwardAxis = _Axis;
}
결과
만약 자신이 멀티 플레이게임을 만든다면 어떤 정보를 복제할지, 복제할 정보량을 어떻게 최소화해야 할지 고민해야 한다
네트워크 프레임워크 구조
서버의 Onwer 액터가 각 클라에 Proxy 형태로 복제된다.
복제된 Proxy는 서버로부터 정보를 받아 행동을 시뮬레이션한다.
Proxy(허상)은 서버에서 클라로 복제된 액터를 말한다.
Proxy는 AutonomusProxy, SimulatedProxy로 구분할 수 있다.
AutonomusProxy : 클라이언트가 제어하는 Proxy로 보통 클라이언트의 컨트롤러에 빙의된 Proxy를 말한다
SimulatedProxy : 클라이언트가 제어하지 않는 Proxy로 오로지 서버로부터 받은 정보로 시뮬레이션된다. 보통 내 클라이언트에 있는 다른 클라이언트의 Proxy를 말한다
추후 포스팅에서 GetNetRole 함수를 이용해 액터의 Role을 확인해 보겠다
리플리케이션
위에서 언급한 서버에서 클라이언트 간 동일한 액터가 나오도록 하는 기능을 액터 리플리케이션이라 한다
언리얼 엔진의 콘텐츠는 사실상 모두 액터기 때문에 액터 리플리케이션은 매우 큰 개념이라고 할 수 있다
액터 리플리케이션 세분화하면 RPC , 프로퍼티 리플리케이션이 있다
RPC : 네트워크를 사용해서 즉각적으로 서버와 클라 간 명령을 주고받아야 하는 경우 사용하는 함수
예시) : 움직임 리플리케이션(캐릭터 무브먼트 컴포넌트) : 캐릭터의 움직임에 관한 정보는 RPC에 의해 동기화된다
RPC를 이용해 통신하는 코드는 다음과 같다
// Pawn의 .h
UFUNCTION(BlueprintCallable, Server, Unreliable)
void SetMoveForwardAxis_Server(float _Axis);
// .cpp
void ACPP_PGCharacter::SetMoveForwardAxis_Server_Implementation(float _Axis)
{
MoveForwardAxis = _Axis;
}
클라에서 SetMoveForwardAxis_Server란 RPC를 호출하면 즉각적으로 서버에서 SetMoveForwardAxis_Server가 호출된다.
이런 방식으로 서버와 클라가 통신할 수 있다
프로퍼티 리플리케이션 : RPC에 비해서는 반응속도가 느리지만 게임을 구성하는 중요한 변화된 속성을 확실하게 전달받고 싶을 때 사용하는 기능
> 게임플레이에서 중요한 기능을 담당한다. 정보를 최적화해서 전달하는 것이 중요하다
내가 원하는 변수에 프로퍼티 리플리케이션을 설정할 수 있다.
프로퍼티 리플리케이션을 설정하면 서버에서 변수 값이 변화되면, 변화된 값을 클라에게 전달한다
'
프로퍼티 리플리케이션을 설정하는 코드는 다음과 같다
//------------------------- Pawn .h -------------------------
//프로퍼티 리플리케이션을 사용하기 위해 UPROPERTY 매크로에서 Replicated 프로퍼티 지정자를 넣어주자
UPROPERTY(Replicated, VisibleDefaultsOnly, BlueprintReadWrite, Category = "Movement")
float MoveForwardAxis = 0;
//------------------------- Pawn .cpp -------------------------
// 프로퍼티 리플리케이션을 설정하는 함수
// 즉 어떤 정보를 복제할지 결정하는 함수
void ACPP_PGCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 변수 MoveForwardAxis를 리플리케이트 한다. 즉 서버에서 MoveForwardAxis 정보가 변경되면,
// Server의 MoveForwardAxis 정보가 서버에 접속된 Client에 복제된다
DOREPLIFETIME(ACPP_PGCharacter, MoveForwardAxis);
}
네트워크에서 블루프린트가 아닌 C++을 이용하는 이유
결론부터 말하면 블루프린트는 가능하지만 C++는 가능한 일이 있기 때문이다
1. 언리얼에서 제공하는 네트워킹 가상 함수를 C++는 모두 이용이 가능하고 오버라이드 하면서 커스터마이징이 가능하지만 블루프린트에서는 오버라이드가 불가능한 함수가 있다
2. 프로퍼티 리플리케이션을 사용할 때 C++ 만 콜백 바인딩이 가능하다.
프로퍼티 리플리케이션에 콜백 함수를 바인딩하면, 서버에서 클라이언트로 변화된 값이 복제될 때 바인딩 된 함수가 호출된다. (즉 클라이언트에서만 콜백 함수가 호출된다)
콜백 바인딩 기능은 네트 컬 디스턴스를 고려하는 게임을 제작할 때 유용하다.
예를 들어 클라이언트의 네트 컬 디스턴스 범위 밖에 있는 액터가 범위 안에 들어왔을 때 처리해야 하는 이벤트를 바인딩할 수 있다.
프로퍼티 리플리케이션에 콜백 바인딩을 한 코드 예제
-------------------------------- Pawn .h ----------------------------
//프로퍼티 지정자로 ReplicatedUsing = 함수이름 사용한다
UPROPERTY(ReplicatedUsing=OnReq_HPChanged, EditAnywhere, BlueprintReadWrite, Category = Status, meta = (AllowPrivateAccess = "true"))
float HP = 100.f;
//콜백에 바인딩되는 함수는 UFUNCTION() 매크로가 있어야 한다
UFUNCTION()
void OnReq_HPChanged();
-------------------------------- Pawn .cpp ----------------------------
//서버에서 HP의 변화된 값이 클라이언트로 복제될 때 이 함수가 호출된다
void ACPP_PlayerBase::OnReq_HPChanged()
{
if (HP <= 0.f)
{
DeadEvent_Blueprint();
DeadEvent();
}
}
void ACPP_PlayerBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ACPP_PlayerBase, HP);
}
3. C++에서만 조건식 프로퍼티 리플리케이션이 가능하다
참고 : 조건식 프로퍼티 리플리케이션 | 언리얼 엔진 4.27 문서 | Epic Developer Community (epicgames.com)
조건식 프로퍼티 리플리케이션을 이용해서 프로퍼티가 리플리케이트 되는 조건을 설정할 수 있다