#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
}
}