Running on Empty

The few things I know, I like to share.

XNA Framework GameEngine Development. (Part 7, ScreenManager:GameComponent)

Introduction

Welcome to Part 7 of the XNA Framework GameEngine Development series.  In this article I will introduce the ScreenManager GameComponent.  This class will manage the game state as well as display menus to flow through the different states of the game.  Finally, the screen manager will maintain a sprite batch that we can use for displaying feedback to the user.  The majority of this work comes from the XNA team site, but I have made changes to implement it in the RoeEngine2.

Add content

For this article you will need four content items.

  • A menufont spritefont (include in the RoeEngine2.Content.Fonts folder).
  • A blank texture (include in the RoeEngine2.Content.Textures folder).
  • A background texture (include in your game content folder).
  • A gradient texture (include in your game content folder).

ScreenManager DrawableGameComponent

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.GameComponents;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2.Texures;
using System.Diagnostics;

namespace RoeEngine2.Managers
{
    public class ScreenManager : DrawableGameComponent
    {
        private static List<GameScreen> _screens = new List<GameScreen>();
        private static List<GameScreen> _screensToUpdate = new List<GameScreen>();

        /// <summary>
        /// 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.
        /// </summary>
        public static GameScreen[] GetScreens()
        {
            return _screens.ToArray();
        }

        private static bool _initialized = false;
        /// <summary>
        /// Is the ScreenManagers Initialized, used for test cases and setup of Effects.
        /// </summary>
        public static bool Initialized
        {
            get { return _initialized; }
        }

        private static SpriteBatch _spriteBatch;
        /// <summary>
        /// A default SpriteBatch shared by all the screens. This saves
        /// each screen having to bother creating their own local instance.
        /// </summary>
        public static SpriteBatch SpriteBatch
        {
            get { return _spriteBatch; }
        }

        private static SpriteFont _font;
        /// <summary>
        /// A default font shared by all the screens. This saves
        /// each screen having to bother loading their own local copy.
        /// </summary>
        public static SpriteFont Font
        {
            get { return _font; }
        }

        bool _traceEnabled = true;
        /// <summary>
        /// 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.
        /// </summary>
        public bool TraceEnabled
        {
            get { return _traceEnabled; }
            set { _traceEnabled = value; }
        }

        /// <summary>
        /// Constructs a new screen manager component.
        /// </summary>
        public ScreenManager(Game game)
            : base(game)
        {
            Enabled = true;
        }

        protected override void LoadContent()
        {
            base.LoadContent();
           
            _spriteBatch = new SpriteBatch(EngineManager.Device);
            _font = EngineManager.ContentManager.Load<SpriteFont>("Content/Fonts/menufont");
            TextureManager.AddTexture(new RoeTexture("Content/Textures/blank"), "blank");

            foreach (GameScreen screen in _screens)
            {
                screen.LoadContent();
            }
        }

        protected override void UnloadContent()
        {
            base.UnloadContent();

            foreach (GameScreen screen in _screens)
            {
                screen.UnloadContent();
            }
        }

        /// <summary>
        /// Initializes each screen and the screen manager itself.
        /// </summary>
        public override void Initialize()
        {
            base.Initialize();           

            _initialized = true;
        }

        /// <summary>
        /// Allows each screen to run logic.
        /// </summary>
        public override void Update(GameTime gameTime)
        {
            // Read the keyboard and gamepad.
            EngineManager.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(EngineManager.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();
        }

        /// <summary>
        /// Prints a list of all the screens, for debugging.
        /// </summary>
        private void TraceScreens()
        {
            List<string> screenNames = new List<string>();

            foreach (GameScreen screen in _screens)
                screenNames.Add(screen.GetType().Name);

            Trace.WriteLine(string.Join(", ", screenNames.ToArray()));
        }

        /// <summary>
        /// Tells each screen to draw itself.
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            foreach (GameScreen screen in _screens)
            {
                if (screen.ScreenState == ScreenState.Hidden)
                    continue;

                screen.Draw(gameTime);
            }

            foreach (GameScreen screen in _screens)
            {
                if (screen.ScreenState == ScreenState.Hidden)
                    continue;
                screen.PostUIDraw(gameTime);
            }
        }

        /// <summary>
        /// Adds a new screen to the screen manager.
        /// </summary>
        public static void AddScreen(GameScreen screen)
        {
            // If we have a graphics device, tell the screen to load content.
            _screens.Add(screen);
            if (_initialized)
            {
                screen.LoadContent();
            }
        }

        /// <summary>
        /// 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.
        /// </summary>
        public static void RemoveScreen(GameScreen screen)
        {
            // If we have a graphics device, tell the screen to unload content.

            if (_initialized)
            {
                screen.UnloadContent();
            }

            _screens.Remove(screen);
            _screensToUpdate.Remove(screen);
        }

        /// <summary>
        /// Helper draws a translucent black fullscreen sprite, used for fading
        /// screens in and out, and for darkening the background behind popups.
        /// </summary>
        public static void FadeBackBufferToBlack(int alpha)
        {
            Viewport viewport = EngineManager.Device.Viewport;

            _spriteBatch.Begin();

            _spriteBatch.Draw(TextureManager.GetTexture("blank").BaseTexture as Texture2D,
                             new Rectangle(0, 0, viewport.Width, viewport.Height),
                             new Color(0, 0, 0, (byte)alpha));

            _spriteBatch.End();
        }
    }
}

GameScreen class

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.Managers;

namespace RoeEngine2.GameComponents
{
    public enum ScreenState
    {
        TransitionOn,
        Active,
        TransitionOff,
        Hidden,
    }

    /// <summary>
    /// A screen is a single layer that has update and draw logic, and which
    /// can be combined with other layers to build up a complex menu system.
    /// For instance the main menu, the options menu, the "are you sure you
    /// want to quit" message box, and the main game itself are all implemented
    /// as screens.
    /// </summary>
    public abstract class GameScreen
    {
        private bool _isPopup = false;
        /// <summary>
        /// Normally when one screen is brought up over the top of another,
        /// the first screen will transition off to make room for the new
        /// one. This property indicates whether the screen is only a small
        /// popup, in which case screens underneath it do not need to bother
        /// transitioning off.
        /// </summary>
        public bool IsPopup
        {
            get { return _isPopup; }
            set { _isPopup = value; }
        }

        private TimeSpan _transitionOnTime = TimeSpan.Zero;
        /// <summary>
        /// Indicates how long the screen takes to
        /// transition on when it is activated.
        /// </summary>
        public TimeSpan TransitionOnTime
        {
            get { return _transitionOnTime; }
            set { _transitionOnTime = value; }
        }

        private TimeSpan _transitionOffTime = TimeSpan.Zero;
        /// <summary>
        /// Indicates how long the screen takes to
        /// transition off when it is deactivated.
        /// </summary>
        public TimeSpan TransitionOffTime
        {
            get { return _transitionOffTime; }
            set { _transitionOffTime = value; }
        }

        private float _transitionPosition = 1;
        /// <summary>
        /// Gets the current position of the screen transition, ranging
        /// from zero (fully active, no transition) to one (transitioned
        /// fully off to nothing).
        /// </summary>
        public float TransitionPosition
        {
            get { return _transitionPosition; }
            set { _transitionPosition = value; }
        }

        /// <summary>
        /// Gets the current alpha of the screen transition, ranging
        /// from 255 (fully active, no transition) to 0 (transitioned
        /// fully off to nothing).
        /// </summary>
        public byte TransitionAlpha
        {
            get { return (byte)(255 - TransitionPosition * 255); }
        }

        private ScreenState _screenState = ScreenState.TransitionOn;
        /// <summary>
        /// Gets the current screen transition state.
        /// </summary>
        public ScreenState ScreenState
        {
            get { return _screenState; }
            set { _screenState = value; }
        }

        private bool _isExiting = false;
        /// <summary>
        /// There are two possible reasons why a screen might be transitioning
        /// off. It could be temporarily going away to make room for another
        /// screen that is on top of it, or it could be going away for good.
        /// This property indicates whether the screen is exiting for real:
        /// if set, the screen will automatically remove itself as soon as the
        /// transition finishes.
        /// </summary>
        public bool IsExiting
        {
            get { return _isExiting; }
            set { _isExiting = value; }
        }

        public bool IsActive
        {
            get
            {
                return !_otherScreenHasFocus &&
                       (_screenState == ScreenState.TransitionOn ||
                        _screenState == ScreenState.Active);
            }
        }

        private bool _otherScreenHasFocus;

        public virtual void LoadContent() { }

        public virtual void UnloadContent() { }

                /// <summary>
        /// Allows the screen to run logic, such as updating the transition position.
        /// Unlike HandleInput, this method is called regardless of whether the screen
        /// is active, hidden, or in the middle of a transition.
        /// </summary>
        public virtual void Update(GameTime gameTime, bool otherScreenHasFocus,
                                                      bool coveredByOtherScreen)
        {
            _otherScreenHasFocus = otherScreenHasFocus;

            if (_isExiting)
            {
                // If the screen is going away to die, it should transition off.
                _screenState = ScreenState.TransitionOff;

                if (!UpdateTransition(gameTime, _transitionOffTime, 1))
                {
                    // When the transition finishes, remove the screen.
                    ScreenManager.RemoveScreen(this);

                    _isExiting = false;
                }
            }
            else if (coveredByOtherScreen)
            {
                // If the screen is covered by another, it should transition off.
                if (UpdateTransition(gameTime, _transitionOffTime, 1))
                {
                    // Still busy transitioning.
                    _screenState = ScreenState.TransitionOff;
                }
                else
                {
                    // Transition finished!
                    _screenState = ScreenState.Hidden;
                }
            }
            else
            {
                // Otherwise the screen should transition on and become active.
                if (UpdateTransition(gameTime, _transitionOnTime, -1))
                {
                    // Still busy transitioning.
                    _screenState = ScreenState.TransitionOn;
                }
                else
                {
                    // Transition finished!
                    _screenState = ScreenState.Active;
                }
            }
        }

        /// <summary>
        /// Helper for updating the screen transition position.
        /// </summary>
        private bool UpdateTransition(GameTime gameTime, TimeSpan time, int direction)
        {
            // How much should we move by?
            float transitionDelta;

            if (time == TimeSpan.Zero)
                transitionDelta = 1;
            else
                transitionDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds /
                                          time.TotalMilliseconds);

            // Update the transition position.
            _transitionPosition += transitionDelta * direction;

            // Did we reach the end of the transition?
            if ((_transitionPosition <= 0) || (_transitionPosition >= 1))
            {
                _transitionPosition = MathHelper.Clamp(_transitionPosition, 0, 1);
                return false;
            }

            // Otherwise we are still busy transitioning.
            return true;
        }

        /// <summary>
        /// Allows the screen to handle user input. Unlike Update, this method
        /// is only called when the screen is active, and not when some other
        /// screen has taken the focus.
        /// </summary>
        public virtual void HandleInput(Input input) { }

        /// <summary>
        /// This is called when the screen should draw itself.
        /// </summary>
        /// <param name="gameTime"></param>
        public virtual void Draw(GameTime gameTime) { }

        /// <summary>
        /// This is called when the screen should draw after the UI has drawn.
        /// </summary>
        /// <param name="gameTime"></param>
        public virtual void PostUIDraw(GameTime gameTime) { }

        /// <summary>
        /// Tells the screen to go away. Unlike ScreenManager.RemoveScreen, which
        /// instantly kills the screen, this method respects the transition timings
        /// and will give the screen a chance to gradually transition off.
        /// </summary>
        public void ExitScreen()
        {
            if (TransitionOffTime == TimeSpan.Zero)
            {
                // If the screen has a zero transition time, remove it immediately.
                ScreenManager.RemoveScreen(this);
            }
            else
            {
                // Otherwise flag that it should transition off and then exit.
                _isExiting = true;
            }
        }
    }
}

Using the ScreenManager DrawableGameComponent

Add the following code to the Properties section of RoeEngine2

private static ScreenManager _screenManagers = null;

And in the RoeEngine2 Constructor

// Init screen Managers
_screenManagers = new ScreenManager(this);
Components.Add(_screenManagers);

//TODO include other inits here!

Optional TDD:

[Test, Category("ScreenManager")]
public void ScreenManagerCreated()
{
    //Assert.IsTrue(ScreenManager.Initialized);
    //Assert.IsNotNull(ScreenManager.Font);
    Assert.IsNotNull(ScreenManager.SpriteBatch);
}

Finally something to see

In your game project you will be adding some GameScreen objects.  These will change from project to project, I just chose these because they were in the original template from the XNA team site.

BackgroundScreen.cs

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.GameComponents;
using RoeEngine2.Managers;
using RoeEngine2.Texures;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2.Interfaces;

namespace Kynskavion.GameScreens
{
    public class BackgroundScreen : GameScreen
    {
        const string texture = "background";

        /// <summary>
        /// Constructor
        /// </summary>
        public BackgroundScreen()
        {
            TransitionOnTime = TimeSpan.FromSeconds(0.5f);
            TransitionOffTime = TimeSpan.FromSeconds(0.5f);
        }

        public override void LoadContent()
        {
            base.LoadContent();

            TextureManager.AddTexture(new RoeTexture("Content/Textures/background"), texture);
        }

        public override void UnloadContent()
        {
            base.UnloadContent();

            TextureManager.RemoveTexture(texture);
        }

        /// <summary>
        /// Updates the background screen. Unlike most screens, this should not
        /// transition off even if it has been covered by another screen: it is
        /// supposed to be covered, after all! This overload forces the
        /// coveredByOtherScreen parameter to false in order to stop the base
        /// Update method wanting to transition off.
        /// </summary>
        public override void Update(GameTime gameTime, bool otherScreenHasFocus,
                                                       bool coveredByOtherScreen)
        {
            base.Update(gameTime, otherScreenHasFocus, false);
        }

        public override void Draw(GameTime gameTime)
        {
            Viewport viewport = EngineManager.Device.Viewport;

            Rectangle fullscreen = new Rectangle(0, 0, viewport.Width, viewport.Height);

            byte fade = TransitionAlpha;

            ScreenManager.SpriteBatch.Begin();

            ScreenManager.SpriteBatch.Draw(TextureManager.GetTexture(texture).BaseTexture as Texture2D,
                                           fullscreen,
                                           new Color(fade, fade, fade));

            ScreenManager.SpriteBatch.End();
        }
    }
}

MainMenuScreen.cs

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.Managers;

namespace Kynskavion.GameScreens
{
    class MainMenuScreen : MenuScreen
    {
        /// <summary>
        /// The main menu screen is the first thing displayed when the game starts up.
        /// </summary>
        public MainMenuScreen()
            : base("Main Menu")
        {
            MenuEntry playGameManuEntry = new MenuEntry("Play Game");
            MenuEntry optionsMenuEntry = new MenuEntry("Options");
            MenuEntry exitMenuEntry = new MenuEntry("Exit");

            playGameManuEntry.Selected += PlayGameMenuEntrySelected;
            optionsMenuEntry.Selected += OptionsMenuEntrySelected;
            exitMenuEntry.Selected += OnCancel;

            MenuEntries.Add(playGameManuEntry);
            MenuEntries.Add(optionsMenuEntry);
            MenuEntries.Add(exitMenuEntry);
        }

        /// <summary>
        /// Event handler for when the Play Game menu entry is selected.
        /// </summary>
        void PlayGameMenuEntrySelected(object sender, EventArgs e)
        {
            //LoadingScreen.Load(ScreenManager, true, new GameplayScreen());
        }

        /// <summary>
        /// Event handler for when the Options menu entry is selected.
        /// </summary>
        void OptionsMenuEntrySelected(object sender, EventArgs e)
        {
            //ScreenManager.AddScreen(new OptionsMenuScreen());
        }

        /// <summary>
        /// When the user cancels the main menu, ask if they want to exit the sample.
        /// </summary>
        protected override void OnCancel()
        {
            const string message = "Are you sure you want to exit this sample?";

            MessageBoxScreen messageBox = new MessageBoxScreen(message);
            messageBox.Accepted += ExitMessageBoxAccepted;
            ScreenManager.AddScreen(messageBox);
        }

        /// <summary>
        /// Event handler for when the user selects ok on the "are you sure
        /// you want to exit" message box.
        /// </summary>
        void ExitMessageBoxAccepted(object sender, EventArgs e)
        {
            EngineManager.Game.Exit();
        }
    }
}

MenuEntry.cs

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2.Managers;

namespace Kynskavion.GameScreens
{
    /// <summary>
    /// Helper class represents a single entry in a MenuScreen. By default this
    /// just draws the entry text string, but it can be customized to display menu
    /// entries in different ways. This also provides an event that will be raised
    /// when the menu entry is selected.
    /// </summary>
    class MenuEntry
    {
        string text;
        /// <summary>
        /// Gets or sets the text of this menu entry.
        /// </summary>
        public string Text
        {
            get { return text; }
            set { text = value; }
        }

        /// <summary>
        /// Tracks a fading selection effect on the entry.
        /// </summary>
        /// <remarks>
        /// The entries transition out of the selection effect when they are deselected.
        /// </remarks>
        float selectionFade;

        /// <summary>
        /// Event raised when the menu entry is selected.
        /// </summary>
        public event EventHandler<EventArgs> Selected;

        /// <summary>
        /// Method for raising the Selected event.
        /// </summary>
        protected internal virtual void OnSelectEntry()
        {
            if (Selected != null)
                Selected(this, EventArgs.Empty);
        }

        /// <summary>
        /// Constructs a new menu entry with the specified text.
        /// </summary>
        public MenuEntry(string text)
        {
            this.text = text;
        }

        /// <summary>
        /// Updates the menu entry.
        /// </summary>
        public virtual void Update(MenuScreen screen, bool isSelected,
                                                      GameTime gameTime)
        {
            // When the menu selection changes, entries gradually fade between
            // their selected and deselected appearance, rather than instantly
            // popping to the new state.
            float fadeSpeed = (float)gameTime.ElapsedGameTime.TotalSeconds * 4;

            if (isSelected)
                selectionFade = Math.Min(selectionFade + fadeSpeed, 1);
            else
                selectionFade = Math.Max(selectionFade - fadeSpeed, 0);
        }

        /// <summary>
        /// Draws the menu entry. This can be overridden to customize the appearance.
        /// </summary>
        public virtual void Draw(MenuScreen screen, Vector2 position,
                                 bool isSelected, GameTime gameTime)
        {
            // Draw the selected entry in yellow, otherwise white.
            Color color = isSelected ? Color.Yellow : Color.White;

            // Pulsate the size of the selected menu entry.
            double time = gameTime.TotalGameTime.TotalSeconds;

            float pulsate = (float)Math.Sin(time * 6) + 1;

            float scale = 1 + pulsate * 0.05f * selectionFade;

            // Modify the alpha to fade text out during transitions.
            color = new Color(color.R, color.G, color.B, screen.TransitionAlpha);

            // Draw text, centered on the middle of each line.

            Vector2 origin = new Vector2(0, ScreenManager.Font.LineSpacing / 2);

            ScreenManager.SpriteBatch.DrawString(ScreenManager.Font, text, position, color, 0,
                                   origin, scale, SpriteEffects.None, 0);
        }

        /// <summary>
        /// Queries how much space this menu entry requires.
        /// </summary>
        public virtual int GetHeight(MenuScreen screen)
        {
            return ScreenManager.Font.LineSpacing;
        }
    }
}

MenuScreen.cs

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.GameComponents;
using Microsoft.Xna.Framework;
using RoeEngine2.Managers;
using Microsoft.Xna.Framework.Graphics;

namespace Kynskavion.GameScreens
{
    abstract class MenuScreen : GameScreen
    {
        List<MenuEntry> _menuEntries = new List<MenuEntry>();
        /// <summary>
        /// Gets the list of menu entry strings, so derived classes can add
        /// or change the menu contents.
        /// </summary>
        protected IList<MenuEntry> MenuEntries
        {
            get { return _menuEntries; }
        }

        int _selectedEntry = 0;
        string _menuTitle;

        /// <summary>
        /// Constructor.
        /// </summary>
        public MenuScreen(string menuTitle)
        {
            _menuTitle = menuTitle;
            TransitionOnTime = TimeSpan.FromSeconds(0.5);
            TransitionOffTime = TimeSpan.FromSeconds(0.5);
        }

        /// <summary>
        /// Responds to user input, changing the selected entry and accepting
        /// or cancelling the menu.
        /// </summary>
        public override void HandleInput(Input input)
        {
            // Move to the previous menu entry?
            if (input.MenuUp)
            {
                _selectedEntry--;

                if (_selectedEntry < 0)
                    _selectedEntry = _menuEntries.Count - 1;
            }

            // Move to the next menu entry?
            if (input.MenuDown)
            {
                _selectedEntry++;

                if (_selectedEntry >= _menuEntries.Count)
                    _selectedEntry = 0;
            }

            // Accept or cancel the menu?
            if (input.MenuSelect)
            {
                OnSelectEntry(_selectedEntry);
            }
            else if (input.MenuCancel)
            {
                OnCancel();
            }
        }

        /// <summary>
        /// Notifies derived classes that a menu entry has been chosen.
        /// </summary>
        protected virtual void OnSelectEntry(int entryIndex)
        {
            _menuEntries[_selectedEntry].OnSelectEntry();
        }

        /// <summary>
        /// Notifies derived classes that the menu has been cancelled.
        /// </summary>
        protected virtual void OnCancel()
        {
            ExitScreen();
        }

        /// <summary>
        /// Helper overload makes it easy to use OnCancel as a MenuEntry event handler.
        /// </summary>
        protected void OnCancel(object sender, EventArgs e)
        {
            OnCancel();
        }

        /// <summary>
        /// Updates the menu.
        /// </summary>
        public override void Update(GameTime gameTime, bool otherScreenHasFocus,
                                                       bool coveredByOtherScreen)
        {
            base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);

            // Update each nested MenuEntry object.
            for (int i = 0; i < _menuEntries.Count; i++)
            {
                bool isSelected = IsActive && (i == _selectedEntry);

                _menuEntries[i].Update(this, isSelected, gameTime);
            }
        }

        /// <summary>
        /// Draws the menu.
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            Vector2 position = new Vector2(100, 150);

            // Make the menu slide into place during transitions, using a
            // power curve to make things look more interesting (this makes
            // the movement slow down as it nears the end).
            float transitionOffset = (float)Math.Pow(TransitionPosition, 2);

            if (ScreenState == ScreenState.TransitionOn)
                position.X -= transitionOffset * 256;
            else
                position.X += transitionOffset * 512;

            ScreenManager.SpriteBatch.Begin();

            // Draw each menu entry in turn.
            for (int i = 0; i < _menuEntries.Count; i++)
            {
                MenuEntry menuEntry = _menuEntries[i];

                bool isSelected = IsActive && (i == _selectedEntry);

                menuEntry.Draw(this, position, isSelected, gameTime);

                position.Y += menuEntry.GetHeight(this);
            }

            // Draw the menu title.
            Vector2 titlePosition = new Vector2(426, 80);
            Vector2 titleOrigin = ScreenManager.Font.MeasureString(_menuTitle) / 2;
            Color titleColor = new Color(192, 192, 192, TransitionAlpha);
            float titleScale = 1.25f;

            titlePosition.Y -= transitionOffset * 100;

            ScreenManager.SpriteBatch.DrawString(ScreenManager.Font, _menuTitle, titlePosition, titleColor, 0,
                                   titleOrigin, titleScale, SpriteEffects.None, 0);

            ScreenManager.SpriteBatch.End();
        }
    }
}

MessageBoxScreen.cs

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.GameComponents;
using RoeEngine2.Managers;
using RoeEngine2.Texures;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;

namespace Kynskavion.GameScreens
{
    /// <summary>
    /// A popup message box screen, used to display "are you sure?"
    /// confirmation messages.
    /// </summary>
    class MessageBoxScreen : GameScreen
    {
        private const string texture = "gradient";
        private string _message;

        public event EventHandler<EventArgs> Accepted;
        public event EventHandler<EventArgs> Cancelled;

        /// <summary>
        /// Constructor automatically includes the standard "A=ok, B=cancel"
        /// usage text prompt.
        /// </summary>
        public MessageBoxScreen(string message)
            : this(message, true)
        { }

        /// <summary>
        /// Constructor lets the caller specify whether to include the standard
        /// "A=ok, B=cancel" usage text prompt.
        /// </summary>
        public MessageBoxScreen(string message, bool includeUsageText)
        {
            const string usageText = "\nA button, Space, Enter = ok" +
                                     "\nB button, Esc = cancel";

            if (includeUsageText)
                _message = message + usageText;
            else
                _message = message;

            IsPopup = true;

            TransitionOnTime = TimeSpan.FromSeconds(0.2);
            TransitionOffTime = TimeSpan.FromSeconds(0.2);
        }

        /// <summary>
        /// Loads graphics content for this screen. This uses the shared ContentManager
        /// provided by the Game class, so the content will remain loaded forever.
        /// Whenever a subsequent MessageBoxScreen tries to load this same content,
        /// it will just get back another reference to the already loaded data.
        /// </summary>
        public override void LoadContent()
        {
            TextureManager.AddTexture(new RoeTexture("Content/Textures/gradient"), texture);
        }

        /// <summary>
        /// Responds to user input, accepting or cancelling the message box.
        /// </summary>
        public override void HandleInput(Input input)
        {
            if (input.MenuSelect)
            {
                // Raise the accepted event, then exit the message box.
                if (Accepted != null)
                    Accepted(this, EventArgs.Empty);

                ExitScreen();
            }
            else if (input.MenuCancel)
            {
                // Raise the cancelled event, then exit the message box.
                if (Cancelled != null)
                    Cancelled(this, EventArgs.Empty);

                ExitScreen();
            }
        }

        /// <summary>
        /// Draws the message box.
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            // Darken down any other screens that were drawn beneath the popup.
            ScreenManager.FadeBackBufferToBlack(TransitionAlpha * 2 / 3);

            // Center the message text in the viewport.
            Viewport viewport = EngineManager.Device.Viewport;
            Vector2 viewportSize = new Vector2(viewport.Width, viewport.Height);
            Vector2 textSize = ScreenManager.Font.MeasureString(_message);
            Vector2 textPosition = (viewportSize - textSize) / 2;

            // The background includes a border somewhat larger than the text itself.
            const int hPad = 32;
            const int vPad = 16;

            Rectangle backgroundRectangle = new Rectangle((int)textPosition.X - hPad,
                                                          (int)textPosition.Y - vPad,
                                                          (int)textSize.X + hPad * 2,
                                                          (int)textSize.Y + vPad * 2);

            // Fade the popup alpha during transitions.
            Color color = new Color(255, 255, 255, TransitionAlpha);

            ScreenManager.SpriteBatch.Begin();

            // Draw the background rectangle.
            ScreenManager.SpriteBatch.Draw(TextureManager.GetTexture(texture).BaseTexture as Texture2D, backgroundRectangle, color);

            // Draw the message box text.
            ScreenManager.SpriteBatch.DrawString(ScreenManager.Font, _message, textPosition, color);

            ScreenManager.SpriteBatch.End();
        }
    }  
}

In program.cs add the following code to the SetupScene() method.

ScreenManager.AddScreen(new BackgroundScreen());
ScreenManager.AddScreen(new MainMenuScreen());

Conclusion

Finally, we have something to see other than the baby blue screen.  In this article I introduced the ScreenManager class, which will manage the game state as well as give us an intuitive menu system.

I look forward to your comments and suggestions.

About these ads

January 28, 2008 - Posted by | C#, GameComponent, TDD, XNA

19 Comments »

  1. Keep it coming. I’m enjoying this series.

    Comment by Rob | January 28, 2008 | Reply

  2. Rob,
    Thank you for your interest. I am enjoying putting the series together, hope you can find some worthwhile bits.

    Comment by roecode | January 28, 2008 | Reply

  3. Great series! Keep it up!

    Comment by Mark | February 3, 2008 | Reply

  4. I get problems:
    paramtername: texture does not accept NULL. It happens in the internaldraw of the spritebatch. It says also in messageboxscreen.cs in line 115…..
    did anybody else run into this problem? I used the code 1:1, did not modify anything.

    Comment by Andi | February 3, 2008 | Reply

  5. Nice work..
    A couple of things tripped me up:
    EngineManager.cs, line 28-29, WAS

    base.Draw(gameTime);
    Device.Clear(BackgroundColor);

    The lines needed to be swapped around. Perhaps I copied the original implementation wrongly. Have some of your other classes changed a bit too? IRoeTexture and RoeTexture didn’t compile for me until I went back and recopied your implementation.

    Anyway, bloody good! Threading next!

    Comment by jaff | February 3, 2008 | Reply

  6. still no luck here. I changed like jaff said, the program runs, but when i select “exit”, then i get the same error

    Comment by Andi | February 3, 2008 | Reply

  7. I am constantly updating and refactoring code. I usually try to go back and update each entry when I do make changes. Once I get to the point of actually displaying meshes and objects in the engine I will post the source code for download.

    Comment by roecode | February 4, 2008 | Reply

  8. Just one other small change, I notice in the code you released for part 11, Line 37 was removed from BackgroundScreen.cs, i.e:

    TextureManager.RemoveTexture(texture);

    Comment by jaff | February 16, 2008 | Reply

  9. Thank you Jaff, I am definitely trying to be very careful about my updates to older code. It is becoming very difficult for me to keep all of these older entries up to date.

    So, in my newest articles, I am going to start releasing code more often. That way readers can enjoy each new article with fresh eyes. I am also going to spend more time explaining concepts rather than focus on the code.

    Comment by roecode | February 18, 2008 | Reply

  10. Hi reocode,
    i have a little question ;-) I seperate all your parts in single solutions to covering it better out. At this part i noticed a slow downfall of the FPS. That means when i start with ca. 1500FPS and after perhabs 30Seconds i only have 600FPS. Then it goes again up to 1500 and so on and so on. Do u know why?Could it be a memory leak or something like this?
    btw sry for my bad english xD

    Comment by J2T | May 26, 2008 | Reply

  11. J2T,
    I really do not know why you need multiple solutions. I assume you mean multiple projects. I suppose there could be a decrease in performance by placing all the game managers in their own projects.

    I normally place my objects in projects based on functionality like… RoeEngine (Engine objects), Testing (TDD), Game (Kynskavion) etc.

    Having a seperate project for each manager… well that just seems like too much encapsulation and sort of defeats the purpose of manager classes.

    Any number of things can control FPS. I have always been told anything above 60 FPS doesn’t really matter so 1500 FPS is essentially the same thing as 600 FPS. The difference could be garbage collection, background processes, or any number of a thousand things outside of your control. If it troubles you, you can set the timestep of the application, that will choke the throttle to 60 FPS.

    Also as a side note, a drop of 1500 to 600 is not as “large” as a drop from 60 FPS to 50 FPS. There are plenty of articles discussing this issue so I wont try to bore you here.

    Comment by roecode | May 27, 2008 | Reply

  12. Yes u’re right i mean multiple projects of course.
    And yes u’re right that in this ranges the differences does’nt matter sure. And things like GC and what u’re said could be possible reasons. I only asked my self because of the continuous repeating and i want to go safe and thought it could be important for you ;-)

    Comment by J2T | May 27, 2008 | Reply

  13. Really Enjoying all your stuff man, well laid out. Have changed a few things in my own engine after reading through some of your stuff, has helped me a great deal.

    Back to working on my winforms editor :S proving to be more of a pain that I had anticipated thus far.

    Kudos for all the hard work, keep it up.

    Comment by Sicarius | December 5, 2008 | Reply

    • Thank you, glad you are enjoying the series. Actually, most of this stuff is very much out of date. I am working on a “new” engine and series currently. Look for it to start sometime early next year.

      Comment by roecode | December 5, 2008 | Reply

  14. This was very helpful for me, I like being able to see some actual results! I’m still a little confused about one thing though, it seems that all the textures that are going to be used must be present at the time the project is built. Is that a flat-out limitation of XNA’s content pipeline or is there some way around it? Needing all the textures present at the time it’s built means there’s no way, for instance, to have the player add their own image for an avatar, or after the game is done, continue expand on it and add new items, levels, or anything that requires new resources. That would also mean that there would be no way to make a world editor that you can use to add new models or textures to the game right? Having to hard code everything into XNA does not seem very reasonable for anything other than a small demo. If I am mistaken please correct me!

    Comment by META_Cog | February 9, 2009 | Reply

  15. There is an article on the XNA website that shows how to organise loading content at runtime…

    Comment by sTeeL | February 16, 2009 | Reply

  16. Do you have those sprites/textures?? It’s rather pointless for me atm. I have no experience with sprites nor textures.

    Comment by Iltar | October 22, 2009 | Reply

  17. Can you attach some textures/sprites so I know what they should look like?

    Comment by Iltar | October 27, 2009 | Reply

  18. how is the ” Accepted(this, EventArgs.Empty);” handled. where is the code for handling this event. please reply.

    Comment by vaibhav | June 14, 2011 | Reply


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: