One of the major themes present in the design of FGF is abstraction of functionality from its implementation. The major reason for this is because one of the major goals of FGF is to provide functionality without forcing developers into a corner. The idea is if the base of the framework is modular and open, the rest of the framework will fall into place very easily. So I begin the implementation of FGF by fixingimproving the XNA Framework’s Game class with an abstraction of its functionality. The reason for which will become apparent when the component classes are improved at a later time.
The important question to ask here is what can a game do? We can run a game, and exit a game but also add and remove components and services. Thus the IGame interface is born (within the FocusedGames.Xna project):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | using Microsoft.Xna.Framework; namespace FocusedGames.Xna { public interface IGame { void Run(); void Exit(); Vector2 ObjectToScreen(Vector2 objectVector); Vector2 ScreenToObject(Vector2 screenVector); GraphicsDeviceManager DeviceManager { get; } DisplayOrientation DisplayOrientation { get; set; } Vector2 DisplaySize { get; set; } bool IsLoaded { get; } float TargetFrameRate { get; set; } ModuleCollection Modules { get; } } } |
It should be immediately obvious that there are elements to this code that do not compile. I have added extra functionality due to the foresight of working with the Zune HD device and its re-orientating capabilities. Mainly the IGame interface (and any class that implements it) needs to provide a mechanism for dealing with how the screen and the objects within that screen are presented to the user. As for the last element, Modules, don’t worry about that for now as it will be covered in the next section.
Before implementing the IGame interface, the DisplayOrientation enumeration needs to be defined. You may have better terminology for all the options, I went with what came to my head initially and admit they aren’t the most descriptive choices. Remember that they have to describe the orientation on all the platforms and that the Zune HD’s landscape mode is much like the PC’s standard mode in terms of aspect ratio.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | namespace FocusedGames.Xna { /// <summary> /// Represents the orientation of the display. /// </summary> public enum DisplayOrientation : byte { /// <summary> /// Represents the standard orientation of the display. For Zune and Zune HD this is portrait mode. /// </summary> Standard = 0, /// <summary> /// Represents a 180 degree flip of the standard orientation. /// </summary> Flipped = 1, /// <summary> /// Represents a 90 degree rotation of the standard orientation. For the Zune and Zune HD this is landscape mode. /// </summary> Rotated = 2, /// <summary> /// Represents a -90 degree rotation of the standard orientation. /// </summary> ReverseRotated = 3 } } |
For now a dummy ModuleCollection class will do:
1 2 3 4 5 6 | namespace FocusedGames.Xna { public class ModuleCollection { } } |
Finally we are ready to start implementing the IGame interface in a base class called Application so as not to confuse it with the XNA Framework’s Game class.
1 2 3 4 5 6 7 8 9 | using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace FocusedGames.Xna { public class Application : Game, IGame { |
The implementation will need a certain number of members to deal with the orientation of the screen. Specifically, a render target will be used to draw everything and then rotated it rather than rotating everything as they are drawn. I have found this makes for simpler code for end developers because rotations can get complicated fast.
1 2 3 4 5 6 7 8 9 | private DisplayOrientation displayOrientation; private float displayRotation = 0f; private SpriteEffects displaySpriteEffects = SpriteEffects.None; private Vector2 orientedDisplaySize = new Vector2(800, 600); private Vector2 displaySize = new Vector2(800, 600); private SpriteBatch spriteBatch; private RenderTarget2D renderTarget; |
Next comes the default constructor for the class which simply initializes several of the members for use later on. Here is where the basic defaults of any game are defined and you’ll note the Zune specific code for "saving" battery life.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public Application() { Modules = new ModuleCollection(); DeviceManager = new GraphicsDeviceManager(this); ClearColor = Color.Black; DisplayOrientation = DisplayOrientation.Standard; Content.RootDirectory = "Content"; #if ZUNE // Frame rate is 30 fps by default for Zune. TargetElapsedTime = TimeSpan.FromSeconds(1 / 30.0); #endif } |
The implementation needs to load some content, set the display size up and then handle when the content needs to be unloaded. To do this the virtual methods provided by the XNA Framework work wonders. Just remember that when inheriting this class in the XNA game template to include calls to the base methods both in LoadContent and UnloadContent (they aren’t included by default – ugh!).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); SetDisplaySize(displaySize); ResetRenderTarget(); base.LoadContent(); IsLoaded = true; } protected override void UnloadContent() { if(spriteBatch != null && !spriteBatch.IsDisposed) spriteBatch.Dispose(); base.UnloadContent(); IsLoaded = false; } |
And now for the creation of the workhorse functions that maintain the orientation and display size and render target all in perfect harmony! The first one up is a function that is called to change the orientation of the display at any time. There are a couple ways of going about this; I have found using the sprite effects to be simple and effective. It is important to note that at the end of the function the render target is reset if the game has been loaded. If you fail to do this, things will get funky!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | protected virtual void SetDisplayOrientation(DisplayOrientation newOrientation) { // Removed so this method can act as a reset as well // if (newOrientation == displayOrientation) return; displayOrientation = newOrientation; // TODO: Handle orientations switch (displayOrientation) { case DisplayOrientation.Standard: orientedDisplaySize = displaySize; displayRotation = 0; displaySpriteEffects = SpriteEffects.None; break; case DisplayOrientation.Flipped: orientedDisplaySize = displaySize; displayRotation = 0; displaySpriteEffects = SpriteEffects.FlipVertically | SpriteEffects.FlipHorizontally; break; case DisplayOrientation.Rotated: orientedDisplaySize.X = displaySize.Y; orientedDisplaySize.Y = displaySize.X; displayRotation = MathHelper.PiOver2; displaySpriteEffects = SpriteEffects.None; break; case DisplayOrientation.ReverseRotated: orientedDisplaySize.X = displaySize.Y; orientedDisplaySize.Y = displaySize.X; displayRotation = MathHelper.PiOver2; displaySpriteEffects = SpriteEffects.FlipVertically | SpriteEffects.FlipHorizontally; //SpriteEffects.FlipHorizontally; break; } if(IsLoaded) ResetRenderTarget(); } |
The following method changes the display size of the screen and calls the previous method, SetDisplayOrientation, because as the screen size is changed, everything else is affected.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | protected virtual void SetDisplaySize(Vector2 newSize) { // Removed so this method can act as a reset as well // if (newSize == displaySize) return; displaySize = newSize; orientedDisplaySize = newSize; // Reset the backbuffer DeviceManager.PreferredBackBufferWidth = (int)displaySize.X; DeviceManager.PreferredBackBufferHeight = (int)displaySize.Y; // If we already loaded, apply the changed if (IsLoaded) DeviceManager.ApplyChanges(); // Reset the orientation details SetDisplayOrientation(displayOrientation); } |
And finally a method that resets the render target. In the future, the stub for Windows / Xbox 360 must be filled in with a robust RT creation method.
1 2 3 4 5 6 7 8 9 10 11 | private void ResetRenderTarget() { #if ZUNE if (renderTarget != null && !renderTarget.IsDisposed) renderTarget.Dispose(); renderTarget = new RenderTarget2D(GraphicsDevice, (int)orientedDisplaySize.X, (int)orientedDisplaySize.Y, 1, SurfaceFormat.Color); #else // TODO: Do some RT work here for Windows / Xbox 360 #endif } |
But really that is only half of the implementation! Now that we have the members setup for drawing an orientated screen, the drawing actually has to happen. This code comes your way from Nick Gravelyn with some minor modifications. The first method, ClearGraphicsDevice, is provided as a virtual member because it should be changed by end-developers when complex clears need to occur. This is done in lieu of many more options and fields on the IGame interface.
1 2 3 4 5 6 7 | /// <summary> /// Clears the GraphicsDevice. /// </summary> protected virtual void ClearGraphicsDevice() { GraphicsDevice.Clear(ClearColor); } |
In the BeginDraw method, the device needs to be cleared and the render target needs to be setup in some cases. In other cases, clearing the device is enough and the function is short circuited.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | protected override bool BeginDraw() { if (base.BeginDraw()) { if (DisplayOrientation == DisplayOrientation.Standard) { ClearGraphicsDevice(); return true; } if (renderTarget == null || renderTarget.IsDisposed) { ClearGraphicsDevice(); return false; } GraphicsDevice.SetRenderTarget(0, renderTarget); ClearGraphicsDevice(); GraphicsDevice.Viewport = new Viewport { X = 0, Y = 0, Width = (int)orientedDisplaySize.X, Height = (int)orientedDisplaySize.Y, MinDepth = GraphicsDevice.Viewport.MinDepth, MaxDepth = GraphicsDevice.Viewport.MaxDepth }; return true; } return false; } |
And last but certainly not least, the EndDraw and supporting methods need to clean up, present the render target and then draw any overlays before presenting to the screen. Here is where the real rotation magic occurs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | protected override void EndDraw() { if (!IsLoaded) return; if (DisplayOrientation == DisplayOrientation.Standard || (renderTarget == null || renderTarget.IsDisposed)) { //base.EndDraw(); FinalizeDraw(); return; } GraphicsDevice.SetRenderTarget(0, null); GraphicsDevice.Viewport = new Viewport { X = 0, Y = 0, Width = (int)displaySize.X, Height = (int)displaySize.Y, MinDepth = GraphicsDevice.Viewport.MinDepth, MaxDepth = GraphicsDevice.Viewport.MaxDepth }; spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.SaveState); spriteBatch.Draw( renderTarget.GetTexture(), displaySize / 2, null, Color.White, DisplayRotation, orientedDisplaySize / 2, 1f, DisplaySpriteEffects, 0 ); spriteBatch.End(); //base.EndDraw(); FinalizeDraw(); } private void FinalizeDraw() { DrawOverlay(); base.EndDraw(); } /// <summary> /// Draw's unrotated graphics. /// </summary> protected virtual void DrawOverlay() { } |
And before we can really call this implementation done (for now anyways), the various properties used need to be defined. These are given to you a la shotgun because they are straight forward and self explanatory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | /// <summary> /// Gets screen coordinates from object coordinates. /// </summary> /// <param name="objectVector">The object's coordinates.</param> /// <returns>Coordinates in screen space.</returns> public Vector2 ObjectToScreen(Vector2 objectVector) { return new Vector2(displaySize.X - objectVector.Y, objectVector.X); } /// <summary> /// Gets object coordinates from screen coordinates. /// </summary> /// <param name="screenVector">The coordinates in screen space.</param> /// <returns>Coordinates in object space.</returns> public Vector2 ScreenToObject(Vector2 screenVector) { return new Vector2(screenVector.Y, displaySize.X - screenVector.X); } public GraphicsDeviceManager DeviceManager { get; private set; } /// <summary> /// Gets or Sets the orientation of the display. /// </summary> public DisplayOrientation DisplayOrientation { get { return displayOrientation; } set { SetDisplayOrientation(value); } } /// <summary> /// Gets or Sets the size of the display. /// </summary> public Vector2 DisplaySize { get { return displaySize; } set { SetDisplaySize(value); } } /// <summary> /// Gets the rotation of the display. /// </summary> /// <seealso cref="DisplaySpriteEffects"/> public float DisplayRotation { get { return displayRotation; } } /// <summary> /// Gets the SpriteEffects for the display's render target. /// </summary> /// <seealso cref="DisplayRotation"/> public SpriteEffects DisplaySpriteEffects { get { return displaySpriteEffects; } } /// <summary> /// Gets whether or not the application has been loaded. /// </summary> public bool IsLoaded { get; private set; } /// <summary> /// Gets or Sets the clear color of the graphics device. /// </summary> protected Color ClearColor { get; set; } /// <summary> /// Gets the SpriteBatch used to draw the main render target. /// </summary> protected SpriteBatch SpriteBatch { get { return spriteBatch; } } /// <summary> /// Gets or Sets the target frame rate for the underlying loop code. /// </summary> public float TargetFrameRate { get { return 1 / (float)TargetElapsedTime.TotalSeconds; } set { TargetElapsedTime = TimeSpan.FromSeconds(1 / value); } } public ModuleCollection Modules { get; private set; } } } |