This is my very first XNA application, even though it started with a porting from my previous Windows Forms game.

It took me about 2 months to create this XNA game, and there are so many interesting features in it that I think would render two articles. But instead, I decided to keep the article as short as possible. Besides, many of the game concepts have been already covered in the other article, so please refer to the C# Snooker article if you feel some of the content is missing.

If you are a developer, I hope the concepts explained here can be useful someday. If you are an addicted gamer, my goal here is to make you spend much of your time having fun with it.

For a better explanation, I uploaded a video to YouTube (me against the computer), so I hope it can save you time in understanding the concepts presented here:

Many thanks to my sweet 10 year old niece Ana Beatriz, who's a big fan of games and helped me test the multiplayer feature for 2 weeks.

Special thanks to my friend Rohit Dixit, who's currently working on the Gaming 123 website to host many free games, including XNA Snooker. Rohit is now developing rooms so that logged on players could connect to each other and play games.

If you are a .NET developer, XNA should be the first option when it comes to writing a game, even if you have no previous experience with the tool. As a newbie, I had prejudiced thoughts about it. But moving to XNA was far easier than I thought. It's frustrating when a technology doesn't follow your creativity, but XNA is different. When it comes to developing a game, you don't ask "is it possible to do it with XNA?" but rather "which technique should I learn to make it work with XNA?". No kidding. I would say that any top commercial game could be made with XNA, you would just need to know how to use the right techniques and, if you are developing for PC, you might need a good video card.

Unlike Windows Forms and WPF, the XNA framework lacks the concept of "UI controls". So, it's up to you to implement your own UI controls. The good news is that I just needed two types of controls: a radio button and a command button. Since I wanted to keep it very simple, both controls just render a string on the screen:

Figure 1. XNA controls (XNARadioButton and XNAButton) in the main menu

Notice that XNARadioButton starts with a "[ ]" string. This is so because the user can make it checked. Once it's checked, the "[ ]" becomes "[x]".

Also, the command button (XNAButton) has nothing more than a string. When the user moves the control over it, it gets highlighted.

Simple and efficient, but maybe I'll replace them later with more appealing controls.

When the user enters the game, he or she is asked to provide the player name, and then is presented the Visual Keyboard. This could be done using the standard keyboard's keypress or keydown events, but instead, I made this Visual Keyboard, which after all looked nice on the screen.

Figure 5. Visual Keyboard

One important thing is that the keyboard is made of a single sprite. When the mouse moves over some key, the VisualKeyboard class identifies the key, and the main game class highlights a yellow sprite right below that key in the keyboard sprite. Also, a soft "click" sound indicates when a key has been selected.

The game allows you to replace the default "No Photo Available" image. Just copy some image to the Clipboard (go to Windows Explorer, select the file, and then press Ctrl+C, or if you prefer, select an image area through some graphics application, such as Paint.Net), and then click the player picture box while in the Sign In screen.

Figure 6. Player picture, copied from Clipboard

Notice that vertical black bars where inserted at both sides of the original picture. This is so because the size ratio (width / height) is different from the size ratio of the target picture. Otherwise, horizontal black bars would be inserted at the top and at the bottom. This is how the original aspect ratio is preserved.

The code here covers how the images are copied from Clipboard and transferred to the texture in the game:

It would be nice if the game looked as much real as possible. Despite the fact that it's a 2D game, it was possible to take advantage of some techniques to render a real-looking (or almost!) snooker game.

The balls rendering represent a vital part in this game. If you take a look at the table below, you'll see that the rendering is not made at once, but in a few steps:

Here we have the table fabric prior to the ball.

Then, we must place the shadows cast by the four lights (one at each corner of the table).

Now, here's the ball over the background, without smoothing. Notice that the borders are a bit "pixelated".

Finally, we apply an "alpha blending" technique around the ball in order to make it smoother.

Figure 7. Rendering the ball with different layers

That being said, here comes the MoveBalls function, which is responsible for calculating the ball positions in a given instant (snapshot).

Notice that this is the heart and soul of the game, because it has to resolve collisions (between ball and ball and ball and borders), calculate ball velocity based on the current velocity and the friction coefficient, calculate the vertical spin velocity, and decide whether the balls are still moving or not.

One of the cool features in the game, in my opinion, is the cue movement.

The cue moving around the cue ball close to it (notice the cue casting shadow over the table).

The cue getting away from the cue, preparing to shoot.

The cue moves freely around the cue ball, following the same direction as the mouse pointer moves. Once the player selects the target, the cue locks in that direction, gets away from the cue for a little while, and then strikes the ball in the desired direction.

The cue also casts a shadow over the table. Besides, it also has a smooth rendering against the table background.

...to implement some kind of artificial intelligence, to challenge your player's intelligence. So, even if your game is intended to have a multiplayer mode, you should also consider having a single player mode. Players love challenges. But, what if you have gone too far with your A.I., and your game got so "intelligent" to the point that your player eventually gives up your game once and for all, completely humiliated and disappointed?

The answer to that last question is: break the challenge into levels. Just like most games, there are three difficulty levels: easy, normal, and hard. The difference between them is too simple: given a number of "ghost balls", the computer randomly picks one of them and internally simulates a shot. Then it calculates the yielded result (the won points and the lost points in that shot). It's easy to beat the computer in this mode. In the case of the Easy mode, the computer has only one chance. In Normal mode, the computer generates 10 simulations and calculates which one has the best results. But, in the Hard mode, the computer simulates up to 20 shots. So, you'll have to be a very good player to beat it (please tell me later if you have succeeded...).

I must confess I never had any training on Artificial Intelligence. In fact, I don't even know if this could be called A.I., but what the computer actually does when it's playing in its turn is to randomly simulate a number of shots and decide which one is better. The crucial point is this:

If all attempts of the computer result in failure (that is, in lost points), then it is given a last chance. This is what I call the final "despair mode". In this mode, the computer will shoot against reflected ("or mirrored") ghost balls.

When the computer gives a shot, it doesn't do so aimlessly. It's not every position in the pool that can be aimed at, because that would be too inefficient. This is why the computer concentrates its efforts in the "ghost balls": they are calculated as the circle that touches the object balls in only one point, so that if you draw a straight line from the center of the ghost ball to the pocket position, that line would include the point of the center of the object ball (see the above picture).

In figure 8 above, the ghost balls are represented by the white circles near the balls. They are represented by implementations of the same Ball class, just like any other ball in the table. The difference is that they are not rendered.

As mentioned before, in "despair" mode, only the reflected ghost balls can be aimed at. This means that the computer is given a chance to create an alternative shot that might result in a better result.

Once a shot is aimed at a reflected ghost ball, the cue ball path is "mirrored" at the pool border, so instead of reaching the reflected ghost ball, the cue ball hits the real ghost ball:

The icing on the top of the Artificial Intelligence that we've just seen, is to make the computer behave a bit like a human. Usually, a human player would look at the possibilities, calculate, move the cue, calculate again, move the cue back to the last position, and only after a little while play a shot. This is exactly what the computer does in the game. In its turn, you'll see the cue moving around and around, aiming here and there, and only then shooting.

You'll see that in the Easy mode, the computer plays with carelessness. In the hard mode, on the other hand, it usually takes longer for the computer to play, because it is "thinking" of the best possibility.

Although it could be fun to play against the computer, two (human) players can also play XNA Snooker Club against each other, thanks to Windows Communication Foundation. But the clients don't connect directly to each other. Instead, they rely on a WCF service running on a different application to broadcast the messages.

It should be said that my job in doing this WCF implementation became much easier after I finished reading Sacha Barber's great article: WCF / WPF Chat Application. So, you'll find similarities between the two implementations. In Sacha's chat application, the clients establish connection by joining the same WCF service. The conversation is done while the messages are sent by clients and broadcasted by that service to the joined chatters. In XNA Snooker Club, on the other side, the clients connect to the snooker service, which in turn broadcasts the game movements, score, sounds, etc. This exchange between snooker players is also a form of conversation.

It's a very nice thing that WCF services offer a variety of hosting options: console, Windows Forms, WPF, Windows Services, IIS, self-hosting... The WCF snooker service is hosted by a console application. The good thing about console applications is its simplicity. They are easy to create and run, and very handy when you need to write messages to the output.

The XNA Snooker Club VS2008 solution is separated into two application layers: the Core project and the XNA project. The WCF client resides on the Core layer. It is responsible for establishing connection with the WCF service, as well as to send and receive messages from the service.

The WCF client knows how to communicate with the WCF Service, thanks to the proxy class. This class can be generated automatically by using the svcutil command line utility, or using the Add Service Reference menu in Visual Studio 2008.

Once the proxy is created, the client can start calling the service methods. But instead, it's a best practice to apply the Service Agent pattern. A service agent is a component on the client side that wraps the proxy methods and performs additional processing in order to further separate client code from the service. In our case, the service agent is a singleton class residing on the Core project.

First, the ServiceContract attribute that decorates the interface defines the operations the service makes available to be invoked as requests are sent to the service from the client. The session mode is set to Required, which indicates to the contract that a session is to be maintained (this is how a conversation is possible).

The CallbackContract sets up the infrastructure for a duplex MEP (message exchange pattern), and this is how it allows the service to initiate sending messages (e.g., notifications about a shot that was just played by a player) to the client.

The Join operation allows the client to join a game. If the current game on the WCF side already has two players, the join operation is refused.

As a response to the Join operation, the service returns the information about the current teams and players.

When the player shoots the cue ball, the XNA application starts recording the ball position movements in a structure stored in the Shot class. This recording goes on as long as there are moving balls on the table.

Once the player has finished a shot (that is, when all balls become still), the Play operation is invoked, so the WCF client calls the Shot in the proxy with a Shot parameter containing the recorded sequence of ball movements, along with sounds and scores.

In opposition to the Join operation, when the XNA client is exiting, it calls the Leave operation, which in turn will broadcast to the game participants a callback indicating that one of the partners has left.

As we have seen in the previous section, the CallbackContract for ISnooker is ISnookerCallback. This means that, when the client calls an operation on the snooker service side, it must also expect that the service calls back the XNA client with the methods defined by ISnookerCallback:

As mentioned before, the ball movements are sent/received to/from the service through the Shot data contract. Notice in the class below that besides the snapshot list, there is some more information to the Shot, such as the scores and a GameOver field indicating whether the player has won the game just after that shot.

The outgoing shot data is held by the outgoingShot object (Shot class), and is created by the XNA client every time the players prepare to shoot.

Every time there is a movement of balls on the table, the XNA client creates a new Snapshot, which in turn holds all the ball positions on the table at a given time, along with the sounds that occur at that instant. This new Snapshot is then added to outgoingShot, as seen below:

In multiplayer mode, everything happening on the XNA client must be enlisted in outgoingShot to be sent later to the other player. But, the outgoingShot is not sent until the shot is complete, all balls are still, and both scores have been calculated, as seen below:

//Here goes the "fade out" effect when the ball enters//the pocket and then disappears
someFalling = ProcessSomeFalling();
//If all balls have fallen, we should prepare //for sending the outgoing shot infoif (!someFalling)
{
if (lastPoolState == PoolState.MovingBalls)
{
if (!fallenBallsProcessed)
{
//Calculate both scores for the fallen balls,//restore illegal potted balls to their correct positions, etc.
ProcessFallenBalls();
foreach (Ball b in ballSprites)
{
b.DrawPosition = b.Position;
}
//Get the final position of each ball on the table//and add the snapshot to the outgoing shot
CreateSnapshot(GetBallPositionList());
//Occurrs only in multiplayer mode. In singleplayer there's no//need to broadcast outgoing shots.if (rbtMultiPlayer.Checked)
{
logList.Add(string.Format("Player {0} sent {1} snapshots",
contractPerson.Name, outgoingShot.SnapshotList.Count()));
//We should propagate both scores
outgoingShot.CurrentTeamScore = teams[0].Points;
outgoingShot.OtherTeamScore = teams[1].Points;
//Now it's up to the WCF service //to receive and broadcast this player's shot
SnookerServiceAgent.GetInstance().Play(contractTeam,
contractPerson, outgoingShot);
//We must reset the outgoing shot for the next turn
ResetOutgoingShot();
}
}
}
}

As this outgoingShot object arrives at the WCF service, it is simply broadcasted to other game players.

///<summary>/// Broadcasts the list of ball positions to all the <seecref="Common.Person">/// Person</see/> whos name matches the to input parameter
/// by looking up the person from the internal list of players
/// and invoking their SnookerEventHandler delegate asynchronously.
/// Where the MyEventHandler() method is called at the start of the
/// asynch call, and the EndAsync() method at the end of the asynch call
///</summary>///<paramname="to">The persons name to send the message to</param>///<paramname="msg">The message to broadcast to all players</param>publicvoid Play(ContractTeam team, ContractPerson person, Shot shot)
{
Console.WriteLine(string.Format("Receiving shot information from team " +
"{0}, player {1}, with {2} snapshots.",
team.Id, person.Name, shot.SnapshotList.Count));
SnookerEventArgs e = new SnookerEventArgs();
e.msgType = MessageType.ReceivePlay;
e.team = team;
e.person = person;
e.shot = shot;
BroadcastMessage(e);
Console.WriteLine(string.Format("Shot information has been broadcasted"));
}

When receiving an incoming shot, the XNA client must reproduce exactly whatever has occurred on the other player's computer. This includes the same ball positions, the same frame rate, the same scores, the same balls potted, coherent turn shifts, and coherent game over processing.

Once the XNA client has replayed the incoming shot completely, it's crucial that both XNA clients have exactly the same state. If this consistency is not achieved, both players will have problems with disparities of ball positions, wrong turn shifts, wrong scores, and so on.

The code below illustrates the vital part of processing the incoming shot:

//Since both players receive the incoming shot//from the WCF service, we must reproduce the shot only//if it was made by the other playerif (contractTeam.Id != playingTeamID)
{
if (currentIncomingShot == null)
{
if (incomingShotList.Count >0)
{
//Since we might receive more than one incoming shot,//we pick only the first shot in the queue, and then//dequeue it.
currentIncomingShot = incomingShotList[0];
incomingShotList.RemoveAt(0);
}
}
if (currentIncomingShot != null)
{
//Now we have to reproduce the other player's movement//snapshot-by-snapshot, exactly as it was seen by the other player.if (currentSnapshotNumber <= currentIncomingShot.SnapshotList.Length)
{
//The movement delay is also exactly the same as the other//player. The snapshots are processed in the same frame rate as the//original player.if (movingBallDelay <== 0)
{
movingBallDelay = maxMovingBallDelay;
//Reproducing the ball positions inside the current snapshot,//we should check whether the ball was potted or not.foreach (BallPosition bp in
currentIncomingShot.SnapshotList[currentSnapshotNumber].ballPositionList)
{
ballSprites[bp.ballIndex].Position = new Vector2(bp.x, bp.y);
ballSprites[bp.ballIndex].IsBallInPocket = bp.isBallInPocket;
}
//The game wouldn't be that cool if your machine couldn't reproduce//the sound exactly as the other player's did, isn't it?
GameSound sound =
currentIncomingShot.SnapshotList[currentSnapshotNumber].sound;
if (sound != GameSound.None)
{
soundBank.PlayCue(sound.ToString());
}
currentSnapshotNumber++;
}
}
else
{
//Once all ball movements have been processed,//the incoming scores are applied. Notice that//the XNA client that is playing is responsible //for calculating both scores.//The receiving XNA client, on the other hand, just//accepts those scores.
teams[0].Points = currentIncomingShot.CurrentTeamScore;
teams[1].Points = currentIncomingShot.OtherTeamScore;
//"GameOver" is a flag of the incoming shot data.//In this line, the user's machine is checking whether it//has lost the gameif (currentIncomingShot.GameOver)
{
UpdateGameState(GameState.GameOver);
}
elseif (currentIncomingShot.HasFinishedTurn)
{
//Now it's time to change turns.
playingTeamID = (playingTeamID == 1) ? 2 : 1;
awaitingTeamID = (playingTeamID == 1) ? 2 : 1;
logList.Add(string.Format("Team {0} is ready to play", playingTeamID));
logList.Add(string.Format("Team {0} is waiting", awaitingTeamID));
//Decide which one is the next ball
teams[playingTeamID - 1].BallOn = GetRandomRedBall();
if (teams[playingTeamID - 1].BallOn == null)
teams[playingTeamID - 1].BallOn = GetMinColouredball();
}
currentSnapshotNumber = 0;
currentIncomingShot = null;
}
}
}

If you had patience to reach this line, I'd like to thank you. Please download the application and evaluate it. And, whether you like it or not, please don't forget to leave your comments at the end of the article, I'll appreciate it. It was a lot of fun to write and to test it. I wish you lots of fun too.

Share

About the Author

Marcelo Ricardo de Oliveira is a senior freelance software developer who lives with his lovely wife Luciana and his little buddy and stepson Kauê in Guarulhos, Brazil, is co-founder of the Brazilian TV Guide TV Map and currently works for Alura Cursos Online.

Comments and Discussions

i have download the source code
but i don't know how to run the project?
i have download visual studio and xna and dotnetframework which is mentioned in the artical
please tell me how can i run this project?

hi marcelo, i need one help from you. i installed xna 4.0 game studio. but when i run through vs10 it shows the error no suitable graphics card found. i changed game profile hidef to reach but i still receive that msg. what will i do? can u give me your email id for further contact.

Ya i can understand. But i think HTML is stateless and the only way to make communication in WCF is Duplex Binding. But still I am not getting satisfied the way of implementation using Duplex. DO you have any idea to make Multiplayer game for web (Specially for HTML page not aspx )???

The Win7 is a Dutch version, but tried it also on a machine with English Win7 with same results. Also tried running SnookerService; this shows a DOS window containing 4 lines very briefly, then crashes.

Is there any installation or setup to be made other than unpacking the two zipped folders?

I suspect (but not sure) that there's some dependency in the XNA project that doesn't exist on the XNA redistributable and neither on your computer (though I tested the xna exe on different computers). Maybe a reference to some class (like GamerServices class). I'll check it out soon and let you know when I fix it, if it is the case. As for the WCF service crash, I don't know what might be happening. Do you remember the error message?