4
\$\begingroup\$

I'm writing up a game server on which thread-safety is very important, basically for this approach I've decided to move my entire server in a single brain loop which handles everything in a single thread, connections are ran separately in their own separate thread, whenever a packet is received, the brain loop is signaled so it doesn't have to wait for the entire interval.

This is truly the best approach I could come up with, given my server is filled with pointers everywhere, and invalidation would kill my player base.

#define GAME_BEAT 50

void Game::brain() {
    int64_t connectionsTimeCounter = 0;
    int64_t decayTimeCounter = 0;
    int64_t releasePointersCounter = 0;
    int64_t creaturesTimeCounter = 0;
    int64_t roundNr = getServerMilliseconds();

    while (isGameRunning() || isGameStarting()) {
        gameSignal.wait(GAME_BEAT);

        int64_t delay = getServerMilliseconds() - roundNr;
        roundNr = getServerMilliseconds();

        // Receive packets from connections with data available to be read (non-blocking)
        // when data is available to be read, the connection which was previously ran in a different thread
        // will be signaled to read packet data and store it in a variable
        // say, a packet for "player talk" was sent, then the function below "moveCreatures" would handle this action
        g_server.receiveData();

        connectionsTimeCounter += delay;
        if (connectionsTimeCounter >= 1000) {
            // This function closes invalid connections/closed connections
            g_server.processConnections();
        }

        decayTimeCounter += delay;
        if (connectionsTimeCounter >= 1500) {
            decayItems(delay);
        }

        releasePointersCounter += delay;
        if (releasePointersCounter >= 1000) {
            // This function simply calls a "delete" to removed objects from the game
            releaseObjects();
        }

        creaturesTimeCounter += delay;
        if (creaturesTimeCounter >= 1000) {
            // Processes creatures idle status
            processCreatures(delay);
        }

        // Moves creatures, players, talking, moving, etc ...
        moveCreatures();

        // Sends all pending packets to be sent to active connections
        g_server.sendAllData();
    }
}

That is pretty much the only way I found for my server to be thread-safe, since I did experienced a few crashes because when connections sent a particular packet (say player move item), it would invalidate other connections from doing so

\$\endgroup\$
1
  • \$\begingroup\$ you mention that moveCreatures() doesn't just move creatures/players but makes them talk? anything else? is it worth reconsidering a different name for this method? like PeformNPCAction or PerformAction? etc.. \$\endgroup\$ – Mike Aug 25 '16 at 7:46
3
\$\begingroup\$
g_server.receiveData();

Does this actually handle all of the message parsing?

As in: Is it really necessary to handle the full netcode in the main thread?

At least unpacking and sorting all inputs to a thread safe queues (e.g. lockfree queue from boost) with unresolved, symbolic references should be easily possible inside the worker threads handling the connections.

Validation of conflicting inputs, or orphaned references can't be avoided in the main loop. But parsing the packages and preliminary filtering can.


releasePointersCounter += delay;
if (releasePointersCounter >= 1000) {
    // This function simply calls a "delete" to removed objects from the game
    releaseObjects();
}

Not using C++11 shared pointers with builtin reference counting? Respectively, any specific reason (like cyclic references) which would prevent these from working?


// Sends all pending packets to be sent to active connections
g_server.sendAllData();

I must assume the world state, respectively the list of updates is not buffered, so this reads from the live game state?

Writing out all changes to the game state to a transaction log (with all dangling pointers resolved, implemented as a list with a single producer and multiple non-consuming iterators), and then walking the transaction log for each active connection asynchronously removes this load from the main loop as well.

There is no good reason why this should be allowed to stall the main loop.


int64_t connectionsTimeCounter = 0;
int64_t decayTimeCounter = 0;
int64_t releasePointersCounter = 0;
int64_t creaturesTimeCounter = 0;

Why are these variables write-only, apart from the initial check when to run the corresponding logic first? Respectively why does the main loop wait a full second before starting any of the workers at all?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.