#region File Description //----------------------------------------------------------------------------- // ClientServerGame.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- #endregion #region Using Statements using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Net; #endregion namespace ClientServer { /// /// Sample showing how to implement a simple multiplayer /// network session, using a client/server network topology. /// public class ClientServerGame : Microsoft.Xna.Framework.Game { #region Fields const int screenWidth = 1067; const int screenHeight = 600; const int maxGamers = 16; const int maxLocalGamers = 4; GraphicsDeviceManager graphics; SpriteBatch spriteBatch; SpriteFont font; KeyboardState currentKeyboardState; GamePadState currentGamePadState; NetworkSession networkSession; PacketWriter packetWriter = new PacketWriter(); PacketReader packetReader = new PacketReader(); string errorMessage; #endregion #region Initialization public ClientServerGame() { graphics = new GraphicsDeviceManager(this); graphics.PreferredBackBufferWidth = screenWidth; graphics.PreferredBackBufferHeight = screenHeight; Content.RootDirectory = "Content"; Components.Add(new GamerServicesComponent(this)); } /// /// Load your content. /// protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); font = Content.Load("Font"); } #endregion #region Update /// /// Allows the game to run logic. /// protected override void Update(GameTime gameTime) { HandleInput(); if (networkSession == null) { // If we are not in a network session, update the // menu screen that will let us create or join one. UpdateMenuScreen(); } else { // If we are in a network session, update it. UpdateNetworkSession(); } base.Update(gameTime); } /// /// Menu screen provides options to create or join network sessions. /// void UpdateMenuScreen() { if (IsActive) { if (Gamer.SignedInGamers.Count == 0) { // If there are no profiles signed in, we cannot proceed. // Show the Guide so the user can sign in. Guide.ShowSignIn(maxLocalGamers, false); } else if (IsPressed(Keys.A, Buttons.A)) { // Create a new session? CreateSession(); } else if (IsPressed(Keys.B, Buttons.B)) { // Join an existing session? JoinSession(); } } } /// /// Starts hosting a new network session. /// void CreateSession() { DrawMessage("Creating session..."); try { networkSession = NetworkSession.Create(NetworkSessionType.SystemLink, maxLocalGamers, maxGamers); HookSessionEvents(); } catch (Exception e) { errorMessage = e.Message; } } /// /// Joins an existing network session. /// void JoinSession() { DrawMessage("Joining session..."); try { // Search for sessions. using (AvailableNetworkSessionCollection availableSessions = NetworkSession.Find(NetworkSessionType.SystemLink, maxLocalGamers, null)) { if (availableSessions.Count == 0) { errorMessage = "No network sessions found."; return; } // Join the first session we found. networkSession = NetworkSession.Join(availableSessions[0]); HookSessionEvents(); } } catch (Exception e) { errorMessage = e.Message; } } /// /// After creating or joining a network session, we must subscribe to /// some events so we will be notified when the session changes state. /// void HookSessionEvents() { networkSession.GamerJoined += GamerJoinedEventHandler; networkSession.SessionEnded += SessionEndedEventHandler; } /// /// This event handler will be called whenever a new gamer joins the session. /// We use it to allocate a Tank object, and associate it with the new gamer. /// void GamerJoinedEventHandler(object sender, GamerJoinedEventArgs e) { int gamerIndex = networkSession.AllGamers.IndexOf(e.Gamer); e.Gamer.Tag = new Tank(gamerIndex, Content, screenWidth, screenHeight); } /// /// Event handler notifies us when the network session has ended. /// void SessionEndedEventHandler(object sender, NetworkSessionEndedEventArgs e) { errorMessage = e.EndReason.ToString(); networkSession.Dispose(); networkSession = null; } /// /// Updates the state of the network session, moving the tanks /// around and synchronizing their state over the network. /// void UpdateNetworkSession() { // Read inputs for locally controlled tanks, and send them to the server. foreach (LocalNetworkGamer gamer in networkSession.LocalGamers) { UpdateLocalGamer(gamer); } // If we are the server, update all the tanks and transmit // their latest positions back out over the network. if (networkSession.IsHost) { UpdateServer(); } // Pump the underlying session object. networkSession.Update(); // Make sure the session has not ended. if (networkSession == null) return; // Read any incoming network packets. foreach (LocalNetworkGamer gamer in networkSession.LocalGamers) { if (gamer.IsHost) { ServerReadInputFromClients(gamer); } else { ClientReadGameStateFromServer(gamer); } } } /// /// Helper for updating a locally controlled gamer. /// void UpdateLocalGamer(LocalNetworkGamer gamer) { // Look up what tank is associated with this local player, // and read the latest user inputs for it. The server will // later use these values to control the tank movement. Tank localTank = gamer.Tag as Tank; ReadTankInputs(localTank, gamer.SignedInGamer.PlayerIndex); // Only send if we are not the server. There is no point sending packets // to ourselves, because we already know what they will contain! if (!networkSession.IsHost) { // Write our latest input state into a network packet. packetWriter.Write(localTank.TankInput); packetWriter.Write(localTank.TurretInput); // Send our input data to the server. gamer.SendData(packetWriter, SendDataOptions.InOrder, networkSession.Host); } } /// /// This method only runs on the server. It calls Update on all the /// tank instances, both local and remote, using inputs that have /// been received over the network. It then sends the resulting /// tank position data to everyone in the session. /// void UpdateServer() { // Loop over all the players in the session, not just the local ones! foreach (NetworkGamer gamer in networkSession.AllGamers) { // Look up what tank is associated with this player. Tank tank = gamer.Tag as Tank; // Update the tank. tank.Update(); // Write the tank state into the output network packet. packetWriter.Write(gamer.Id); packetWriter.Write(tank.Position); packetWriter.Write(tank.TankRotation); packetWriter.Write(tank.TurretRotation); } // Send the combined data for all tanks to everyone in the session. LocalNetworkGamer server = (LocalNetworkGamer)networkSession.Host; server.SendData(packetWriter, SendDataOptions.InOrder); } /// /// This method only runs on the server. It reads tank inputs that /// have been sent over the network by a client machine, storing /// them for later use by the UpdateServer method. /// void ServerReadInputFromClients(LocalNetworkGamer gamer) { // Keep reading as long as incoming packets are available. while (gamer.IsDataAvailable) { NetworkGamer sender; // Read a single packet from the network. gamer.ReceiveData(packetReader, out sender); if (!sender.IsLocal) { // Look up the tank associated with whoever sent this packet. Tank remoteTank = sender.Tag as Tank; // Read the latest inputs controlling this tank. remoteTank.TankInput = packetReader.ReadVector2(); remoteTank.TurretInput = packetReader.ReadVector2(); } } } /// /// This method only runs on client machines. It reads /// tank position data that has been computed by the server. /// void ClientReadGameStateFromServer(LocalNetworkGamer gamer) { // Keep reading as long as incoming packets are available. while (gamer.IsDataAvailable) { NetworkGamer sender; // Read a single packet from the network. gamer.ReceiveData(packetReader, out sender); // This packet contains data about all the players in the session. // We keep reading from it until we have processed all the data. while (packetReader.Position < packetReader.Length) { // Read the state of one tank from the network packet. byte gamerId = packetReader.ReadByte(); Vector2 position = packetReader.ReadVector2(); float tankRotation = packetReader.ReadSingle(); float turretRotation = packetReader.ReadSingle(); // Look up which gamer this state refers to. NetworkGamer remoteGamer = networkSession.FindGamerById(gamerId); // This might come back null if the gamer left the session after // the host sent the packet but before we received it. If that // happens, we just ignore the data for this gamer. if (remoteGamer != null) { // Update our local state with data from the network packet. Tank tank = remoteGamer.Tag as Tank; tank.Position = position; tank.TankRotation = tankRotation; tank.TurretRotation = turretRotation; } } } } #endregion #region Draw /// /// This is called when the game should draw itself. /// protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); if (networkSession == null) { // If we are not in a network session, draw the // menu screen that will let us create or join one. DrawMenuScreen(); } else { // If we are in a network session, draw it. DrawNetworkSession(); } base.Draw(gameTime); } /// /// Draws the startup screen used to create and join network sessions. /// void DrawMenuScreen() { string message = string.Empty; if (!string.IsNullOrEmpty(errorMessage)) message += "Error:\n" + errorMessage.Replace(". ", ".\n") + "\n\n"; message += "A = create session\n" + "B = join session"; spriteBatch.Begin(); spriteBatch.DrawString(font, message, new Vector2(161, 161), Color.Black); spriteBatch.DrawString(font, message, new Vector2(160, 160), Color.White); spriteBatch.End(); } /// /// Draws the state of an active network session. /// void DrawNetworkSession() { spriteBatch.Begin(); // For each person in the session... foreach (NetworkGamer gamer in networkSession.AllGamers) { // Look up the tank object belonging to this network gamer. Tank tank = gamer.Tag as Tank; // Draw the tank. tank.Draw(spriteBatch); // Draw a gamertag label. string label = gamer.Gamertag; Color labelColor = Color.Black; Vector2 labelOffset = new Vector2(100, 150); if (gamer.IsHost) label += " (server)"; // Flash the gamertag to yellow when the player is talking. if (gamer.IsTalking) labelColor = Color.Yellow; spriteBatch.DrawString(font, label, tank.Position, labelColor, 0, labelOffset, 0.6f, SpriteEffects.None, 0); } spriteBatch.End(); } /// /// Helper draws notification messages before calling blocking network methods. /// void DrawMessage(string message) { if (!BeginDraw()) return; GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); spriteBatch.DrawString(font, message, new Vector2(161, 161), Color.Black); spriteBatch.DrawString(font, message, new Vector2(160, 160), Color.White); spriteBatch.End(); EndDraw(); } #endregion #region Handle Input /// /// Handles input. /// private void HandleInput() { currentKeyboardState = Keyboard.GetState(); currentGamePadState = GamePad.GetState(PlayerIndex.One); // Check for exit. if (IsActive && IsPressed(Keys.Escape, Buttons.Back)) { Exit(); } } /// /// Checks if the specified button is pressed on either keyboard or gamepad. /// bool IsPressed(Keys key, Buttons button) { return (currentKeyboardState.IsKeyDown(key) || currentGamePadState.IsButtonDown(button)); } /// /// Reads input data from keyboard and gamepad, and stores /// it into the specified tank object. /// void ReadTankInputs(Tank tank, PlayerIndex playerIndex) { // Read the gamepad. GamePadState gamePad = GamePad.GetState(playerIndex); Vector2 tankInput = gamePad.ThumbSticks.Left; Vector2 turretInput = gamePad.ThumbSticks.Right; // Read the keyboard. KeyboardState keyboard = Keyboard.GetState(playerIndex); if (keyboard.IsKeyDown(Keys.Left)) tankInput.X = -1; else if (keyboard.IsKeyDown(Keys.Right)) tankInput.X = 1; if (keyboard.IsKeyDown(Keys.Up)) tankInput.Y = 1; else if (keyboard.IsKeyDown(Keys.Down)) tankInput.Y = -1; if (keyboard.IsKeyDown(Keys.A)) turretInput.X = -1; else if (keyboard.IsKeyDown(Keys.D)) turretInput.X = 1; if (keyboard.IsKeyDown(Keys.W)) turretInput.Y = 1; else if (keyboard.IsKeyDown(Keys.S)) turretInput.Y = -1; // Normalize the input vectors. if (tankInput.Length() > 1) tankInput.Normalize(); if (turretInput.Length() > 1) turretInput.Normalize(); // Store these input values into the tank object. tank.TankInput = tankInput; tank.TurretInput = turretInput; } #endregion } #region Entry Point /// /// The main entry point for the application. /// static class Program { static void Main() { using (ClientServerGame game = new ClientServerGame()) { game.Run(); } } } #endregion }