Monday, December 24, 2007

2D Deformable Level

Today I'm going to give you a quick overview on how to create deformable 2D level (similar to what is seen in games like Worms and Lemmings)

First I will create a new XNA 2.0 project, next I'm going to create a level using Paint.Net.

I'll create the new image with a size 800 (w) x 600 (h) as this is the default size a XNA 2.0 project uses. Now we are ready to start drawing the level, select the Paintbrush tool and change the Antialiased button to Antialiasing Disabled, then draw your outline of your level (preferably in black).

You should now have something similar to this, from here select the top section (above the black line) with the Magic Wand tool, and press the delete key. The top section should now change to grey and white checkered boxes, this represents the alpha layer.
Choose your desired level colour and then select the Paint Bucket tool and click on the bottom sectin of your level.



You should now have something along the lines of the following:


From here you are ready to save, click Save As and select the "Save as type:" to "PNG (*.png)" and call it "level"
(click here to download a copy of my level)

So thats our level done, now lets create our deform image, we do this in a very similar way as the level so I wont go threw it step by step.

Im going to make the deform image 128(w) x 128(h), basically its going to be an circle (so i'll draw it using the Ellipse tool using the same Brush width and Anti-Aliasing options as I used for the leve.

Because we are going to be using this image as the image that will deform the level we need to specify that the iniside of the level is white (we will replace this in code with alpha pixels) and the outside of te ellipse will be transparent / alpha like this. Then save this as a PNG file as well and call it "deform". Or you can download my copy here.




Finally lets create a nice sky background, create a new image 800x600, then select a nice blue colour for your Primary color and make leave your secondary colour white. Then simply select from the Effects menu "Effects -> Render -> Clouds" and click the OK button. Save this image as sky.jpg (Note! We don't need to save this as a PNG file as the sky image contains no alpha layer, so by using a JPG format we can save a little on disk space)


Right, now we are ready to begin coding.

Drag & Drop the above two png files and the sky.jpg you created into you content folder in your project.

Now open the Game1.cs file and lets add the following below the following line:
SpriteBatch spriteBatch;
We need to declare the sky, level, and deform sprites:

private Texture2D textureSky;
private Texture2D textureLevel;
private Texture2D textureDeform;
Then in the LoadContent class lets replace the // TODO comment by loading our content:
textureSky = Content.Load<Texture2D>("sky");
textureLevel = Content.Load<Texture2D>("level");
textureDeform = Content.Load<Texture2D>("deform");
We are already ready to start on the draw class, so lets replace the // TODO comment in the Draw function with the following:
spriteBatch.Begin();

spriteBatch.Draw(textureSky, new Vector2(0, 0), Color.White);
spriteBatch.Draw(textureLevel, new Vector2(0, 0), Color.White);
spriteBatch.Draw(textureDeform, new Vector2(100, 100), Color.White);

spriteBatch.End();
Right, already we are ready to build the game and see our level, it should look something like this:


Not bad for just 11 lines of new code!

Moving the deform sprite around

Too move the deform sprite we are going to use the mouse. So if you run the game now you will notice that the mouse cursor is hidden over the game window. Lets change that to make is visible.

In the Initialize function replace the // TODO comment with the following line of code:
this.IsMouseVisible = true;
Next we declare the Vector2 variable to store the mouse position (add this below the where we added the Texture2D declarations) and also declare the current mouse state:
private Vector2 mousePosition;
private MouseState currentMouseState;
Now we need to update the mousePosition, so lets create a new function do this for us:
protected void UpdateMouse()
{
currentMouseState = Mouse.GetState();

// This gets the mouse co-ordinates
// relative to the upper left of the game window
mousePosition = new Vector2(currentMouseState.X, currentMouseState.Y);
}
We then replace the // TODO: comment line in the Update function with a call to the UpdateMouse function we just created.
UpdateMouse();
And finally modify the draw call for the textureDeform sprite to use the mouse position:
spriteBatch.Draw(textureDeform, mousePosition, Color.White);
Hit F5 and try it out.

Deforming the Level

In order to visually deform the level we need to grab the texture data of the level and put it into an array, this will then hold the uint value for each co-ordinate of the texture, we then modify the array values of the section of the level where the user "pastes" the deform image down, by setting the level array to equal the deform sprites texture array. And finally we update the level texture with the new array of values.

So lets first load the deform pixel array, so below where you declared the currentMouseState add the following line:
private uint[] pixelDeformData;
Then in the LoadContent function add the following after you load the textureDeform:
// Declare an array to hold the pixel data
pixelDeformData = new uint[textureDeform.Width * textureDeform.Height];
// Populate the array
textureDeform.GetData(pixelDeformData, 0, textureDeform.Width * textureDeform.Height);
Remember because we can deform the level an endless amount of times we need to load the pixel array each time you try deform the level, so lets do that in a separate function which we can call when the mouse is clicked.
protected void DeformLevel()
{
// Declare an array to hold the pixel data
uint[] pixelLevelData = new uint[textureLevel.Width * textureLevel.Height];
// Populate the array
textureLevel.GetData(pixelLevelData, 0, textureLevel.Width * textureLevel.Height);

for (int x = 0; x &lt; textureDeform.Width; x++)
{
for (int y = 0; y &lt; textureDeform.Height; y++)
{
pixelLevelData[((int)mousePosition.X + x) + ((int)mousePosition.Y + y)
* textureLevel.Width] = pixelDeformData[x + y * textureDeform.Width];
}
}

// Update the texture with the changes made above
textureLevel.SetData(pixelLevelData);
}
Before we can test it we need to actually call the function by left clicking with the mouse.
So to add mouse click support we will need to call currentMouseState.LeftButton == ButtonState.Pressed, the problem with this is that it would then call the UpdateMouse function every frame whilst the mouse button is pressed down. We only want to call it just once per click.
To do this we need to update the "UpdateMouse" function to keep a history of the previous mouse state, and then check that the previous mouse state is set pressed and the current mouse state is set to released (which would represent a click).

Here is the updated function with the call to DeformLevel():

protected void UpdateMouse()
{
MouseState previousMouseState = currentMouseState;

currentMouseState = Mouse.GetState();

// This gets the mouse co-ordinates
// relative to the upper left of the game window
mousePosition = new Vector2(currentMouseState.X, currentMouseState.Y);

// Here we make sure that we only call the deform level function
// when the left mouse button is released
if (previousMouseState.LeftButton == ButtonState.Pressed &&
currentMouseState.LeftButton == ButtonState.Released)
{
DeformLevel();
}
}

Now you can fire up the game and give it a try... (Note! we haven't done any error checking when deforming the terrain, so make sure you only click in valid areas on the level and not too close to any of the edges, else you may go outside the bounds of the array)

You will probably end up with something like this when you click near the level:

So all we have done now is updated the texture of the level with a copy of the deform level, now lets test the colour of the pixel in the deform array to see if its an alpha pixel, if it is then we don't need to update the level pixel colour, that will prevent the square alpha layer shown above around the deform sprite. We can do the same pixel test on the level array as well, this will then prevent the deform sprite from being drawn where ever the cloud is visible.

put the pixelLevelData within and if statement like so:

// Here we check that the current co-ordinate of the deform texture is not an alpha value
// And that the current level texture co-ordinate is not an alpha value
if (pixelDeformData[x + y * textureDeform.Width] != 16777215
&& pixelLevelData[((int)mousePosition.X + x) +
((int)mousePosition.Y + y) * textureLevel.Width] != 16777215)
{

pixelLevelData[((int)mousePosition.X + x) + ((int)mousePosition.Y + y)
* textureLevel.Width] = pixelDeformData[x + y * textureDeform.Width];
}

Now if you run the game it will look something like this when you click around, which is much closer to the desired effect:

The last step is to make any pixel that is white in the deform array to alpha. And to make it a more robust we need to do some error checking to avoid any issues when deforming the level too close to the borders.




Here is the final function:

/// &lt;summary>
/// 16777215 = Alpha
/// 4294967295 = White
/// &lt;/summary>
protected void DeformLevel()
{
// Declare an array to hold the pixel data
uint[] pixelLevelData = new uint[textureLevel.Width * textureLevel.Height];
// Populate the array
textureLevel.GetData(pixelLevelData, 0, textureLevel.Width * textureLevel.Height);

for (int x = 0; x &lt; textureDeform.Width; x++)
{
for (int y = 0; y &lt; textureDeform.Height; y++)
{
// Do some error checking so we dont draw out of bounds of the array etc..
if (((mousePosition.X + x) &lt; (textureLevel.Width)) &&
((mousePosition.Y + y) &lt; (textureLevel.Height)))
{
if ((mousePosition.X + x) >= 0 && (mousePosition.Y + y) >= 0)
{
// Here we check that the current co-ordinate of the deform texture is not an alpha value
// And that the current level texture co-ordinate is not an alpha value
if (pixelDeformData[x + y * textureDeform.Width] != 16777215
&& pixelLevelData[((int)mousePosition.X + x) +
((int)mousePosition.Y + y) * textureLevel.Width] != 16777215)
{
// We then check to see if the deform texture's current pixel is white (4294967295)
if (pixelDeformData[x + y * textureDeform.Width] == 4294967295)
{
// It's white so we replace it with an Alpha pixel
pixelLevelData[((int)mousePosition.X + x) + ((int)mousePosition.Y + y)
* textureLevel.Width] = 16777215;
}
else
{
// Its not white so just set the level texture pixel to the deform texture pixel
pixelLevelData[((int)mousePosition.X + x) + ((int)mousePosition.Y + y)
* textureLevel.Width] = pixelDeformData[x + y * textureDeform.Width];
}
}
}
}
}
}

// Update the texture with the changes made above
textureLevel.SetData(pixelLevelData);
}

Now it should look like this (note how where the deforms occur there is still a nice black outline):

You could apply the same principles to add to the level as well, for example you could paste the dead body of a player to the terrain or a tombstone where the player died.

Here is the project source to download.

For pixel perfect collision detection you have two options, you could get the current colour value of the pixel each frame from the texture and check if it's an alpha or not (I think this would be pretty slow), or preferably you could create a bool collision array at the start of the level that sets all alpha pixels to false and level pixels to true. Then just update the collision array in the deform function where you set the level pixel to alpha.

Thursday, December 13, 2007

Right... back to Networking

Now that I can finally test my code im going to resume the networking blog posts.

So, on the last networking post we had added the ability to view available sessions. Now we are going to try attempt to join a session.

Lets start by editing the SearchScreen.cs and modify the OnSelectEntry function to add the available session as new buttons.

/// <summary>
/// Responds to user menu selections.
/// </summary>
protected override void OnSelectEntry(int entryIndex)
{
int numAvailableSessions = 0;

if (availableSessions != null)
{
numAvailableSessions = availableSessions.Count;
}

if (entryIndex < numAvailableSessions)
{
LIVESearch = NetworkSession.BeginJoin(availableSessions[entryIndex], new AsyncCallback(LoadLobbyScreen), null);
}
else
{
switch (entryIndex)
{
case 0:

Search();

break;

case 1:
// If the current selected index is less than the
// number of available sessions then attempt to join
ScreenManager.AddScreen(new XboxLiveScreen(this.networkSessionType));

break;
}
}
}

And then much like we did when we created a session we now have to write the callback function for the joining of the session:

/// <summary>
/// Callback to load the lobby screen with the session you have joined
/// </summary>
void LoadLobbyScreen(IAsyncResult result)
{
if ((LIVESearch != null) && LIVESearch.IsCompleted)
{
NetworkSession networkSession = null;
isEnabled = true;

try
{
networkSession = NetworkSession.EndJoin(result);
}
catch (NetworkException error)
{
MessageBoxScreen messageBox = new MessageBoxScreen("Failed to join the session");
messageBox.Accepted += FailedMessageBox;
messageBox.Cancelled += FailedMessageBox;
ScreenManager.AddScreen(messageBox);

System.Console.WriteLine("Failed to join the session: " + error.Message);
}

// A valid network session has been created
if (networkSession != null)
{
// Set a few properties:
networkSession.AllowHostMigration = true;
networkSession.AllowJoinInProgress = true;

LobbyScreen lobbyScreen = new LobbyScreen(networkSession);
lobbyScreen.ScreenManager = this.ScreenManager;
ScreenManager.AddScreen(lobbyScreen);
}

// Dont need this anymore so set it to null
LIVESearch = null;
}
}

/// <summary>
/// Event handler for when the user selects ok on the network-operation-failed message box.
/// </summary>
void FailedMessageBox(object sender, EventArgs e) { }

And hopefully thats it, we should now be able so join an available session and if successful we should end up in the lobby and be able to see the other player's tags.

Great new feature !

One really cool new feature (besides Network support) is now you can simply right click on your Windows game project and convert it, and vice versa.

Once converted both game projects are kept in sync, however, adding and removing code files must be done to each project individually.

Simply add a new code file to one project and then on the other right click and select Add -> Existing Item... and select your new file.

XNA Game Studio Connect for XBox 360

The Game Studio Connect has now also been released.
It can be found under:
Marketplace -> Game Store -> More... -> Genres -> Other -> XNA Creators Club -> XNA Game Studio Connect

XNA Game Studio 2 Finally Released

Well its finally here... which means I can almost continue start testing the networking.. now just waiting for the release of XNA Game Studio Connect which should be available on Xbox LIVE Marketplace shortly.

In the meantime im downloading the final release of XNA Game Studio 2.

Here is the link.

Thursday, December 6, 2007

XNA2 coming this week-end... hopefully

Well, the XNA 2 beta ends this Friday, so I assume that this Saturday will be the release of XNA 2.

Once it's released I will continue on the networking posts.

Tuesday, December 4, 2007

Searching for a Session

Hmm, so after trying out the search it's pretty obvious that its lacking any feedback to the user.

So lets update the SearchScreen.cs to display a bit more info like "Searching..." and "Unable to find a Session" and a "Back" link etc..

Lets move out the searching of a session from the Constructor into its own function so that we can call it from the menu when a user clicks search:

public SearchScreen(NetworkSessionType networkSessionType)
{
this.networkSessionType = networkSessionType;

Search();
}

/// <summary>
/// Searches for available network sessions
/// </summary>
private void Search()
{
MenuEntries.Clear();
MenuEntries.Add("Searching...");

// Disable movement
this.isEnabled = false;

NetworkSessionProperties searchProperties = new NetworkSessionProperties();

if (this.networkSessionType == NetworkSessionType.PlayerMatch)
{
LIVESearch = NetworkSession.BeginFind(NetworkSessionType.PlayerMatch, maxLocalPlayers, searchProperties,
new AsyncCallback(SessionsFound), null);
}
if (this.networkSessionType == NetworkSessionType.SystemLink)
{
LIVESearch = NetworkSession.BeginFind(NetworkSessionType.SystemLink, maxLocalPlayers, searchProperties,
new AsyncCallback(SessionsFound), null);
}
}

/// <summary>
/// Responds to user menu selections.
/// </summary>
protected override void OnSelectEntry(int entryIndex)
{
switch (entryIndex)
{
case 0:

Search();

break;

case 1:
// Exit
ScreenManager.AddScreen(new XboxLiveScreen(this.networkSessionType));

break;
}
}

Modify the SessionFound callback function to re-enable the menu movement and also display menu entries if the available sessions is zero:

public void SessionsFound(IAsyncResult result)
{
// Re-enable movement in the menu
this.isEnabled = true;

if ((LIVESearch != null) && LIVESearch.IsCompleted)
{
availableSessions = NetworkSession.EndFind(result);
MenuEntries.Clear();

if (availableSessions != null)
{
foreach (AvailableNetworkSession availableSession in availableSessions)
{
// Only show sessions where there are available slots
if (availableSession.CurrentGamerCount < 8)
{
MenuEntries.Add(availableSession.HostGamertag + " (" +
availableSession.CurrentGamerCount.ToString() + "/8)");
}

// Limit the number of results to return
if (MenuEntries.Count >= 5)
{
break;
}
}

// Check if no sessions were found
if (availableSessions.Count == 0)
{
MenuEntries.Add("No results found, try again?");
MenuEntries.Add("Back");
}
}

LIVESearch = null;
}
}

Give it a try, im still unable to test with available sessions so let me know if you have a problem with that bit. We still need to make it that if you select an available session it will try join the session.

System Link Support - Phase 2: Searching for session

To support both types of network modes is easy, all we have to do is modify the SearchScreen.cs constructor to allow passing in of NetworkSessionType, and then check which type of network session has been passed in and search depending on that:

NetworkSessionType networkSessionType;

public SearchScreen(NetworkSessionType networkSessionType)
{
this.networkSessionType = networkSessionType;

NetworkSessionProperties searchProperties = new NetworkSessionProperties();

if (this.networkSessionType == NetworkSessionType.PlayerMatch)
{
LIVESearch = NetworkSession.BeginFind(NetworkSessionType.PlayerMatch, maxLocalPlayers, searchProperties,
new AsyncCallback(SessionsFound), null);
}
if (this.networkSessionType == NetworkSessionType.SystemLink)
{
LIVESearch = NetworkSession.BeginFind(NetworkSessionType.SystemLink, maxLocalPlayers, searchProperties,
new AsyncCallback(SessionsFound), null);
}
}

Finally go back to the XboxLiveScreen.cs and edit the OnSelectEntry function and modify this line under case 1:
ScreenManager.AddScreen();
to:
ScreenManager.AddScreen(new SearchScreen(networkSessionType));
And thats it.

Other good news is that the new Dashboard Update released today will now be able to show your XNA 2.0 games amongst your other games (so you can fire it up without having to first open the XNA Game Launcher like before) here is the official link.

Monday, December 3, 2007

System Link Support

Well because it's pretty difficult to test LIVE Networking in the beta i've decided it's time to add System Link support, so lets go back to our XBoxLiveScreen class and modify the Constructor to pass in the NetworkSessionType and display the appropriate menu items for the selected network type:

NetworkSessionType networkSessionType;

public XboxLiveScreen(NetworkSessionType networkSessionType)
{
this.networkSessionType = networkSessionType;

if (this.networkSessionType == NetworkSessionType.PlayerMatch)
{
MenuEntries.Add("Create XBox LIVE Session");
MenuEntries.Add("Join XBox LIVE Session");
}
if (this.networkSessionType == NetworkSessionType.SystemLink)
{
MenuEntries.Add("Create System Link Session");
MenuEntries.Add("Join System Link Session");
}

MenuEntries.Add("Back");
}

Now we will need to go back to the LiveOptionMenuScreen and modify the line:
ScreenManager.AddScreen(new XboxLiveScreen());
to pass in the session type, you will also need to add Microsoft.Xna.Framework.Net to the using statements :
ScreenManager.AddScreen(new XboxLiveScreen(NetworkSessionType.PlayerMatch));
And under case 2: lets add the following line:
ScreenManager.AddScreen(new XboxLiveScreen(NetworkSessionType.SystemLink));
You will also need to the LobbyScreen.cs file and modify this line and move it to above where we checking if the networkSession is null:
ScreenManager.AddScreen(new XboxLiveScreen();
To the following (here we passing in the current network session type so that it goes back to the correct menu):
ScreenManager.AddScreen(new XboxLiveScreen(networkSession.SessionType));
Ok, so if we had to run the following and tried to login to System Link (without signing into LIVE first) we would get this error:

"A signed in gamer profile is required to perform this operation. A Live profile may also be required. There are no profiles currently signed in, or the profile is not signed in to Live."

Basically what this means is that you need to first login with a normal gamer profile or a Silver LIVE gamer profile, so lets do a check to see if we have at least got a normal gamer profile loaded before trying to move into the next screen:

[LiveOptionsMenuScreen.cs]
case 2:
// System Link
if (Gamer.SignedInGamers.Count < 1)
{
if (!Guide.IsVisible)
{
Guide.ShowSignIn(1, true);
}
}
else
{
ScreenManager.AddScreen(new XboxLiveScreen(NetworkSessionType.SystemLink));
}

break;

So now we support Creating a Session for both LIVE and System Link network types.

Next we will implement the Searching of a System Link session similar to how we just did the above.

Networking Step 5: Searching for a LIVE Session

Unfortunately im unable to test this just yet (as I don't know anybody who has a beta key to host a game for me on LIVE) so I hope it works.

I'll keep it as simple as possible for now, so we are going to add another new screen to search for available sessions:

#region using
using System;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework;
#endregion

namespace GameStateManagement
{
class SearchScreen : MenuScreen
{
AvailableNetworkSessionCollection availableSessions;
int maxLocalPlayers = 2;

// Used for making the Searching Asynchronous.
IAsyncResult LIVESearch = null;

public SearchScreen()
{
NetworkSessionProperties searchProperties = new NetworkSessionProperties();

LIVESearch = NetworkSession.BeginFind(NetworkSessionType.PlayerMatch, maxLocalPlayers, searchProperties,
new AsyncCallback(SessionsFound), null);
}

/// <summary>
/// Responds to user menu selections.
/// </summary>
protected override void OnSelectEntry(int entryIndex)
{

}

/// <summary>
/// When the user cancels the options screen, go back to the main menu.
/// </summary>
protected override void OnCancel()
{
ExitScreen();
}

/// <summary>
/// Callback to receive the search results of the available LIVE Sessions
/// </summary>
public void SessionsFound(IAsyncResult result)
{
if ((LIVESearch != null) && LIVESearch.IsCompleted)
{
availableSessions = NetworkSession.EndFind(result);
MenuEntries.Clear();

if (availableSessions != null)
{
foreach (AvailableNetworkSession availableSession in availableSessions)
{
// Only show sessions where there are available slots
if (availableSession.CurrentGamerCount < 8)
{
MenuEntries.Add(availableSession.HostGamertag + " (" +
availableSession.CurrentGamerCount.ToString() + "/8)");
}

// Limit the number of results to return
if (MenuEntries.Count >= 5)
{
break;
}
}
}

LIVESearch = null;
}
}
}
}

Finally lets modify the XBoxLiveScreen.cs and add to case 1 in the OnSelectEntry function the following line:
ScreenManager.AddScreen(new SearchScreen());

And that's it for now, I'm going to move onto System Link next so that I can at least test what im writing.

Sunday, December 2, 2007

Networking Step 4: The Lobby Phase 2.

Right! Now we are going to try display the connected players names in the Lobby and if they are ready or not.

Lets override the Draw function, so that we can render the connected players:

/// <summary>
/// Draw the lobby screen.
/// </summary>
/// <param name="gameTime"></param>
public override void Draw(Microsoft.Xna.Framework.GameTime gameTime)
{
ScreenManager.SpriteBatch.Begin();

// Display all the connected players
if (networkSession != null)
{
for (int i = 0; i < networkSession.AllGamers.Count; i++)
{
// Display the players gamer tag followed by true or false (true = ready to play)
ScreenManager.SpriteBatch.DrawString(ScreenManager.Font, networkSession.AllGamers[i].Gamertag + " Ready: "
+ networkSession.AllGamers[i].IsReady, new Vector2(500, 120 + (i * 20)), Color.White);
}
}

ScreenManager.SpriteBatch.End();

base.Draw(gameTime);
}

That was pretty simple, now if you try it out you should see your XBox Live GamerTag displayed with a True next to it, next we need to allow players to join this session so that we can test that it displays all connected GamerTags and that it actually updates the display correctly when the player is ready sets there status to ready to play.

Networking Step 3: The Lobby Phase 1.

Ok, this is going to be where the majority of the networking features happen, so im going to try break it up into different phases and I'll try make it that at the end of each phase you will still be able to build your project without issues and have something to try out.

Lets create a new class under the Screens subfolder called "LobbyScreen.cs", this time we will pass into the constructor the networkSession from the XBoxLiveScreen where we initially created it. We will also create some network events that we will make use of later.

We also want to add a bit more functionality into the Lobby so we are going to overrider the Update function. Here we will check to see what state the Lobby is currently in i.e. what to do under the different situations which would probably be:
Host
  • Waiting for all players to click ready
  • Ready to start game
Joined Player
  • Click Ready
  • Waiting for host to start game
Here is what I have so far:

#region using
using Microsoft.Xna.Framework.Net;
using System;
#endregion

namespace GameStateManagement
{
class LobbyScreen : MenuScreen
{
NetworkSession networkSession;

/// <summary>
/// Constructs a new LobbyScreen object.
/// </summary>
public LobbyScreen(NetworkSession networkSession)
: base()
{
this.networkSession = networkSession;

// add the single menu entry
MenuEntries.Add("Play Game");
MenuEntries.Add("Close Lobby");

// set the transition time
TransitionOnTime = TimeSpan.FromSeconds(1.0);
TransitionOffTime = TimeSpan.FromSeconds(0.0);

// set the networking events
networkSession.GamerJoined += new EventHandler<GamerJoinedEventArgs>(networkSession_GamerJoined);
networkSession.GamerLeft += new EventHandler<GamerLeftEventArgs>(networkSession_GamerLeft);
networkSession.GameStarted += new EventHandler<GameStartedEventArgs>(networkSession_GameStarted);
networkSession.SessionEnded += new EventHandler<NetworkSessionEndedEventArgs>(networkSession_SessionEnded);
}

/// <summary>
/// Responds to user menu selections.
/// </summary>
protected override void OnSelectEntry(int entryIndex)
{
switch (entryIndex)
{
case 0:

// Play the game (only if everyone is ready)
if ((networkSession != null) && networkSession.IsHost && networkSession.IsEveryoneReady
&& networkSession.SessionState == NetworkSessionState.Lobby)
{
networkSession.StartGame();
}
break;

case 1:
// Exit
if (networkSession != null)
{
networkSession.Dispose();
networkSession = null;
}

ScreenManager.AddScreen(new XboxLiveScreen());
break;
}
}

/// <summary>
/// Updates the lobby. This method checks the GameScreen.IsActive
/// property, so the game will stop updating when the pause menu is active,
/// or if you tab away to a different application.
/// </summary>
public override void Update(Microsoft.Xna.Framework.GameTime gameTime, bool otherScreenHasFocus,
bool coveredByOtherScreen)
{
if (networkSession != null)
{
// update the network session
networkSession.Update();
}

// update the menu entry text
if (otherScreenHasFocus == false)
{
if (networkSession.LocalGamers.Count > 0 && networkSession.SessionState == NetworkSessionState.Lobby)
{
if (networkSession.LocalGamers.Count > 0 && networkSession.SessionState == NetworkSessionState.Lobby)
{
if (!networkSession.LocalGamers[0].IsReady)
{
if (!networkSession.IsHost)
{
MenuEntries[0] = "Press X to Mark as Ready";
}
else
{
// You are the host so no need for you to manually tick ready... as you will start the game
networkSession.LocalGamers[0].IsReady = true;
}
}
else if (!networkSession.IsEveryoneReady)
{
MenuEntries[0] = "Waiting for all players to mark as ready...";
}
else if (!networkSession.IsHost)
{
MenuEntries[0] = "Waiting for host to start the game...";
}
else
{
MenuEntries[0] = "Press A to Start Game";
}
}
else if (networkSession.SessionState == NetworkSessionState.Playing)
{
MenuEntries[0] = "Game starting...";
}
}

// if the game is playing then start up
if (networkSession.SessionState == NetworkSessionState.Playing)
{
MenuEntries[0] = "Game is now in progress..";
// This is where we will lod the Gameplay Screen once we get around to it
}
}

base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
}

/// <summary>
/// When the user cancels the main menu
/// </summary>
protected override void OnCancel()
{
}

/// <summary>
/// Handle the start of the game session.
/// </summary>
void networkSession_GameStarted(object sender, GameStartedEventArgs e)
{
}

/// <summary>
/// Handle a new player joining the session.
/// </summary>
void networkSession_GamerJoined(object sender, GamerJoinedEventArgs e)
{
}

/// <summary>
/// Handle a player leaving the session.
/// </summary>
void networkSession_GamerLeft(object sender, GamerLeftEventArgs e)
{
}

/// <summary>
/// Handle the end of the network session.
/// </summary>
void networkSession_SessionEnded(object sender, NetworkSessionEndedEventArgs e)
{
}
}
}

In Phase 2 we will start looking at displaying the list of connected players and their ready status.

Saturday, December 1, 2007

Networking Step 2: Create/Join Session

Now we want to be able to select from the following options:
Under XBox Live:
- Create XBox LIVE Session
- Join Xbox LIVE Session
So lets go ahead and add a new menu screen like we did before for the Multiplayer menu screen.
I'll call this screen XboxLiveScreen.cs very similar setup to before:

#region Using Statements
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.GamerServices;
#endregion

namespace GameStateManagement
{
class XboxLiveScreen : MenuScreen
{
public XboxLiveScreen()
{
MenuEntries.Add("Create XBox LIVE Session");
MenuEntries.Add("Join XBox LIVE Session");

MenuEntries.Add("Back");
}

/// <summary>
/// Responds to user menu selections.
/// </summary>
protected override void OnSelectEntry(int entryIndex)
{
switch (entryIndex)
{
case 0:
// Create XBox LIVE Session
break;
case 1:
// Join XBox LIVE Session
break;
case 2:
// Go back to Multiplayer Menu
ScreenManager.AddScreen(new LiveOptionsMenuScreen());
break;
}
}

/// <summary>
/// When the user cancels the options screen, go back to the main menu.
/// </summary>
protected override void OnCancel()
{
ExitScreen();
}
}
}

Now go back to the LiveOptionsMenuScreen and add the following to the OnSelectEntry function under case 0 (the part in italic bold is what needs to be added to call the Xbox Live screen):

// If not signed in lets display the Sign-in Guide
if (!isSignedInToLive)
{
if (!Guide.IsVisible)
{
Guide.ShowSignIn(1, true);
}
}
else
{
ScreenManager.AddScreen(new XboxLiveScreen());
}

You should also change case 3 to this:

// Go back to the main menu.
ScreenManager.AddScreen(new MainMenuScreen());
break;

the above change just makes sure that you dont end up going to the previous screen (like Xbox Live Screen) but instead go to the Main Menu.

Now we want to create the network session, whilst its busy trying to create it the session it will freeze the game which isn't a great user experience. Instead lets use IAsyncResult to create it asynchronously, but whilst its creating we dont want the user to be able to move around the menu either, so we will need to introduce a way of disabling the joypad/keyboard from moving threw the menu options whilst the async task is busy.

Lets edit the MenuScreen.cs by adding a new property:

bool
_isEnabled = true;

public bool isEnabled
{
get { return _isEnabled; }
set { _isEnabled = value; }
}

Then in the HandleInput function surround everything with a if (isEnabled) so that the user cannot move around the menu whilst its disabled.

Now lets go back to XBoxLiveScreen class and add the following field:
IAsyncResult LIVEResult = null;
In the OnSelectEntry function under case 0: add the following:

this.isEnabled = false;

LIVEResult = NetworkSession.BeginCreate(NetworkSessionType.PlayerMatch, 2, 8, new AsyncCallback(LoadLobbyScreen), null);
Feel free to change the parameters above (the 2 = local players and 8 = max network players) to whatever you want for your project. You will also want to let the player know that something has happend when the pressed the button to start the LIVE session... so lets change the menu text to the following:
MenuEntries[0] = "Creating Session...";
Next we need to add the Callback function we used above called "LobbyLoadScreen"... add this to you XBoxLiveScreen class:

///
/// Callback to load the lobby screen with the new session.
///
void LoadLobbyScreen(IAsyncResult result)
{
if ((LIVEResult != null) && LIVEResult.IsCompleted)
{
networkSession = null;
isEnabled = true;

try
{
networkSession = NetworkSession.EndCreate(result);
}
catch (NetworkException error)
{
MessageBoxScreen messageBox = new MessageBoxScreen("Failed to create the session");
messageBox.Accepted += FailedMessageBox;
messageBox.Cancelled += FailedMessageBox;
ScreenManager.AddScreen(messageBox);

System.Console.WriteLine("Failed to create the session: " + error.Message);
}

MenuEntries[0] = "Session Created";

// A valid network session has been created
if (networkSession != null)
{
// Set a few properties:
networkSession.AllowHostMigration = true;
networkSession.AllowJoinInProgress = true;
// ** This bit we will add in the next article so lets leave it
// commented out for now so the project will still build **
//LobbyScreen lobbyScreen = new LobbyScreen(networkSession);
//lobbyScreen.ScreenManager = this.ScreenManager;
//ScreenManager.AddScreen(lobbyScreen);
}

// Dont need this anymore so set it to null
LIVEResult = null;
}
}

We will also need to end the above Session if we decide to go back to the Multiplayer Menu... so lets add this in to the OnSelectEntry function inside XBoxLiveScreen class:

case 2:
// Go back to Multiplayer Menu
if (networkSession != null)
{
if (networkSession.AllGamers.Count == 1)
{
if (networkSession.SessionState == NetworkSessionState.Playing)
{
networkSession.EndGame();
}
}

networkSession.Dispose();
networkSession = null;
}



Ok.. so now what?

At this stage in the menu of a game you would be ready to Host a LIVE game.... to do this you would need a Lobby whereby you can customize the session you are hosting, these would generally be Network options like:
  • Max Players
  • Number of reserved entries for friends
Other options would be more game specific like:
  • Level / Map
  • Match type (Capture the Flag, Deathmatch, Team etc..)
And the Lobby is a good place to get all the players ready before sttarting the game.

So next we will create a LobbyScreen.cs file where we will get into more detail on networking.

Networking Step 1: SignIn to LIVE

Ok, so to play a network game XNA uses Microsofts XBox LIVE service which is now supported on both PC and XBox 360. So we going to need to get our game to give the option of signing into the LIVE network.

Lets start off by adding the Gamer Services game component which is required before networking API calls can be completed.

So lets go and add this to our Game.cs class in the constructor just before we setup the ScreenManager:
Components.Add(new GamerServicesComponent(this));
Position the cursor after the word GamerServicesComponent and then press "CTRL" + "." and you should be prompted to add the GamerServices (see image below)

Ok, so the best place to sign-in would probably be on when selecting XBox Live from the menu... so lets go back there and add the following: (this is in the LiveOptionsMenusScreen.cs)

Edit the OnSelectEntry to have the following:

/// <summary>
/// Responds to user menu selections.
/// </summary>
protected override void OnSelectEntry(int entryIndex)
{
switch (entryIndex)
{
case 0:
// XBox LIVE

// Check to see if we have an account thats signed into LIVE
bool isSignedInToLive = false;
if (Gamer.SignedInGamers.Count > 0)
{
foreach (SignedInGamer signedInGamer in Gamer.SignedInGamers)
{
if (signedInGamer.IsSignedInToLive)
{
isSignedInToLive = true;
break;
}
}
}

// If not signed in lets display the Sign-in Guide
if (!isSignedInToLive)
{
if (!Guide.IsVisible)
{
Guide.ShowSignIn(1, true);
}
}

break;

case 1:
// Split Screen

break;

case 2:
// System Link

break;

case 3:
// Back
// Go back to the main menu.
ExitScreen();
break;
}
}

So basically what we doing here is checking to see if a LIVE account is signed in... if not display the Sign In window (which is created and handled by XNA).

So now run the game and go to Multiplayer -> XBox Live... if its your first "Games for Windows" game you playing or if you have Auto Sign-in set to disabled, then you'll be asked to SignIn and be presented with something similar to this:
Otherwise if you have signed in before it should automatically sign you in when the game starts (it will try sign you in from the moment "Components.Add(new GamerServicesComponent(this));" is called)
If successful you should see a toaster popup saying you have signed in after a few seconds of loading the game... similar to this:
Once Signed in you can now use the Xbox button on your 360 joypad or the HOME key on your keyboard to access your GamerTag Profile and messages etc..

Getting your game ready to support multiplayer.

Ok, so whats the point of using XNA 2 over XNA 1 if we not going to implement its major new feature... Network (Xbox Live) Support!

So lets modify the Game State Management sample a bit to make it more suitable for a Network game.

  1. Open the project you downloaded earlier (see previous post)
  2. Edit the MainMenuScreen.cs file (under Screens)
Modify the MainMenuScreen() to look like this:
/// <summary>
/// Constructor fills in the menu contents.
/// </summary>
public MainMenuScreen()
{
MenuEntries.Add("Single Player");
MenuEntries.Add("Multiplayer");
MenuEntries.Add("Options");
MenuEntries.Add("Exit");
}

Modify the OnSelectEntry(int entryIndex) to look like this:

/// <summary>
/// Responds to user menu selections.
/// </summary>
protected override void OnSelectEntry(int entryIndex)
{
switch (entryIndex)
{
case 0:

// Play the game in single player mode
LoadingScreen.Load(ScreenManager, LoadGameplayScreen, true);
break;

case 1:

// Go to multiplayer selection screen
// On this screen you will select either on Local or Live
ScreenManager.AddScreen(new LiveOptionsMenuScreen());
break;

case 2:

// Go to the options screen.
ScreenManager.AddScreen(new OptionsMenuScreen());
break;

case 3:

// Exit the sample.
OnCancel();
break;
}
}

3. Create a new class under the Screens subfolder called "LiveOptionsMenuScreen.cs"
4. Edit the LiveOptionsMenuScreen.cs file:

#region Using Statements
using Microsoft.Xna.Framework;
#endregion

namespace GameStateManagement
{
class LiveOptionsMenuScreen : MenuScreen
{
/// <summary>
/// Constructor populates the menu with empty strings: the real values
/// are filled in by the Update method to reflect the changing settings.
/// </summary>
public LiveOptionsMenuScreen()
{
MenuEntries.Add("XBox LIVE");
MenuEntries.Add("Split Screen");
MenuEntries.Add("System Link");
MenuEntries.Add("Back");
}

/// <summary>
/// Responds to user menu selections.
/// </summary>
protected override void OnSelectEntry(int entryIndex)
{
switch (entryIndex)
{
case 0:
// XBox LIVE

break;

case 1:
// Split Screen

break;

case 2:
// System Link

break;

case 3:
// Back
// Go back to the main menu.
ExitScreen();
break;
}
}

/// <summary>
/// When the user cancels the options screen, go back to the main menu.
/// </summary>
protected override void OnCancel()
{
ExitScreen();
}
}
}

Now if you press F5 to Build the game you should see something like this:

Clicking Multiplayer should now take you to a screen like this: