Tank.cs 16 KB

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