NetworkPredictionGame.cs 22 KB

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