#region File Description //----------------------------------------------------------------------------- // LoadingScreen.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- #endregion #region Using Statements using System; using System.Threading; using System.Diagnostics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Net; #endregion namespace NetworkStateManagement { /// /// The loading screen coordinates transitions between the menu system and the /// game itself. Normally one screen will transition off at the same time as /// the next screen is transitioning on, but for larger transitions that can /// take a longer time to load their data, we want the menu system to be entirely /// gone before we start loading the game. This is done as follows: /// /// - Tell all the existing screens to transition off. /// - Activate a loading screen, which will transition on at the same time. /// - The loading screen watches the state of the previous screens. /// - When it sees they have finished transitioning off, it activates the real /// next screen, which may take a long time to load its data. The loading /// screen will be the only thing displayed while this load is taking place. /// class LoadingScreen : GameScreen { #region Fields bool loadingIsSlow; bool otherScreensAreGone; GameScreen[] screensToLoad; Thread backgroundThread; EventWaitHandle backgroundThreadExit; GraphicsDevice graphicsDevice; NetworkSession networkSession; IMessageDisplay messageDisplay; GameTime loadStartTime; TimeSpan loadAnimationTimer; #endregion #region Initialization /// /// The constructor is private: loading screens should /// be activated via the static Load method instead. /// private LoadingScreen (ScreenManager screenManager,bool loadingIsSlow, GameScreen[] screensToLoad) { this.loadingIsSlow = loadingIsSlow; this.screensToLoad = screensToLoad; TransitionOnTime = TimeSpan.FromSeconds (0.5); // If this is going to be a slow load operation, create a background // thread that will update the network session and draw the load screen // animation while the load is taking place. if (loadingIsSlow) { backgroundThread = new Thread (BackgroundWorkerThread); backgroundThreadExit = new ManualResetEvent (false); graphicsDevice = screenManager.GraphicsDevice; // Look up some services that will be used by the background thread. IServiceProvider services = screenManager.Game.Services; networkSession = (NetworkSession)services.GetService ( typeof(NetworkSession)); messageDisplay = (IMessageDisplay)services.GetService ( typeof(IMessageDisplay)); } } /// /// Activates the loading screen. /// public static void Load (ScreenManager screenManager, bool loadingIsSlow, PlayerIndex? controllingPlayer, params GameScreen[] screensToLoad) { // Tell all the current screens to transition off. foreach (GameScreen screen in screenManager.GetScreens ()) screen.ExitScreen (); // Create and activate the loading screen. LoadingScreen loadingScreen = new LoadingScreen (screenManager, loadingIsSlow, screensToLoad); screenManager.AddScreen (loadingScreen, controllingPlayer); } #endregion #region Update and Draw /// /// Updates the loading screen. /// public override void Update (GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { base.Update (gameTime, otherScreenHasFocus, coveredByOtherScreen); // If all the previous screens have finished transitioning // off, it is time to actually perform the load. if (otherScreensAreGone) { // Start up the background thread, which will update the network // session and draw the animation while we are loading. if (backgroundThread != null) { loadStartTime = gameTime; backgroundThread.Start (); } // Perform the load operation. ScreenManager.RemoveScreen (this); foreach (GameScreen screen in screensToLoad) { if (screen != null) { ScreenManager.AddScreen (screen, ControllingPlayer); } } // Signal the background thread to exit, then wait for it to do so. if (backgroundThread != null) { backgroundThreadExit.Set (); backgroundThread.Join (); } // Once the load has finished, we use ResetElapsedTime to tell // the game timing mechanism that we have just finished a very // long frame, and that it should not try to catch up. ScreenManager.Game.ResetElapsedTime (); } } /// /// Draws the loading screen. /// public override void Draw (GameTime gameTime) { // If we are the only active screen, that means all the previous screens // must have finished transitioning off. We check for this in the Draw // method, rather than in Update, because it isn't enough just for the // screens to be gone: in order for the transition to look good we must // have actually drawn a frame without them before we perform the load. if ((ScreenState == ScreenState.Active) && (ScreenManager.GetScreens ().Length == 1)) { otherScreensAreGone = true; } // The gameplay screen takes a while to load, so we display a loading // message while that is going on, but the menus load very quickly, and // it would look silly if we flashed this up for just a fraction of a // second while returning from the game to the menus. This parameter // tells us how long the loading is going to take, so we know whether // to bother drawing the message. if (loadingIsSlow) { SpriteBatch spriteBatch = ScreenManager.SpriteBatch; SpriteFont font = ScreenManager.Font; string message = Resources.Loading; // Center the text in the viewport. Viewport viewport = ScreenManager.GraphicsDevice.Viewport; Vector2 viewportSize = new Vector2 (viewport.Width, viewport.Height); Vector2 textSize = font.MeasureString (message); Vector2 textPosition = (viewportSize - textSize) / 2; Color color = Color.White * TransitionAlpha; // Animate the number of dots after our "Loading..." message. loadAnimationTimer += gameTime.ElapsedGameTime; int dotCount = (int)(loadAnimationTimer.TotalSeconds * 5) % 10; message += new string ('.', dotCount); // Draw the text. spriteBatch.Begin (); spriteBatch.DrawString (font, message, textPosition, color); spriteBatch.End (); } } #endregion #region Background Thread /// /// Worker thread draws the loading animation and updates the network /// session while the load is taking place. /// void BackgroundWorkerThread () { long lastTime = Stopwatch.GetTimestamp (); // EventWaitHandle.WaitOne will return true if the exit signal has // been triggered, or false if the timeout has expired. We use the // timeout to update at regular intervals, then break out of the // loop when we are signalled to exit. while (!backgroundThreadExit.WaitOne (1000 / 30)) { //GameTime gameTime = GetGameTime (ref lastTime); //DrawLoadAnimation (gameTime); UpdateNetworkSession (); } } // /// // /// Works out how long it has been since the last background thread update. // /// // GameTime GetGameTime (ref long lastTime) // { // long currentTime = Stopwatch.GetTimestamp (); // long elapsedTicks = currentTime - lastTime; // lastTime = currentTime; // // TimeSpan elapsedTime = TimeSpan.FromTicks (elapsedTicks * // TimeSpan.TicksPerSecond / // Stopwatch.Frequency); // // return new GameTime (loadStartTime.TotalGameTime + elapsedTime, elapsedTime); // } /// /// Calls directly into our Draw method from the background worker thread, /// so as to update the load animation in parallel with the actual loading. /// void DrawLoadAnimation (GameTime gameTime) { if ((graphicsDevice == null) || graphicsDevice.IsDisposed) return; try { graphicsDevice.Clear (Color.Black); // Draw the loading screen. Draw (gameTime); // If we have a message display component, we want to display // that over the top of the loading screen, too. if (messageDisplay != null) { messageDisplay.Update (gameTime); messageDisplay.Draw (gameTime); } graphicsDevice.Present (); } catch { // If anything went wrong (for instance the graphics device was lost // or reset) we don't have any good way to recover while running on a // background thread. Setting the device to null will stop us from // rendering, so the main game can deal with the problem later on. graphicsDevice = null; } } /// /// Updates the network session from the background worker thread, to avoid /// disconnecting due to network timeouts even if loading takes a long time. /// void UpdateNetworkSession () { if ((networkSession == null) || (networkSession.SessionState == NetworkSessionState.Ended)) return; try { networkSession.Update (); } catch { // If anything went wrong, we don't have a good way to report that // error while running on a background thread. Setting the session to // null will stop us from updating it, so the main game can deal with // the problem later on. networkSession = null; } } #endregion } }