개발 배경
MMORPG 팀 프로젝트 기획 중 서버에서 혼자 구현하지 못할 만큼의 컨텐츠를 기획하게 되었다...
이 상황을 해결하기 위해 고민하던 중 포톤에서 사용하던 RPC방식이 떠올렸다.
RPC는 네트워크에서 복제된 객체에 대해 원격으로 함수를 호출해 서버에서 따로 로직을 구현하지 않고 클라이언트에서 구현하므로 개발 속도가 빨라지기 때문이다.
구현 이론
RPC를 구현하기 위해선 다음 정보를 직렬화해야 한다.
- 메서드 식별용 아이디
- 브로드캐스팅 타깃
- RPC 호출자 아이디
- 메서드 인수 목록
클라이언트에서 RPC를 호출하면 데이터를 직렬화시켜 서버에게 보내고, 서버는 타깃에게 이 직렬화 데이터를 브로드캐스팅 하게 된다. 그 후 클라이언트에서 RPC 데이터를 받게 된다면 메서드 식별 아이디와 메서드 인수 목록을 역직렬화 시켜 호출하게 된다.
호출자 아이디가 자신이라면 내가 호출한 RPC이고, 아니라면 남이 호출한 RPC임을 알 수 있다.
본격 구현
첫번째로, 메서드 식별용 아이디를 만들어 보자. 나는 RPC 식별 아이디를 정수형 15자리의 비트로 만들어 줬다.
왜냐하면 RPC와 메세지 시스템을 같이 사용할 예정이기 때문에 상위 1비트를 통해 RPC인지, 메세지인지 판별하기 위함이다.
나는 매크로를 사용해 메서드에 RPC아이디를 부여해줄 것이다. 아래 매크로는 메서드 이름을 받아와
헬퍼 메서드를 만들어준다. 헬퍼 메서드는 메서드를 RpcView클래스에 등록하고 메서드 식별 아이디를 리턴한다.
#define THIS_CLASS std::remove_pointer<decltype(this)>::type /* 내 클래스 타입 얻어오기 */
#define RPC(Func) _RPC##Func() /* 헬퍼 메서드 */
#define RPC_FUNCTION(func) uint16 RPC(Func)\
{\
static auto RpcId = RpcView::Register(&THIS_CLASS::func);\
return RpcId;\
}\
RpcView클래스는 RPC 메서드를 관리하는 정적 클래스이다. Register를 호출하면 메서드가 RPC로 등록이 되고, 메서드 식별 아이디가 리턴된다.
template<class T, class... Args>
static uint16 Register(void(T::*RpcFunc)(Args...))
{
auto RpcObject = new Rpc<T, Args...>(RpcFunc, ++RpcId); // 새로운 Rpc 객체 생성
RpcInterface Interface // 템플릿 타입 단순화용 객체
{
.Owner = RpcObject, // 템플릿 객체를 담기 위한 void*
.Execute = std::bind(&Rpc<T, Args...>::Execute, RpcObject, std::placeholders::_1)
};
RpcFuncTable.Add(RpcId, Interface); // Rpc 함수 등록
return RpcId; // RPC 아이디 리턴
}
그 다음으로 메서드 아이디, 브로드캐스팅 타입, RPC 호출자, 그리고 인수 목록을 캡슐화 시킨 클래스인 Rpc클래스를 구현했다.
이 클래스는 템플릿으로 구현되어 있고, 주 역할은 인수를 직렬/역직렬화 시키고 메서드를 호출하는 역할을 한다.
Register를 호출될 때 Rpc객체가 생성되고, RpcView는 이 Rpc객체를 저장하고 있게 된다.
template<class T, class... Args> requires std::is_base_of_v<NetObject, T> // C++20's Concept
class Rpc : Packet // 직렬화 & 메세지 전송가능 클래스
{
public:
Rpc(void(T::*RpcFunc)(Args...), uint16 Id) : Packet(Id, RPC), RpcFunc(RpcFunc)
{
}
public:
void Execute(std::span<char> Buffer) // RPC 수신 후 호출
{
...
}
void WriteParameter(Args... ArgsList)
{
*this << static_cast<uint8>(Target) << CallerId; // Target, CallerId 직렬화
((*this << ArgsList), ...); // 인수 목록 직렬화
}
void ReadParameter(Args&... ArgsList)
{
*this >> reinterpret_cast<uint8&>(Target) >> CallerId;
((*this >> ArgsList), ...); // 인수 목록 역직렬화
Reset();
}
public:
void SetTarget(RpcTarget NotifyTarget) { ... }
void SetBuffer(std::span<char> Buffer) { ... }
void SetCaller(uint16 Id) { CallerId = Id; }
private:
uint64 CallerId; // 호출자 아이디
RpcTarget Target; // 타깃
void(T::*RpcFunc)(Args...); // 메서드 포인터
};
마지막으로 RPC를 호출하는 과정이다.
template<class T, class... Args>
static void Execute(void(T::*)(Args...), T* Owner, uint16 Id, RpcTarget Target, Args... ArgsList)
{
auto RpcObj = static_cast<Rpc<T, Args...>*>(RpcFuncTable[Id].Owner);
RpcObj->SetTarget(Target);
RpcObj->SetCaller(Owner->GetId());
RpcObj->WriteParameter(Forward<Args>(ArgsList)...);
SendRpc(reinterpret_cast<Packet*>(RpcObj));
}
#define CallRPC(Func, Target, ...) Execute(&THIS_CLASS::Func, this, RPC(Func), Target, __VA_ARGS__);
RpcView::CallRPC(메서드, 타깃, 인수 목록...)을 통해 호출하게 된다.
먼저 Execute메서드를 보면, 첫번째 인수로 메서드를 받고, 다음으로 소유자, 메서드 아이디, 타깃, 인수 목록을 받는다.
첫번째 인수인 메서드는 꺾쇠(<,>)없이 템플릿 타입을 결정하기 위한용도이고, 3번째로 받는 아이디를 통해 RPC 데이터를 보내게 된다.
RpcView::Execute의 인수는 너무 길기 때문에 단축하기 위해 CallRPC라는 매크로를 만들었다.
위에서 이름으로 헬퍼 메서드를 추출하고 리턴을 통해 아이디를 가져와 간편화 시켰다.
마지막으로
최종적으로 구현한 RPC의 예제를 보면 다음과 같다.
// PlayerCharacter.h
...
void CallRpcTest();
RPC_FUNCTION(TestRPC)
void TestRPC(int Data);
...
-------------------------------------------------------------------------------------
// PlayerCharacter.cpp
...
APlayerCharacter::APlayerCharacter()
{
...
RPC(TestRPC);
}
void APlayerCharacter::CallRpcTest()
{
RpcView::CallRPC(TestRPC, RpcTarget::All, 100);
}
void APlayerCharacter::TestRPC(int Data)
{
GEngine->AddOnScreenDebugMessage(-1, 5, FColor::White, FString::FromInt(Data));
}
...
BeginPlay이후에 CallRpcTest를 호출하면 TestRPC가 호출되는 것을 볼 수 있다.
'C++' 카테고리의 다른 글
[C++] vcpkg 라이브러리 패키징 (0) | 2024.08.07 |
---|---|
[C++] 제대로 알고 사용하자. atd::async (0) | 2024.07.13 |
[C++] C++에서 라이브러리를 관리해보자 (0) | 2024.06.27 |
[C++] 에러코드를 메세지로 format하기 (0) | 2023.11.17 |
[C++] 클래스 멤버 메소드를 함수포인터, std::function 객체로 변환하기 (0) | 2023.11.06 |