RPC
가상함수테이블
Remote Procedure Call의 줄임말로, 함수를 호출하는 PC와 호출받은 함수가 실행되는 PC가 달라도 되게끔 해주는 통신기법
예를들어, 화면에 문자를 출력하는 함수를 서버에서 호출하면 해당 출력시키는 함수는 각 클라이언트들에서 실행되게끔도 해줌
게임에 큰 영향을 끼치지 않는 간단한 동작들(사운드, 파티클 등)에 주로 사용
즉, RPC를 사용하면 다른 PC에 있는 해당 함수를 즉시 실행시켜줌
void greet() { /* ... */ }
greet(); // call (직접 호출)
함수의 직접 호출보다는 실행하라는 동작 자체를 의미
함수나 메서드가 다른 함수의 인수로 전달되어 실행되거나, 레퍼런스, 델리게이트, 콜백 등을 통해 간접적으로 실행될 때 사용
void executeCallback(void (*callback)()) {
callback(); // invoke (간접 호출)
}
이전 TIL에서 공부했듯이, 서버에서 스폰되고 클라로 복제된 액터는 ClientConnction의 패밀리에 들어가야 서버와 송수신 가능
ClientConnection이 소유하는 PlayerController가 소유(Own)하는 액터들은 이 패밀리에 들어가게 되고, Client-owned actor임
즉, 클라에 존재하는 Autonomous Proxy인 액터 = Client-owned actor
그리고 이 액터들만 서버에 RPC 및 요청 가능
서버의 경우, 이러한 액터들(Remote Role이 Autonomous Proxy인 액터) 역시 Client-owned actor라고 함
ClientConnection패밀리에 없는 액터, 즉 특별한 클라이언트 Owner가 없이 서버에 스폰되고, 클라에 복제만 된 액터들
위에 해당하는 액터로, 서버, 클라에 존재하는 액터 둘 다 Server-owned actor라고 함
Server-owned actor는 Authority가 전적으로 서버가 가지므로, 서버에서 동기화만 당하고 서버로 요청을 보내는 것은 제한적
Unowned actor는 클라이언트와 서버 어느 쪽에서도 소유자가 명확히 지정되지 않은 액터
임시적으로 존재하긴 함
Server-owned actor와 동작자체는 동일하지만, 네트워크와는 연관성이 거의 없음

모든 PC에게 RPC를 실행시켜달라는 요청 (서버에서 호출해야 함)
클라에서 호출하면 자기만 실행됨 (밑에 총정리 사진에 다 나와있음)

서버의 Pawn의 NetMulticast RPC를 호출하면, 본인(서버)포함 모든 PC의 Pawn에서 해당 RPC 실행
마찬가지로 컨트롤러에서 똑같이 한다면, 서버 컨트롤러와 해당 컨트롤러를 가지고 있는 PC에서만 해당 RPC 실행됨
부하가 심해 자주사용하면 안 됨
클라이언트에서 Server RPC를 호출하면, 서버의 RPC가 실행됨
이때, 당연히 해당 RPC를 요청하는 액터는 Client-owned actor여야만 함
Dedicated Server의 경우 이 Server RPC를 무조건 수락하므로, _Validate()함수로 RPC요청을 수락할지 말지 결정함


Client-owned actor)
// CXPlayerController.h
UCLASS()
class CHATX_API ACXPlayerController : public APlayerController
{
// ... //
UFUNCTION(Client, Reliable) // Client RPC
void ClientRPCPrintChatMessageString(const FString& InChatMessageString);
UFUNCTION(Server, Reliable) // Server RPC
void ServerRPCPrintChatMessageString(const FString& InChatMessageString);
};
// CXPlayerController.cpp
#include "EngineUtils.h"
// 채팅 입력시 호출되는 함수
void ACXPlayerController::SetChatMessageString(const FString& InChatMessageString)
{
if (IsLocalController())
// 서버 RPC 호출
ServerRPCPrintChatMessageString(InChatMessageString);
}
// 모든 RPC들은 함수명 뒤에 _Implementation붙여서 구현부를 생성
void ACXPlayerController::ClientRPCPrintChatMessageString_Implementation(
const FString& InChatMessageString)
{
// 화면에 문자열 출력해주는 함수
PrintChatMessageString(InChatMessageString);
}
void ACXPlayerController::ServerRPCPrintChatMessageString_Implementation(
const FString& InChatMessageString)
{
// 서버에 존재하는 모든 컨트롤러 가져오기
for (TActorIterator<ACXPlayerController> It(GetWorld()); It; ++It) {
ACXPlayerController* CXPlayerController = *It;
// 클라이언트 RPC호출
if (IsValid(CXPlayerController))
CXPlayerController->ClientRPCPrintChatMessageString(InChatMessageString);
}
}
서버실행 로직은 무조건 동작하므로, RPC의 경우 이 매크로를 이용하여 신뢰성을 검증해야함
UFUNCTION과 함께 사용되는 매크로로, 이 매크로가 붙은 RPC는 _Implementation() 함수와 _Validate() 함수로 나뉘어 구현해야함
전자 함수는 실제 실행부분이고, 후자부는 검증부분으로 true이면 전자함수를 실행, false이면 drop함
UFUNCTION에 사용하는 매크로로, Reliable을 사용하면, 원격 PC에서 무조건 실행됨
호출이 누락되어 전송이 실패하면 재전송을 하여 무조건 실행됨을 보장해줌
데미지 로직, 스폰 등 중요한 게임 로직에 사용
아무것도 사용 안 하면 Unreliable이 기본 키워드로 동작하여 딱 한 번만 호출요청하고, 누락되면 재전송 안해 신뢰성이 없음
Unreliable 키워드를 통해 네트워크의 과부하를 줄임
사운드, 이펙트처럼 게임에 큰 영향 없는 로직에 사용
OOP의 특징중 하나인 다형성에는 오버라이딩과 오버로딩이 있었다
오버로딩의 경우, 컴파일타임에 함수가 결정되는 정적함수이지만, 오버라이딩은 런타임에 호출될 함수가 결정되는 동적 함수이다
그래서 컴파일러는 가상함수테이블을 이용하여 동적으로 함수를 호출한다
이 가상함수테이블은 컴파일러마다 조금 다르지만, 일반적으로 컴파일러가 메모리의 data부분에서도 rdata에 생성함.
이부분은 초기화된 전역변수와 static변수가 저장되는 data와 다름
data는 읽기/쓰기가 가능하지만 rdata는 읽기만 가능한 부분
vptr과 vtable을 직접 접근해 사용은 못 함. 우회하면 가능한 방법도 있으나 위험하므로 접근하지 말기
class Parent {
virtual void foo1() {}
void foo2() {}
virtual void foo3() {}
};
class Child : public Parent {
virtual void foo3() override {}
virtual void foo4() {}
};
컴파일타임에 컴파일러가 virtual키워드를 가지는 모든 클래스마다 가상함수테이블을 .rdata에 생성하고, 가상함수테이블을 가리키는 포인터변수를 클래스에 생성
당연히 부모클래스가 virtual을 가지면 자식도 물려받으니까 자식에게도 해당.
vptr은 객체내의 메모리에서 가장 앞에 위치


순수가상함수의 경우는 컴파일러마다 다르지만, NULL을 적기도하고, 아니면 _purecall같은 에러핸들러의 주소를 저장
자식클래스도 부모클래스의 가상함수테이블을 기본으로하여 만듦
만약, 자식클래스에서 오버라이딩을 했다면, 오버라이딩한 해당 가상함수포인터만 오버라이딩한 함수포인터로 저장

vtable 주소로 초기화됨.vtable포인터 변수를 가짐. 부모, 자식 둘 다 가상함수를 가져도 마찬가지. 근데, 다중상속의 경우는 vtable을 가지는 부모 클래스 수만큼 vptr을 가짐
vptr에 접근하고, 해당 가상함수가 선언된 순서로 인덱스로 접근하여 가상함수포인터를 읽고 메모리에 접근하게 된다Parent* myClass = new Child;
myClass->foo3();
// myClass의 vptr은 Child의 vtable을 가리킴
// foo3은 2번째이므로, vptr + 1의 위치에(64bit니깐 8byte) 있는 함수포인터에 접근