This tutorial shows how to create and use Remote Procedure Calls (RPCs) to trigger actions across machines. You will define an RPC function, register it, and call it from a client to execute on the server and vice versa.
A custom component inheriting from plNetworkComponent.
What Are RPCs?
RPCs let you call a function on one machine and have it execute on another. Common examples:
Client tells the server "I want to fire my weapon" (Client -> Server).
Server tells all clients "Player X was eliminated" (Server -> All Clients).
Server tells one client "Your inventory updated" (Server -> Owner).
Step 1: Define the RPC Function
Create a component with the RPC function. The function must be a regular C++ method:
// MyWeaponComponent.h
class MyWeaponComponent : public plNetworkComponent
{
PL_DECLARE_COMPONENT_TYPE(MyWeaponComponent, plNetworkComponent, MyWeaponComponentManager);
public:
// Called on the server when a client wants to fire
void Server_Fire(plVec3 vDirection, float fSpread);
// Called on all clients when a hit occurs
void AllClients_OnHit(plVec3 vHitPosition, plUInt32 uiVictimID);
};
Step 2: Implement the Functions
// MyWeaponComponent.cpp
void MyWeaponComponent::Server_Fire(plVec3 vDirection, float fSpread)
{
// This runs on the server
plLog::Info("Client fired weapon! Direction: {}", vDirection);
// Perform server-side hit detection
// ...
// Notify all clients of the hit
plVariant args[] = { plVariant(hitPos), plVariant(victimID) };
m_pNetworkModule->GetRpcManager()->CallRpc(
this, "AllClients_OnHit",
plArrayPtr<plVariant>(args, 2));
}
void MyWeaponComponent::AllClients_OnHit(plVec3 vHitPosition, plUInt32 uiVictimID)
{
// This runs on all clients (and server)
// Spawn hit effect, play sound, update UI
plLog::Info("Hit at {} on player {}", vHitPosition, uiVictimID);
}
Step 3: Register in Reflection and Register RPCs
Expose the functions via PL_FUNCTION_PROPERTY in the reflection block, then register them as RPCs at runtime in OnSimulationStarted():
PL_BEGIN_COMPONENT_TYPE(MyWeaponComponent, 1, plComponentMode::Static)
{
PL_BEGIN_FUNCTIONS
{
PL_FUNCTION_PROPERTY(Server_Fire, In, "Direction", In, "Spread"),
PL_FUNCTION_PROPERTY(AllClients_OnHit, In, "HitPosition", In, "VictimID"),
}
PL_END_FUNCTIONS;
}
PL_END_COMPONENT_TYPE
void MyWeaponComponent::OnSimulationStarted()
{
plNetworkComponent::OnSimulationStarted();
auto* pRpc = m_pNetworkModule->GetRpcManager();
pRpc->RegisterRpc(GetDynamicRTTI(), "Server_Fire",
plNetworkRpcTarget::Server, plNetworkRpcReliability::ReliableOrdered);
pRpc->RegisterRpc(GetDynamicRTTI(), "AllClients_OnHit",
plNetworkRpcTarget::AllClientsAndServer, plNetworkRpcReliability::ReliableOrdered);
}
Step 4: Call the RPC
From the client's input handler (or any code that runs on the owning machine):
void MyWeaponComponent::OnFireInput()
{
if (!m_pNetworkModule || !IsLocallyOwned())
return;
plVec3 direction = GetOwner()->GetGlobalDirForwards();
float spread = 0.05f;
// Using the convenience macro
PL_CALL_RPC(this, Server_Fire, direction, spread);
}