Click here to Skip to main content
Click here to Skip to main content

Navy Battle Game - III

, 11 Aug 2005 CPOL
Rate this:
Please Sign up or sign in to vote.
An article on creating a multi-player game using the TGSDK.

Introduction

This article demonstrates how to use the multi-player online game development SDK (TGSDK), provided to third party developers, to implement the Navy Battle game. This article is inspired by Alex Cutovoi who wrote the original two part Navy Battle articles.

Background

At TrayGames we provide the APIs and a simple-to-use test harness that allow you do develop and test your multi-player game on your local computer without the need for an expensive multiple computer test system. When your game is ready to go you send it to us and we simply plug it into our network of hosting servers.

When I saw a C# implementation of the Navy Battle game was written, a game I like a lot, I immediately thought it would be a perfect game to port to the TGSDK. This SDK would allow us to reuse all of the game code from the original article, we only need to remove the socket and IO code and replace it with some TGSDK code. However in order to add functionality and fix some bugs a lot of the original game code had to be rewritten along with the changes required for the TGSDK. The nice part is that the APIs make the client-server system totally seamless and invisible to us. We don't have to do anything at all with TCP/IP or matching players, so we can focus on making an awesome game, so let's dig in!

Using the code

Both the downloadable demo and downloadable source archive files contain a "Game Demo" folder. This folder has the TrayGames libraries you need to build a game for the TGSDK. This folder also contains everything you need to try out the Navy Battle game. A TrayGames game server is actually just one or more DLL's built by a developer to go with their game. The main DLL (if there is more than one) is known as the "Game Daemon". To use it you need to run a tool called the "Daemon Harness" (TGDaemonHarness.exe), which you'll find in the "Game Demo" folder. When the server is running in its production environment on a TrayGames server, it has plenty of support services running with it to provide for the game matching and connections to the game clients. This set up is not practical for developers however, who often have only one computer to develop on. For this reason there is a special mode that the game daemon can run in called "Test Harness" mode within the Daemon Harness. To run a game daemon in this mode you need a game daemon initialization file. The location of this file needs to be in the same folder as the Daemon Harness. For this article the initialization file has been provided for the Navy Battle game in the "Game Demo" folder. In this file you will find the line:

  NumPlayers = 2

This entry serves the purpose of telling the server how many clients to wait for, before launching the game. If this file is not present and in the right place, the server thinks it is in a live-production environment, and looks for back end support systems that are not present on a developer's machine. No guarantee is made as to the server's behavior in this situation (though you are likely to get an Exception). Now you are ready to run the Daemon Harness. Run it from the "Game Demo" folder, click the "Load Game Daemon" button and navigate to the "NavyBattleDaemon.dll". Select the file and open it. The Daemon Harness should report that the game daemon was started in Test Harness mode, and the current time. Now we need to get a couple of games connected to this daemon.

We need to run two game clients, because it's a two player game. Run one instance of the Navy Battle game application, and then another. Upon running the second instance of the game the game daemon should realize it has enough players and proceed to start the game. Note that this setup works only across one machine in development mode. You can run one client inside the development environment to aid in debugging. It is not easy to debug a game daemon, but you can use DiagnosticMsg events to get feedback on what's going on. If you download the source, you can find a "NavyBattleGame.sln" solution file under the "Game Sample" folder that will build all of the projects mentioned in this article.

The client

We have a single class NavyBattleForm that contains the code for drawing the battlefield, game logic, and TGSDK calls. Since I will be focusing on the TGSDK, let's take a look at the steps needed to facilitate communication for a game:

  • Add a reference to the TGGameLib library and import the TG.Game namespace.
  • Declare and initialize a TGGameClass class member, and call its Start method.
  • Establish handlers for events that the game server will raise. These event handlers will be responsible for receiving and processing the game and system related messages.
  • Set a timer (or use some other mechanism) to check for incoming messages, which will raise events that must be handled.
  • Use the TGGameClass.SendToServer method to send game messages to the other player.

Let's take a look at these steps in detail now, I've tried to organize the code in #region blocks in the downloadable source code for clarity. The downloadable source archive contains the libraries that you will need to reference in your game project. First there is the import statement:

using TG.Game;

Then there are some necessary members we have to add to the Form class. These are the data members that are directly related to using the TGSDK. You'll see how these are actually used throughout this section.

private TG.Game.TGGameClass m_TgGame = 
                         new TG.Game.TGGameClass();
private System.Threading.AutoResetEvent m_NewData = 
           new System.Threading.AutoResetEvent(false);
private System.Windows.Forms.Timer m_Timer = 
                     new System.Windows.Forms.Timer();
private int m_WhoStarts = 0;
private int m_WhosTurn = -1;
private Int32 m_LocalPlayerIndex;
// stores both player names
private string[] m_PlayerNames = new string[2]; 
public static string m_GameName = "NavyBattle";

To establish the event handlers for the TGGameClass object you will handle the Load event on a Windows Form and add the following code to your handler method:

// Wire the appropriate handlers to their 
// incoming TrayGames message events.
// this first handler is responsible for receiving 
// and processing all 'normal' game messages
m_TgGame.GameEvent += 
    new TGGameClass.TGGameEventHandler(GameEventHandler);

// This handler is for all incomming administration events. 
// Administration events are differentiated by an
// AdminEventType, and include Connected, Disconnected, 
// Minimize, and Shutdown. 
m_TgGame.AdminEvent += 
   new TGGameClass.TGAdminEventHandler(AdminEventHandler);

// This handler is used to signal our main GUI 
// thread (this one) that new data has arrived. Be
// aware that the handler for this event is not running 
// in the Main thread, but rather in a 
// TrayGames Communications thread. 
m_TgGame.DataEvent += new EventHandler(DataEventHandler);

// Now set a timer going to check for incoming data
m_Timer.Interval = 200;
m_Timer.Tick += new EventHandler(TimerTickEventHandler);
m_Timer.Start();

// The handlers are set up so the TGGameClass 
// object is now properly initialized.
// We can call TgGame.Start which results 
// in a connection to the server.
m_TgGame.Start(8000, true); // 8K memQ

Once the handlers are set up the TGGameClass object is properly initialized, and we can call TGGameClass.Start to make a connection to the server. Establishing the connection upon loading the Windows Form will indicate that we are "Ready", we will send an "InitialPlacement" message later to indicate that all of our ships have been placed on the board so the play can start. Once the server knows that the client has started up successfully, it can send the AdminEventType.Connected message (more on this later) out to both game clients so that you can see the name of the person you're playing against, maybe for any chat messages that you might want to send while placing the pieces (for example telling your opponent to hurry up!). Although this version of Navy Battle doesn't support chat, the TGSDK makes it fairly easy to add in so maybe a future version will.

Now let's look at the event handlers. The first thing I want to point out is that most of the messages that the game client is going to handle are dictated by your game server. So the messages described here are specific to my implementation of the Navy Battle game, with a few exceptions. The TGSDK is completely flexible in terms of the messages that are passed and what the contents of those messages are. It doesn't care about what you want to pass for your game, it only provides the message passing mechanism.

The GameEventHandler method will receive game messages. Message information is always contained in a System.Collections.Hashtable class. The two messages that must be processed for this game are the Ready and Update messages. The Ready message is always sent by the TGSDK to all players once the required number of players, in this case 2, come online. This message will allow us to get our player number and name, whose turn it is, and other player data if needed. The Update message is a custom message we have created for this game.

private void GameEventHandler(object sender, GameEventArgs e)
{
  Hashtable data = e.Value;
 
  // The data received is encapsulated in a 
  // Hashtable, it is important to fully
  // appreciate the power of the hashtable 
  // to use TrayGames effectively.
  if (data.Contains("MsgType"))
  {
    string MsgType = (string)data["MsgType"];
    switch (MsgType)
    {
      case "Ready":
        m_LocalPlayerIndex = (int)data["YourPlayerNum"];
       
        for (int i = 0; i <= 1; i++)
          m_PlayerNames[i] = (string)data[i];

        m_WhosTurn = m_WhoStarts;
        break;
     
      // . . . 

We can expect an Update message every time our opponent makes a move. This message will contain the player, what move they have made, whose turn it is next and let us know if someone has won the game. This allows us to enforce player turns during the Navy Battle, and because the winner logic is in the game server cheating is difficult. A message like this one is typical of a game using the TGSDK since the games are turn based. This message is not mandatory however; you can pass whatever messages you like in your game, containing whatever you wish. The Hashtable must be prepared in the way that your game server will expect for every custom messages like this one:

      // . . .

      case "Update":
            int Turn = (int)data["Turn"];
            int Winner = (int)data["Winner"];
            m_WhosTurn = Turn;
           
            if (data.Contains("AttackCoordinates"))
            {
              // Update the appropriate board 
              // with the attack info
              int coordinates = 
                  (int)data["AttackCoordinates"]; //0..24
              int x = coordinates % BoardXSize;
              int y = coordinates / BoardYSize;
              bool hit = (bool)data["Hit"];
              int[,] board;
              if (Turn == m_LocalPlayerIndex)
                // If it's my turn now, then the 
                // last shot fired was at *me*.
                board = m_YourBoard;
              else
                // If it's not my turn then I fired 
                // the last shot.
                board = m_EnemyBoard;
                board[x,y] = (hit)?3:2;

              // Refresh the affected square so it updates
              if (Turn == m_LocalPlayerIndex)
                panel1.Invalidate(new Rectangle(x*40+1, 
                                         y*40+1, 39, 39));
              else
                panel2.Invalidate(new Rectangle(x*40+1, 
                                         y*40+1, 39, 39));
            }
           
            if (Winner == 0 || Winner == 1)
            {
              // Somebody won
              if (Winner == m_LocalPlayerIndex)
                ClientLabel.Text = 
                   "You sunk your enemy's navy! You win!";
              else
                ClientLabel.Text = 
                  m_PlayerNames[Winner] + " has won the game.";
            }
            else
            {
              // Nobody has won yet so keep playing          
              if (m_WhosTurn == m_LocalPlayerIndex)
                ClientLabel.Text = "It's your move, " + 
                          "click on an Enemy Grid location...";
              else
                ClientLabel.Text = "It's " + 
                      m_PlayerNames[m_WhosTurn] + "'s move...";
            }

            break;
    }
  }
}

To send a message you simply need to prepare a Hashtable with the message contents in it. You can send anything you like in the Hashtable, as long as it is serialiazable (implements ISerializable), or it's a primitive type, such as string and int. In this way, if you have a class you want to fire through to the server, you can build it to implement ISerializable and add it to the Hashtable message, and it will appear on the server side as a reconstituted class. As you might imagine, references from a serializable object to other objects need special handling when serializing, and the server must know about all the types you send to it.

We need to let the game server know that we have placed all of our ships on our board, so we created an InitialPlacement message. Once the server has received this message from both the game clients the play can begin.

// If five ships have been placed, we exit Placement Mode...
if (PlacementCount == ShipsCount)
{
  m_IsInitialPlacement = false;
  panel2.Visible = true;
  ClientLabel.Text = "Waiting for your opponent...";

  // Send message to server to say we're ready
  Hashtable msg = new Hashtable();
  msg.Add("MsgType", "InitialPlacement");
  msg.Add("Player", m_LocalPlayerIndex);
  msg.Add("Placement", m_YourBoard);
  m_TgGame.SendToServer(msg);
}

We use a MouseUp event handler for sending the attack coordinates using the TGGameClass.SendToServer method. First we make sure that it's not a repeat shot, then we register the shot on our enemy board, and finally send a Move message containing the shot coordinates to the server. This is another custom message specific to our game:

if ((m_EnemyBoard[UpGridX, UpGridY] & 2) == 0 &&
      m_WhosTurn == m_LocalPlayerIndex)
{
  m_EnemyBoard[UpGridX, UpGridY] |= 2;

  Hashtable msg = new Hashtable();
  msg.Add("MsgType", "Move");
  msg.Add("Player", m_LocalPlayerIndex);
  msg.Add("AttackCoordinates", UpGridY * 5 + UpGridX); 
  m_TgGame.SendToServer(msg);

  m_WhosTurn = ((m_WhosTurn + 1) % 2);
  ClientLabel.Text = "It's " + 
      m_PlayerNames[m_WhosTurn] + "'s move...";
}

That's a quick look at the message passing mechanism the TGSDK provides. The game client handles the GameEvent event to receive a message from the server, and it calls the SendToServer method to send a message to the server which will route it to the other game client(s). These messages, for the most part, are specific to your game logic. It's that simple. We'll see the complement to all of these messages in the game daemon in the section about the server later in this article. Let's continue looking at the events that the server raises.

Besides the GameEvent event there is also the AdminEvent event. These events are raised by the TGSDK to indicate different administrative states. These are directives from the TrayGames server or ClientManager which you should comply with. The AdminEventType.Connected event type is important because it provides us with detailed information about the player including their name, ranking (if implemented for this game), the number of times they have played the game, etc. First let's look at the Hashtable that contains all of this information as prepared by our server code, note the inner Hashtable:

Dim PlayerData As New Hashtable ' Data for all players
For Each Player As GamePerson In Group.Players.Values
    ' Info on a particular player
    Dim PlayerInfo As Hashtable = New Hashtable 
    PlayerInfo.Add("Nick", Player.Nick)
    PlayerInfo.Add("Rank", Player.Rank)
    PlayerInfo.Add("TimesPlayed", Player.TimesPlayed)
    PlayerInfo.Add("HasBoughtGameLevel", _
              CType(Player.BoughtGameLevel, Integer))
    PlayerData.Add(player.PlayerNumber, PlayerInfo)
Next

Msg.Add("PlayerData", PlayerData)

For Each player As GamePerson In Group.Players.Values
    Msg("PlayerNum") = player.PlayerNumber
    If IsTestHarnessMode Then
        TestMsgToPlayer(Group.Id, player.Id, Msg)
    Else
        SendMsgToPlayer(Group.Id, player.id, Msg)
    End If
Next

Now here is our AdminEvent event handler code accessing all of that information the server provides:

public void AdminEventHandler(object sender, AdminEventArgs e)
{
  switch (e.Type)
  {
    case AdminEventType.Connected:
      // Extract the PlayerData from the EventArgs object
      int LocalPlayerIndex = (int)e.Data["PlayerNum"];
      Hashtable playerData = (Hashtable)e.Data["PlayerData"];
      foreach (DictionaryEntry de in playerData)
      {
        int PlayerNumber = (int)de.Key;
        Hashtable PlayerInfo = (Hashtable)de.Value;
        string PlayerName = (string)PlayerInfo["Nick"];
        int PlayerRank = (int)PlayerInfo["Rank"];
        int TimesPlayed = (int)PlayerInfo["TimesPlayed"];
        int HasBoughtGameLevel = 
                   (int)PlayerInfo["HasBoughtGameLevel"];
      }

      // We now have a connection to the 
      // game server, we may receive
      // messages at any time, and are 
      // free to send messages also.
      break;

    case AdminEventType.Disconnected:
      // We may no longer send messages, nor 
      // will we receive any, until we reconnect
      break;

    case AdminEventType.Shutdown:
      // The client manager has called 
      // for the shutdown of all games.
      OnClose(true);
      break;
  }
}

Lastly, we look at the DataEvent event handler. The handler for this event is not running in the main thread, but rather in a TrayGames communications thread. It is advised that you simply use this event to signal your main thread to process the new messages because this handler is being called from a non-GUI thread. So all we do here is just set the m_NewData event to signaled, so our main thread realizes that there's something to process.

public void DataEventHandler(object sender, EventArgs e)
{
  m_NewData.Set();
}

The event handler for the Timer that we have created in our Load event handler for the Windows Form waits for m_NewData to be signaled. When it is signaled the TGGameClass.RetrieveMessages method is called, resulting in AdminEvent and GameEvent events getting raised from this thread, which should prevent any thread related conflicts.

private void TimerTickEventHandler(object sender, EventArgs e)
{
  if (m_NewData.WaitOne(0, false))
  {
    // We have new data!
    m_TgGame.RetrieveMessages();
  }
}

That's all about the TGSDK specific code that there is for the Navy Battle game client. The rest of the code is drawing and game logic.

The server

The counterpart of the game client in the TrayGames world is the game daemon. This is the server side code that you implement that coordinates the turns of all players, keeps track of scores, etc. The game daemon waits for all players to get connected and send their startup message. It handles the NewGroupEvent event, typically creating a game instance, and sending some game initialization data out to all players. The game daemon also handles the PlayerMsgEvent event to receive a message from a game client, this message is usually passed on to the game instance, to which that game client belongs. Lastly it handles the PostErrorEvent event for error handling.

  • Add a reference to the TGDaemonLib library and import the TG.Daemon namespace.
  • Create a GameDaemon object that implements the IGameDaemon interface.
  • Establish methods to handle key events that the system will raise.
  • Establish methods to handle events that the Game object will raise.
  • Create a Game object that has your game specific code.

We'll look at what's involved in creating the GameDaemon class first. It encapsulates the game instance manager and game states as well as the calls and logic to manage and progress the game. The GameDaemon object implements the IGameDaemon interface which is defined here:

Event GameStartedEvent(ByVal sender As Object, _
                      ByVal e As GameDaemonEventArgs)
Event GameEndedEvent(ByVal sender As Object, _
                      ByVal e As GameDaemonEventArgs)
Event PlayerJoinedEvent(ByVal sender As Object, _
                      ByVal e As GameDaemonEventArgs)
Event PlayerLeftEvent(ByVal sender As Object, _
                      ByVal e As GameDaemonEventArgs)
Event MsgIntoDaemonEvent(ByVal sender As Object, _
                      ByVal e As GameDaemonMsgEventArgs)
Event MsgOutOfDaemonEvent(ByVal sender As Object, _
                      ByVal e As GameDaemonMsgEventArgs)
Event DiagnosticMsgEvent(ByVal sender As Object, _
                      ByVal e As GameDaemonMsgEventArgs)
ReadOnly Property Id() As Guid
ReadOnly Property Games() As Hashtable
ReadOnly Property Channel() As String
Property AllowNewGames() As Boolean
Sub Initialize()
Sub Close()
Sub EndGame(ByVal groupId As Guid)
Sub EndAllGames()

First there is the import statement in our GameDaemon class:

using TG.Daemon;

Then there are some necessary members we have to add to the GameDaemon class. These are the data members that are directly related to using the TGSDK. You'll see how these are actually used throughout this section.

First we have to declare some data members that our class will need:

private TGDaemonClass _myTgD = new TGDaemonClass();
private Hashtable _games = new Hashtable(); // of clsGames
private bool _allowNewGames;
private Guid _id = Guid.NewGuid();

Implementing the properties of the IGameDaemon interface is straightforward. We're just returning some of our class data member described above:

public Guid Id
{
  get { return(_id); }
}

public string Channel
{
  // Must be unique in the TrayGames system
  get { return("NavyBattle"); }
}

public Hashtable Games
{
  get { return(_games); }
}

public bool AllowNewGames
{
  get { return(_allowNewGames); }
  set { _allowNewGames = value; }
}

Next, we need to establish the event handlers and call the Startup method. This is done in the Initialize method shown below:

void TG.Daemon.IGameDaemon.Initialize()
{
  _myTgD.NewGroupEvent += new 
         TGDaemonClass.NewGroupEventHandler(GameGroupAdd);
  _myTgD.PlayerMsgEvent += new 
         TGDaemonClass.PlayerMsgEventHandler(MsgReceive);
  Game.PostErrorEvent += new 
         Game.PostErrorEventHandler(PostErrorHandler);
 
  _allowNewGames = true;
  if (_myTgD.Startup(Channel))

  // . . .
}

The other methods that we implement to complete this interface deal with ending a game and which includes broadcasting a message to the group, cleaning up the Hashtable of games and closing down the daemon:

public void Close()
{
  EndAllGames();
  _myTgD.Shutdown();
 
  // If you don't call dispose, then the Listener
  // thread in the DaemonLib is never killed.
  _myTgD.Dispose();
}

In the Initialize method we assigned the NewGroupEvent event to the GameGroupAdd method. When this event is raised by the system GameGroupAdd take the following steps:

  • Get the GameGroup object using our GroupId a unique ID for our group assigned by the system. This ID is sent as an event argument.
  • Create a new instance of the Game object. We will be looking at the details of this object later.
  • Setup the Game object and add it to our table of games, which is the _games data member. Setup includes adding event handlers to the delegates defined by the Game object.
  • Send out a "Ready" message to all players.
  • Start the first game.

The following shows the GameGroupAdd method. Note that some of the diagnostic messages and peripheral stuff has been left out to keep the method more readable:

public void GameGroupAdd(object sender, NewGroupEventArgs e)
{
  Guid GroupId = e.GroupId;
  GameGroup gameGroup = 
        (GameGroup)_myTgD.GameGroups[GroupId];
  
  // Add a new group of players to the server
  Game ng = new Game(GroupId, gameGroup);
 
  //_myTgD.Msg2Player
  ng.MsgToPlayerEvent += new 
      Game.MsgToPlayerEventHandler(MsgToPlayerHandler); 
  //_myTgD.Msg2Group
  ng.MsgToGroupEvent += new 
      Game.MsgToGroupEventHandler(MsgToGroupHandler); 
  ng.GameOverEvent += new 
           Game.GameOverEventHandler(SignalGameOver);
  ng.PlayerLeftGameEvent += new 
    Game.PlayerLeftGameEventHandler(PlayerLeftHandler);
 
  _games.Add(GroupId, ng);
 
  // If you are ever sending messages to clients 
  // that ARE NOT RESULTING FROM AN INCOMING MESSAGE,
  // then it is important that you hold incomming 
  // messages while you do the processing.
  _myTgD.HoldMyMessages(gameGroup.Id, true);
 
  // Get the ball rolling with the 
  // first message to each human player
  Hashtable msg = new Hashtable();
  // First message from the server. 
  // Your Player number is tp.PlayerNum
  msg.Add("MsgType", "Ready"); 
  msg.Add("YourPlayerNum", -1);
  foreach(GamePerson tp in gameGroup.Players.Values)
  {
    msg.Add(tp.PlayerNumber, tp.Nick);
 
    // All players start as connected to the game.
    tp.PlayerStatus = GamePerson.UserStatus.Connected;
  }
 
  // Send out 'Ready' messages to non AI players
  foreach(GamePerson tp in gameGroup.Players.Values)
  {
    if (!tp.AI)
    {
      msg["YourPlayerNum"] = tp.PlayerNumber;
      _myTgD.MsgToPlayer(gameGroup.Id, 
                          tp.PlayerNumber, msg);
    }
  }

  // Start the first game
  ng.StartFirstGame();
 
  // Don't forget to stop holding messages
  _myTgD.HoldMyMessages(gameGroup.Id, false);
  _myTgD.GameStartedForGroup(gameGroup.Id);
 
  GameStartedEvent(this, 
      new GameDaemonEventArgs(gameGroup.Id));
  foreach(GamePerson player in 
                     gameGroup.Players.Values)
  {
    if (!player.AI)
      PlayerJoinedEvent(this, 
        new GameDaemonEventArgs(GroupId, 
        (Guid)gameGroup.PlayerNumberToGuid[player.PlayerNumber], 
        player.PlayerNumber, player.Nick));
  }
 
  e.Success = true;
}

Note that the call to HoldMyMessages is just for thread safety. If your timer is sending a message and (on a different thread) a message comes in and is being processed, there is the possibility for problems if your game daemon is not thread safe. What HoldMyMessages does for you is allow you to prohibit the message thread from doing stuff while you're doing things with the timer thread for example.

In the Initialize method we also assigned the PlayerMsgEvent event to the MsgReceive method. This event is raised when a message is received from a game client. The following shows the normal handling of a message. Typically we just want to route the message to the game involved which is what we are doing in this example. However there are some cases where we want to do additional processing. For persistent world/drop in games where the game encapsulates only one player, but the daemon might be running the whole world where these Player/Games are taking place in, then all the processing of the message would most likely take place here:

public void MsgReceive(object sender, PlayerMsgEventArgs e)
{
  Guid groupId = e.GroupId;
  int playerNumber = e.PlayerNumber;
  Hashtable msg = e.Msg;
  if (_games.Contains(groupId))
  {
    MsgIntoDaemonEvent(this, new GameDaemonMsgEventArgs(groupId, 
                                             playerNumber, msg));
    Game MyGame = (Game)_games[groupId];
    MyGame.MsgReceive(playerNumber, msg);
  }
}

Now that we can receive messages we also need the capability to send messages to the players, the following methods do just that:

private void MsgToPlayerHandler(Guid groupId, 
                      int playerNumber, Hashtable msg)
{
  MsgOutOfDaemonEvent(this, new GameDaemonMsgEventArgs(groupId, 
                                           playerNumber, msg));
  _myTgD.MsgToPlayer(groupId, playerNumber, msg);
}

private void MsgToGroupHandler(Guid groupId, Hashtable msg)
{
  MsgOutOfDaemonEvent(this, 
             new GameDaemonMsgEventArgs(groupId, -1, msg));
  _myTgD.MsgToGroup(groupId, msg);
}

That's all there is to set up the GameDaemon. Now let's move on to the implementation details of the Game class. First we need the same import statement that the GameDaemon has:

using TG.Daemon;

Then we have to declare some data members that this class will use, and events (with delegates) that the daemon class can attach to:

public delegate void MsgToGroupEventHandler(Guid id, 
                                         Hashtable msg);
public delegate void MsgToPlayerEventHandler(Guid id, 
                       int PlayerNumber, Hashtable msg);
// Send game-over message to all players
public delegate void GameOverEventHandler(Guid id); 
public delegate void PlayerLeftGameEventHandler(Guid id, 
                                        Guid playerGuid);
public delegate void PostErrorEventHandler(string text);

public event MsgToGroupEventHandler MsgToGroupEvent;
public event MsgToPlayerEventHandler MsgToPlayerEvent;
public event GameOverEventHandler GameOverEvent;
public event PlayerLeftGameEventHandler PlayerLeftGameEvent;
public static event PostErrorEventHandler PostErrorEvent;

private const int NumPlayers = 2;
private Guid m_id;
// Consisting of 2 players min
private TG.Daemon.GameGroup m_group; 
private int m_turn;
private int[][,] m_Placement = new int[2][,];

Next there is the constructor code, code to handle the start of a new game, and code to end a game. Remember that StartFirstGame is called from the GameDaemon.GameGroupAdd method, while EndGame is called from the GameDaemon.SignalGameOver method. For this particular game we don't do much when a game is starting, when a game ends we can clean up any objects the game uses, and tell each player that the other has left the game (which is true enough):

public Game(Guid groupId, TG.Daemon.GameGroup group)
{
  m_id = groupId;
  m_group = group;
}

public void StartFirstGame()
{
  StartNewGame();
}

public void StartNewGame()
{
  // TODO: Add code here to start up game here
}

public void EndGame()
{
  Hashtable msg = new Hashtable();
  msg.Add("MsgType", "UserLeavingGame");
  msg.Add("Player", 0);
  MsgToPlayerEvent(m_id, 1, msg);
  msg["Player"] = 1;
  MsgToPlayerEvent(m_id, 0, msg);
}

The last thing we have to do is handle the actual messages coming in from our game clients. This code will always be specific to your game. If you recall, the GameDaemon object adds its MsgReceive method to the PlayerMsgEventHandler delegate. That method will figure out which particular game instance the message belongs to and call the Game object's MsgReceive method. For Navy Battle we handle three message types, InitialPlacement, Move, and UserLeavingGame.

The InitialPlacement message is unique to this game because we have to wait for both players to place their ships before we can start. When we receive this message from both players we send out an Update message in response, which is handled by our game client. We also save the positions of both players' ships:

public void MsgReceive(int playerNum, Hashtable msg)
{
  if (msg.Contains("MsgType"))
  {
    string MsgType = (string)msg["MsgType"];
    switch(MsgType)
    {
      case "InitialPlacement"
      {
        int player = (int)msg["Player"];
        int[,] Placement = (int[,])msg["Placement"];
        m_Placement[player] = Placement;
 
        if (m_Placement[0] != null && m_Placement[1] != null)
        {
          // Send out the first update
          Hashtable updateMessage = new Hashtable();
          updateMessage.Add("MsgType", "Update");
          updateMessage.Add("Winner", -1); // No winner yet
          updateMessage.Add("Turn", 0);
          // NOTE: No 'AttackCoordinates' or 'Hits' are added
          // to this message since this is the first time.
          MsgToGroupEvent(m_id, updateMessage);
        }
      }
     
      // . . .

The Move message gets handled by determining if there is a winner, keeping the count of destroyed ships, and determining which player gets to make the next move. It then sends out a message with this information to both the players. Although this is a custom message, it's typical for all games to have a message that is something like this, since TrayGames games are turn based:

      case "Move":
      {
        // Retrieve information from message
        int player = (int)msg["Player"];
        int coordinates = 
            (int)msg["AttackCoordinates"]; //0..24
        int x = coordinates % 5;
        int y = coordinates / 5;
        // The other player is the target
        int target = (player + 1) % NumPlayers; 
       
        // Record the shot
        m_Placement[target][x,y] |= 2;
       
        // See if it was a hit
        bool hit = false;
        if ((m_Placement[target][x,y] & 1) > 0)
          hit = true;
       
        int winner = -1; // No winner
       
        // Increment the turn
        m_turn = (m_turn + 1) % NumPlayers;
             
        // Check for all ships destroyed
        if (hit)
        {
          // If the 'target' player has no ships left 
          // that haven't been hit then they lose.
          // The Grid value will be 1 for an undamaged 
          // ship, 2 for a shot that missed, and
          // 3 for a destroyed ship
          int UndamagedShips = 0;
          for(int i = 0; i < 5; i++)
          {
            for(int j = 0; j < 5; j++)
            {
              if (m_Placement[target][i,j] == 1)
                UndamagedShips++;
            }
          }
             
          // The game is over
          if (UndamagedShips == 0)
          {
            winner = player;
          }
        }
       
        Hashtable updateMessage = new Hashtable();
        updateMessage.Add("MsgType", "Update");
        updateMessage.Add("Winner", winner);
        updateMessage.Add("Turn", m_turn);
        updateMessage.Add("AttackCoordinates", coordinates);
        updateMessage.Add("Hit", hit);
       
        if (winner > -1)
        {
          // This is so we can expose the 
          // winning players ship locations
          // to the losing player 
          // (kind of rubbing his nose in it)
          updateMessage.Add("Placement", m_Placement[winner]);
        }
        // Broadcast update message to both players
        MsgToGroupEvent(m_id, updateMessage);
        break;
      }
 
      // . . .

To handle the UserLeavingGame message all we need to do is notify the other player that their opponent has left the game, and end the game since a two player game can't continue with only one player. Although this is a custom message, it's typical for all games to have such a message:

      case "UserLeavingGame":
      {
        int leavingPlayerNum = Convert.ToInt32(msg["Player"]);
        int opponent = (leavingPlayerNum + 1) % NumPlayers;
        MsgToPlayerEvent(m_id, opponent, msg);
        TG.Daemon.GamePerson leavingPlayer = 
          (TG.Daemon.GamePerson)m_group.Players[leavingPlayerNum];
        leavingPlayer.PlayerStatus = 
            TG.Daemon.GamePerson.UserStatus.Dropped;
        int numPlayersInTheGame = 0;
 
        foreach(TG.Daemon.GamePerson player in m_group.Players.Values)
        {
          if (player.PlayerStatus == 
                  TG.Daemon.GamePerson.UserStatus.Connected)
            numPlayersInTheGame += 1;
        }
 
        // If num players left in game is less 
        // than min players, then game is over
        PlayerLeftGameEvent(m_id, 
          (Guid)m_group.PlayerNumberToGuid[leavingPlayerNum]);
        if (numPlayersInTheGame < 2)
          GameOverEvent(m_id);
        break;
      }    
    }
  }
}

That's all there is to the server side of the TGSDK. One of the advantages of using a client-server model instead of a peer-to-peer model when designing your game is that it prevents cheating, that can be a big plus. Most of the code in the GameDaemon class is boiler plate code that you can take from a sample like this and re-use. If your game is simple then the same can be said for the Game class. In fact I copied most of the daemon code for Navy Battle from the C# implementation of our Tic-Tac-Toe sample. Remember when you run your client or server you need to have the TrayGames support libraries in the same folder as your executable.

Points of interest

That's all there is to do to get a simple multi-player game working in the TGSDK. As you can see the APIs make the client-server system totally seamless and invisible to you as a developer. You don't have to do anything at all with TCP/IP, matchmaking or hosting. There are many more advanced features that we are not taking advantage of here, such as support for Managed DirectX, a powerful Skinning Library, and an Ogg Vorbis file player for sounds. If you are interested in checking out the full TGSDK for producing your own multi-player online games, you can get it at the TrayGames web site.

Acknowledgments

A special thanks to Paul Naylor, a game programming guru, for his help in making this game look and work as good as it does.

Revision history

  • 11th August, 2005 - Initial revision.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Perry Marchant
Founder SpreadTrends.com
United States United States
I've authored many articles that tackle real-world issues to save my peers in the development community valuable time. For example I've written articles that: show how to decode Ogg Vorbis audio files using the .NET Framework; describe best practices for Improving Entity Framework performance; and demonstrate step-by-step how to create a multi-player game.

Comments and Discussions

 
GeneralTesting client/server apps [modified] PinmemberThe Chevmeister15-Jul-06 16:14 
GeneralRe: Testing client/server apps [modified] PinmemberPerry Marchant16-Jul-06 11:08 
GeneralMan, your navy battle is very good PinmemberAlex Cutovoi22-Aug-05 12:06 
GeneralExcellent PinmemberJudah Himango12-Aug-05 6:56 
GeneralRe: Excellent PinmemberPerry Marchant12-Aug-05 9:17 
I'm glad to hear you like the game and thank you for your vote. I still have Electronic Battleship and it remains one of my favorites! To answer some of your questions. . .
 
The Managed DirecX (MDX) project was started as a skunk-works project within Microsoft with no formal backing or plan. Once it was proven to be viable it got some organization behind it and the creator was forced to bring his design in line with Microsoft practices. This caused the major break in compatibility, so we don’t expect to see that again, though we’ll see a big change with the release of Vista.
 
Having multiple versions of MDX side-by-side is no problem, and we ensure the version(s) we need are present. The main TrayGames client installer handles putting the .NET Framework, DirectX, and MDX on their system so it’s not a problem, especially with broadband these days. We haven’t seen any resistance from users on this point.

 
Perry

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.141022.2 | Last Updated 11 Aug 2005
Article Copyright 2005 by Perry Marchant
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid