Tank.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. //-----------------------------------------------------------------------------
  2. // Tank.cs
  3. //
  4. // Microsoft XNA Community Game Platform
  5. // Copyright (C) Microsoft Corporation. All rights reserved.
  6. //-----------------------------------------------------------------------------
  7. using System;
  8. using Microsoft.Xna.Framework;
  9. using Microsoft.Xna.Framework.Content;
  10. using Microsoft.Xna.Framework.Graphics;
  11. using Microsoft.Xna.Framework.Input;
  12. using Microsoft.Xna.Framework.Net;
  13. namespace NetworkPrediction
  14. {
  15. /// <summary>
  16. /// Each player controls a tank, which they can drive around the screen.
  17. /// This class implements the logic for moving and drawing the tank, sending
  18. /// and receiving network packets, and applying prediction and smoothing to
  19. /// compensate for network latency.
  20. /// </summary>
  21. class Tank
  22. {
  23. // Constants control how fast the tank moves and turns.
  24. const float TankTurnRate = 0.01f;
  25. const float TurretTurnRate = 0.03f;
  26. const float TankSpeed = 0.3f;
  27. const float TankFriction = 0.9f;
  28. // To implement smoothing, we need more than one copy of the tank state.
  29. // We must record both where it used to be, and where it is now, an also
  30. // a smoothed value somewhere in between these two states which is where
  31. // we will draw the tank on the screen. To simplify managing these three
  32. // different versions of the tank state, we move all the state fields into
  33. // this internal helper structure.
  34. struct TankState
  35. {
  36. public Vector2 Position;
  37. public Vector2 Velocity;
  38. public float TankRotation;
  39. public float TurretRotation;
  40. }
  41. // This is the latest master copy of the tank state, used by our local
  42. // physics computations and prediction. This state will jerk whenever
  43. // a new network packet is received.
  44. TankState simulationState;
  45. // This is a copy of the state from immediately before the last
  46. // network packet was received.
  47. TankState previousState;
  48. // This is the tank state that is drawn onto the screen. It is gradually
  49. // interpolated from the previousState toward the simultationState, in
  50. // order to smooth out any sudden jumps caused by discontinuities when
  51. // a network packet suddenly modifies the simultationState.
  52. TankState displayState;
  53. // Used to interpolate displayState from previousState toward simulationState.
  54. float currentSmoothing;
  55. // Averaged time difference from the last 100 incoming packets, used to
  56. // estimate how our local clock compares to the time on the remote machine.
  57. RollingAverage clockDelta = new RollingAverage(100);
  58. // Input controls can be read from keyboard, gamepad, or the network.
  59. Vector2 tankInput;
  60. Vector2 turretInput;
  61. // Textures used to draw the tank.
  62. Texture2D tankTexture;
  63. Texture2D turretTexture;
  64. Vector2 screenSize;
  65. /// <summary>
  66. /// Gets the current position of the tank.
  67. /// </summary>
  68. public Vector2 Position
  69. {
  70. get { return displayState.Position; }
  71. }
  72. /// <summary>
  73. /// Constructs a new Tank instance.
  74. /// </summary>
  75. public Tank(int gamerIndex, ContentManager content,
  76. int screenWidth, int screenHeight)
  77. {
  78. // Use the gamer index to compute a starting position, so each player
  79. // starts in a different place as opposed to all on top of each other.
  80. float x = screenWidth / 4 + (gamerIndex % 5) * screenWidth / 8;
  81. float y = screenHeight / 4 + (gamerIndex / 5) * screenHeight / 5;
  82. simulationState.Position = new Vector2(x, y);
  83. simulationState.TankRotation = -MathHelper.PiOver2;
  84. simulationState.TurretRotation = -MathHelper.PiOver2;
  85. // Initialize all three versions of our state to the same values.
  86. previousState = simulationState;
  87. displayState = simulationState;
  88. // Load textures.
  89. tankTexture = content.Load<Texture2D>("Tank");
  90. turretTexture = content.Load<Texture2D>("Turret");
  91. screenSize = new Vector2(screenWidth, screenHeight);
  92. }
  93. /// <summary>
  94. /// Moves a locally controlled tank in response to the specified inputs.
  95. /// </summary>
  96. public void UpdateLocal(Vector2 tankInput, Vector2 turretInput)
  97. {
  98. this.tankInput = tankInput;
  99. this.turretInput = turretInput;
  100. // Update the master simulation state.
  101. UpdateState(ref simulationState);
  102. // Locally controlled tanks have no prediction or smoothing, so we
  103. // just copy the simulation state directly into the display state.
  104. displayState = simulationState;
  105. }
  106. /// <summary>
  107. /// Applies prediction and smoothing to a remotely controlled tank.
  108. /// </summary>
  109. public void UpdateRemote(int framesBetweenPackets, bool enablePrediction)
  110. {
  111. // Update the smoothing amount, which interpolates from the previous
  112. // state toward the current simultation state. The speed of this decay
  113. // depends on the number of frames between packets: we want to finish
  114. // our smoothing interpolation at the same time the next packet is due.
  115. float smoothingDecay = 1.0f / framesBetweenPackets;
  116. currentSmoothing -= smoothingDecay;
  117. if (currentSmoothing < 0)
  118. currentSmoothing = 0;
  119. if (enablePrediction)
  120. {
  121. // Predict how the remote tank will move by updating
  122. // our local copy of its simultation state.
  123. UpdateState(ref simulationState);
  124. // If both smoothing and prediction are active,
  125. // also apply prediction to the previous state.
  126. if (currentSmoothing > 0)
  127. {
  128. UpdateState(ref previousState);
  129. }
  130. }
  131. if (currentSmoothing > 0)
  132. {
  133. // Interpolate the display state gradually from the
  134. // previous state to the current simultation state.
  135. ApplySmoothing();
  136. }
  137. else
  138. {
  139. // Copy the simulation state directly into the display state.
  140. displayState = simulationState;
  141. }
  142. }
  143. /// <summary>
  144. /// Applies smoothing by interpolating the display state somewhere
  145. /// in between the previous state and current simulation state.
  146. /// </summary>
  147. void ApplySmoothing()
  148. {
  149. displayState.Position = Vector2.Lerp(simulationState.Position,
  150. previousState.Position,
  151. currentSmoothing);
  152. displayState.Velocity = Vector2.Lerp(simulationState.Velocity,
  153. previousState.Velocity,
  154. currentSmoothing);
  155. displayState.TankRotation = MathHelper.Lerp(simulationState.TankRotation,
  156. previousState.TankRotation,
  157. currentSmoothing);
  158. displayState.TurretRotation = MathHelper.Lerp(simulationState.TurretRotation,
  159. previousState.TurretRotation,
  160. currentSmoothing);
  161. }
  162. /// <summary>
  163. /// Writes our local tank state into a network packet.
  164. /// </summary>
  165. public void WriteNetworkPacket(PacketWriter packetWriter, GameTime gameTime)
  166. {
  167. // Send our current time.
  168. packetWriter.Write((float)gameTime.TotalGameTime.TotalSeconds);
  169. // Send the current state of the tank.
  170. packetWriter.Write(simulationState.Position);
  171. packetWriter.Write(simulationState.Velocity);
  172. packetWriter.Write(simulationState.TankRotation);
  173. packetWriter.Write(simulationState.TurretRotation);
  174. // Also send our current inputs. These can be used to more accurately
  175. // predict how the tank is likely to move in the future.
  176. packetWriter.Write(tankInput);
  177. packetWriter.Write(turretInput);
  178. }
  179. /// <summary>
  180. /// Reads the state of a remotely controlled tank from a network packet.
  181. /// </summary>
  182. public void ReadNetworkPacket(PacketReader packetReader,
  183. GameTime gameTime, TimeSpan latency,
  184. bool enablePrediction, bool enableSmoothing)
  185. {
  186. if (enableSmoothing)
  187. {
  188. // Start a new smoothing interpolation from our current
  189. // state toward this new state we just received.
  190. previousState = displayState;
  191. currentSmoothing = 1;
  192. }
  193. else
  194. {
  195. currentSmoothing = 0;
  196. }
  197. // Read what time this packet was sent.
  198. float packetSendTime = packetReader.ReadSingle();
  199. // Read simulation state from the network packet.
  200. simulationState.Position = packetReader.ReadVector2();
  201. simulationState.Velocity = packetReader.ReadVector2();
  202. simulationState.TankRotation = packetReader.ReadSingle();
  203. simulationState.TurretRotation = packetReader.ReadSingle();
  204. // Read remote inputs from the network packet.
  205. tankInput = packetReader.ReadVector2();
  206. turretInput = packetReader.ReadVector2();
  207. // Optionally apply prediction to compensate for
  208. // how long it took this packet to reach us.
  209. if (enablePrediction)
  210. {
  211. ApplyPrediction(gameTime, latency, packetSendTime);
  212. }
  213. }
  214. /// <summary>
  215. /// Incoming network packets tell us where the tank was at the time the packet
  216. /// was sent. But packets do not arrive instantly! We want to know where the
  217. /// tank is now, not just where it used to be. This method attempts to guess
  218. /// the current state by figuring out how long the packet took to arrive, then
  219. /// running the appropriate number of local updates to catch up to that time.
  220. /// This allows us to figure out things like "it used to be over there, and it
  221. /// was moving that way while turning to the left, so assuming it carried on
  222. /// using those same inputs, it should now be over here".
  223. /// </summary>
  224. void ApplyPrediction(GameTime gameTime, TimeSpan latency, float packetSendTime)
  225. {
  226. // Work out the difference between our current local time
  227. // and the remote time at which this packet was sent.
  228. float localTime = (float)gameTime.TotalGameTime.TotalSeconds;
  229. float timeDelta = localTime - packetSendTime;
  230. // Maintain a rolling average of time deltas from the last 100 packets.
  231. clockDelta.AddValue(timeDelta);
  232. // The caller passed in an estimate of the average network latency, which
  233. // is provided by the XNA Framework networking layer. But not all packets
  234. // will take exactly that average amount of time to arrive! To handle
  235. // varying latencies per packet, we include the send time as part of our
  236. // packet data. By comparing this with a rolling average of the last 100
  237. // send times, we can detect packets that are later or earlier than usual,
  238. // even without having synchronized clocks between the two machines. We
  239. // then adjust our average latency estimate by this per-packet deviation.
  240. float timeDeviation = timeDelta - clockDelta.AverageValue;
  241. latency += TimeSpan.FromSeconds(timeDeviation);
  242. TimeSpan oneFrame = TimeSpan.FromSeconds(1.0 / 60.0);
  243. // Apply prediction by updating our simulation state however
  244. // many times is necessary to catch up to the current time.
  245. while (latency >= oneFrame)
  246. {
  247. UpdateState(ref simulationState);
  248. latency -= oneFrame;
  249. }
  250. }
  251. /// <summary>
  252. /// Updates one of our state structures, using the current inputs to turn
  253. /// the tank, and applying the velocity and inertia calculations. This
  254. /// method is used directly to update locally controlled tanks, and also
  255. /// indirectly to predict the motion of remote tanks.
  256. /// </summary>
  257. void UpdateState(ref TankState state)
  258. {
  259. // Gradually turn the tank and turret to face the requested direction.
  260. state.TankRotation = TurnToFace(state.TankRotation,
  261. tankInput, TankTurnRate);
  262. state.TurretRotation = TurnToFace(state.TurretRotation,
  263. turretInput, TurretTurnRate);
  264. // How close the desired direction is the tank facing?
  265. Vector2 tankForward = new Vector2((float)Math.Cos(state.TankRotation),
  266. (float)Math.Sin(state.TankRotation));
  267. Vector2 targetForward = new Vector2(tankInput.X, -tankInput.Y);
  268. float facingForward = Vector2.Dot(tankForward, targetForward);
  269. // If we have finished turning, also start moving forward.
  270. if (facingForward > 0)
  271. {
  272. float speed = facingForward * facingForward * TankSpeed;
  273. state.Velocity += tankForward * speed;
  274. }
  275. // Update the position and velocity.
  276. state.Position += state.Velocity;
  277. state.Velocity *= TankFriction;
  278. // Clamp so the tank cannot drive off the edge of the screen.
  279. state.Position = Vector2.Clamp(state.Position, Vector2.Zero, screenSize);
  280. }
  281. /// <summary>
  282. /// Gradually rotates the tank to face the specified direction.
  283. /// See the Aiming sample (creators.xna.com) for details.
  284. /// </summary>
  285. static float TurnToFace(float rotation, Vector2 target, float turnRate)
  286. {
  287. if (target == Vector2.Zero)
  288. return rotation;
  289. float angle = (float)Math.Atan2(-target.Y, target.X);
  290. float difference = rotation - angle;
  291. while (difference > MathHelper.Pi)
  292. difference -= MathHelper.TwoPi;
  293. while (difference < -MathHelper.Pi)
  294. difference += MathHelper.TwoPi;
  295. turnRate *= Math.Abs(difference);
  296. if (difference < 0)
  297. return rotation + Math.Min(turnRate, -difference);
  298. else
  299. return rotation - Math.Min(turnRate, difference);
  300. }
  301. /// <summary>
  302. /// Draws the tank and turret.
  303. /// </summary>
  304. public void Draw(SpriteBatch spriteBatch)
  305. {
  306. Vector2 origin = new Vector2(tankTexture.Width / 2, tankTexture.Height / 2);
  307. spriteBatch.Draw(tankTexture, displayState.Position, null, Color.White,
  308. displayState.TankRotation, origin, 1,
  309. SpriteEffects.None, 0);
  310. spriteBatch.Draw(turretTexture, displayState.Position, null, Color.White,
  311. displayState.TurretRotation, origin, 1,
  312. SpriteEffects.None, 0);
  313. }
  314. }
  315. }