//-----------------------------------------------------------------------------
// ScreenManager.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
using System;
using System.Diagnostics;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input.Touch;
using System.IO;
using System.IO.IsolatedStorage;
namespace GameStateManagement
{
///
/// The screen manager is a component which manages one or more GameScreen
/// instances. It maintains a stack of screens, calls their Update and Draw
/// methods at the appropriate times, and automatically routes input to the
/// topmost active screen.
///
public class ScreenManager : DrawableGameComponent
{
List screens = new List();
List screensToUpdate = new List();
public InputState input = new InputState();
SpriteBatch spriteBatch;
SpriteFont font;
Texture2D blankTexture;
Texture2D buttonBackground;
bool isInitialized;
bool traceEnabled;
internal const int BACK_BUFFER_WIDTH = 800;
internal const int BACK_BUFFER_HEIGHT = 480;
private int backbufferWidth;
/// Gets or sets the current backbuffer width.
public int BackbufferWidth { get => backbufferWidth; set => backbufferWidth = value; }
private int backbufferHeight;
/// Gets or sets the current backbuffer height.
public int BackbufferHeight { get => backbufferHeight; set => backbufferHeight = value; }
private Vector2 baseScreenSize = new Vector2(BACK_BUFFER_WIDTH, BACK_BUFFER_HEIGHT);
/// Gets or sets the base screen size used for scaling calculations.
public Vector2 BaseScreenSize { get => baseScreenSize; set => baseScreenSize = value; }
private Matrix globalTransformation;
/// Gets or sets the global transformation matrix for scaling and positioning.
public Matrix GlobalTransformation { get => globalTransformation; set => globalTransformation = value; }
///
/// A default SpriteBatch shared by all the screens. This saves
/// each screen having to bother creating their own local instance.
///
public SpriteBatch SpriteBatch
{
get { return spriteBatch; }
}
public Texture2D ButtonBackground
{
get { return buttonBackground; }
}
public Texture2D BlankTexture
{
get { return blankTexture; }
}
///
/// A default font shared by all the screens. This saves
/// each screen having to bother loading their own local copy.
///
public SpriteFont Font
{
get { return font; }
}
///
/// If true, the manager prints out a list of all the screens
/// each time it is updated. This can be useful for making sure
/// everything is being added and removed at the right times.
///
public bool TraceEnabled
{
get { return traceEnabled; }
set { traceEnabled = value; }
}
///
/// Returns the portion of the screen where drawing is safely allowed.
///
public Rectangle SafeArea
{
get
{
return Game.GraphicsDevice.Viewport.TitleSafeArea;
}
}
///
/// Constructs a new screen manager component.
///
public ScreenManager(Game game)
: base(game)
{
// we must set EnabledGestures before we can query for them, but
// we don't assume the game wants to read them.
TouchPanel.EnabledGestures = GestureType.None;
}
///
/// Initializes the screen manager component.
///
public override void Initialize()
{
base.Initialize();
isInitialized = true;
}
///
/// Load your graphics content.
///
protected override void LoadContent()
{
// Load content belonging to the screen manager.
ContentManager content = Game.Content;
spriteBatch = new SpriteBatch(GraphicsDevice);
font = content.Load("Fonts/MenuFont");
blankTexture = content.Load("Images/blank");
buttonBackground = content.Load("Images/ButtonRegular");
// Tell each of the screens to load their content.
foreach (GameScreen screen in screens)
{
screen.LoadContent();
}
}
///
/// Unload your graphics content.
///
protected override void UnloadContent()
{
// Tell each of the screens to unload their content.
foreach (GameScreen screen in screens)
{
screen.UnloadContent();
}
}
///
/// Allows each screen to run logic.
///
public override void Update(GameTime gameTime)
{
// Read the keyboard and gamepad.
input.Update();
// Make a copy of the master screen list, to avoid confusion if
// the process of updating one screen adds or removes others.
screensToUpdate.Clear();
foreach (GameScreen screen in screens)
screensToUpdate.Add(screen);
bool otherScreenHasFocus = !Game.IsActive;
bool coveredByOtherScreen = false;
// Loop as long as there are screens waiting to be updated.
while (screensToUpdate.Count > 0)
{
// Pop the topmost screen off the waiting list.
GameScreen screen = screensToUpdate[screensToUpdate.Count - 1];
screensToUpdate.RemoveAt(screensToUpdate.Count - 1);
// Update the screen.
screen.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
if (screen.ScreenState == ScreenState.TransitionOn ||
screen.ScreenState == ScreenState.Active)
{
// If this is the first active screen we came across,
// give it a chance to handle input.
if (!otherScreenHasFocus)
{
screen.HandleInput(input);
otherScreenHasFocus = true;
}
// If this is an active non-popup, inform any subsequent
// screens that they are covered by it.
if (!screen.IsPopup)
coveredByOtherScreen = true;
}
}
// Print debug trace?
if (traceEnabled)
TraceScreens();
}
///
/// Prints a list of all the screens, for debugging.
///
void TraceScreens()
{
List screenNames = new List();
foreach (GameScreen screen in screens)
screenNames.Add(screen.GetType().Name);
Debug.WriteLine(string.Join(", ", screenNames.ToArray()));
}
///
/// Tells each screen to draw itself.
///
public override void Draw(GameTime gameTime)
{
foreach (GameScreen screen in screens)
{
if (screen.ScreenState == ScreenState.Hidden)
continue;
screen.Draw(gameTime);
}
}
///
/// Adds a new screen to the screen manager.
///
public void AddScreen(GameScreen screen, PlayerIndex? controllingPlayer)
{
screen.ControllingPlayer = controllingPlayer;
screen.ScreenManager = this;
screen.IsExiting = false;
// If we have a graphics device, tell the screen to load content.
if (isInitialized)
{
screen.LoadContent();
}
screens.Add(screen);
// update the TouchPanel to respond to gestures this screen is interested in
TouchPanel.EnabledGestures = screen.EnabledGestures;
}
///
/// Removes a screen from the screen manager. You should normally
/// use GameScreen.ExitScreen instead of calling this directly, so
/// the screen can gradually transition off rather than just being
/// instantly removed.
///
public void RemoveScreen(GameScreen screen)
{
// If we have a graphics device, tell the screen to unload content.
if (isInitialized)
{
screen.UnloadContent();
}
screens.Remove(screen);
screensToUpdate.Remove(screen);
// if there is a screen still in the manager, update TouchPanel
// to respond to gestures that screen is interested in.
if (screens.Count > 0)
{
TouchPanel.EnabledGestures = screens[screens.Count - 1].EnabledGestures;
}
}
///
/// Expose an array holding all the screens. We return a copy rather
/// than the real master list, because screens should only ever be added
/// or removed using the AddScreen and RemoveScreen methods.
///
public GameScreen[] GetScreens()
{
return screens.ToArray();
}
///
/// Helper draws a translucent black fullscreen sprite, used for fading
/// screens in and out, and for darkening the background behind popups.
///
public void FadeBackBufferToBlack(float alpha)
{
Viewport viewport = GraphicsDevice.Viewport;
spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, GlobalTransformation);
spriteBatch.Draw(blankTexture,
new Rectangle(0, 0, viewport.Width, viewport.Height),
Color.Black * alpha);
spriteBatch.End();
}
///
/// Scales the game presentation area to match the screen's aspect ratio.
///
public void ScalePresentationArea()
{
// Validate parameters before calculation
if (GraphicsDevice == null || baseScreenSize.X <= 0 || baseScreenSize.Y <= 0)
{
throw new InvalidOperationException("Invalid graphics configuration");
}
// Fetch screen dimensions
backbufferWidth = GraphicsDevice.PresentationParameters.BackBufferWidth;
backbufferHeight = GraphicsDevice.PresentationParameters.BackBufferHeight;
// Prevent division by zero
if (backbufferHeight == 0 || baseScreenSize.Y == 0)
{
return;
}
// Calculate aspect ratios
float baseAspectRatio = baseScreenSize.X / baseScreenSize.Y;
float screenAspectRatio = backbufferWidth / (float)backbufferHeight;
// Determine uniform scaling factor
float scalingFactor;
float horizontalOffset = 0;
float verticalOffset = 0;
if (screenAspectRatio > baseAspectRatio)
{
// Wider screen: scale by height
scalingFactor = backbufferHeight / baseScreenSize.Y;
// Centre things horizontally.
horizontalOffset = (backbufferWidth - baseScreenSize.X * scalingFactor) / 2;
}
else
{
// Taller screen: scale by width
scalingFactor = backbufferWidth / baseScreenSize.X;
// Centre things vertically.
verticalOffset = (backbufferHeight - baseScreenSize.Y * scalingFactor) / 2;
}
// Update the transformation matrix
globalTransformation = Matrix.CreateScale(scalingFactor) *
Matrix.CreateTranslation(horizontalOffset, verticalOffset, 0);
// Update the inputTransformation with the Inverted globalTransformation
// TODO inputState.UpdateInputTransformation(Matrix.Invert(globalTransformation));
// Debug info
Debug.WriteLine($"Screen Size - Width[{backbufferWidth}] Height[{backbufferHeight}] ScalingFactor[{scalingFactor}]");
}
///
/// Informs the screen manager to serialize its state to disk.
///
public void SerializeState()
{
// open up isolated storage
using (IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication())
{
// if our screen manager directory already exists, delete the contents
if (storage.DirectoryExists("ScreenManager"))
{
DeleteState(storage);
}
// otherwise just create the directory
else
{
storage.CreateDirectory("ScreenManager");
}
// create a file we'll use to store the list of screens in the stack
using (IsolatedStorageFileStream stream = storage.CreateFile("ScreenManager\\ScreenList.dat"))
{
using (BinaryWriter writer = new BinaryWriter(stream))
{
// write out the full name of all the types in our stack so we can
// recreate them if needed.
foreach (GameScreen screen in screens)
{
if (screen.IsSerializable)
{
writer.Write(screen.GetType().AssemblyQualifiedName);
}
}
}
}
// now we create a new file stream for each screen so it can save its state
// if it needs to. we name each file "ScreenX.dat" where X is the index of
// the screen in the stack, to ensure the files are uniquely named
int screenIndex = 0;
foreach (GameScreen screen in screens)
{
if (screen.IsSerializable)
{
string fileName = string.Format("ScreenManager\\Screen{0}.dat", screenIndex);
// open up the stream and let the screen serialize whatever state it wants
using (IsolatedStorageFileStream stream = storage.CreateFile(fileName))
{
screen.Serialize(stream);
}
screenIndex++;
}
}
}
}
public bool DeserializeState()
{
// open up isolated storage
using (IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication())
{
// see if our saved state directory exists
if (storage.DirectoryExists("ScreenManager"))
{
try
{
// see if we have a screen list
if (storage.FileExists("ScreenManager\\ScreenList.dat"))
{
// load the list of screen types
using (IsolatedStorageFileStream stream =
storage.OpenFile("ScreenManager\\ScreenList.dat", FileMode.Open,
FileAccess.Read))
{
using (BinaryReader reader = new BinaryReader(stream))
{
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
// read a line from our file
string line = reader.ReadString();
// if it isn't blank, we can create a screen from it
if (!string.IsNullOrEmpty(line))
{
Type screenType = Type.GetType(line);
GameScreen screen = Activator.CreateInstance(screenType) as GameScreen;
AddScreen(screen, PlayerIndex.One);
}
}
}
}
}
// next we give each screen a chance to deserialize from the disk
for (int i = 0; i < screens.Count; i++)
{
string filename = string.Format("ScreenManager\\Screen{0}.dat", i);
using (IsolatedStorageFileStream stream = storage.OpenFile(filename,
FileMode.Open, FileAccess.Read))
{
screens[i].Deserialize(stream);
}
}
return true;
}
catch (Exception)
{
// if an exception was thrown while reading, odds are we cannot recover
// from the saved state, so we will delete it so the game can correctly
// launch.
DeleteState(storage);
}
}
}
return false;
}
///
/// Deletes the saved state files from isolated storage.
///
private void DeleteState(IsolatedStorageFile storage)
{
// get all of the files in the directory and delete them
string[] files = storage.GetFileNames("ScreenManager\\*");
foreach (string file in files)
{
storage.DeleteFile(Path.Combine("ScreenManager", file));
}
}
}
}