NetworkPredictionGame.cs 22 KB

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