How do you separate networking / packet handling from gameplay layer?

Started by
2 comments, last by Kirlim 7 years, 2 months ago
I currently have a NetworkPublisher/INetworkSubscriber system, where classes can subscribe to packets by some ID (enum, const int, #define, etc) to receive a const RakNet::Packet* from which they can read the relevant data and pass it on to other systems.
The PacketCallback it requires can take a lambda, a member function, etc. The NetworkPublisher is updated by passing it all of the packets received since last frame, which it then passes out to all subscribers based on the first byte of the packet's data.

void ClientList::Subscribe(NetworkPublisher* a_networkPublisher)
{
    a_networkPublisher->Register(ID_NEW_INCOMING_CONNECTION, PacketCallback(&ClientList::OnClientConnected, this));
    a_networkPublisher->Register(ID_CONNECTION_LOST, PacketCallback(&ClientList::OnClientDisconnected, this));
    a_networkPublisher->Register(ID_DISCONNECTION_NOTIFICATION, PacketCallback(&ClientList::OnClientDisconnected, this));
}
I then have a system of StaticEvents to which anything can subscribe. This is ClientList::OnClientConnected firing off one of those events:

void ClientList::OnClientConnected(const RakNet::Packet* a_packet)
{
    auto id = m_nextClientID++;
    m_addresses.emplace(id, a_packet->systemAddress);


    m_connectedClients.push_back(NetClient(id));


    printf("Client connected: %s\n", a_packet->systemAddress.ToString(true));


    SystemEvents::ClientCreated.Invoke(id);
}
GamePacketHandler then acts as a mediator between these network-related layers and the GameFramework and its components:

GamePacketHandler::GamePacketHandler(GameFramework* a_gameFramework)
    : m_gameFramework(a_gameFramework)
{
    SystemEvents::ClientCreated.Subscribe([this](NetClient::ClientID a_clientID) {
        auto& player = m_gameFramework->GetPlayerList()->CreatePlayer(a_clientID);


        // send sync here for now
        RakNet::BitStream bs;
        bs.Write((unsigned char)EPacketType::SERVER_INITIAL_SYNC);
        bs.Write(player.GetID());
        bs.Write(999); // num other ships
        m_gameFramework->GetNetworkMessenger()->SendToPlayer(player.GetID(), bs, HIGH_PRIORITY, UNRELIABLE);
    });


    SystemEvents::ClientDestroyed.Subscribe([this](NetClient::ClientID a_clientID) {
        auto playerID = m_gameFramework->GetPlayerList()->ClientIDToPlayerID(a_clientID);
        m_gameFramework->GetPlayerList()->DestroyPlayer(playerID);
    });
}
What kind of layout do you use for this stuff? I don't want the packets and network system to be directly handled by gameplay, so this mediator + publisher/subscriber pattern seems to accomplish what I'm after right now. Not sure of how practical it will be as things can larger, although I could break the mediators out into a separate class per 'area' of the game so there's fewer 'god' classes. Still a bit interconnected, though, esp. in the GameFramework (which would need to hold each of these mediators and give them a pointer to itself).
Let me know your thoughts!
Advertisement

Typically, I find that I need two layers of dispatch for incoming messages:

1) Dispatch to the handling-subsystem. This separates things like "entity lifetime control" from "user input control" from "entity update control" from "text chat" from "physics events" and so forth.

2) For each kind of subsystem, a second layer of dispatch may be needed. User input control needs to know which user input stream it's adjusting. Entity update control needs to know both which entity is having properties updated, and which properties those are.

Note that the semantic translation is specific to each subsystem! The "text chat" subsystem doesn't know anything about "entity property update" type messages.

Each layer is a hash table of some sort, with interested components registering in that table for getting dispatch. For network messages, I don't need more than one target for messages. However, some subsystems may in turn broadcast the events -- a physics explosion at a particular location will affect many entities' local simulation state. Again, that broadcast is specific to the subsystem.

To make it easy for each subsystem to take their input layer and generate data structures, a nice serialization library that goes between structs/bytestreams with little muss/fuss is important. Typically, you'll end up with macros wrapping some templated functions, because macros can easily do things like extracting the names of things, without you having to repeat yourself. (Compile-time introspection is one of the missing features of C++!)

enum Bool { True, False, FileNotFound };

I disagree with the macro part (I hate the macro nightmares that some code bases develop), but everything else is what I've commonly done on all the networked games I've worked with.

Nearly every large game uses events for various systems to talk together. They go by various names, an event bus, event listeners, message bus, message broadcasters, message dispatchers, etc.

The network system generates events for various systems when the messages arrive, and the various systems can handle the events and data packets as they wish. For example a chat message may be picked up by the chat display,but also picked up by the audio system to play a sound, and by the system log. On the flip side, if you're careful about it the network system can listen for events that need to go across the wire.

Done very well, it means there is no difference in the gameplay for who generates the events. They could be generated from the human player, the network player, or an AI player, all the gameplay side cares about is that an event came through.

I'm doing my first multiplayer ever, so I'm kind of sure that I'll need to scrap my solution, but I'll post it here in case it might bring ideas to the discussion.

When a game message arrives, it is stored in a "received" channel until the network handling system can fetch it. It will then use the message type id to choose a handling class (i.e join game request handler, join game reply handler, etc). Every handling class has an execution method, and the dependencies they need to actually do their work are passed by constructor.

The handling classes themselves are initialized early on, and their state must not change with each message execution.

Pros (that I believe are pros):

- Objects have their methods called without needing to register to a listener or observer

- Changing the handling classes have little impact in the system

- I don't need to deal with possibilities like destroying an object while somehow failing to unregister it from a listener

Cons (that I'm quite sure, are cons):

- Constructing the handlers is hell. The dependencies can stretch quite far, and there are times when it is really hard to initialize since some classes (i.e explosions systems) won't exist during startup.

- Objects don't know where their calls come from, but the handler needs to know each one of the objects they'll reach.

This topic is closed to new replies.

Advertisement