NetworkPredictionGame.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  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. packetWriter.Position = 0;
  269. }
  270. }
  271. /// <summary>
  272. /// Helper for reading incoming network packets.
  273. /// </summary>
  274. void ReadIncomingPackets(LocalNetworkGamer gamer, GameTime gameTime)
  275. {
  276. // Keep reading as long as incoming packets are available.
  277. while (gamer.IsDataAvailable)
  278. {
  279. NetworkGamer sender;
  280. // Read a single packet from the network.
  281. gamer.ReceiveData(packetReader, out sender);
  282. // Discard packets sent by local gamers: we already know their state!
  283. if (sender.IsLocal)
  284. continue;
  285. // Look up the tank associated with whoever sent this packet.
  286. Tank tank = sender.Tag as Tank;
  287. // Estimate how long this packet took to arrive.
  288. TimeSpan latency = networkSession.SimulatedLatency +
  289. TimeSpan.FromTicks(sender.RoundtripTime.Ticks / 2);
  290. // Read the state of this tank from the network packet.
  291. tank.ReadNetworkPacket(packetReader, gameTime, latency,
  292. enablePrediction, enableSmoothing);
  293. }
  294. }
  295. /// <summary>
  296. /// Updates the latency and packet loss simulation options. Only the
  297. /// host can alter these values, which are then synchronized over the
  298. /// network by storing them into NetworkSession.SessionProperties. Any
  299. /// changes to the SessionProperties data are automatically replicated
  300. /// on all the client machines, so there is no need to manually send
  301. /// network packets to transmit this data.
  302. /// </summary>
  303. void UpdateOptions()
  304. {
  305. if (networkSession.IsHost)
  306. {
  307. // Change the network quality simultation?
  308. if (IsPressed(Keys.A, Buttons.A))
  309. {
  310. networkQuality++;
  311. if (networkQuality > NetworkQuality.Perfect)
  312. networkQuality = 0;
  313. }
  314. // Change the packet send rate?
  315. if (IsPressed(Keys.B, Buttons.B))
  316. {
  317. if (framesBetweenPackets == 6)
  318. framesBetweenPackets = 3;
  319. else if (framesBetweenPackets == 3)
  320. framesBetweenPackets = 1;
  321. else
  322. framesBetweenPackets = 6;
  323. }
  324. // Toggle prediction on or off?
  325. if (IsPressed(Keys.X, Buttons.X))
  326. enablePrediction = !enablePrediction;
  327. // Toggle smoothing on or off?
  328. if (IsPressed(Keys.Y, Buttons.Y))
  329. enableSmoothing = !enableSmoothing;
  330. // Stores the latest settings into NetworkSession.SessionProperties.
  331. networkSession.SessionProperties["0"] = (int)networkQuality;
  332. networkSession.SessionProperties["1"] = framesBetweenPackets;
  333. networkSession.SessionProperties["2"] = enablePrediction ? 1 : 0;
  334. networkSession.SessionProperties["3"] = enableSmoothing ? 1 : 0;
  335. }
  336. else
  337. {
  338. // Client machines read the latest settings from the session properties.
  339. networkQuality = (NetworkQuality)(int)networkSession.SessionProperties["0"];
  340. framesBetweenPackets = (int)networkSession.SessionProperties["1"];
  341. enablePrediction = (int)networkSession.SessionProperties["2"] != 0;
  342. enableSmoothing = (int)networkSession.SessionProperties["3"] != 0;
  343. }
  344. // Update the SimulatedLatency and SimulatedPacketLoss properties.
  345. switch (networkQuality)
  346. {
  347. case NetworkQuality.Typical:
  348. networkSession.SimulatedLatency = TimeSpan.FromMilliseconds(100);
  349. networkSession.SimulatedPacketLoss = 0.1f;
  350. break;
  351. case NetworkQuality.Poor:
  352. networkSession.SimulatedLatency = TimeSpan.FromMilliseconds(200);
  353. networkSession.SimulatedPacketLoss = 0.2f;
  354. break;
  355. case NetworkQuality.Perfect:
  356. networkSession.SimulatedLatency = TimeSpan.Zero;
  357. networkSession.SimulatedPacketLoss = 0;
  358. break;
  359. }
  360. }
  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.MonoGameOrange);
  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. // Skip if no tank is associated with this gamer yet
  408. if (tank == null)
  409. continue;
  410. // Draw the tank.
  411. tank.Draw(spriteBatch);
  412. // Draw a gamertag label.
  413. spriteBatch.DrawString(font, gamer.Gamertag, tank.Position,
  414. Color.Black, 0, new Vector2(100, 150),
  415. 0.6f, SpriteEffects.None, 0);
  416. }
  417. spriteBatch.End();
  418. }
  419. /// <summary>
  420. /// Draws the current latency and packet loss simulation settings.
  421. /// </summary>
  422. void DrawOptions()
  423. {
  424. string isHost = networkSession.IsHost ? "Host" : "Client";
  425. string quality =
  426. string.Format("Network simulation = {0} ms, {1}% packet loss",
  427. networkSession.SimulatedLatency.TotalMilliseconds,
  428. networkSession.SimulatedPacketLoss * 100);
  429. string sendRate = string.Format("Packets per second = {0}",
  430. 60 / framesBetweenPackets);
  431. string prediction = string.Format("Prediction = {0}",
  432. enablePrediction ? "on" : "off");
  433. string smoothing = string.Format("Smoothing = {0}",
  434. enableSmoothing ? "on" : "off");
  435. // If we are the host, include prompts telling how to change the settings.
  436. if (networkSession.IsHost)
  437. {
  438. quality += " (A to change)";
  439. sendRate += " (B to change)";
  440. prediction += " (X to toggle)";
  441. smoothing += " (Y to toggle)";
  442. }
  443. // Draw combined text to the screen.
  444. string message =
  445. isHost + "\n" + "\n" +
  446. quality + "\n" +
  447. sendRate + "\n" +
  448. prediction + "\n" +
  449. smoothing;
  450. spriteBatch.DrawString(font, message, new Vector2(161, 321), Color.Black);
  451. spriteBatch.DrawString(font, message, new Vector2(160, 320), Color.White);
  452. }
  453. /// <summary>
  454. /// Helper draws notification messages before calling blocking network methods.
  455. /// </summary>
  456. void DrawMessage(string message)
  457. {
  458. if (!BeginDraw())
  459. return;
  460. GraphicsDevice.Clear(Color.MonoGameOrange);
  461. spriteBatch.Begin();
  462. spriteBatch.DrawString(font, message, new Vector2(161, 161), Color.Black);
  463. spriteBatch.DrawString(font, message, new Vector2(160, 160), Color.White);
  464. spriteBatch.End();
  465. EndDraw();
  466. }
  467. /// <summary>
  468. /// Handles input.
  469. /// </summary>
  470. private void HandleInput()
  471. {
  472. previousKeyboardState = currentKeyboardState;
  473. previousGamePadState = currentGamePadState;
  474. currentKeyboardState = Keyboard.GetState();
  475. currentGamePadState = GamePad.GetState(PlayerIndex.One);
  476. // Check for exit.
  477. if (IsActive && IsPressed(Keys.Escape, Buttons.Back))
  478. {
  479. #if !IOS
  480. Exit();
  481. #endif
  482. }
  483. }
  484. /// <summary>
  485. /// Checks if the specified button is pressed on either keyboard or gamepad.
  486. /// </summary>
  487. bool IsPressed(Keys key, Buttons button)
  488. {
  489. return ((currentKeyboardState.IsKeyDown(key) &&
  490. previousKeyboardState.IsKeyUp(key)) ||
  491. (currentGamePadState.IsButtonDown(button) &&
  492. previousGamePadState.IsButtonUp(button)));
  493. }
  494. /// <summary>
  495. /// Reads input data from keyboard and gamepad, and returns
  496. /// this as output parameters ready for use by the tank update.
  497. /// </summary>
  498. static void ReadTankInputs(PlayerIndex playerIndex, out Vector2 tankInput,
  499. out Vector2 turretInput)
  500. {
  501. // Read the gamepad.
  502. GamePadState gamePad = GamePad.GetState(playerIndex);
  503. tankInput = gamePad.ThumbSticks.Left;
  504. turretInput = gamePad.ThumbSticks.Right;
  505. // Read the keyboard.
  506. KeyboardState keyboard = Keyboard.GetState();
  507. if (keyboard.IsKeyDown(Keys.Left))
  508. tankInput.X = -1;
  509. else if (keyboard.IsKeyDown(Keys.Right))
  510. tankInput.X = 1;
  511. if (keyboard.IsKeyDown(Keys.Up))
  512. tankInput.Y = 1;
  513. else if (keyboard.IsKeyDown(Keys.Down))
  514. tankInput.Y = -1;
  515. if (keyboard.IsKeyDown(Keys.K))
  516. turretInput.X = -1;
  517. else if (keyboard.IsKeyDown(Keys.OemSemicolon))
  518. turretInput.X = 1;
  519. if (keyboard.IsKeyDown(Keys.O))
  520. turretInput.Y = 1;
  521. else if (keyboard.IsKeyDown(Keys.L))
  522. turretInput.Y = -1;
  523. // Normalize the input vectors.
  524. if (tankInput.Length() > 1)
  525. tankInput.Normalize();
  526. if (turretInput.Length() > 1)
  527. turretInput.Normalize();
  528. }
  529. }
  530. }