Game Architecture
Reorganizing your code…
Reorganizing your code…
Now that you’ve moved through much of the foundations of building games, let’s take a step back and talk about how to best organize that task. After all, games are one of the most complex software systems you can build. In the words of Jason Gregory, a game is:
A soft real-time interactive agent-based simulation
This means that not only do you need to process user input, update a simulated world, and then render that simulated world, you also have to do this in realtime (i.e. within 1/60th of a second)! This is not a trivial challenge!
As with any software system, organization can go a long way to managing this complexity. Consider this diagram of a AAA game engine’s software architecture:
Note how the engine is broken into systems and organized into layers. Building an engine like this is outside the scope of this book (I encourage you to read Jason Gregory’s Game Engine Architecture if you’d like to delve into it), but the idea of loosely coupled systems is certainly something we can adapt. Moreover, it is already explicitly supported by the MonoGame framework. We’ll explore how in this chapter.
A common approach in software architecture for loose coupling of systems is the use of services. Services are implemented with 1) a service provider - essentially a collection of services that can be searched for a service, and new services can be registered with, 2) interfaces that define specific services how to work with the service, and 3) classes that implement these interfaces. This is the Service Locator Pattern as implemented in C#.
For a Service Provider, MonoGame provides the GameServiceContainer
class, and the Game
class has one as its Service
property. The default game class adds at least two services: an IGraphicsDeviceService
and an IGraphicsDeviceManager
. If we need to retrieve the graphics device for some reason we could use the code:
var gds = game.Services.GetService(typeof(IGraphicDeviceService));
GraphicsDevice gd = gds.GraphicsDevice;
We can add any service we want to with the GameServicesContainer.AddService(Type type, object provider)
. In effect, the GameServicesContainer
acts as a dictionary for finding initialized instances of services you would use across the game. For example, we might want to have a custom service for reporting achievements in the game. Because the implementation would be different for the Xbox than the Playstation, we could define an interface to represent our type:
public class IAchievementService
{
public void RegisterAchievement(Achievement achievement);
}
Then we could author two classes implementing this interface, one for the Xbox and one for the Playstation. We would initalize and register the appropriate one for the build of our program:
game.Services.AddService(IAchievementService, new XBoxAchievementService());
This provides us with that desirable “loose coupling”, where the only change we’d need to make between the two builds is what achievement service we initialize. A second common use for the GameServicesContainer
is it can be passed to a constructor to provide multiple services as a single parameter, instead of having to pass each one separately. It can also be held onto to retrieve the service at a later point in the execution, as is the case with the ContentManager
constructor.
Game services are a good replacement for systems that you might otherwise use the Singleton Pattern to implement.
A second useful decoupling pattern in MonoGame is the use of game components. You’ve probably noticed that many of the classes you have written have a similar pattern: they each have a LoadContent()
, Update()
, and Draw()
method, and these often take the same arguments. In your game class, you probably mostly invoke these methods for each class in turn. MonoGame provides the concept of game components to help manage this task. This involves: 1) a GameComponentCollection
which game components are registered with, and components are implemented by extending the GameComponent
or DrawableGameComponent
base classes.
The Game
implements a GameCollection
in its Components
property. It will iterate through all components it holds, invoking the Update()
and Draw()
methods within the game loop.
The GameComponent
base class implements IGameComponent
, IUpdatable
, and IDisposable
interfaces. It has the following properties:
Enabled
- this boolean indicates if the component will be updatedGame
- the game this component belongs toUpdateOrder
- an integer used to sort the order in which game component’s Update()
method is invokedIt also has the following virtual methods, which can be overridden:
Dispose(bool disposing)
- Disposes of the componentInitialize()
- Initializes the component; used to set/load any non-graphical content; invoked during the game’s initialization stepUpdate(GameTime gameTime)
- Invoked every pass through the game loopFinally, it also implements the following events:
EnabledChanged
- triggered when the Enabled
property changesUpdateOrderChanged
- triggered when the UpdateOrder
property changesThe DrawableGameComponent
inherits from the GameComponent
base class, and additionally implements the IDrawable
interface. In addition to its inherited properties, it declares:
DrawOrder
- an integer that determines the order game components are drawn inGraphicsDevice
- the graphics device used to draw the game componentVisible
- a boolean that determines if the game component should be drawnIt also has the additional virtual methods:
LoadContent()
- Loads graphical content, invoked by the game during its content loading stepDraw(GameTime gameTime)
- Draws the game component, invoked during the game loopFinally, it implements the additional properties:
DrawOrderChanged
- triggered when the DrawOrder
property changesVisibleChanged
- triggered when the Visible
property changesThe concept of Game Component espoused by MonoGame is not the same one defined by the Component Pattern, though it could potentially be leveraged to implement that pattern.
XNA offered a sample building with these ideas that further organized a game into screens that has been ported to MonoGame.This was heavily influenced by Windows Phone, and includes gestures and “tombstoning” support. A more simplified form is presented here. It organizes a game into “screens”, each with its own logic and rendering, such as a menu, puzzle, cutscene, etc.
A scene manager game component manages a stack of these screens, and updates and renders the topmost. Thus, from a gameplay screen, if we trigger a cutscene it would be pushed onto the stack, play, and then pop itself from the stack. Similarly, pressing the “menu” button would push the menu screen onto the stack, leaving the player to interact with the menu instead of the game. Screens manage their transition on and off this stack - and can incorporate visual effects into the transition.
This enumeration represents the states a GameScreen can be in.
/// <summary>
/// Enumerations of the possible screen states
/// </summary>
public enum ScreenState
{
TransitionOn,
Active,
TransitionOff,
Hidden
}
The GameScreen
class is an abstract base class that represents a single screen.
/// <summary>
/// A screen is a single layer of game content that has
/// its own update and draw logic and can be combined
/// with other layers to create complex menus or game
/// experiences
/// </summary>
public abstract class GameScreen
{
/// <summary>
/// Indicates if this screen is a popup
/// </summary>
/// <remarks>
/// Normally when a new screen is brought over another,
/// the covered screen will transition off. However, this
/// bool indicates the covering screen is only a popup, and
/// the covered screen will remain partially visible
/// </remarks>
public bool IsPopup { get; protected set; }
/// <summary>
/// The amount of time taken for this screen to transition on
/// </summary>
protected TimeSpan TransitionOnTime {get; set;} = TimeSpan.Zero;
/// <summary>
/// The amount of time taken for this screen to transition off
/// </summary>
protected TimeSpan TransitionOffTime {get; set;} = TimeSpan.Zero;
/// <summary>
/// The screen's position in the transition
/// </summary>
/// <value>Ranges from 0 to 1 (fully on to fully off)</value>
protected float TransitionPosition { get; set; } = 1;
/// <summary>
/// The alpha value based on the current transition position
/// </summary>
public float TransitionAlpha => 1f - TransitionPosition;
/// <summary>
/// The current state of the screen
/// </summary>
public ScreenState ScreenState { get; set; } = ScreenState.TransitionOn;
/// <summary>
/// Indicates the screen is exiting for good (not simply obscured)
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public bool IsExiting { get; protected internal set; }
/// <summary>
/// Indicates if this screen is active
/// </summary>
public bool IsActive => !_otherScreenHasFocus && (
ScreenState == ScreenState.TransitionOn ||
ScreenState == ScreenState.Active);
private bool _otherScreenHasFocus;
/// <summary>
/// The ScreenManager in charge of this screen
/// </summary>
public ScreenManager ScreenManager { get; internal set; }
/// <summary>
/// Gets the index of the player who is currently controlling this screen,
/// or null if it is accepting input from any player.
/// </summary>
/// <remarks>
/// This is used to lock the game to a specific player profile. The main menu
/// responds to input from any connected gamepad, but whichever player makes
/// a selection from this menu is given control over all subsequent screens,
/// so other gamepads are inactive until the controlling player returns to the
/// main menu.
/// </remarks>
public PlayerIndex? ControllingPlayer { protected get; set; }
/// <summary>
/// Activates the screen. Called when the screen is added to the screen manager
/// or the game returns from being paused.
/// </summary>
public virtual void Activate() { }
/// <summary>
/// Deactivates the screen. Called when the screen is removed from the screen manager
/// or when the game is paused.
/// </summary>
public virtual void Deactivate() { }
/// <summary>
/// Unloads content for the screen. Called when the screen is removed from the screen manager
/// </summary>
public virtual void Unload() { }
/// <summary>
/// Updates the screen. 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 forever, it should transition off
ScreenState = ScreenState.TransitionOff;
if (!UpdateTransitionPosition(gameTime, TransitionOffTime, 1))
ScreenManager.RemoveScreen(this);
}
else if(coveredByOtherScreen)
{
// if the screen is covered by another, it should transition off
ScreenState = UpdateTransitionPosition(gameTime, TransitionOffTime, 1)
? ScreenState.TransitionOff
: ScreenState.Hidden;
}
else
{
// Otherwise the screen should transition on and become active.
ScreenState = UpdateTransitionPosition(gameTime, TransitionOnTime, -1)
? ScreenState.TransitionOn
: ScreenState.Active;
}
}
/// <summary>
/// Updates the TransitionPosition property based on the time
/// </summary>
/// <param name="gameTime">an object representing time in the game</param>
/// <param name="time">The amount of time the transition should take</param>
/// <param name="direction">The direction of the transition</param>
/// <returns>true if still transitioning, false if the transition is done</returns>
private bool UpdateTransitionPosition(GameTime gameTime, TimeSpan time, int direction)
{
// How much should we move by?
float transitionDelta = (time == TimeSpan.Zero)
? 1
: (float)(gameTime.ElapsedGameTime.TotalMilliseconds / time.TotalMilliseconds);
// Update the transition time
TransitionPosition += transitionDelta * direction;
// Did we reach the end of the transition?
if(direction < 0 && TransitionPosition <= 0 || direction > 0 && TransitionPosition >= 0)
{
TransitionPosition = MathHelper.Clamp(TransitionPosition, 0, 1);
return false;
}
// if not, we are still transitioning
return true;
}
/// <summary>
/// Handles input for this screen. Only called when the screen is active,
/// and not when another screen has taken focus.
/// </summary>
/// <param name="gameTime">An object representing time in the game</param>
/// <param name="input">An object representing input</param>
public virtual void HandleInput(GameTime gameTime, InputState input) { }
/// <summary>
/// Draws the GameScreen. Only called with the screen is active, and not
/// when another screen has taken the focus.
/// </summary>
/// <param name="gameTime">An object representing time in the game</param>
public virtual void Draw(GameTime gameTime) { }
/// <summary>
/// This method tells the screen to exit, allowing it time to transition off
/// </summary>
public void ExitScreen()
{
if (TransitionOffTime == TimeSpan.Zero)
ScreenManager.RemoveScreen(this); // If the screen has a zero transition time, remove it immediately
else
IsExiting = true; // Otherwise flag that it should transition off and then exit.
}
}
The ScreenManager
class manages the screens, updating and drawing only when appropriate.
/// <summary>
/// The ScreenManager is a component which manages one or more GameScreen instance.
/// It maintains a stack of screens, calls their Update and Draw methods when
/// appropriate, and automatically routes input to the topmost screen.
/// </summary>
public class ScreenManager : DrawableGameComponent
{
private readonly List<GameScreen> _screens = new List<GameScreen>();
private readonly List<GameScreen> _tmpScreensList = new List<GameScreen>();
private readonly ContentManager _content;
private readonly InputState _input = new InputState();
private bool _isInitialized;
/// <summary>
/// A SpriteBatch shared by all GameScreens
/// </summary>
public SpriteBatch SpriteBatch { get; private set; }
/// <summary>
/// A SpriteFont shared by all GameScreens
/// </summary>
public SpriteFont MenuFont { get; private set; }
/// <summary>
/// Constructs a new ScreenManager
/// </summary>
/// <param name="game">The game this ScreenManager belongs to</param>
public ScreenManager(Game game) : base(game)
{
_content = new ContentManager(game.Services, "Content");
}
/// <summary>
/// Initializes the ScreenManager
/// </summary>
public override void Initialize()
{
base.Initialize();
_isInitialized = true;
}
/// <summary>
/// Loads content for the ScreenManager and its screens
/// </summary>
protected override void LoadContent()
{
SpriteBatch = new SpriteBatch(GraphicsDevice);
MenuFont = _content.Load<SpriteFont>("MenuFont");
// Tell each of the screens to load thier content
foreach(var screen in _screens)
{
screen.Activate();
}
}
/// <summary>
/// Unloads content for the ScreenManager's screens
/// </summary>
protected override void UnloadContent()
{
foreach(var screen in _screens)
{
screen.Unload();
}
}
/// <summary>
/// Updates all screens managed by the ScreenManager
/// </summary>
/// <param name="gameTime">An object representing time in the game</param>
public override void Update(GameTime gameTime)
{
// Read in the keyboard and gamepad
_input.Update();
// Make a copy of the screen list, to avoid confusion if
// the process of updating a screen adds or removes others
_tmpScreensList.Clear();
_tmpScreensList.AddRange(_screens);
bool otherScreenHasFocus = !Game.IsActive;
bool coveredByOtherScreen = false;
while(_tmpScreensList.Count > 0)
{
// Pop the topmost screen
var screen = _tmpScreensList[_tmpScreensList.Count - 1];
_tmpScreensList.RemoveAt(_tmpScreensList.Count - 1);
screen.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
if (screen.ScreenState == ScreenState.TransitionOn || screen.ScreenState == ScreenState.Active)
{
// if this is the first active screen, let it handle input
if (!otherScreenHasFocus)
{
screen.HandleInput(gameTime, _input);
otherScreenHasFocus = true;
}
// if this is an active non-popup, all subsequent
// screens are covered
if (!screen.IsPopup) coveredByOtherScreen = true;
}
}
}
/// <summary>
/// Draws the appropriate screens managed by the SceneManager
/// </summary>
/// <param name="gameTime">An object representing time in the game</param>
public override void Draw(GameTime gameTime)
{
foreach(var screen in _screens)
{
if (screen.ScreenState == ScreenState.Hidden) continue;
screen.Draw(gameTime);
}
}
/// <summary>
/// Adds a screen to the ScreenManager
/// </summary>
/// <param name="screen">The screen to add</param>
public void AddScreen(GameScreen screen)
{
screen.ScreenManager = this;
screen.IsExiting = false;
// If we have a graphics device, tell the screen to load content
if (_isInitialized) screen.Activate();
_screens.Add(screen);
}
public void RemoveScreen(GameScreen screen)
{
// If we have a graphics device, tell the screen to unload its content
if (_isInitialized) screen.Unload();
_screens.Remove(screen);
_tmpScreensList.Remove(screen);
}
/// <summary>
/// Exposes an array holding all the screens managed by the ScreenManager
/// </summary>
/// <returns>An array containing references to all current screens</returns>
public GameScreen[] GetScreens()
{
return _screens.ToArray();
}
}
This sample also uses the InputState
class introduced in chapter 7. In your game class, you need to create the ScreenManager
, and then add your custom screen classes, which can be done in your constructor or Initialize()
method:
var screenManager = new ScreenManager(this);
screenManager.AddScreen(new ExampleScreenA());
screenManager.AddScreen(new ExampleScreenB());
...
Once added, the screen’s Initialize
, LoadContent()
, Update()
, and Draw()
methods will all be invoked automatically as appropriate by the Game
class.
It might also make sense to register the ScreenManager
as a service, especially if you expect to add additional screens as the game is running:
// From within your Game class
this.Services.AddService(typeof(ScreenManager), screenManager);
Screens can be added at any time, which pushes them to the top of the stack - a common use would be to open a menu or submenu.
You can also stack as many screens as you like at the start of the game - you might arrange a multilevel game this way:
screenManager.AddScreen(new CreditsScreen());
screenManager.AddScreen(new Level8Screen());
screenManager.AddScreen(new Level7Screen());
screenManager.AddScreen(new Level6Screen());
...
screenManager.AddScreen(new Level1Screen());
screenManager.AddScreen(new OpeningScreen());
And invoke each screen’s ExitScreen()
when the level is completed.
In this chapter we explored some new tools for organizing our game code. We learned about how MonoGame utilizes services to provide loosely-coupled access between a service provider and consumer. We also saw how the MonoGame concept of Game Components works, and how we can define custom game components and add them to the Game.Component
collection. Finally, we explored one further organization tool in the Game Screen concept from the XNA GameStateManagement sample. Each of these can help make larger games easier to build and maintain.