
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;
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:
m_TgGame.GameEvent +=
new TGGameClass.TGGameEventHandler(GameEventHandler);
m_TgGame.AdminEvent +=
new TGGameClass.TGAdminEventHandler(AdminEventHandler);
m_TgGame.DataEvent += new EventHandler(DataEventHandler);
m_Timer.Interval = 200;
m_Timer.Tick += new EventHandler(TimerTickEventHandler);
m_Timer.Start();
m_TgGame.Start(8000, true);
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;
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"))
{
int coordinates =
(int)data["AttackCoordinates"];
int x = coordinates % BoardXSize;
int y = coordinates / BoardYSize;
bool hit = (bool)data["Hit"];
int[,] board;
if (Turn == m_LocalPlayerIndex)
board = m_YourBoard;
else
board = m_EnemyBoard;
board[x,y] = (hit)?3:2;
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)
{
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
{
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 (PlacementCount == ShipsCount)
{
m_IsInitialPlacement = false;
panel2.Visible = true;
ClientLabel.Text = "Waiting for your opponent...";
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
For Each Player As GamePerson In Group.Players.Values
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:
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"];
}
break;
case AdminEventType.Disconnected:
break;
case AdminEventType.Shutdown:
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))
{
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();
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
{
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();
_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];
Game ng = new Game(GroupId, gameGroup);
ng.MsgToPlayerEvent += new
Game.MsgToPlayerEventHandler(MsgToPlayerHandler);
ng.MsgToGroupEvent += new
Game.MsgToGroupEventHandler(MsgToGroupHandler);
ng.GameOverEvent += new
Game.GameOverEventHandler(SignalGameOver);
ng.PlayerLeftGameEvent += new
Game.PlayerLeftGameEventHandler(PlayerLeftHandler);
_games.Add(GroupId, ng);
_myTgD.HoldMyMessages(gameGroup.Id, true);
Hashtable msg = new Hashtable();
msg.Add("MsgType", "Ready");
msg.Add("YourPlayerNum", -1);
foreach(GamePerson tp in gameGroup.Players.Values)
{
msg.Add(tp.PlayerNumber, tp.Nick);
tp.PlayerStatus = GamePerson.UserStatus.Connected;
}
foreach(GamePerson tp in gameGroup.Players.Values)
{
if (!tp.AI)
{
msg["YourPlayerNum"] = tp.PlayerNumber;
_myTgD.MsgToPlayer(gameGroup.Id,
tp.PlayerNumber, msg);
}
}
ng.StartFirstGame();
_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);
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;
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()
{
}
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)
{
Hashtable updateMessage = new Hashtable();
updateMessage.Add("MsgType", "Update");
updateMessage.Add("Winner", -1);
updateMessage.Add("Turn", 0);
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":
{
int player = (int)msg["Player"];
int coordinates =
(int)msg["AttackCoordinates"];
int x = coordinates % 5;
int y = coordinates / 5;
int target = (player + 1) % NumPlayers;
m_Placement[target][x,y] |= 2;
bool hit = false;
if ((m_Placement[target][x,y] & 1) > 0)
hit = true;
int winner = -1;
m_turn = (m_turn + 1) % NumPlayers;
if (hit)
{
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++;
}
}
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)
{
updateMessage.Add("Placement", m_Placement[winner]);
}
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;
}
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.