NetworkPredictionGame.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. #region File Description
  2. //-----------------------------------------------------------------------------
  3. // NetworkPredictionGame.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.GamerServices;
  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. /// Sample showing how to use prediction and smoothing to compensate
  21. /// for the effects of network latency, and for the low packet send
  22. /// rates needed to conserve network bandwidth.
  23. /// </summary>
  24. public class NetworkPredictionGame : Microsoft.Xna.Framework.Game
  25. {
  26. #region Constants
  27. const int screenWidth = 1067;
  28. const int screenHeight = 600;
  29. const int maxGamers = 16;
  30. const int maxLocalGamers = 4;
  31. #endregion
  32. #region Fields
  33. // Graphics objects.
  34. GraphicsDeviceManager graphics;
  35. SpriteBatch spriteBatch;
  36. SpriteFont font;
  37. // Current and previous input states.
  38. KeyboardState currentKeyboardState;
  39. GamePadState currentGamePadState;
  40. KeyboardState previousKeyboardState;
  41. GamePadState previousGamePadState;
  42. // Network objects.
  43. NetworkSession networkSession;
  44. PacketWriter packetWriter = new PacketWriter();
  45. PacketReader packetReader = new PacketReader();
  46. string errorMessage;
  47. // What kind of network latency and packet loss are we simulating?
  48. enum NetworkQuality
  49. {
  50. Typical, // 100 ms latency, 10% packet loss
  51. Poor, // 200 ms latency, 20% packet loss
  52. Perfect, // 0 latency, 0% packet loss
  53. }
  54. NetworkQuality networkQuality;
  55. // How often should we send network packets?
  56. int framesBetweenPackets = 6;
  57. // How recently did we send the last network packet?
  58. int framesSinceLastSend;
  59. // Is prediction and/or smoothing enabled?
  60. bool enablePrediction = true;
  61. bool enableSmoothing = true;
  62. #endregion
  63. #region Initialization
  64. public NetworkPredictionGame()
  65. {
  66. graphics = new GraphicsDeviceManager(this);
  67. graphics.PreferredBackBufferWidth = screenWidth;
  68. graphics.PreferredBackBufferHeight = screenHeight;
  69. Content.RootDirectory = "Content";
  70. Components.Add(new GamerServicesComponent(this));
  71. }
  72. /// <summary>
  73. /// Load your content.
  74. /// </summary>
  75. protected override void LoadContent()
  76. {
  77. spriteBatch = new SpriteBatch(GraphicsDevice);
  78. font = Content.Load<SpriteFont>("Font");
  79. }
  80. #endregion
  81. #region Update
  82. /// <summary>
  83. /// Allows the game to run logic.
  84. /// </summary>
  85. protected override void Update(GameTime gameTime)
  86. {
  87. HandleInput();
  88. if (networkSession == null)
  89. {
  90. // If we are not in a network session, update the
  91. // menu screen that will let us create or join one.
  92. UpdateMenuScreen();
  93. }
  94. else
  95. {
  96. // If we are in a network session, update it.
  97. UpdateNetworkSession(gameTime);
  98. }
  99. base.Update(gameTime);
  100. }
  101. /// <summary>
  102. /// Menu screen provides options to create or join network sessions.
  103. /// </summary>
  104. void UpdateMenuScreen()
  105. {
  106. if (IsActive)
  107. {
  108. if (Gamer.SignedInGamers.Count == 0 && !Guide.IsVisible)
  109. {
  110. // If there are no profiles signed in, we cannot proceed.
  111. // Show the Guide so the user can sign in.
  112. Guide.ShowSignIn(maxLocalGamers, false);
  113. }
  114. else
  115. if (IsPressed(Keys.A, Buttons.A))
  116. {
  117. // Create a new session?
  118. CreateSession();
  119. }
  120. else if (IsPressed(Keys.B, Buttons.B))
  121. {
  122. // Join an existing session?
  123. JoinSession();
  124. }
  125. }
  126. }
  127. /// <summary>
  128. /// Starts hosting a new network session.
  129. /// </summary>
  130. void CreateSession()
  131. {
  132. DrawMessage("Creating session...");
  133. try
  134. {
  135. networkSession = NetworkSession.Create(NetworkSessionType.SystemLink,
  136. maxLocalGamers, maxGamers);
  137. HookSessionEvents();
  138. }
  139. catch (Exception e)
  140. {
  141. errorMessage = e.Message;
  142. }
  143. }
  144. /// <summary>
  145. /// Joins an existing network session.
  146. /// </summary>
  147. void JoinSession()
  148. {
  149. DrawMessage("Joining session...");
  150. try
  151. {
  152. // Search for sessions.
  153. using (AvailableNetworkSessionCollection availableSessions =
  154. NetworkSession.Find(NetworkSessionType.SystemLink,
  155. maxLocalGamers, null))
  156. {
  157. if (availableSessions.Count == 0)
  158. {
  159. errorMessage = "No network sessions found.";
  160. return;
  161. }
  162. // Join the first session we found.
  163. networkSession = NetworkSession.Join(availableSessions[0]);
  164. HookSessionEvents();
  165. }
  166. }
  167. catch (Exception e)
  168. {
  169. errorMessage = e.Message;
  170. }
  171. }
  172. /// <summary>
  173. /// After creating or joining a network session, we must subscribe to
  174. /// some events so we will be notified when the session changes state.
  175. /// </summary>
  176. void HookSessionEvents()
  177. {
  178. networkSession.GamerJoined += GamerJoinedEventHandler;
  179. networkSession.SessionEnded += SessionEndedEventHandler;
  180. }
  181. /// <summary>
  182. /// This event handler will be called whenever a new gamer joins the session.
  183. /// We use it to allocate a Tank object, and associate it with the new gamer.
  184. /// </summary>
  185. void GamerJoinedEventHandler(object sender, GamerJoinedEventArgs e)
  186. {
  187. int gamerIndex = networkSession.AllGamers.IndexOf(e.Gamer);
  188. e.Gamer.Tag = new Tank(gamerIndex, Content, screenWidth, screenHeight);
  189. }
  190. /// <summary>
  191. /// Event handler notifies us when the network session has ended.
  192. /// </summary>
  193. void SessionEndedEventHandler(object sender, NetworkSessionEndedEventArgs e)
  194. {
  195. errorMessage = e.EndReason.ToString();
  196. networkSession.Dispose();
  197. networkSession = null;
  198. }
  199. /// <summary>
  200. /// Updates the state of the network session, moving the tanks
  201. /// around and synchronizing their state over the network.
  202. /// </summary>
  203. void UpdateNetworkSession(GameTime gameTime)
  204. {
  205. // Is it time to send outgoing network packets?
  206. bool sendPacketThisFrame = false;
  207. framesSinceLastSend++;
  208. if (framesSinceLastSend >= framesBetweenPackets)
  209. {
  210. sendPacketThisFrame = true;
  211. framesSinceLastSend = 0;
  212. }
  213. // Update our locally controlled tanks, sending
  214. // their latest state at periodic intervals.
  215. foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
  216. {
  217. UpdateLocalGamer(gamer, gameTime, sendPacketThisFrame);
  218. }
  219. // Pump the underlying session object.
  220. try
  221. {
  222. networkSession.Update();
  223. }
  224. catch (Exception e)
  225. {
  226. errorMessage = e.Message;
  227. networkSession.Dispose();
  228. networkSession = null;
  229. }
  230. // Make sure the session has not ended.
  231. if (networkSession == null)
  232. return;
  233. // Read any packets telling us the state of remotely controlled tanks.
  234. foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
  235. {
  236. ReadIncomingPackets(gamer, gameTime);
  237. }
  238. // Apply prediction and smoothing to the remotely controlled tanks.
  239. foreach (NetworkGamer gamer in networkSession.RemoteGamers)
  240. {
  241. Tank tank = gamer.Tag as Tank;
  242. tank.UpdateRemote(framesBetweenPackets, enablePrediction);
  243. }
  244. // Update the latency and packet loss simulation options.
  245. UpdateOptions();
  246. }
  247. /// <summary>
  248. /// Helper for updating a locally controlled gamer.
  249. /// </summary>
  250. void UpdateLocalGamer(LocalNetworkGamer gamer, GameTime gameTime,
  251. bool sendPacketThisFrame)
  252. {
  253. // Look up what tank is associated with this local player.
  254. Tank tank = gamer.Tag as Tank;
  255. // Read the inputs controlling this tank.
  256. PlayerIndex playerIndex = gamer.SignedInGamer.PlayerIndex;
  257. Vector2 tankInput;
  258. Vector2 turretInput;
  259. ReadTankInputs(playerIndex, out tankInput, out turretInput);
  260. // Update the tank.
  261. tank.UpdateLocal(tankInput, turretInput);
  262. // Periodically send our state to everyone in the session.
  263. if (sendPacketThisFrame)
  264. {
  265. tank.WriteNetworkPacket(packetWriter, gameTime);
  266. gamer.SendData(packetWriter, SendDataOptions.InOrder);
  267. }
  268. }
  269. /// <summary>
  270. /// Helper for reading incoming network packets.
  271. /// </summary>
  272. void ReadIncomingPackets(LocalNetworkGamer gamer, GameTime gameTime)
  273. {
  274. // Keep reading as long as incoming packets are available.
  275. while (gamer.IsDataAvailable)
  276. {
  277. NetworkGamer sender;
  278. // Read a single packet from the network.
  279. gamer.ReceiveData(packetReader, out sender);
  280. // Discard packets sent by local gamers: we already know their state!
  281. if (sender.IsLocal)
  282. continue;
  283. // Look up the tank associated with whoever sent this packet.
  284. Tank tank = sender.Tag as Tank;
  285. // Estimate how long this packet took to arrive.
  286. TimeSpan latency = networkSession.SimulatedLatency +
  287. TimeSpan.FromTicks(sender.RoundtripTime.Ticks / 2);
  288. // Read the state of this tank from the network packet.
  289. tank.ReadNetworkPacket(packetReader, gameTime, latency,
  290. enablePrediction, enableSmoothing);
  291. }
  292. }
  293. /// <summary>
  294. /// Updates the latency and packet loss simulation options. Only the
  295. /// host can alter these values, which are then synchronized over the
  296. /// network by storing them into NetworkSession.SessionProperties. Any
  297. /// changes to the SessionProperties data are automatically replicated
  298. /// on all the client machines, so there is no need to manually send
  299. /// network packets to transmit this data.
  300. /// </summary>
  301. void UpdateOptions()
  302. {
  303. if (networkSession.IsHost)
  304. {
  305. // Change the network quality simultation?
  306. if (IsPressed(Keys.A, Buttons.A))
  307. {
  308. networkQuality++;
  309. if (networkQuality > NetworkQuality.Perfect)
  310. networkQuality = 0;
  311. }
  312. // Change the packet send rate?
  313. if (IsPressed(Keys.B, Buttons.B))
  314. {
  315. if (framesBetweenPackets == 6)
  316. framesBetweenPackets = 3;
  317. else if (framesBetweenPackets == 3)
  318. framesBetweenPackets = 1;
  319. else
  320. framesBetweenPackets = 6;
  321. }
  322. // Toggle prediction on or off?
  323. if (IsPressed(Keys.X, Buttons.X))
  324. enablePrediction = !enablePrediction;
  325. // Toggle smoothing on or off?
  326. if (IsPressed(Keys.Y, Buttons.Y))
  327. enableSmoothing = !enableSmoothing;
  328. // Stores the latest settings into NetworkSession.SessionProperties.
  329. networkSession.SessionProperties[0] = (int)networkQuality;
  330. networkSession.SessionProperties[1] = framesBetweenPackets;
  331. networkSession.SessionProperties[2] = enablePrediction ? 1 : 0;
  332. networkSession.SessionProperties[3] = enableSmoothing ? 1 : 0;
  333. }
  334. else
  335. {
  336. // Client machines read the latest settings from the session properties.
  337. networkQuality = (NetworkQuality)networkSession.SessionProperties[0];
  338. framesBetweenPackets = networkSession.SessionProperties[1].Value;
  339. enablePrediction = networkSession.SessionProperties[2] != 0;
  340. enableSmoothing = networkSession.SessionProperties[3] != 0;
  341. }
  342. // Update the SimulatedLatency and SimulatedPacketLoss properties.
  343. switch (networkQuality)
  344. {
  345. case NetworkQuality.Typical:
  346. networkSession.SimulatedLatency = TimeSpan.FromMilliseconds(100);
  347. networkSession.SimulatedPacketLoss = 0.1f;
  348. break;
  349. case NetworkQuality.Poor:
  350. networkSession.SimulatedLatency = TimeSpan.FromMilliseconds(200);
  351. networkSession.SimulatedPacketLoss = 0.2f;
  352. break;
  353. case NetworkQuality.Perfect:
  354. networkSession.SimulatedLatency = TimeSpan.Zero;
  355. networkSession.SimulatedPacketLoss = 0;
  356. break;
  357. }
  358. }
  359. #endregion
  360. #region Draw
  361. /// <summary>
  362. /// This is called when the game should draw itself.
  363. /// </summary>
  364. protected override void Draw(GameTime gameTime)
  365. {
  366. GraphicsDevice.Clear(Color.CornflowerBlue);
  367. if (networkSession == null)
  368. {
  369. // If we are not in a network session, draw the
  370. // menu screen that will let us create or join one.
  371. DrawMenuScreen();
  372. }
  373. else
  374. {
  375. // If we are in a network session, draw it.
  376. DrawNetworkSession();
  377. }
  378. base.Draw(gameTime);
  379. }
  380. /// <summary>
  381. /// Draws the startup screen used to create and join network sessions.
  382. /// </summary>
  383. void DrawMenuScreen()
  384. {
  385. string message = string.Empty;
  386. if (!string.IsNullOrEmpty(errorMessage))
  387. message += "Error:\n" + errorMessage.Replace(". ", ".\n") + "\n\n";
  388. message += "A = create session\n" +
  389. "B = join session";
  390. spriteBatch.Begin();
  391. spriteBatch.DrawString(font, message, new Vector2(161, 161), Color.Black);
  392. spriteBatch.DrawString(font, message, new Vector2(160, 160), Color.White);
  393. spriteBatch.End();
  394. }
  395. /// <summary>
  396. /// Draws the state of an active network session.
  397. /// </summary>
  398. void DrawNetworkSession()
  399. {
  400. spriteBatch.Begin();
  401. DrawOptions();
  402. // For each person in the session...
  403. foreach (NetworkGamer gamer in networkSession.AllGamers)
  404. {
  405. // Look up the tank object belonging to this network gamer.
  406. Tank tank = gamer.Tag as Tank;
  407. // Draw the tank.
  408. tank.Draw(spriteBatch);
  409. // Draw a gamertag label.
  410. spriteBatch.DrawString(font, gamer.Gamertag, tank.Position,
  411. Color.Black, 0, new Vector2(100, 150),
  412. 0.6f, SpriteEffects.None, 0);
  413. }
  414. spriteBatch.End();
  415. }
  416. /// <summary>
  417. /// Draws the current latency and packet loss simulation settings.
  418. /// </summary>
  419. void DrawOptions()
  420. {
  421. string quality =
  422. string.Format("Network simulation = {0} ms, {1}% packet loss",
  423. networkSession.SimulatedLatency.TotalMilliseconds,
  424. networkSession.SimulatedPacketLoss * 100);
  425. string sendRate = string.Format("Packets per second = {0}",
  426. 60 / framesBetweenPackets);
  427. string prediction = string.Format("Prediction = {0}",
  428. enablePrediction ? "on" : "off");
  429. string smoothing = string.Format("Smoothing = {0}",
  430. enableSmoothing ? "on" : "off");
  431. // If we are the host, include prompts telling how to change the settings.
  432. if (networkSession.IsHost)
  433. {
  434. quality += " (A to change)";
  435. sendRate += " (B to change)";
  436. prediction += " (X to toggle)";
  437. smoothing += " (Y to toggle)";
  438. }
  439. // Draw combined text to the screen.
  440. string message = quality + "\n" +
  441. sendRate + "\n" +
  442. prediction + "\n" +
  443. smoothing;
  444. spriteBatch.DrawString(font, message, new Vector2(161, 321), Color.Black);
  445. spriteBatch.DrawString(font, message, new Vector2(160, 320), Color.White);
  446. }
  447. /// <summary>
  448. /// Helper draws notification messages before calling blocking network methods.
  449. /// </summary>
  450. void DrawMessage(string message)
  451. {
  452. if (!BeginDraw())
  453. return;
  454. GraphicsDevice.Clear(Color.CornflowerBlue);
  455. spriteBatch.Begin();
  456. spriteBatch.DrawString(font, message, new Vector2(161, 161), Color.Black);
  457. spriteBatch.DrawString(font, message, new Vector2(160, 160), Color.White);
  458. spriteBatch.End();
  459. EndDraw();
  460. }
  461. #endregion
  462. #region Handle Input
  463. /// <summary>
  464. /// Handles input.
  465. /// </summary>
  466. private void HandleInput()
  467. {
  468. previousKeyboardState = currentKeyboardState;
  469. previousGamePadState = currentGamePadState;
  470. currentKeyboardState = Keyboard.GetState();
  471. currentGamePadState = GamePad.GetState(PlayerIndex.One);
  472. // Check for exit.
  473. if (IsActive && IsPressed(Keys.Escape, Buttons.Back))
  474. {
  475. Exit();
  476. }
  477. }
  478. /// <summary>
  479. /// Checks if the specified button is pressed on either keyboard or gamepad.
  480. /// </summary>
  481. bool IsPressed(Keys key, Buttons button)
  482. {
  483. return ((currentKeyboardState.IsKeyDown(key) &&
  484. previousKeyboardState.IsKeyUp(key)) ||
  485. (currentGamePadState.IsButtonDown(button) &&
  486. previousGamePadState.IsButtonUp(button)));
  487. }
  488. /// <summary>
  489. /// Reads input data from keyboard and gamepad, and returns
  490. /// this as output parameters ready for use by the tank update.
  491. /// </summary>
  492. static void ReadTankInputs(PlayerIndex playerIndex, out Vector2 tankInput,
  493. out Vector2 turretInput)
  494. {
  495. // Read the gamepad.
  496. GamePadState gamePad = GamePad.GetState(playerIndex);
  497. tankInput = gamePad.ThumbSticks.Left;
  498. turretInput = gamePad.ThumbSticks.Right;
  499. // Read the keyboard.
  500. KeyboardState keyboard = Keyboard.GetState(playerIndex);
  501. if (keyboard.IsKeyDown(Keys.Left))
  502. tankInput.X = -1;
  503. else if (keyboard.IsKeyDown(Keys.Right))
  504. tankInput.X = 1;
  505. if (keyboard.IsKeyDown(Keys.Up))
  506. tankInput.Y = 1;
  507. else if (keyboard.IsKeyDown(Keys.Down))
  508. tankInput.Y = -1;
  509. if (keyboard.IsKeyDown(Keys.K))
  510. turretInput.X = -1;
  511. else if (keyboard.IsKeyDown(Keys.OemSemicolon))
  512. turretInput.X = 1;
  513. if (keyboard.IsKeyDown(Keys.O))
  514. turretInput.Y = 1;
  515. else if (keyboard.IsKeyDown(Keys.L))
  516. turretInput.Y = -1;
  517. // Normalize the input vectors.
  518. if (tankInput.Length() > 1)
  519. tankInput.Normalize();
  520. if (turretInput.Length() > 1)
  521. turretInput.Normalize();
  522. }
  523. #endregion
  524. }
  525. }