3/27/2017
Introduction In this tutorial, we’ll explore the principles of screen management, then begin writing a basic screen management system in XNA. So what makes my Screen Management Tutorial better than the others? Nothing. Every screen management system has it’s strengths and weaknesses, and that’s true for tutorials as well. This particular screen management system is designed to make each screen operate on its own, and have the same structure as a new XNA project. This tutorial is designed to be easy to follow and straight forward. I do make two assumptions about the reader to make things easier for me. First, I’m assuming you know C# programming. Second, I’m assuming you are familiar with XNA. This tutorial will teach you how to build a basic screen management system. It will not provide you a game engine. It will not teach you how to fade between screens. It will not teach you how to build a menu system. In my opinion, anything that I’ve left out of this tutorial should be in a separate tutorial. Most game players aren’t interested in knowing how a game works. If you mentioned a game screen to the average game player, they would probably think you were referring to their television set. As game developers, we have an entirely different definition of what a game screen is. In the short definition I can think of, a game screen is a single screen within a game. The main menu might be one screen, the game world is another, the hud displayed over the game world might be yet another. A game screen might cover the entire viewing area of the video display, or it might only cover a small portion overlaid over other game screens. We’ll only be building a basic screen management system. It is left up to the reader to expand upon what is provided to fit the needs of your game. To prepare for this tutorial, you’ll need to create a new XNA project, then delete all the cs files. You’ll be creating your own cs files from the example code. Screen Management Explained Most games use a lot of assets (images, sounds, music, etc.), and maintaining them can be a virtual nightmare. By using screen management, we make this task a little easier. In this section, we are going to take a moment to look at the concept of screen management and how it should work. You may skip this section if you want, but this conceptual knowledge may help you to better understand how the system works later. The first concept in screen management is the definition and separation of screens. To define a screen, we’ll need to define something that all screens can inherit that declares all the public methods and parameters that every screen will have. When designing a screen management system, the largest decision here is whether to use an Interface, or an inheritable class to define a screen. The separation of screens is where we decide what parts of our games can stand alone as individual screens. In general, only one screen gets input at a time, but that is not a rule set in stone. It’s entirely possible to build the screen management system to allow more than one screen to accept input at the same time, in this case, it’s up to the developer to decide which screen gets the input when more than one screen accepts the same input, or if both screens should react at once. The second major concept of screen management is the screen manager itself. The screen manager is the heart of screen management, it tracks what screens have been added, which ones are visible, and which ones should get input. It’s the memory management class that initializes new screens and tells existing screens to perform updates and draw themselves. It’s also in the memory management class where screens that are no longer needed are closed, unloaded, and destroyed. One main thing to remember is that screens are never directly interacted with in the code except through the screen manager class. It’s also important to understand that very little game code will exist outside of screens. As we proceed forward, these concepts should make more sense the closer we get to having a completed screen management system. Screen Definition In defining a screen, we can choose to use either an Interface or a class that every screen must inherit. We must then decide what properties and attributes a screen must inherit. The second decision is conveniently answered for us by the default XNA project. To answer the first, I suggest using a class to be inherited as that allows you to provide basic code common to all screens, for instance, this base class is where an asset management system would be built, allowing each screen to completely manage its own assets. Using a class also allows you to declare default values for the variables declared in it. To start with, however, we’re going to build an empty skeleton class as that will be enough for now. In your project, you shouldn’t have any cs files. In the solution explorer, right-click on the project name, and select add, new folder. Name this folder “Screens”. Now add a class to that folder named GameScreen.cs. Replace the entire contents of that file with the code below, rename the namespace accordingly. The declaration of this class is fairly self-explanatory, but our intended usage of the variables may need a little clarification. The screen will only receive player input if IsActive is set to true. BackgroundColor is only used if the screen covers the entire visual display, signified by IsPopup being false. The Screen Manager Class Finally, we come to the meat and potatoes of screen management, the screen manager itself. At this point, our project should contain only one folder containing GameScreen.cs. Let’s start by adding the ScreenManager class. Right-click on your project’s name in the Solution Explorer and select add – class. Name this file ScreenManager.cs and delete its entire contents. This file’s going to be a little large, so we’re going to step through the file from top to bottom one section/method at a time. We won’t be jumping back and forth like so many other tutorials do, I find that to be just too confusing to follow. As each piece of code is provided, past it below the last code you pasted in your source file. Let’s start by declaring what assemblies we will be using in this file. Past the following into your source file: So far this is all fairly basic. We’ve declared what assemblies we are going to use, started the class, and defined a few public variables. The first two are fairly basic to XNA. The dictionaries might have you wondering though. By having these dictionaries in the screen manager, we can load assets into the screen manager for multiple screens to use. That is, we can load it once and use it everywhere. ScreenList is a list of screens that have been added to the game stored in an array format. Finally, we created a content manager variable so we can have access to it from within screens themselves. You may also note that the screen manager class inherits from XNA’s Game class. That means that the screen manager is the starting location for the game itself. When you think about it, that makes sense because the screen manager essentially acts as the brain of the game. This first method is the screen managers constructor. Take note that it’s in this method that we’re setting the size of the screen and specifying that it should run in full-screen mode. On the XBox 360 you don’t need to specify that it will run in full screen as all applications run in full screen on the console. As it’s name suggests, this method initializes the screen manager and sets it’s variables to new instances to prevent them from being null later on. It’s in this method that you will want to load any assets that should be accessible to the whole game. It’s also here where we declare what the first screen of our game should be. In this case, our first screen will be a screen named “TestScreen”. When the game is preparing to shutdown, this method will be used to unload any assets or other content from memory. In XNA, the update method is where all game logic is updated and any necessary calculations are performed. Since this is a screen manager that we’re building, then we’ll cycle through all the top screens and call upon their update methods. Normally we would call upon an input manager before we update the screens, but we don’t have one and aren’t building on in this tutorial. Instead, I’ve added some temporary code to allow you to exit the game by pressing Esc on your keyboard. XNA calls upon this method several times a second. Here we cycle through the top screens yet again to find the first visible screen, then call upon each screens draw method after it. This allows the most recent screen to be drawn on top of the previous screens. It’s also here where we set the background color to the background color specified in the first visible screen. All of these methods allow you to add or remove assets from the screen managers asset dictionaries. To prevent errors, when you add assets we first make sure you supplied an asset, then we make sure the asset doesn’t already exist in the dictionary. Likewise, it makes sure the dictionary actually contains the asset your removing before it attempts to remove it. Please note that the same asset can exist more than once in a dictionary, provided that each copy of the asset is supplied a different identifier string. Whenever possible, this should be avoided, and assets should be reused instead. AddScreen and RemoveScreen do just what they say, they add or remove a screen from the screen managers list of screens. When a screen is added, it is automatically added to the end of the list so that it has the highest index value in the list. However, screens are removed from where ever in the list they may exist. This also checks to make sure that at least one screen is present in the list, and adds a default screen if not. Finally, ChangeScreens is a shortcut method that can be used to both remove an existing screen and add a new screen at the same time. Hey look, we’ve finally reached the end of the class! At this point, we have a fully functional screen management system. It’s very basic, but it serves the purpose and leaves plenty of possibilities for future expansion. A Test Screen Just showing you the definition of a screen and building the screen manager isn’t enough. This tutorial wouldn’t be complete without adding a test screen to teach you how to build the screens themselves. To start, right-click on the Screens folder in the Solution Explorer and add a new class named TestScreen.cs to it. Our new file should look like this: This is definitely not a screen yet, so let’s give it its core. Add these to the using statements at the top: We may not need all of these using statements, but I always start with them to ensure I don’t miss anything. Next, change the class to inherit from GameScreen and make it public. Believe it or not, we now have ourselves a screen to display. Ok, so it’s an empty screen, but the entire program will compile cleanly at this point, which means we haven’t left anything. It will also run without errors or crashing, which means the screen works as well, but it can’t stay like this. We have to add something to it, but what? Since this is just a test screen, I don’t want to have to add any content files (assets), so we’re going to draw some primitives instead. Put the following code inside the TestScreen class: Looking back at the GameScreen class you’ll see that we declared a few virtual methods. Those virtual methods are overridden in the screen to provide functionality to the screen. If you compile and run the program at this point, you should get a screen with a light blue background and a multi-colored triangle similar to this:using Microsoft.Xna.Framework;
namespace XNATutorials.Screens
{
public class GameScreen
{
public bool IsActive = true;
public bool IsPopup = false;
public Color BackgroundColor = Color.CornflowerBlue;
public virtual void LoadAssets() { }
public virtual void Update(GameTime gameTime) { }
public virtual void Draw(GameTime gameTime) { }
public virtual void UnloadAssets() { }
}
}using System;
using System.Collections.Generic;
using XNATutorials.Screens;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace XNATutorials
{
public class ScreenManager : Game
{
public static GraphicsDeviceManager GraphicsDeviceMgr;
public static SpriteBatch Sprites;
public static Dictionary<string, Texture2D> Textures2D;
public static Dictionary<string, Texture3D> Textures3D;
public static Dictionary<string, SpriteFont> Fonts;
public static Dictionary<string, Model> Models;
public static List<GameScreen> ScreenList;
public static ContentManager ContentMgr;
public static void Main()
{
using(ScreenManager manager = newScreenManager())
{
manager.Run();
}
} public ScreenManager()
{
GraphicsDeviceMgr = new GraphicsDeviceManager(this);
GraphicsDeviceMgr.PreferredBackBufferWidth = 800;
GraphicsDeviceMgr.PreferredBackBufferHeight = 600;
GraphicsDeviceMgr.IsFullScreen = true;
Content.RootDirectory = "Content";
} protected override void Initialize()
{
Textures2D = new Dictionary<string,Texture2D>();
Textures3D = new Dictionary<string,Texture3D>();
Models = new Dictionary<string,Model>();
Fonts = new Dictionary<string,SpriteFont>();
base.Initialize();
} protected override void LoadContent()
{
ContentMgr = Content;
Sprites = new SpriteBatch(GraphicsDevice);
// Load any full game assets here
AddScreen(newTestScreen());
} protected override void UnloadContent()
{
foreach(var screen in ScreenList)
{
screen.UnloadAssets();
}
Textures2D.Clear();
Textures3D.Clear();
Fonts.Clear();
Models.Clear();
ScreenList.Clear();
Content.Unload();
} protected override void Update(GameTime gameTime)
{
try
{
// TODO Remove temp code
if(Keyboard.GetState().IsKeyDown(Keys.Escape))
{
Exit();
}
var startIndex = ScreenList.Count - 1;
while(ScreenList[startIndex].IsPopup && ScreenList[startIndex].IsActive)
{
startIndex--;
}
for(var i = startIndex; i < ScreenList.Count; i++)
{
ScreenList[i].Update(gameTime);
}
}
catch(Exception ex)
{
// ErrorLog.AddError(ex);
throw ex;
}
finally
{
base.Update(gameTime);
}
} protected override void Draw(GameTime gameTime)
{
var startIndex = ScreenList.Count - 1;
while(ScreenList[startIndex].IsPopup)
{
startIndex--;
}
GraphicsDevice.Clear(ScreenList[startIndex].BackgroundColor);
GraphicsDeviceMgr.GraphicsDevice.Clear(ScreenList[startIndex].BackgroundColor);
for(var i = startIndex; i < ScreenList.Count; i++)
{
ScreenList[i].Draw(gameTime);
}
base.Draw(gameTime);
} public static void AddFont(string fontName)
{
if(Fonts == null)
{
Fonts = new Dictionary<string,SpriteFont>();
}
if(!Fonts.ContainsKey(fontName))
{
Fonts.Add(fontName, ContentMgr.Load<SpriteFont>(fontName));
}
}
public static void RemoveFont(string fontName)
{
if(Fonts.ContainsKey(fontName))
{
Fonts.Remove(fontName);
}
}
public static void AddTexture2D(string textureName)
{
if(Textures2D == null)
{
Textures2D = new Dictionary<string,Texture2D>();
}
if(!Textures2D.ContainsKey(textureName))
{
Textures2D.Add(textureName, ContentMgr.Load<Texture2D>(textureName));
}
}
public static void RemoveTexture2D(string textureName)
{
if(Textures2D.ContainsKey(textureName))
{
Textures2D.Remove(textureName);
}
}
public static void AddTexture3D(string textureName)
{
if(Textures3D == null)
{
Textures3D = newDictionary<string,Texture3D>();
}
if(!Textures3D.ContainsKey(textureName))
{
Textures3D.Add(textureName, ContentMgr.Load<Texture3D>(textureName));
}
}
public static void RemoveTexture3D(string textureName)
{
if(Textures3D.ContainsKey(textureName))
{
Textures3D.Remove(textureName);
}
}
public static void AddModel(string modelName)
{
if(Models == null)
{
Models = new Dictionary<string,Model>();
}
if(!Models.ContainsKey(modelName))
{
Models.Add(modelName, ContentMgr.Load<Model>(modelName));
}
}
public static void RemoveModel(string modelName)
{
if(Models.ContainsKey(modelName))
{
Models.Remove(modelName);
}
} public static void AddScreen(GameScreen gameScreen)
{
gameScreen.LoadAssets();
if(ScreenList == null)
{
ScreenList=new List<GameScreen>();
}
ScreenList.Add(gameScreen);
gameScreen.LoadAssets();
}
public static void RemoveScreen(GameScreen gameScreen)
{
gameScreen.UnloadAssets();
ScreenList.Remove(gameScreen);
if(ScreenList.Count < 1)
AddScreen(newTestScreen());
}
public static void ChangeScreens(GameScreen currentScreen, GameScreen targetScreen)
{
RemoveScreen(currentScreen);
AddScreen(targetScreen);
}
}
}using System;
using System.Collections.Generic;
using System.Linq;usingSystem.Text;
namespace XNATutorials.Screens
{
classTestScreen
{
}
}using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;public class TestScreen : GameScreen { }
private readonly Matrix _projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), 800f / 480f, 0.01f, 100f);
private readonly Matrix _view = Matrix.CreateLookAt(newVector3(0, 0, 3), newVector3(0, 0, 0), newVector3(0, 1, 0));
private readonly Matrix _world = Matrix.CreateTranslation(0, 0, 0);
private BasicEffect _basicEffect;
private VertexBuffer _vertexBuffer;
public override void LoadAssets()
{
_basicEffect = new BasicEffect(ScreenManager.GraphicsDeviceMgr.GraphicsDevice);
var vertices = new VertexPositionColor[3];
vertices[0] = new VertexPositionColor(newVector3(0, 1, 0), Color.Red);
vertices[1] = new VertexPositionColor(newVector3(+0.5f, 0, 0), Color.Green);
vertices[2] = new VertexPositionColor(newVector3(-0.5f, 0, 0), Color.Blue);
_vertexBuffer = new VertexBuffer(ScreenManager.GraphicsDeviceMgr.GraphicsDevice, typeof(VertexPositionColor), 3, BufferUsage.WriteOnly);
_vertexBuffer.SetData(vertices);
}
public override voidDraw(GameTime gameTime)
{
_basicEffect.World = _world;
_basicEffect.View = _view;
_basicEffect.Projection = _projection;
_basicEffect.VertexColorEnabled = true;
ScreenManager.GraphicsDeviceMgr.GraphicsDevice.SetVertexBuffer(_vertexBuffer);
var rasterizerState = newRasterizerState
{
CullMode = CullMode.None
};
ScreenManager.GraphicsDeviceMgr.GraphicsDevice.RasterizerState = rasterizerState;
foreach(var pass in _basicEffect.CurrentTechnique.Passes)
{
pass.Apply();
ScreenManager.GraphicsDeviceMgr.GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);
}
}