#region File Description //----------------------------------------------------------------------------- // Tank.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.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Net; #endregion namespace NetworkPrediction { /// /// Each player controls a tank, which they can drive around the screen. /// This class implements the logic for moving and drawing the tank, sending /// and receiving network packets, and applying prediction and smoothing to /// compensate for network latency. /// class Tank { #region Constants // Constants control how fast the tank moves and turns. const float TankTurnRate = 0.01f; const float TurretTurnRate = 0.03f; const float TankSpeed = 0.3f; const float TankFriction = 0.9f; #endregion #region Fields // To implement smoothing, we need more than one copy of the tank state. // We must record both where it used to be, and where it is now, an also // a smoothed value somewhere in between these two states which is where // we will draw the tank on the screen. To simplify managing these three // different versions of the tank state, we move all the state fields into // this internal helper structure. struct TankState { public Vector2 Position; public Vector2 Velocity; public float TankRotation; public float TurretRotation; } // This is the latest master copy of the tank state, used by our local // physics computations and prediction. This state will jerk whenever // a new network packet is received. TankState simulationState; // This is a copy of the state from immediately before the last // network packet was received. TankState previousState; // This is the tank state that is drawn onto the screen. It is gradually // interpolated from the previousState toward the simultationState, in // order to smooth out any sudden jumps caused by discontinuities when // a network packet suddenly modifies the simultationState. TankState displayState; // Used to interpolate displayState from previousState toward simulationState. float currentSmoothing; // Averaged time difference from the last 100 incoming packets, used to // estimate how our local clock compares to the time on the remote machine. RollingAverage clockDelta = new RollingAverage(100); // Input controls can be read from keyboard, gamepad, or the network. Vector2 tankInput; Vector2 turretInput; // Textures used to draw the tank. Texture2D tankTexture; Texture2D turretTexture; Vector2 screenSize; #endregion #region Properties /// /// Gets the current position of the tank. /// public Vector2 Position { get { return displayState.Position; } } #endregion /// /// Constructs a new Tank instance. /// public Tank(int gamerIndex, ContentManager content, int screenWidth, int screenHeight) { // Use the gamer index to compute a starting position, so each player // starts in a different place as opposed to all on top of each other. float x = screenWidth / 4 + (gamerIndex % 5) * screenWidth / 8; float y = screenHeight / 4 + (gamerIndex / 5) * screenHeight / 5; simulationState.Position = new Vector2(x, y); simulationState.TankRotation = -MathHelper.PiOver2; simulationState.TurretRotation = -MathHelper.PiOver2; // Initialize all three versions of our state to the same values. previousState = simulationState; displayState = simulationState; // Load textures. tankTexture = content.Load("Tank"); turretTexture = content.Load("Turret"); screenSize = new Vector2(screenWidth, screenHeight); } /// /// Moves a locally controlled tank in response to the specified inputs. /// public void UpdateLocal(Vector2 tankInput, Vector2 turretInput) { this.tankInput = tankInput; this.turretInput = turretInput; // Update the master simulation state. UpdateState(ref simulationState); // Locally controlled tanks have no prediction or smoothing, so we // just copy the simulation state directly into the display state. displayState = simulationState; } /// /// Applies prediction and smoothing to a remotely controlled tank. /// public void UpdateRemote(int framesBetweenPackets, bool enablePrediction) { // Update the smoothing amount, which interpolates from the previous // state toward the current simultation state. The speed of this decay // depends on the number of frames between packets: we want to finish // our smoothing interpolation at the same time the next packet is due. float smoothingDecay = 1.0f / framesBetweenPackets; currentSmoothing -= smoothingDecay; if (currentSmoothing < 0) currentSmoothing = 0; if (enablePrediction) { // Predict how the remote tank will move by updating // our local copy of its simultation state. UpdateState(ref simulationState); // If both smoothing and prediction are active, // also apply prediction to the previous state. if (currentSmoothing > 0) { UpdateState(ref previousState); } } if (currentSmoothing > 0) { // Interpolate the display state gradually from the // previous state to the current simultation state. ApplySmoothing(); } else { // Copy the simulation state directly into the display state. displayState = simulationState; } } /// /// Applies smoothing by interpolating the display state somewhere /// in between the previous state and current simulation state. /// void ApplySmoothing() { displayState.Position = Vector2.Lerp(simulationState.Position, previousState.Position, currentSmoothing); displayState.Velocity = Vector2.Lerp(simulationState.Velocity, previousState.Velocity, currentSmoothing); displayState.TankRotation = MathHelper.Lerp(simulationState.TankRotation, previousState.TankRotation, currentSmoothing); displayState.TurretRotation = MathHelper.Lerp(simulationState.TurretRotation, previousState.TurretRotation, currentSmoothing); } /// /// Writes our local tank state into a network packet. /// public void WriteNetworkPacket(PacketWriter packetWriter, GameTime gameTime) { // Send our current time. packetWriter.Write((float)gameTime.TotalGameTime.TotalSeconds); // Send the current state of the tank. packetWriter.Write(simulationState.Position); packetWriter.Write(simulationState.Velocity); packetWriter.Write(simulationState.TankRotation); packetWriter.Write(simulationState.TurretRotation); // Also send our current inputs. These can be used to more accurately // predict how the tank is likely to move in the future. packetWriter.Write(tankInput); packetWriter.Write(turretInput); } /// /// Reads the state of a remotely controlled tank from a network packet. /// public void ReadNetworkPacket(PacketReader packetReader, GameTime gameTime, TimeSpan latency, bool enablePrediction, bool enableSmoothing) { if (enableSmoothing) { // Start a new smoothing interpolation from our current // state toward this new state we just received. previousState = displayState; currentSmoothing = 1; } else { currentSmoothing = 0; } // Read what time this packet was sent. float packetSendTime = packetReader.ReadSingle(); // Read simulation state from the network packet. simulationState.Position = packetReader.ReadVector2(); simulationState.Velocity = packetReader.ReadVector2(); simulationState.TankRotation = packetReader.ReadSingle(); simulationState.TurretRotation = packetReader.ReadSingle(); // Read remote inputs from the network packet. tankInput = packetReader.ReadVector2(); turretInput = packetReader.ReadVector2(); // Optionally apply prediction to compensate for // how long it took this packet to reach us. if (enablePrediction) { ApplyPrediction(gameTime, latency, packetSendTime); } } /// /// Incoming network packets tell us where the tank was at the time the packet /// was sent. But packets do not arrive instantly! We want to know where the /// tank is now, not just where it used to be. This method attempts to guess /// the current state by figuring out how long the packet took to arrive, then /// running the appropriate number of local updates to catch up to that time. /// This allows us to figure out things like "it used to be over there, and it /// was moving that way while turning to the left, so assuming it carried on /// using those same inputs, it should now be over here". /// void ApplyPrediction(GameTime gameTime, TimeSpan latency, float packetSendTime) { // Work out the difference between our current local time // and the remote time at which this packet was sent. float localTime = (float)gameTime.TotalGameTime.TotalSeconds; float timeDelta = localTime - packetSendTime; // Maintain a rolling average of time deltas from the last 100 packets. clockDelta.AddValue(timeDelta); // The caller passed in an estimate of the average network latency, which // is provided by the XNA Framework networking layer. But not all packets // will take exactly that average amount of time to arrive! To handle // varying latencies per packet, we include the send time as part of our // packet data. By comparing this with a rolling average of the last 100 // send times, we can detect packets that are later or earlier than usual, // even without having synchronized clocks between the two machines. We // then adjust our average latency estimate by this per-packet deviation. float timeDeviation = timeDelta - clockDelta.AverageValue; latency += TimeSpan.FromSeconds(timeDeviation); TimeSpan oneFrame = TimeSpan.FromSeconds(1.0 / 60.0); // Apply prediction by updating our simulation state however // many times is necessary to catch up to the current time. while (latency >= oneFrame) { UpdateState(ref simulationState); latency -= oneFrame; } } /// /// Updates one of our state structures, using the current inputs to turn /// the tank, and applying the velocity and inertia calculations. This /// method is used directly to update locally controlled tanks, and also /// indirectly to predict the motion of remote tanks. /// void UpdateState(ref TankState state) { // Gradually turn the tank and turret to face the requested direction. state.TankRotation = TurnToFace(state.TankRotation, tankInput, TankTurnRate); state.TurretRotation = TurnToFace(state.TurretRotation, turretInput, TurretTurnRate); // How close the desired direction is the tank facing? Vector2 tankForward = new Vector2((float)Math.Cos(state.TankRotation), (float)Math.Sin(state.TankRotation)); Vector2 targetForward = new Vector2(tankInput.X, -tankInput.Y); float facingForward = Vector2.Dot(tankForward, targetForward); // If we have finished turning, also start moving forward. if (facingForward > 0) { float speed = facingForward * facingForward * TankSpeed; state.Velocity += tankForward * speed; } // Update the position and velocity. state.Position += state.Velocity; state.Velocity *= TankFriction; // Clamp so the tank cannot drive off the edge of the screen. state.Position = Vector2.Clamp(state.Position, Vector2.Zero, screenSize); } /// /// Gradually rotates the tank to face the specified direction. /// See the Aiming sample (creators.xna.com) for details. /// static float TurnToFace(float rotation, Vector2 target, float turnRate) { if (target == Vector2.Zero) return rotation; float angle = (float)Math.Atan2(-target.Y, target.X); float difference = rotation - angle; while (difference > MathHelper.Pi) difference -= MathHelper.TwoPi; while (difference < -MathHelper.Pi) difference += MathHelper.TwoPi; turnRate *= Math.Abs(difference); if (difference < 0) return rotation + Math.Min(turnRate, -difference); else return rotation - Math.Min(turnRate, difference); } /// /// Draws the tank and turret. /// public void Draw(SpriteBatch spriteBatch) { Vector2 origin = new Vector2(tankTexture.Width / 2, tankTexture.Height / 2); spriteBatch.Draw(tankTexture, displayState.Position, null, Color.White, displayState.TankRotation, origin, 1, SpriteEffects.None, 0); spriteBatch.Draw(turretTexture, displayState.Position, null, Color.White, displayState.TurretRotation, origin, 1, SpriteEffects.None, 0); } } }