Quick implementation of Network for existing Game
There are 2 common choices of architecture for game network implementation:
- client-server
- peer-to-peer
What I did was some kind of hybrid of those two. I used Remote Procedure Calls (RPCs). In this post I will describe this technique giving some basic examples showing the role of server and actually giving it some tools to do it’s role as a server.
First, hybrid?
Client is the Server for logic management. But it is not a technical server. Client is written in C#, Server in Erlang. What Erlang Server does is just receive packets and sends them to other clients who has not sent those. In future it will be doing a vast variety of things. That states for client-server architecture.
Every client could by a Server who decides the game state (which is a synchronisation between clients). Because of RPC, client calls procedures on other clients but through logic server which is client… and that states for part of peer-to-peer.
Show me your Hybrid
Let’s consider such function in a former form:
void destroyUnit(Unit unit) { ... }
Now, let me modify it a little. Let’s state that every unit’s ID is a unique value in entire game and is known to all players (clients). So Player 1 don’t have a unit with any same ID as Player 2, 3 and so on.
Let’s continue:
void destroyUnit(int unitId) { ... }
What did I change? I made possible to call the function from outside. Server decides which unit dies and can tell about it to all of players. Actually, server calls this function and tells other players to call the same function. But in the other hand, players can’t decide to call this function on their own.
So, my implementation looks like this, let’s say it’s in UnitManager:
void destroyUnit(int unitId, RPCState rpc) {
if (rpc.sendRPC) {
// Call RPC if you are Server
// and if function caller did not made this call already.
NetworkManager.CallOtherPlayersRPC("destroyUnit", unitId);
}
if (!rpc.callInternals) {
// Ignore this call for clients
// OR for function which called this function
// and already have made RPC call.
return;
}
/*
* TODO: our internal code of destroying unit in the logic and graphics
*/
}
void destroyUnit(int unitId) {
destroyUnit(unitId, NetworkManager.GetRPCState(false));
}
In your NetworkManager write such function:
[RPC]
void destroyUnit(int unitId) {
UnitManager.destroyUnit(unitId, NetworkManager.GetRPCState(true));
}
Of course this call could be done through Reflection Mechanism but it’s easier to debug like this, actually. So I recommend to implement RPC receiver functions for every message/RPC call type.
What is important - look at two calls of GetRPCState(bool isMessageFromNetwork). Thanks to this our first function knows whether to send Remote Procedure Call and whether to call internal behaviour right now or wait for RPC to be sent from server (to call this internal behaviour then). Calling internal behaviour has to have proper Unit object which could be a little hard to send over the network and make some incosistences due to references, so that’s why the first argument is unitId.
public class RPCState {
public bool sendRPC;
public bool callInternals;
public RPCState() {
}
public void setup(bool isConnected, bool isServer, bool isClient, bool isMessageFromNetwork) {
sendRPC = isConnected && isServer;
callInternals = !isConnected || isServer || (isClient && isMessageFromNetwork);
}
public RPCState DontDuplicateCall() {
sendRPC = false;
return this;
}
}
Note about isConnected argument: if I test the game without connecting anywhere I want to always call my internal behaviours, no matter whether I am a client or server. Also being offline means no sending RPCs.
Don’t duplicate call
That’ll be the last example describing all of this technique. Let’s consider that we have a bomb in game which destroys many units. Because of unideal position synchronisation on clients, there’s a possiblity (actually, it happens pretty often) that some clients will see 4 killed units and some 5 killed units. It cannot be! To prevent that we have Server of course. Server decides: kill this and this, and this unit. And this one, too. Let’s see:
void bombUnits(IList<int> unitsIdsToKill) {
// TODO implement some bombing ANIMATION :P and then...
foreach (int unitId in unitsIdsToKill) {
destroyUnit(unitId);
}
}
Server calls bombUnits(unitsIdsToKill) function and he sees bombing animation but no one more does it. destroyUnit**(unitId)** sends RPCs to other players but only about destroying units and not bombing them… Let’s modify it to send bombing RPC!
void bombUnits(IList<int> unitsIdsToKill, RPCState rpc) {
if (rpc.sendRPC) {
NetworkManager.CallOtherPlayersRPC("bombUnits", unitsIdsToKill);
}
if (!rpc.callInternals) {
return;
}
// TODO implement some explosion ANIMATION :P and then...
foreach (int unitId in unitsIdsToKill) {
destroyUnit(unitId);
}
}
// Note: this functions is called by Server only
void bombUnits(IList<int> unitsIdsToKill) {
bombUnits(unitsIdsToKill, NetworkManager.GetRPCState(false));
}
You see, it’s cool. But there’s a problem.. Let’s see… there’s being send an RPC bombing units… and wait, there’s more! We’re not sending RPCs for destroying units.
Let’ see how this would work:
foreach (int unitId in unitsIdsToKill) {
destroyUnit(unitId, rpc); //<-- given RPCState object
}
Almost. Now every unit dies twice because every client gets info about bombing units and destroying them. Client should destroy unit based on bombing info. And that’s where all RPCState.DontDuplicateCall() function comes in handy:
foreach (int unitId in unitsIdsToKill) {
destroyUnit(unitId, rpc.DontDuplicateCall());
}
I hope it’s simple enough to figure out understanding of this util.
Client can send RPC too
When client wants to make some move/command and tell about it other players, he can simply send RPC. Just implement such function the same way as destroyUnit() but instead of sending RPC to other clients… send it to the server, check and modifty it in there and send from server to other players to call same or another function implementing the move/command.
Summary
All this implementation appeared at the moment when I have started implementing a network functionality into ready game prototype so I didn’t want to change all the code around at the time. The other cause was that a colleague sitting near to me was still changing code for unit behaviours and such.
The basic question about such technique is simple - is it production-ready? It depends on architecture. Also, although it seems to work best in turn-based logic, I did it in two different realtime game prototypes (one is much more realtime than the other). For best result it’s sure not enough to just send commands over the network. There has to exist some kind of game objects position synchronisation based on time and some predictions. Also in this form it’s still not ready for anti-cheating mechanisms.
So, to sum up, it’s a good and fast technique for lagless network prototyping. It’s also fairly easy to implement into the existing game.
Implementation was made in C# and Unity3D but I have done it in pure Java (using Reflection), too.
Resources
Here are some of my inspirations: