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

"The FoxRabbitCarrot Client Program"
So far we've discussed the server side of the FoxRabbitCarrot game system; now it is time to take a look at the client. Unlike the server program, which can run quietly in the background or in a shell window, the client program requires a GUI in order for most people to enjoy using it. For FoxRabbitCarrot, I chose to use TrollTech's Qt GUI Toolkit to create my client GUI. I chose Qt for several reasons: First, because I am familiar with Qt and have used in previous projects. Second, because MUSCLE already contains support classes that make it easy to integrate MUSCLE and Qt together into a single program. And third, because Qt allows me to generate executables for Windows, MacOS/X and Linux from a single set of source code, so that I don't have to spend time writing separate code for each operating system. Of course there is nothing in MUSCLE that requires you to use any particular GUI toolkit (or any GUI at all, for that matter). But if you are looking for a cross-platform GUI toolkit to use, I highly recommend Qt.

To start off, let's look at the class MUSCLE provides to interface with Qt. This class, called QMessageTransceiverThread, handles connecting to the server, sends and receives Messages in a separate thread (so that the Qt GUI won't lock up), and uses Qt's signals-and-slots notification mechanism to signal the GUI whenever a Message has been received from the server.

Since you may not be familiar with signals-and-slots, I'll describe them briefly here. Qt uses some special preprocessor magic to implement a new language feature on top of C++: by adding special tags to the class declaration of any class that subclasses QObject, you can outfit that class with "signals", "slots", or both. Signals and slots are similar to normal C++ methods, but with an added feature: you can use the QObject::connect() function at run time to connect any "signal" method to any "slot" method you choose. The owner of a signal can then "emit" the signal at any time, causing all the "slot" methods that are connected to that signal to be called. Qt handles the mechanics of the calls internally, and is flexible enough to do things like dropping extra arguments from the signal method if they are not required in the slot method. Because any signal can be connected to any slot, and because multiple signals can be connected to a single slot (or, alternatively, a single signal can be connected to multiple slots at once), this provides a high degree of flexibility and convenience in structuring Qt GUI code.

The QMessageTransceiverThread features over a dozen signals that are emitted to signal that occurrence of various network events, but there are three signals that are the most commonly used:


   void SessionConnected(const String & sessionID);  
   void SessionDisconnected(const String & sessionID);
   void MessageReceived(MessageRef msg, const String & sessionID);

As you might have guessed, the first signal (SessionConnected()) is emitted when the TCP connection to the server has been successfully established. The second signal (SessionDisconnected()) is emitted when the TCP connection has been closed, or if the connection attempt failed. The third signal (MessageReceived()) is emitted whenever a Message is received from the server. These three signals are enough to let our client do everything it needs to do -- below is the code (from the FRCWindow constructor) that sets up the QMessageTransceiverThread object's connections:


FRCWindow :: FRCWindow()
{
[...]
   connect(&_mtt, SIGNAL(SessionConnected(const String &)), SLOT(ConnectedToServer()));
   connect(&_mtt, SIGNAL(SessionDisconnected(const String &)), SLOT(DisconnectedFromServer()));
   connect(&_mtt, SIGNAL(MessageReceived(MessageRef, const String &)), SLOT(MessageReceivedFromServer(MessageRef)));

As you can see, our QMessageTransceiverThread object's name is _mtt. _mtt is a member variable of the FRCWindow class. We use the Qt connect() command to connect _mtt's three signals mentioned above to three slots that are declared in the FRCWindow class. In this way, whenever the TCP connection is created or broken, and whenever a Message is received from the server, the appropriate FRCWindow method is automatically called to handle the event.

But, just connecting signals to slots isn't enough to get the connection going -- we need to tell the QMessageTransceiver where to connect to. So later on, in FRCWindow::ConnectToServer(), we have the following code execute:


   if (_mtt.StartInternalThread() == B_NO_ERROR)
   {
      if (_mtt.AddNewConnectSession(hostname, atoi(port)) == B_NO_ERROR)
      {
         _currentServerName = s;
         _isConnectingToServer = true;
         UpdateStatus();
         return;  // success!
      }
      QMessageBox::critical(this, "FRC", "Couldn't add connect session!");
   }
   QMessageBox::critical(this, "FRC", "Couldn't start networking thread!");   

The StartInternalThread() call spawns the internal networking thread that the QMessageTransceiverThread object uses to handle all the asynchronous networking tasks for us. Once that thread has been successfully started, we then call AddNewConnection() with the appropriate server hostname and port; this call instructs the QMessageTransceiverThread to begin connecting to the server. Since a TCP connection can take a significant amount of time to complete, AddNewConnectSession() does not wait for TCP connection to finish -- instead, it always returns immediately. Later on, when the TCP connection is complete, the SessionConnected() signal will be emitted, and therefore our FRCServer::ConnectedToServer() slot will be called. At that point, we will update the GUI to indicate that the client has connected to the server. If, on the other hand, the TCP connection fails (which could happen if the server is down, or the user's modem is turned off, or etc), then the SessionDisconnected() signal will be emitted, and so the FRCServer::DisconnectedFromServer() slot will be called, which will print out an appropriate error message for the user to see.

So now we have covered the basics of how the FoxRabbitCarrot client handles connecting and disconnecting from the server. Once connected, sending a Message to the server is trivial: All we have to do is call the QMessageTransceiverThread::SendMessageToSessions() method, and pass it a MessageRef containing the Message to send. The Message is then automatically queued up in an outgoing-messages-queue and sent to the server as quickly as possible. This is done, for example, whenever the user clicks and drags on one of his pieces to indicate that he wants that piece to move to an adjacent square:


   // From frc/client/FRCCanvas.cpp, in the CanvasMouseEvent() method
   MessageRef propMoveMsg = GetMessageFromPool(FRC_COMMAND_PROPOSE_MOVES);
   if (propMoveMsg())
   {
      propMoveMsg()->AddInt32("turn", _currentTurnNumber);
      MessageRef movePieceMsg = GetMessageFromPool();
      if (movePieceMsg())
      {
         movePieceMsg()->AddInt32("pos", _curMousePos[0]);
         movePieceMsg()->AddInt32("pos", _curMousePos[1]);

         char buf[64]; sprintf(buf, "%lu", key.GetPieceID());
         propMoveMsg()->AddMessage(buf, movePieceMsg);
         _mtt.SendMessageToSessions(propMoveMsg);

         gpc->SetProposedPosition(_curMousePos);
         PlaySound(_scheduleMoveSound);
      }
   }        

As you can see here, in order to tell the server that the user wants to move a piece, we allocate a FRC_COMMAND_PROPOSE_MOVES Message from the Message Pool, and then we add the various necessary data to the Message to describe the move: Which piece the user wants to move, which square he wants to move it to, etc. When all the necessary data is in the Message, we simply call SendMessageToSessions() on our QMessageTransceiverThread object, and off it goes. Note that we don't actually move the piece on the game board -- that won't happen until the end of the current turn, and even then it will only happen if the server allows the move and updates the MUSCLE node database to reflect the piece's new position.

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