#region File Description //----------------------------------------------------------------------------- // NetworkPredictionGame.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 NetworkPrediction { /// /// Sample showing how to use prediction and smoothing to compensate /// for the effects of network latency, and for the low packet send /// rates needed to conserve network bandwidth. /// public class NetworkPredictionGame : Microsoft.Xna.Framework.Game { #region Constants const int screenWidth = 1067; const int screenHeight = 600; const int maxGamers = 16; const int maxLocalGamers = 4; #endregion #region Fields // Graphics objects. GraphicsDeviceManager graphics; SpriteBatch spriteBatch; SpriteFont font; // Current and previous input states. KeyboardState currentKeyboardState; GamePadState currentGamePadState; KeyboardState previousKeyboardState; GamePadState previousGamePadState; // Network objects. NetworkSession networkSession; PacketWriter packetWriter = new PacketWriter(); PacketReader packetReader = new PacketReader(); string errorMessage; // What kind of network latency and packet loss are we simulating? enum NetworkQuality { Typical, // 100 ms latency, 10% packet loss Poor, // 200 ms latency, 20% packet loss Perfect, // 0 latency, 0% packet loss } NetworkQuality networkQuality; // How often should we send network packets? int framesBetweenPackets = 6; // How recently did we send the last network packet? int framesSinceLastSend; // Is prediction and/or smoothing enabled? bool enablePrediction = true; bool enableSmoothing = true; #endregion #region Initialization public NetworkPredictionGame() { 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(gameTime); } 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(GameTime gameTime) { // Is it time to send outgoing network packets? bool sendPacketThisFrame = false; framesSinceLastSend++; if (framesSinceLastSend >= framesBetweenPackets) { sendPacketThisFrame = true; framesSinceLastSend = 0; } // Update our locally controlled tanks, sending // their latest state at periodic intervals. foreach (LocalNetworkGamer gamer in networkSession.LocalGamers) { UpdateLocalGamer(gamer, gameTime, sendPacketThisFrame); } // Pump the underlying session object. try { networkSession.Update(); } catch (Exception e) { errorMessage = e.Message; networkSession.Dispose(); networkSession = null; } // Make sure the session has not ended. if (networkSession == null) return; // Read any packets telling us the state of remotely controlled tanks. foreach (LocalNetworkGamer gamer in networkSession.LocalGamers) { ReadIncomingPackets(gamer, gameTime); } // Apply prediction and smoothing to the remotely controlled tanks. foreach (NetworkGamer gamer in networkSession.RemoteGamers) { Tank tank = gamer.Tag as Tank; tank.UpdateRemote(framesBetweenPackets, enablePrediction); } // Update the latency and packet loss simulation options. UpdateOptions(); } /// /// Helper for updating a locally controlled gamer. /// void UpdateLocalGamer(LocalNetworkGamer gamer, GameTime gameTime, bool sendPacketThisFrame) { // Look up what tank is associated with this local player. Tank tank = gamer.Tag as Tank; // Read the inputs controlling this tank. PlayerIndex playerIndex = gamer.SignedInGamer.PlayerIndex; Vector2 tankInput; Vector2 turretInput; ReadTankInputs(playerIndex, out tankInput, out turretInput); // Update the tank. tank.UpdateLocal(tankInput, turretInput); // Periodically send our state to everyone in the session. if (sendPacketThisFrame) { tank.WriteNetworkPacket(packetWriter, gameTime); gamer.SendData(packetWriter, SendDataOptions.InOrder); } } /// /// Helper for reading incoming network packets. /// void ReadIncomingPackets(LocalNetworkGamer gamer, GameTime gameTime) { // 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); // Discard packets sent by local gamers: we already know their state! if (sender.IsLocal) continue; // Look up the tank associated with whoever sent this packet. Tank tank = sender.Tag as Tank; // Estimate how long this packet took to arrive. TimeSpan latency = networkSession.SimulatedLatency + TimeSpan.FromTicks(sender.RoundtripTime.Ticks / 2); // Read the state of this tank from the network packet. tank.ReadNetworkPacket(packetReader, gameTime, latency, enablePrediction, enableSmoothing); } } /// /// Updates the latency and packet loss simulation options. Only the /// host can alter these values, which are then synchronized over the /// network by storing them into NetworkSession.SessionProperties. Any /// changes to the SessionProperties data are automatically replicated /// on all the client machines, so there is no need to manually send /// network packets to transmit this data. /// void UpdateOptions() { if (networkSession.IsHost) { // Change the network quality simultation? if (IsPressed(Keys.A, Buttons.A)) { networkQuality++; if (networkQuality > NetworkQuality.Perfect) networkQuality = 0; } // Change the packet send rate? if (IsPressed(Keys.B, Buttons.B)) { if (framesBetweenPackets == 6) framesBetweenPackets = 3; else if (framesBetweenPackets == 3) framesBetweenPackets = 1; else framesBetweenPackets = 6; } // Toggle prediction on or off? if (IsPressed(Keys.X, Buttons.X)) enablePrediction = !enablePrediction; // Toggle smoothing on or off? if (IsPressed(Keys.Y, Buttons.Y)) enableSmoothing = !enableSmoothing; // Stores the latest settings into NetworkSession.SessionProperties. networkSession.SessionProperties[0] = (int)networkQuality; networkSession.SessionProperties[1] = framesBetweenPackets; networkSession.SessionProperties[2] = enablePrediction ? 1 : 0; networkSession.SessionProperties[3] = enableSmoothing ? 1 : 0; } else { // Client machines read the latest settings from the session properties. networkQuality = (NetworkQuality)networkSession.SessionProperties[0]; framesBetweenPackets = networkSession.SessionProperties[1].Value; enablePrediction = networkSession.SessionProperties[2] != 0; enableSmoothing = networkSession.SessionProperties[3] != 0; } // Update the SimulatedLatency and SimulatedPacketLoss properties. switch (networkQuality) { case NetworkQuality.Typical: networkSession.SimulatedLatency = TimeSpan.FromMilliseconds(100); networkSession.SimulatedPacketLoss = 0.1f; break; case NetworkQuality.Poor: networkSession.SimulatedLatency = TimeSpan.FromMilliseconds(200); networkSession.SimulatedPacketLoss = 0.2f; break; case NetworkQuality.Perfect: networkSession.SimulatedLatency = TimeSpan.Zero; networkSession.SimulatedPacketLoss = 0; break; } } #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(); DrawOptions(); // 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. spriteBatch.DrawString(font, gamer.Gamertag, tank.Position, Color.Black, 0, new Vector2(100, 150), 0.6f, SpriteEffects.None, 0); } spriteBatch.End(); } /// /// Draws the current latency and packet loss simulation settings. /// void DrawOptions() { string quality = string.Format("Network simulation = {0} ms, {1}% packet loss", networkSession.SimulatedLatency.TotalMilliseconds, networkSession.SimulatedPacketLoss * 100); string sendRate = string.Format("Packets per second = {0}", 60 / framesBetweenPackets); string prediction = string.Format("Prediction = {0}", enablePrediction ? "on" : "off"); string smoothing = string.Format("Smoothing = {0}", enableSmoothing ? "on" : "off"); // If we are the host, include prompts telling how to change the settings. if (networkSession.IsHost) { quality += " (A to change)"; sendRate += " (B to change)"; prediction += " (X to toggle)"; smoothing += " (Y to toggle)"; } // Draw combined text to the screen. string message = quality + "\n" + sendRate + "\n" + prediction + "\n" + smoothing; spriteBatch.DrawString(font, message, new Vector2(161, 321), Color.Black); spriteBatch.DrawString(font, message, new Vector2(160, 320), Color.White); } /// /// 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() { previousKeyboardState = currentKeyboardState; previousGamePadState = currentGamePadState; 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) && previousKeyboardState.IsKeyUp(key)) || (currentGamePadState.IsButtonDown(button) && previousGamePadState.IsButtonUp(button))); } /// /// Reads input data from keyboard and gamepad, and returns /// this as output parameters ready for use by the tank update. /// static void ReadTankInputs(PlayerIndex playerIndex, out Vector2 tankInput, out Vector2 turretInput) { // Read the gamepad. GamePadState gamePad = GamePad.GetState(playerIndex); tankInput = gamePad.ThumbSticks.Left; 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.K)) turretInput.X = -1; else if (keyboard.IsKeyDown(Keys.OemSemicolon)) turretInput.X = 1; if (keyboard.IsKeyDown(Keys.O)) turretInput.Y = 1; else if (keyboard.IsKeyDown(Keys.L)) turretInput.Y = -1; // Normalize the input vectors. if (tankInput.Length() > 1) tankInput.Normalize(); if (turretInput.Length() > 1) turretInput.Normalize(); } #endregion } }