posted by Jeremy Friesner on Mon 12th Aug 2002 20:52 UTC

"The FRCGameStateSession class"
If FRCPlayerSession objects server as the interface between the server and each client, then the FRCGameStateSession object is the interface between the server and the game itself. All the actual game logic takes place inside the FRCGameStateSession; the FRCPlayerSessions, the TCP connections to the client computers, and the FoxRabbitCarrot programs running on the client computers can all be thought of as nothing more than elaborate I/O devices for this session. This is truly the nerve center of the game.

That said, the FRCGameStateSession is structured very similarly to the FRCPlayerSession (they both derive from the same base class, after all). Here is the declaration of the FRCGameStateSession class, from frc/server/FRCGameStateSession.h:


class FRCGameStateSession : public StorageReflectSession
{
public:
   FRCGameStateSession(const Message & prefs);
   virtual ~FRCGameStateSession();

   virtual status_t AttachedToServer();

   virtual void MessageReceivedFromSession(AbstractReflectSession & from, MessageRef msgRef, v
oid * userData);

   virtual void Pulse(uint64 now, uint64 schedTime);
   virtual uint64 GetPulseTime(uint64 now, uint64 prevResult);
[...]
};             

In the AttachedToServer method(), the FRCGameStateSession does some basic setup. One of the MUSCLE database nodes it's going to publish is the node representing the Game Board itself (not the pieces, mind you, just the board). The FoxRabbitCarrot client programs will all use the MUSCLE database-subscription facility to "watch" this node, so that if it ever changes (i.e. the person running the server decides they want a 16x16 board instead of an 8x8 board), they can all update their GUIs to match it.


status_t FRCGameStateSession :: AttachedToServer()
{
   if (StorageReflectSession::AttachedToServer() != B_NO_ERROR) return B_ERROR;

   MessageRef gameBoardRef = GetMessageFromPool();
   if (_gameState.GetBoard().SaveToArchive(*gameBoardRef()) == B_NO_ERROR) 
   {
      return SetDataNode("frc/board", gameBoardRef);
   }
   else return B_ERROR;
}

As you can see, the publishing of the GameBoard's state into the MUSCLE database takes several steps. First, a Message is allocated from the Message pool. Then, the GameBoard object's SaveToArchive() method is called, which dumps the state of the GameBoard object into the Message. Then, SetDataNode() is called. SetDataNode places the Message into the MUSCLE database tree, as a grandchild of the FRCGameStateSession's "home node" in the tree. I like to use pre-defined constants to build my database paths up, because it eliminates the chances of typos -- that's why the first argument to SetDataNode() looks so strange. Once it's gone through the C preprocessor, however, that argument is equivalent to "frc/board". So the fully qualified path name of our GameBoard node in the MUSCLE database (assuming that the FRCGameStateSession is session 0) will be: "//0/frc/board". (the first node name in the path is "" instead of an IP address, because the FRCGameStateSession has no associated client).

There is, of course, no MessageReceivedFromGateway() method defined for the FRCGameStateSession, for the simple reason that the FRCGameStateSession has no client associated with it, and thus will never receive Messages from its client. So overriding that method would be pointless.

The MessageReceivedFromSession() method is where most of the action occurs. You'll recall that this method is called whenever another session passes a Message to the FRCGameStateSession. Since this method is the only way that FRCPlayerSessions can interact with the FRCGameStateSession, this method handles all the game players' move requests and other input. Here is an abridged listing of this method's contents:


void
FRCGameStateSession :: MessageReceivedFromSession(AbstractReflectSession & from, MessageRef msgRef, void *)
{
   const Message * msg = msgRef();
   if (msg)
   {
      uint32 fromSessionID = (uint32) atol(from.GetSessionIDString());  // get the session ID of the session who passed us this Message
      switch(msg->what)
      {
[...]
         case FRC_COMMAND_ENTER_GAME:
            _gameState.PlayerWantsToEnter(fromSessionID);  // note that this client wants to start playing
 
            // If there weren't already any players already playing, force a call to GetPulseTime()
            // so that the new turn will start immediately.  It's silly to wait for (no players)!
            if (_gameState.GetPlayers().GetNumItems() == 0) InvalidatePulseTime(true);
         break;

         case FRC_COMMAND_EXIT_GAME:
            _gameState.PlayerWantsToExit(fromSessionID);  // note that this client wants out of the game
         break;

         case FRC_COMMAND_PROPOSE_MOVES:
         {
            // User specified a move he would like to do.  Check to make sure it's a legal
            // move, and if it is, put it into our list of moves-to-be-done-when-the-turn-is-over
[...]

As you can see from the above code snippet, this method first identifies which other Session passed it the Message, and then looks at the Message to see which 'what' code it has. If it has one of the 'what' codes that were pre-defined to have a special meaning in our game, it switches to the proper block of code, where it takes the appropriate action, such as adding a player to the game, or updating the game's state to reflect the player's move. The actual game logic is contained inside the _gameState member variable, and since it isn't really MUSCLE-specific, I won't go into it here.

The FRCGameStateSession also overrides two "hook" methods that the FRCPlayerSession didn't need to: these are the Pulse() and GetPulseTime() methods. If you remember the FoxRabbitCarrot game description back on page two, you'll recall that in FoxRabbitCarrot, each turn lasts a certain number of seconds, at which point all the pieces move at once. Because there is a time-dependent element in the game, the server can't be purely "reactive" -- that is, it can't be implemented entirely based on responding to the inputs it gets from its various clients' TCP streams. Instead, once every thirty seconds or so, it has to initiate an action "by itself" -- it has to move all the pieces and start the next turn. It has to do this whether or not it receives any Messages during that period, and so we need access to some sort of timer that will wake up the server to do it. Pulse() and GetPulseTime() provide just such a timer. They are fairly easy to use: you override GetPulseTime() to indicate when you would like Pulse() to be called... and then, at time you specified (or shortly thereafter), MUSCLE makes sure that Pulse() is called. Let's look at how FRCGameStateSession implements these two methods:


uint64 FRCGameStateSession :: GetPulseTime(uint64 now, uint64 prevResult)
{
   return now + ((prevResult == MUSCLE_TIME_NEVER) ? 0 : (_gameState.GetMaxSecondsPerTurn()*1000000));
}
    
void FRCGameStateSession :: Pulse(uint64, uint64)
{
   _readyToGo.Clear();

   // Run the sim for one turn.  This call also populates the two hashtables.
[...]
}

When dealing with time, MUSCLE foresakes such awkward structures as "struct timevals", in favor of unsigned 64-bit integers. These 64-bit integers represent time in microseconds (there are one million microseconds in a second). Specifying time this way gives us the accuracy we need for fine-grained timing (assuming the underlying OS can provide it, of course), as well as the convenience of being able to use standard arithmetic operators on the time values.

There are two arguments passed in to GetPulseTime() -- the first is the approximate current time, and the second argument is the last value our GetPulseTime() returned. If this call is the very first call that GetPulseTime() has had, then the second argument will be set to the special value MUSCLE_TIME_NEVER (which is equal to the largest uint64 there is).

Since we want Pulse() to be called at even 30-second intervals, we add 30 seconds (actually 30 million microseconds) to the current time and return that result. Since GetPulseTime() is called only when necessary (once at startup, and then once again after each call to Pulse()), this gives us the behaviour we want. There is one exception, though -- when the very first player joins the game, we want his pieces added to the board immediately; he shouldn't have to wait 30 seconds for the current turn (which nobody is playing in anyway) to finish. So in that case, we return (now), which causes Pulse() to be called immediately.

The Pulse() method implementation is straightforward -- it is called at the time specified in GetPulseTime(), and it handles the updating of the internal game state, then the updating of the relevant MUSCLE database nodes that represent the various pieces in the game. The MUSCLE database nodes are updated through calls to the standard SetDataNode() method, or, in the case of a node being removed from the database, by calling RemoveDataNodes(). Because all of the FoxRabbitCarrot clients have made MUSCLE subscriptions to these nodes, all the clients are automagically notified of the new state of the database (and hence, of the game) whenever these nodes are changed. When they get the database-update notification Messages, they all update their board displays accordingly, and all the players and spectators thus see the new state of the game.

Table of contents
  1. "Foxes, Rabbits, and Carrots"
  2. "The Game and How to Play it"
  3. "A Brief Review of the MUSCLE Networking Layer"
  4. "muscled, The basic MUSCLE server"
  5. "Customizing the MUSCLE server"
  6. "How to Set up the Custom Server Logic"
  7. "The FRCPlayerSession Class"
  8. "The FRCGameStateSession class"
  9. "Overview of the FRC server"
  10. "The FoxRabbitCarrot Client Program"
  11. "How the Client Handles Database Update Messages"
  12. "MUSCLE gaming Performance Issues"
e p (0)    8 Comment(s)

Technology White Papers

See More