//----------------------------------------------------------------------------- // 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; using CardsFramework; 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(); InputState inputState = new InputState(BASE_BUFFER_WIDTH, BASE_BUFFER_HEIGHT); public InputState InputState => inputState; SpriteBatch spriteBatch; SpriteFont font; // MenuFont SpriteFont regularFont; SpriteFont boldFont; Texture2D blankTexture; Texture2D buttonBackground; Texture2D buttonPressed; bool isInitialized; bool traceEnabled; internal const int BASE_BUFFER_WIDTH = 1280; internal const int BASE_BUFFER_HEIGHT = 720; 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(BASE_BUFFER_WIDTH, BASE_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 ButtonPressed { get { return buttonPressed; } } 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; } set { font = value; } } /// /// Regular font (automatically switches between Regular and Regular_CJK based on language) /// public SpriteFont RegularFont { get { return regularFont; } } /// /// Bold font (automatically switches between Bold and Bold_CJK based on language) /// public SpriteFont BoldFont { get { return boldFont; } } /// /// 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; } } Rectangle safeArea = new Rectangle(0, 0, BASE_BUFFER_WIDTH, BASE_BUFFER_HEIGHT); /// /// Returns the portion of the screen where drawing is safely allowed. /// public Rectangle SafeArea { get { return safeArea; } } /// /// 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); // Load the appropriate fonts based on the saved language setting // This handles the case where the game was closed with a CJK language selected string currentLanguage = Blackjack.GameSettings.Instance.Language; bool useCJKFont = currentLanguage == "日本語" || currentLanguage == "中文"; string menuFontPath = useCJKFont ? "Fonts/MenuFont_CJK" : "Fonts/MenuFont"; string regularFontPath = useCJKFont ? "Fonts/Regular_CJK" : "Fonts/Regular"; string boldFontPath = useCJKFont ? "Fonts/Bold_CJK" : "Fonts/Bold"; font = content.Load(menuFontPath); regularFont = content.Load(regularFontPath); boldFont = content.Load(boldFontPath); blankTexture = content.Load("Images/blank"); buttonBackground = content.Load("Images/ButtonRegular"); buttonPressed = content.Load("Images/ButtonPressed"); // Tell each of the screens to load their content. foreach (GameScreen screen in screens) { screen.LoadContent(); } } /// /// Reloads the font based on the current language setting. /// Uses CJK fonts for Japanese and Chinese, regular fonts for other languages. /// NOTE: This only loads fonts. Call RefreshScreensAfterLanguageChange() after /// the language has been set to rebuild screen content. /// public void ReloadFontForLanguage(string language) { ContentManager content = Game.Content; // Determine if we need CJK font support bool useCJKFont = language == "日本語" || language == "中文"; // Load all appropriate fonts string menuFontPath = useCJKFont ? "Fonts/MenuFont_CJK" : "Fonts/MenuFont"; string regularFontPath = useCJKFont ? "Fonts/Regular_CJK" : "Fonts/Regular"; string boldFontPath = useCJKFont ? "Fonts/Bold_CJK" : "Fonts/Bold"; font = content.Load(menuFontPath); regularFont = content.Load(regularFontPath); boldFont = content.Load(boldFontPath); } /// /// Refreshes all screens after language change. Call this AFTER setting the language /// to ensure screens rebuild with matching language and fonts. /// public void RefreshScreensAfterLanguageChange() { foreach (GameScreen screen in screens) { if (screen is Blackjack.SettingsScreen settingsScreen) { // SettingsScreen rebuilds itself in the cycle methods } else if (screen is Blackjack.GameplayScreen gameplayScreen) { // Update gameplay button text (Deal, Clear, Hit, Stand, etc.) gameplayScreen.UpdateButtonText(); } else if (screen is GameStateManagement.MenuScreen menuScreen) { // Force menu screens to rebuild their entries with the new language menuScreen.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. inputState.Update(gameTime); // 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(inputState); 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) { spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, GlobalTransformation); spriteBatch.Draw(blankTexture, new Rectangle(0, 0, BASE_BUFFER_WIDTH, BASE_BUFFER_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 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)); } } } }