Running on Empty

The few things I know, I like to share.

Model-View-ViewModel (MVVM) – Part 1

I have been working with MVVM for about a year now as part of work related projects. For the larger portion of my career I have focused on Model View Controller or Model View Presenter schemas for user interface. However, after learning as much as possible about MVVM from experts around the web, I have adopted it as my primary method UI implementation.

So what is MVVM? For the most part it is MVC on steroids, it is a way to split presentation design from codified logic specific to the application. In WPF, it allows easy access to core features such as databinding. It allows the developer to write UI seperately from business logic, which seperates the visible behavior from the logical behavior of the application. A nice side effect is unit testing of the application becomes much easier, since most if not all application logic is handled as methods. It is easier to describe in parts:

ViewModel – is the entity(ies) that the application is trying to manage and consume

  • This is your entity class, everything here should be Properties.

View-Model – is an intermediary layer where most of the logic live for specific business applications.

  • This layer is not aware of UI controls.
  • Here you expose Public properties that the UI layer will be bound.
  • Use public methods invoked by events/commands
  • Update the View through databinding
  • Update the Model through bound properties

View  – is how the View-Model is displayed, the UI.

  • Contains the XAML in a WPF application
  • Command bindings – routed commands
  • Property bindings
  • Code behind should be as clean as possible
  • Should include basic wiring to instantiate the view
  • General Rule of Thumb – if you have x:Name in your XAML, you are doing it wrong.

In the next article I will demonstrate the view model base class.

Advertisements

July 22, 2011 Posted by | C#, MVVM, TDD, WPF, XAML | Leave a comment

XNA Framework GameEngine Development. (Part 11, Static Meshes)

Introduction

Welcome to Part 11 of the XNA Framework GameEngine Development series.  This article will be a huge update to the engine and will introduce some of the first static meshes to the game engine as well as movement within the game.

Part 11

Release

I finally put together a release of the code up until now.  You will see the sample game, the engine, the shader generator and test cases.  Hope you enjoy.  Link to Part 11.

Add Content

For this article you will need to add content items from the XNA team blog.

  • Ship.fbx (from several team site examples)
  • ShipDiffuse.tga (from several team site examples)

Changes to SceneGraphManager GameComponent

I had some issues with the way the SceneGraphManager worked when it was a DrawableGameComponent.  I had very little control over when the SceneGraphManager actually began the draw cycle.

So as a minor improvement I made it a GameComponent instead.  This means, I will have to call Draw, LoadContent and UnloadContent on my own rather than allow the game loop to do so.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.SceneObject.SceneGraph;
using RoeEngine2.SceneObject;

namespace RoeEngine2.Managers
{
    public class SceneGraphManager : GameComponent
    {
        private static Node _root;
        /// <summary>
        /// The root of the scene graph
        /// </summary>
        public static Node Root
        {
            get { return _root; }
        }
    
        /// <summary>
        /// Create the scenegraph Managers.
        /// </summary>
        /// <param name="game"></param>
        public SceneGraphManager(Game game)
            : base(game)
        {
            _root = new Node();
        }

        /// <summary>
        /// Draw objects
        /// </summary>
        /// <param name="gameTime"></param>
        public static void Draw(GameTime gameTime)
        {
            _root.Draw(gameTime);
        }

        /// <summary>
        /// Load the content of all the objects in the scenegraph
        /// </summary>
        public static void LoadContent()
        {
            _root.LoadContent();
        }

        /// <summary>
        /// Unload the content of all the objects in the scenegraph
        /// </summary>
        public static void UnloadContent()
        {
            _root.UnloadContent();
        }

        /// <summary>
        /// Add an object to the scenegraph.
        /// </summary>
        /// <param name="newObject"></param>
        public static void AddObject(RoeSceneObject newObject)
        {
            SceneObjectNode node = new SceneObjectNode(newObject);
            _root.AddNode(node);
        }
    }
}

Material.cs

Material will be used to contain information about the shader used by an object and a list of all the textures used by that object.  I will use this later.

using System;
using System.Collections.Generic;
using System.Text;

namespace RoeEngine2.SceneObject
{
    public class Material
    {
        private string _shader;
        public string Shader
        {
            get { return _shader; }
            set { _shader = value; }
        }

        private List<string> _textureList;
        public List<string> TextureList
        {
            get { return _textureList; }
            set { _textureList = value; }
        }
    }
}

RoeSceneObject.cs

Some updates to this class from last time.

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

namespace RoeEngine2.SceneObject
{
    public class RoeSceneObject : IRoeSceneObject
    {
        private bool _readyToRender = false;
        /// <summary>
        /// Is this object ready to render?
        /// </summary>
        public bool ReadyToRender
        {
            get { return _readyToRender; }
            set { _readyToRender = value; }
        }

        private BoundingBox _boundingBox;
        /// <summary>
        /// The bounding box of this object, used for culling.
        /// </summary>
        public BoundingBox BoundingBox
        {
            get { return _boundingBox; }
            set { _boundingBox = value; }
        }

        private Material _material;
        public Material Material
        {
            get { return _material; }
            set { _material = value; }
        }

        private string _modelName;
        public string ModelName
        {
            get { return _modelName; }
            set { _modelName = value; }
        }    

        private Vector3 _position = Vector3.Zero;
        /// <summary>
        /// The position of this object in 3d space.
        /// </summary>
        public Vector3 Position
        {
            get { return _position; }
            set { _position = value; }
        }

        private Vector3 _scale = Vector3.One;
        /// <summary>
        /// Scale of the object.
        /// </summary>
        public Vector3 Scale
        {
            get { return _scale; }
            set { _scale = value; }
        }

        private Quaternion _rotation = Quaternion.Identity;
        /// <summary>
        /// Yaw, pitch and roll of the object.
        /// </summary>
        public Quaternion Rotation
        {
            get { return _rotation; }
            set { _rotation = value; }
        }

        public virtual void Draw(GameTime gameTime)
        {
            IRoeModel model = ModelManager.GetModel(_modelName);
            if (model != null && model.ReadyToRender)
            {
                Matrix[] transforms = new Matrix[model.BaseModel.Bones.Count];
                model.BaseModel.CopyAbsoluteBoneTransformsTo(transforms);

                foreach (ModelMesh mesh in model.BaseModel.Meshes)
                {
                    foreach (BasicEffect effect in mesh.Effects)
                    {
                        effect.EnableDefaultLighting();
                        effect.PreferPerPixelLighting = true;
                        effect.World = transforms[mesh.ParentBone.Index] * Matrix.Identity;

                        effect.View = CameraManager.ActiveCamera.View;
                        effect.Projection = CameraManager.ActiveCamera.Projection;
                    }
                    mesh.Draw();
                }
            }           
        }
    }  
}

LoadingScreen.cs

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

namespace Kynskavion.GameScreens
{
    class LoadingScreen : GameScreen
    {
        bool _loadingIsSlow;
        bool _otherScreensAreGone;

        GameScreen[] _screensToLoad;

        private LoadingScreen(bool loadingIsSlow, GameScreen[] screensToLoad)
        {
            _loadingIsSlow = loadingIsSlow;
            _screensToLoad = screensToLoad;

            TransitionOnTime = TimeSpan.FromSeconds(0.5);
        }

        public static void Load(bool loadingIsSlow, params GameScreen[] screensToLoad)
        {
            foreach (GameScreen screen in ScreenManager.GetScreens())
            {
                screen.ExitScreen();
            }

            LoadingScreen loadingScreen = new LoadingScreen(loadingIsSlow, screensToLoad);

            ScreenManager.AddScreen(loadingScreen);
        }

        public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
        {
            base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);

            if (_otherScreensAreGone)
            {
                ScreenManager.RemoveScreen(this);

                foreach (GameScreen screen in _screensToLoad)
                {
                    if (screen != null)
                    {
                        ScreenManager.AddScreen(screen);
                    }
                }

                EngineManager.Game.ResetElapsedTime();
            }
        }

        public override void Draw(GameTime gameTime)
        {
            if ((ScreenState == ScreenState.Active) &&
                (ScreenManager.GetScreens().Length == 1))
            {
                _otherScreensAreGone = true;
            }

            if (_loadingIsSlow)
            {
                const string message = "Loading...";

                // Center the 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;

                Color color = new Color(255, 255, 255, TransitionAlpha);

                ScreenManager.SpriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Deferred,
                                                SaveStateMode.SaveState);
                ScreenManager.SpriteBatch.DrawString(ScreenManager.Font, message, textPosition, color);
                ScreenManager.SpriteBatch.End();
            }
        }
    }
}

PauseMenuScreen.cs

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

namespace Kynskavion.GameScreens
{
    class PauseMenuScreen : MenuScreen
    {
        /// <summary>
        /// Constructor.
        /// </summary>
        public PauseMenuScreen()
            : base("Paused")
        {
            // Flag that there is no need for the game to transition
            // off when the pause menu is on top of it.
            IsPopup = true;

            // Create our menu entries.
            MenuEntry resumeGameMenuEntry = new MenuEntry("Resume Game");
            MenuEntry quitGameMenuEntry = new MenuEntry("Quit Game");

            // Hook up menu event handlers.
            resumeGameMenuEntry.Selected += OnCancel;
            quitGameMenuEntry.Selected += QuitGameMenuEntrySelected;

            // Add entries to the menu.
            MenuEntries.Add(resumeGameMenuEntry);
            MenuEntries.Add(quitGameMenuEntry);
        }

        /// <summary>
        /// Event handler for when the Quit Game menu entry is selected.
        /// </summary>
        void QuitGameMenuEntrySelected(object sender, EventArgs e)
        {
            const string message = "Are you sure you want to quit this game?";

            MessageBoxScreen confirmQuitMessageBox = new MessageBoxScreen(message);

            confirmQuitMessageBox.Accepted += ConfirmQuitMessageBoxAccepted;

            ScreenManager.AddScreen(confirmQuitMessageBox);
        }

        /// <summary>
        /// Event handler for when the user selects ok on the "are you sure
        /// you want to quit" message box. This uses the loading screen to
        /// transition from the game back to the main menu screen.
        /// </summary>
        void ConfirmQuitMessageBoxAccepted(object sender, EventArgs e)
        {
            LoadingScreen.Load(false, new BackgroundScreen(),
                                      new MainMenuScreen());
        }

        /// <summary>
        /// Draws the pause menu screen. This darkens down the gameplay screen
        /// that is underneath us, and then chains to the base MenuScreen.Draw.
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            ScreenManager.FadeBackBufferToBlack(TransitionAlpha * 2 / 3);

            base.Draw(gameTime);
        }
    }
}

GameplayScreen.cs

This is where all the magic happens.  In this class we will load all of the game content, Update cameras, draw game content, and handle input from the user.

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.GameComponents;
using RoeEngine2.Managers;
using Microsoft.Xna.Framework;
using System.Threading;
using RoeEngine2.Models;
using RoeEngine2.SceneObject;
using Microsoft.Xna.Framework.Input;

namespace Kynskavion.GameScreens
{
    class GameplayScreen : GameScreen
    {
        private static double delta = 0.0;

        public GameplayScreen()
        {
            TransitionOnTime = TimeSpan.FromSeconds(1.5);
            TransitionOffTime = TimeSpan.FromSeconds(0.5);

            CameraManager.ActiveCamera.CameraType = Camera.CameraEnumType.FirstPerson;
            CameraManager.ActiveCamera.NearPlane = 10.0f;
            CameraManager.ActiveCamera.FarPlane = 100000.0f;
            CameraManager.ActiveCamera.SetPosition(new Vector3(0, 0, -10000));
            CameraManager.SetAllCamerasProjectionMatrix((float)EngineManager.Device.Viewport.Width / EngineManager.Device.Viewport.Height); 
        }

        /// <summary>
        /// Load graphics content for the game.
        /// </summary>
        public override void LoadContent()
        {
            RoeModel model = new RoeModel("Content/Models/Ship");

            ModelManager.AddModel(model, "ship");
            RoeSceneObject obj = new RoeSceneObject();
            obj.ModelName = "ship";
            SceneGraphManager.AddObject(obj);

            SceneGraphManager.LoadContent();

            // once the load has finished, we use ResetElapsedTime to tell the game's
            // timing mechanism that we have just finished a very long frame, and that
            // it should not try to catch up.
            EngineManager.Game.ResetElapsedTime();
        }

        /// <summary>
        /// Unload graphics content used by the game.
        /// </summary>
        public override void UnloadContent()
        {
            SceneGraphManager.UnloadContent();
        }

        public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
        {
            base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
            delta = gameTime.ElapsedGameTime.TotalSeconds;
        }

        public override void HandleInput(Input input)
        {           
            if (input.PauseGame)
            {
                ScreenManager.AddScreen(new PauseMenuScreen());
            }
            if (input.CurrentKeyboardState.IsKeyDown(Keys.Q))
            {
                CameraManager.ActiveCamera.Translate(new Vector3(0, 3000f * (float)delta, 0.0f));
            }
            if (input.CurrentKeyboardState.IsKeyDown(Keys.Z))
            {
                CameraManager.ActiveCamera.Translate(new Vector3(0, -3000f * (float)delta, 0.0f));
            }
            if (input.CurrentKeyboardState.IsKeyDown(Keys.W))
            {
                CameraManager.ActiveCamera.Translate(new Vector3(0, 0, 1000f * (float)delta));
            }
            if (input.CurrentKeyboardState.IsKeyDown(Keys.S))
            {
                CameraManager.ActiveCamera.Translate(new Vector3(0, 0, -1000f * (float)delta));
            }
            if (input.CurrentKeyboardState.IsKeyDown(Keys.D))
            {
                CameraManager.ActiveCamera.Translate(new Vector3(-1000f * (float)delta, 0, 0));
            }
            if (input.CurrentKeyboardState.IsKeyDown(Keys.A))
            {
                CameraManager.ActiveCamera.Translate(new Vector3(1000f * (float)delta, 0, 0));
            }
            if (input.CurrentMouseState.RightButton == ButtonState.Pressed)
            {
                CameraManager.ActiveCamera.RotateX(input.MouseMoved.Y);
                CameraManager.ActiveCamera.RotateY(input.MouseMoved.X);
            }
        }

        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);

            SceneGraphManager.Draw(gameTime);
        }

        public override void PostUIDraw(GameTime gameTime)
        {
            base.PostUIDraw(gameTime);
        }
    }
}

Conclusion

In this article, I finally demonstrated some objects in the engine using the basic effect and model loading.  I also included a link to the first release of the engine code.  I hope you enjoy.

I am very interested in your thoughts and suggestions, your feedback is very important.

February 12, 2008 Posted by | C#, GameComponent, TDD, XBOX360, XNA | 5 Comments

XNA Framework GameEngine Development. (Part 10, CameraManager GameComponents)

Introduction

Welcome to Part 10 of the XNA Framework GameEngine Development series.  In this article I will implement the CameraManager and first person camera.  I will be expanding on the camera class in future atricles, for now I will frame out the class and get some simple input logic in place for the next article.

CameraManager GameComponent

The CameraManager class will manage all of the camera interaction and updates.

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

namespace RoeEngine2.Managers
{
    public class CameraManager : GameComponent
    {
        private static Hashtable _cameras = new Hashtable();

        public enum CameraNumber
        {
            _default = 1,
            _dolly = 2,
            _3 = 3,
            _4 = 4,
            _5 = 5,
            _6 = 6,
            _7 = 7,
            _8 = 8,
            _9 = 9,
            _10 = 10
        }

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

        private static Camera _activeCamera;
        /// <summary>
        /// The camera where all the action takes place.
        /// </summary>
        public static Camera ActiveCamera
        {
            get { return _activeCamera; }
        }

        /// <summary>
        /// Create the camera Managers.
        /// </summary>
        /// <param name="game"></param>
        public CameraManager(Game game)
            : base(game)
        {
            Enabled = true;
        }
        
        /// <summary>
        /// Create the cameras.
        /// </summary>
        public override void Initialize()
        {
            base.Initialize();

            AddCamera(new Camera(Game), CameraNumber._default);
            SetActiveCamera(CameraNumber._default);
            AddCamera(new Camera(Game), CameraNumber._dolly);

            _initialized = true;
        }

        /// <summary>
        /// Update the active camera.
        /// </summary>
        /// <param name="gameTime"></param>
        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
            _activeCamera.Update();
        }

        /// <summary>
        /// Adds a new camera to the CameraManagers
        /// </summary>
        /// <param name="newCamera"></param>
        /// <param name="cameraLabel"></param>
        public static void AddCamera(Camera newCamera, CameraNumber cameraNumber)
        {
            if (!_cameras.Contains(cameraNumber))
            {
                _cameras.Add(cameraNumber, newCamera);
            }
        }

        /// <summary>
        /// Change the projection matrix of all cameras.
        /// </summary>
        /// <param name="aspectRatio"></param>
        public static void SetAllCamerasProjectionMatrix(float aspectRatio)
        {
            foreach (Camera camera in _cameras.Values)
            {
                camera.Projection = Matrix.CreatePerspectiveFieldOfView(
                                        camera.FieldOfView, aspectRatio, camera.NearPlane, camera.FarPlane);
            }
        }

        /// <summary>
        /// Changes the active camera by label
        /// </summary>
        /// <param name="cameraLabel"></param>
        public static void SetActiveCamera(CameraNumber cameraNumber)
        {
            if (_cameras.ContainsKey(cameraNumber))
            {
                _activeCamera = _cameras[cameraNumber] as Camera;
            }
        }     
    }
}

Camera Class

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace RoeEngine2.GameComponents
{
    public class Camera
    {
        /// <summary>
        /// The type of the Camera.
        /// </summary>
        public enum CameraEnumType
        {
            Fixed = 0,
            FirstPerson = 1, 
            ThirdPerson = 2
        }

        private float _fieldOfView = MathHelper.Pi / 3.0f;
        /// <summary>
        /// The viewable angle.
        /// </summary>
        public float FieldOfView
        {
            get { return _fieldOfView; }
            set { _fieldOfView = value; }
        }

        private float _nearPlane = 0.1f;
        /// <summary>
        /// The near plane used to determine the viewable area.
        /// </summary>
        public float NearPlane
        {
            get { return _nearPlane; }
            set { _nearPlane = value; }
        }

        private float _farPlane = 3500.0f;
        /// <summary>
        /// The far plane used to determine the viewable area.
        /// </summary>
        public float FarPlane
        {
            get { return _farPlane; }
            set { _farPlane = value; }
        }

        /// <summary>
        /// Slightly smaller viewable field of view for culling.
        /// </summary>
        public float ViewableFieldOfView
        {
            get { return FieldOfView / 1.125f; }
        }

        private CameraEnumType _cameraType = CameraEnumType.Fixed;
        /// <summary>
        /// The type of the camera.
        /// </summary>
        public CameraEnumType CameraType
        {
            get { return _cameraType; }
            set { _cameraType = value; }
        }

        private Vector3 _position = Vector3.Zero;
        /// <summary>
        /// Postition of the camera.
        /// </summary>
        public Vector3 Position
        {
            get { return _position; }
        }

        private Vector3 _cameraReference = new Vector3(0, 0, 10);
        /// <summary>
        /// The spot in 3d space where the camera is looking.
        /// </summary>
        public Vector3 CameraReference
        {
            get { return _cameraReference; }
        }

        private float _yaw = 0.0f;
        /// <summary>
        /// Angle around the Y axis.
        /// </summary>
        public float Yaw
        {
            get { return _yaw; }
        }

        private float _pitch = 0.0f;
        /// <summary>
        /// Angle around the X axis.
        /// </summary>
        public float Pitch
        {
            get { return _pitch; }
        }

        private Matrix _world;
        /// <summary>
        /// The world matrix.
        /// </summary>
        public Matrix World
        {
            get { return _world; }
            set { _world = value; }
        }

        private Matrix _view;
        /// <summary>
        /// Matrix containing coordinates of the camera.
        /// </summary>
        public Matrix View
        {
            get { return _view; }
            set { _view = value; }
        }

        private Matrix _reflectedView;
        /// <summary>
        /// Reflected View matrix around an arbitrary plane.
        /// </summary>
        public Matrix ReflectedView
        {
            get { return _reflectedView; }
            set { _reflectedView = value; }
        }

        private Matrix _projection;
        /// <summary>
        /// The projection matrix, what can be seen.
        /// </summary>
        public Matrix Projection
        {
            get { return _projection; }
            set { _projection = value; }
        }

        private BoundingFrustum _frustum;
        /// <summary>
        /// The trapezoid that contains everything that the camera can see.
        /// </summary>
        public BoundingFrustum Frustum
        {
            get { return _frustum; }
        }

        private BoundingFrustum _reflectedFrustum;
        /// <summary>
        /// The trapezoid that contains everything that the camera can see if it was reflected.
        /// </summary>
        public BoundingFrustum ReflectedFrustum
        {
            get { return _reflectedFrustum; }
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="game">The game.</param>
        public Camera(Game game)
        {
            // TODO: Construct any child components here
        }

        /// <summary>
        /// Set the position in 3d space.
        /// </summary>
        /// <param name="newPosition"></param>
        public void SetPosition(Vector3 newPosition)
        {
            _position = newPosition;
        }

        /// <summary>
        /// Set the point in 3d space where the camera is looking.
        /// </summary>
        /// <param name="newReference"></param>
        public void SetCameraReference(Vector3 newReference)
        {
            _cameraReference = newReference;
        }

        /// <summary>
        /// Move the camera in 3d space.
        /// </summary>
        /// <param name="move"></param>
        public void Translate(Vector3 move)
        {
            Matrix forwardMovement = Matrix.CreateRotationY(_yaw);
            Vector3 v = new Vector3(0, 0, 0);
            v = Vector3.Transform(move, forwardMovement);
            _position.Z += v.Z;
            _position.X += v.X;
            _position.Y += v.Y;
        }

        /// <summary>
        /// Rotate around the Y, Default Up axis.  Usually called Yaw.
        /// </summary>
        /// <param name="angle">Angle in degrees to rotate the camera.</param>
        public void RotateY(float angle)
        {
            angle = MathHelper.ToRadians(angle);
            if (_yaw >= MathHelper.Pi * 2)
                _yaw = MathHelper.ToRadians(0.0f);
            else if (_yaw <= -MathHelper.Pi * 2)
                _yaw = MathHelper.ToRadians(0.0f);
            _yaw += angle;
        }

        /// <summary>
        /// Rotate the camera around the X axis.  Usually called pitch.
        /// </summary>
        /// <param name="angle">Angle in degrees to rotate the camera.</param>
        public void RotateX(float angle)
        {
            angle = MathHelper.ToRadians(angle);
            _pitch += angle;
            if (_pitch >= MathHelper.ToRadians(75))
                _pitch = MathHelper.ToRadians(75);
            else if (_pitch <= MathHelper.ToRadians(-75))
                _pitch = MathHelper.ToRadians(-75);
        }

        private void UpdateFirstPerson()
        {
            Vector3 cameraPosition = _position;
            Matrix rotationMatrix = Matrix.CreateRotationY(_yaw);
            Matrix pitchMatrix = Matrix.Multiply(Matrix.CreateRotationX(_pitch), rotationMatrix);
            Vector3 transformedReference = Vector3.Transform(_cameraReference, pitchMatrix);
            Vector3 cameraLookat = cameraPosition + transformedReference;

            _view = Matrix.CreateLookAt(cameraPosition, cameraLookat, Vector3.Up);

            _frustum = new BoundingFrustum(Matrix.Multiply(_view, _projection));
            _reflectedFrustum = new BoundingFrustum(Matrix.Multiply(_reflectedView, _projection));
        }

        private void UpdateFixed()
        {
            _view = Matrix.CreateLookAt(_position, _cameraReference, Vector3.Up);

            _frustum = new BoundingFrustum(Matrix.Multiply(_view, _projection));
            _reflectedFrustum = new BoundingFrustum(Matrix.Multiply(_reflectedView, _projection));
        }

        /// <summary>
        /// Update the Camera.
        /// </summary>
        public void Update()
        {
            if (CameraType == CameraEnumType.Fixed)
            {
                UpdateFixed();
            }
            else if (CameraType == CameraEnumType.FirstPerson)
            {
                UpdateFirstPerson();
            }
            else if (CameraType == CameraEnumType.ThirdPerson)
            {

            }

        }
    }
}

Using the CameraManager GameComponent

Add the following code to the properties section of RoeEngine2.

private static CameraManager _cameraManagers = null;

And in the RoeEngine2 Constructor

// Init camera Managers
_cameraManagers = new CameraManager(this);
Components.Add(_cameraManagers);

Optional TDD:

[Test, Category("CameraManager")]
public void CameraManagerCreated()
{
     Assert.IsTrue(CameraManager.Initialized);
}

Conclusion

In this article I implemented a simple first person camera class and camera manager.  We can now move the camera around the world, the next article will build on everything we have done so far to draw some objects on the screen and move the camera around to actually view them.

February 11, 2008 Posted by | C#, GameComponent, TDD, XBOX360, XNA | Leave a comment

XNA Framework GameEngine Development. (Part 9, SceneGraphManager GameComponents)

Introduction

Welcome to Part 9 of the XNA Framework GameEngine Development series.  This article will focus on simple scene management using a scene graph.  This implementation is little more than a list of objects to be rendered each frame, but it is a nice and simple way to prepare for more complex implementations of scene management.

Essentially, the SceneGraph will run through all the objects in the game as quickly as possible and render them to the screen, nice and simple.

Interfaces

It is always nice to be able to be able to have custom objects and meshes in a game, but we need a common interface in order to deal with them.

IRoeSceneObject — common ancestor.

using System;
using System.Collections.Generic;
using System.Text;

namespace RoeEngine2.Interfaces
{
    public interface IRoeSceneObject
    {
    }
}

IRoeDrawable — determines if an object is drawable.

using System;
using System.Collections.Generic;
using System.Text;

namespace RoeEngine2.Interfaces
{
    public interface IRoeDrawable : IRoeSceneObject
    {
        void Draw();
    }
}

IRoeLoadable — determines if an object is loadable/unloadable.

using System;
using System.Collections.Generic;
using System.Text;

namespace RoeEngine2.Interfaces
{
    public interface IRoeLoadable : IRoeSceneObject
    {
        void LoadContent();
        void UnloadContent();
    }
}

IRoeUpdateable — determines if an object is Updateable.

using System;
using System.Collections.Generic;
using System.Text;

namespace RoeEngine2.Interfaces
{
    public interface IRoeUpdateable : IRoeSceneObject
    {
        void Update();
    }
}

SceneGraph — Lists of Nodes

Node.cs

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

namespace RoeEngine2.SceneObject.SceneGraph
{
    public class Node
    {
        protected NodeList _nodes;
        public NodeList Nodes
        {
            get { return _nodes; }
        }

        public Node()
        {
            _nodes = new NodeList();
        }

        public void AddNode(Node newNode)
        {
            _nodes.Add(newNode);
        }

        public virtual void Update()
        {
            _nodes.ForEach(
                delegate(Node node)
                {
                    node.Update();
                });
        }

        public virtual void UnloadContent()
        {
            _nodes.ForEach(
               delegate(Node node)
               {
                   node.UnloadContent();
               });
        }

        public virtual void LoadContent()
        {
            _nodes.ForEach(
               delegate(Node node)
               {
                   node.LoadContent();
               });
        }

        public virtual void Draw(GameTime gameTime)
        {
            _nodes.ForEach(
               delegate(Node node)
               {
                   node.Draw(gameTime);
               });
        }
    }
}

 NodeList.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace RoeEngine2.SceneObject.SceneGraph
{
    public class NodeList : List<Node>
    {
    }
}

 SceneObjectNode.cs

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

namespace RoeEngine2.SceneObject.SceneGraph
{
    public class SceneObjectNode : Node
    {
        private RoeSceneObject _sceneObject;
        public RoeSceneObject SceneObject
        {
            get { return _sceneObject; }
            set { _sceneObject = value; }
        }

        public SceneObjectNode(RoeSceneObject newObject)
        {
            _sceneObject = newObject;
        }

        public override void Update()
        {
            if (SceneObject is IRoeUpdateable)
                ((IRoeUpdateable)SceneObject).Update();
        }

        public override void UnloadContent()
        {
            if (SceneObject is IRoeLoadable)
                ((IRoeLoadable)SceneObject).UnloadContent();
        }

        public override void LoadContent()
        {
            if (SceneObject is IRoeLoadable)
                ((IRoeLoadable)SceneObject).LoadContent();
        }

        public override void Draw(GameTime gameTime)
        {
            if (SceneObject.ReadyToRender)
                SceneObject.Draw(gameTime);
        }
    }
}

RoeSceneObject.cs – Update

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.Interfaces;
namespace RoeEngine2.SceneObject
{
    public class RoeSceneObject : IRoeSceneObject
    {
        private bool _readyToRender = false;
        /// <summary>
        /// Is this object ready to render?
        /// </summary>
        public bool ReadyToRender
        {
            get { return _readyToRender; }
            set { _readyToRender = value; }
        }
        private BoundingBox _boundingBox;
        /// <summary>
        /// The bounding box of this object, used for culling.
        /// </summary>
        public BoundingBox BoundingBox
        {
            get { return _boundingBox; }
            set { _boundingBox = value; }
        }
        private Vector3 _position = Vector3.Zero;
        /// <summary>
        /// The position of this object in 3d space.
        /// </summary>
        public Vector3 Position
        {
            get { return _position; }
            set { _position = value; }
        }
        private Vector3 _scale = Vector3.One;
        /// <summary>
        /// Scale of the object.
        /// </summary>
        public Vector3 Scale
        {
            get { return _scale; }
            set { _scale = value; }
        }
        private Quaternion _rotation = Quaternion.Identity;
        /// <summary>
        /// Yaw, pitch and roll of the object.
        /// </summary>
        public Quaternion Rotation
        {
            get { return _rotation; }
            set { _rotation = value; }
        }
        /// <summary>
        /// Draw the object.
        /// </summary>
        /// <param name="gameTime"></param>
        public void Draw(GameTime gameTime)
        {            
            if (this is IRoeDrawable)
            {
                //Draw the object
            }
        }
    }   
}

SceneGraphManager

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.SceneObject.SceneGraph;
using RoeEngine2.SceneObject;

namespace RoeEngine2.Managers
{
    public class SceneGraphManager : DrawableGameComponent
    {
        private static Node _root;
        /// <summary>
        /// The root of the scene graph
        /// </summary>
        public static Node Root
        {
            get { return _root; }
        }
    
        /// <summary>
        /// Create the scenegraph Managers.
        /// </summary>
        /// <param name="game"></param>
        public SceneGraphManager(Game game)
            : base(game)
        {
            _root = new Node();
        }

        /// <summary>
        /// Draw objects
        /// </summary>
        /// <param name="gameTime"></param>
        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);

            _root.Draw(gameTime);
        }

        /// <summary>
        /// Load the content of all the objects in the scenegraph
        /// </summary>
        protected override void LoadContent()
        {
            base.LoadContent();

            _root.LoadContent();
        }

        /// <summary>
        /// Unload the content of all the objects in the scenegraph
        /// </summary>
        protected override void UnloadContent()
        {
            base.UnloadContent();

            _root.UnloadContent();
        }

        /// <summary>
        /// Add an object to the scenegraph.
        /// </summary>
        /// <param name="newObject"></param>
        public static void AddObject(RoeSceneObject newObject)
        {
            SceneObjectNode node = new SceneObjectNode(newObject);
            _root.AddNode(node);
        }
    }
}

Using the SceneGraphManager DrawableGameComponent

Add the following code to the properties section of RoeEngine2

private static SceneGraphManager _sceneGraphManager = null;

And in the RoeEngine2 Constructor

// Init SceneGraph Managers
_sceneGraphManager = new SceneGraphManager(this);
Components.Add(_sceneGraphManager);

//TODO include other inits here!

Optional TDD:

[Test, Category("SceneGraphManager")]
public void SceneGraphManagerCreated()
{
    Assert.IsNotNull(SceneGraphManager.Root);
}

[Test, Category("SceneGraphManager")]
public void SceneObjectsLoaded()
{
    RoeSceneObject sceneObject = new RoeSceneObject();
    sceneObject.Position = new Vector3(100, 100, 100);
    SceneGraphManager.AddObject(sceneObject);
    Assert.AreEqual(1, SceneGraphManager.Root.Nodes.Count);
    Assert.AreEqual(sceneObject.Position, ((SceneObjectNode)SceneGraphManager.Root.Nodes[0]).SceneObject.Position);
}

Conclusion

In this article, I introduced a very simple implementation of a Scenegraph.  Scene Management is a very important part of good engine design, with this simple implementation we now have a very good backbone for creating better and more powerful scene management systems.

I look forward to your comments and suggestions.  Please feel free to leave suggestions for future articles.

February 4, 2008 Posted by | C#, GameComponent, TDD, XBOX360, XNA | 3 Comments

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&#91;i&#93;.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&#91;i&#93;;

                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();
        }
    }
}&#91;/sourcecode&#93;<strong>MessageBoxScreen.cs</strong>

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.

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

XNA Framework GameEngine Development. (Part 6, Input:GameComponent)

Introduction

Welcome to Part 6 of the XNA Framework GameEngine Development series.  In this article I will be introducing the Input GameComponent.  This class will be changing as we add more functionality to the engine.  For now, I will just implement some simple handlers to set up for Part 7 in this series.  Also, I will be revisiting the TDD implementation to make some changes for ContentLoadingExceptions.

Review of articles in this series.

Preview of upcoming articles in this series.

  • Part 7, ScreenManager:GameComponent and GameScreens
  • Part 8, Multi-Threading GameComponents
  • Part 9, Scene management, scene objects, and SceneGraphManager

Input GameComponent

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

namespace RoeEngine2.GameComponents
{
    public class Input : GameComponent
    {
        public KeyboardState CurrentKeyboardState;
        public GamePadState CurrentGamePadState;
        public MouseState CurrentMouseState;

        public KeyboardState LastKeyboardState;
        public GamePadState LastGamePadState;
        public MouseState LastMouseState;

        private Point _lastMouseLocation;

        private Vector2 _mouseMoved;
        public Vector2 MouseMoved
        {
            get { return _mouseMoved; }
        }
       
        public Input(Game game)
            : base(game)
        {
            Enabled = true;
        }       
       
        /// <summary>
        /// Reads the latest state of the keyboard and gamepad.
        /// </summary>
        public void Update()
        {
            LastKeyboardState = CurrentKeyboardState;
            LastGamePadState = CurrentGamePadState;
            LastMouseState = CurrentMouseState;

            CurrentKeyboardState = Keyboard.GetState();
            CurrentGamePadState = GamePad.GetState(PlayerIndex.One);
            CurrentMouseState = Mouse.GetState();

            _mouseMoved = new Vector2(LastMouseState.X - CurrentMouseState.X, LastMouseState.Y - CurrentMouseState.Y);
            _lastMouseLocation = new Point(CurrentMouseState.X, CurrentMouseState.Y);
        }

        /// <summary>
        /// Checks for a "menu up" input action (on either keyboard or gamepad).
        /// </summary>
        public bool MenuUp
        {
            get
            {
                return IsNewKeyPress(Keys.Up) ||
                       (CurrentGamePadState.DPad.Up == ButtonState.Pressed &&
                        LastGamePadState.DPad.Up == ButtonState.Released) ||
                       (CurrentGamePadState.ThumbSticks.Left.Y > 0 &&
                        LastGamePadState.ThumbSticks.Left.Y <= 0);
            }
        }

        /// <summary>
        /// Checks for a "menu down" input action (on either keyboard or gamepad).
        /// </summary>
        public bool MenuDown
        {
            get
            {
                return IsNewKeyPress(Keys.Down) ||
                       (CurrentGamePadState.DPad.Down == ButtonState.Pressed &&
                        LastGamePadState.DPad.Down == ButtonState.Released) ||
                       (CurrentGamePadState.ThumbSticks.Left.Y < 0 &&
                        LastGamePadState.ThumbSticks.Left.Y >= 0);
            }
        }

        /// <summary>
        /// Checks for a "menu select" input action (on either keyboard or gamepad).
        /// </summary>
        public bool MenuSelect
        {
            get
            {
                return IsNewKeyPress(Keys.Space) ||
                       IsNewKeyPress(Keys.Enter) ||
                       (CurrentGamePadState.Buttons.A == ButtonState.Pressed &&
                        LastGamePadState.Buttons.A == ButtonState.Released) ||
                       (CurrentGamePadState.Buttons.Start == ButtonState.Pressed &&
                        LastGamePadState.Buttons.Start == ButtonState.Released);
            }
        }

        /// <summary>
        /// Checks for a "menu cancel" input action (on either keyboard or gamepad).
        /// </summary>
        public bool MenuCancel
        {
            get
            {
                return IsNewKeyPress(Keys.Escape) ||
                       (CurrentGamePadState.Buttons.B == ButtonState.Pressed &&
                        LastGamePadState.Buttons.B == ButtonState.Released) ||
                       (CurrentGamePadState.Buttons.Back == ButtonState.Pressed &&
                        LastGamePadState.Buttons.Back == ButtonState.Released);
            }
        }

        /// <summary>
        /// Checks for a "pause the game" input action (on either keyboard or gamepad).
        /// </summary>
        public bool PauseGame
        {
            get
            {
                return IsNewKeyPress(Keys.Escape) ||
                       (CurrentGamePadState.Buttons.Back == ButtonState.Pressed &&
                        LastGamePadState.Buttons.Back == ButtonState.Released) ||
                       (CurrentGamePadState.Buttons.Start == ButtonState.Pressed &&
                        LastGamePadState.Buttons.Start == ButtonState.Released);
            }
        }

        /// <summary>
        /// Helper for checking if a key was newly pressed during this update.
        /// </summary>
        bool IsNewKeyPress(Keys key)
        {
            return (CurrentKeyboardState.IsKeyDown(key) &&
                    LastKeyboardState.IsKeyUp(key));
        }
    }
}

Using the Input GameComponent

Add the following code to the Properties section of RoeEngine2

private static Input _input = null;
/// <summary>
/// The input helper for menus, gamepads, keyboard and mouse.
/// </summary>
public static Input Input
{
    get { return _input; }
}

And in the RoeEngine2 Constructor

// Init the Input
_input = new Input(this);
Components.Add(_input);

Optional TDD : Required changes

I have had to make some changes to the test cases project due to exceptions when loading content.  Since we really are not testing content loading I decided to Assert ExpectedExceptions in the SetupTextFixture.  So your test cases should look similar to the following now.

using System;
using System.Collections.Generic;
using System.Text;
using NUnit.Framework;
using RoeEngine2.Managers;
using Microsoft.Xna.Framework;
using RoeEngine2.Shaders;
using Microsoft.Xna.Framework.Graphics;
using System.Runtime.InteropServices;
using Microsoft.Xna.Framework.Content;

namespace RoeEngine2UnitTests
{
    [TestFixture]   
    public class RoeEngineTestCases
    {
        private EngineManager _game = null;
        private const string _windowTitle = "UNITTEST";

        [TestFixtureSetUp]
        [ExpectedException(typeof(ContentLoadException))]
        public void SetupTestFixture()
        {
            try
            {
                _game = new EngineManager(_windowTitle);
                EngineManager.Game = _game;
                _game.Activated += new EventHandler(game_Activated);
                _game.Run();
            }
            catch (ContentLoadException)
            {
                _game.Exit();
            }
        }

        void game_Activated(object sender, EventArgs e)
        {
            _game.Exit();
        }

        [TestFixtureTearDown]
        public void TeardownTestFixture()
        {
            _game = null;
        }

        [Test]
        public void GameStartup()
        {
            Assert.IsNotNull(_game);
            Assert.IsNotNull(EngineManager.Game);
            Assert.IsNotNull(EngineManager.Device);
            Assert.IsNotNull(EngineManager.ContentManager);
            Assert.AreEqual(_windowTitle, EngineManager.WindowTitle.ToString());
        }

        [Test, Category("FpsCounter")]
        public void FpsCounterCreated()
        {
            Assert.IsNotNull(EngineManager.FpsCounter);
            Assert.AreEqual("0", EngineManager.FpsCounter.FPS.ToString());
        }

        [Test, Category("Input")]
        public void InputCreated()
        {
            Assert.IsNotNull(EngineManager.Input);
        }

        [Test, Category("ShaderManager")]
        public void ShaderManagerCreated()
        {
            Assert.IsTrue(ShaderManager.Initialized);
            Assert.IsNotNull(ShaderManager.GetShader("BasicEffect"));
            Assert.AreEqual(typeof(basicEffect), ShaderManager.GetShader("BasicEffect").GetType());
        }

        [Test, Category("ShaderManager")]
        public void ShaderManagerAddGetFailed()
        {
            Assert.IsNull(ShaderManager.GetShader("testshader"));
        }       
       
        [Test, Category("TextureManager")]
        public void TextureManagerCreated()
        {
            Assert.IsTrue(TextureManager.Initialized);
        }

        [Test, Category("TextureManager")]
        public void TextureManagerAddGetFailed()
        {
            Assert.IsNull(TextureManager.GetTexture("testtexture"));
        }
    }
}

Conclusion

In this article I implemented a simple Input handler GameComponent.  Obviously, this class is not complete, but should be a good basis for the next article in this series.  I also revisited our TDD to make some fixes for content loading.

Please leave a comment or suggestion.  I am taking feedback for some more advanced articles using this engine.  Since this series is a gift to the open source community, I would enjoy your suggestions for the direction this series should take.

January 17, 2008 Posted by | C#, GameComponent, TDD, XBOX360, XNA | 2 Comments

XNA Framework GameEngine Development. (Part 5, TextureManager:GameComponent)

Introduction

Welcome to Part 5 of the XNA Framework GameEngine Development series.  In this article, I will implement the texture interface and the TextureManager.

IRoeTexture interface.  Updated, Use LoadContent, UnloadContent instead of Initialize.

The IRoeTexture interface will be used to identify any texture in the engine.  It will also be used in the same way IRoeShader is used to allow users to extend the base engine with their own texture types.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Graphics;

namespace RoeEngine2.Interfaces
{
    public interface IRoeTexture
    {
        string FileName
        {
            get;
            set;
        }

        Texture BaseTexture
        {
            get;
        }

        bool ReadyToRender
        {
            get;
        }

        void LoadContent();

        void UnloadContent();
    }
}

RoeTexture class.  Updated, Use LoadContent, UnloadContent instead of Initialize.

This class will be used to store and initialize all textures in the engine.  Essentially, it will contain the FileName of the texture we want to load from the content pipeline, the ReadyToRender property will tell the engine if the texture is loaded and the Initialize method which will use the content manager to load the texture.  Finally, the RoeTexture class will make use of the BaseTexture property to reference the newly loaded texture.

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

namespace RoeEngine2.Texures
{
    public class RoeTexture : IRoeTexture
    {
        private string _fileName;
        /// <summary>
        /// The file name of the asset.
        /// </summary>
        public string FileName
        {
            get { return _fileName; }
            set { _fileName = value; }
        }

        private Texture _baseTexture;
        ///<summary>
        ///Gets the underlying Effect.
        ///</summary>
        public Texture BaseTexture
        {
            get { return _baseTexture; }
        }

        private bool _readyToRender = false;
        ///<summary>
        ///Is the texture ready to be rendered.
        ///</summary>
        public bool ReadyToRender
        {
            get { return _readyToRender; }
        }

        /// <summary>
        /// Construct a new RoeTexture.
        /// </summary>
        public RoeTexture()
        {

        }

        /// <summary>
        /// Construct a new RoeTexture.
        /// </summary>
        /// <param name="fileName">The asset file name.</param>
        public RoeTexture(string fileName)
        {
            _fileName = fileName;
        }

        public void LoadContent()
        {
            if (!String.IsNullOrEmpty(_fileName))
            {
                _baseTexture = EngineManager.ContentManager.Load<Texture>(_fileName);
                _readyToRender = true;
            }
        }

        public void UnloadContent()
        {
            _baseTexture.Dispose();
        }
    }
}

TextureManager class.  Added RemoveTexture.

This class is the main container for everything that has to do with Textures.  You may recognize the implementation from the ShaderManager, it is purposefully done.

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

namespace RoeEngine2.Managers
{
    public class TextureManager : GameComponent
    {
        private static Dictionary<string, IRoeTexture> _textures = new Dictionary<string, IRoeTexture>();

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

        private static int _texturesLoaded = 0;
        /// <summary>
        /// The number of textures that are currently loaded.
        /// Use this for user loading bar feedback.
        /// </summary>
        public static int TexturesLoaded
        {
            get { return _texturesLoaded; }
        }

        /// <summary>
        /// Create the shader Managers.
        /// </summary>
        /// <param name="game"></param>
        public TextureManager(Game game)
            : base(game)
        {
        }

        /// <summary>
        /// Add a shader of type RoeTexture.
        /// </summary>
        /// <param name="newTexture"></param>
        /// <param name="textureName"></param>
        public static void AddTexture(IRoeTexture newTexture, string textureName)
        {
            if (textureName != null && !_textures.ContainsKey(textureName))
            {
                _textures.Add(textureName, newTexture);
                if (_initialized)
                {
                    newTexture.LoadContent();
                }
            }
        }

        public static void RemoveTexture(string textureName)
        {
            if (textureName != null && _textures.ContainsKey(textureName))
            {               
                if (_initialized)
                {
                    _textures[textureName].UnloadContent();
                    _textures.Remove(textureName);
                    _texturesLoaded--;
                }               
            }
        }

        /// <summary>
        /// Get a texture
        /// </summary>
        /// <param name="textureId"></param>
        /// <returns></returns>
        public static IRoeTexture GetTexture(string textureName)
        {
            if (textureName != null && _textures.ContainsKey(textureName))
            {
                return _textures[textureName];
            }
            return null;
        }

        /// <summary>
        /// Create the shaders.
        /// </summary>
        public override void Initialize()
        {
            base.Initialize();

            foreach (IRoeTexture texture in _textures.Values)
            {
                if (!texture.ReadyToRender)
                {
                    texture.LoadContent();
                }
            }

            _initialized = true;
        }
    }
}

Using the TextureManager GameComponent

Add the following code to the Properties section of RoeEngine2.

private static TextureManager _textureManagers = null;

And in the RoeEngine2 Constructor

// Init texture Managers
_textureManagers = new TextureManager(this);
Components.Add(_textureManagers);

//TODO include other inits here!

Optional TDD

Add the following test cases to the RoeEngineTestCases class

[Test, Category("TextureManager")]
public void TextureManagerCreated()
{
    Assert.IsTrue(TextureManager.Initialized);
}

[Test, Category("TextureManager")]
public void TextureManagerAddGetFailed()
{
    Assert.IsNull(TextureManager.GetTexture("testtexture"));
}

Conclusion

In this article I introduce the IRoeTexture, the RoeTexture class and the TextureManager.  I will be revisiting these classes from time to time.

Your feedback is very important, I very much enjoy reading your comments and suggestions.

January 16, 2008 Posted by | C#, GameComponent, TDD, XBOX360, XNA | 7 Comments

XNA Framework GameEngine Development. (Part 4, ShaderManager:GameComponent)

Introduction

Welcome to Part 4 of the XNA Framework GameEngine Development series.  In this article, I will introduce the shader interface, ShaderManager and the custom effect code generator.  This article is the first part of a sub series that will deal with shaders, textures and materials.  I know many readers are itching for eye candy by now, it is coming, but for now I am focusing on good base classes and design.

First of many interfaces.

Just a reminder this series of articles is not about using C#, design practices or architecture.  That being said, as an Engine developer we need to focus on keeping common interfaces and design principles.  This first interface will be used to group effects together into a common parent class.  We want to make certain every effect that is used in the engine have common features and are stored in a common way.

Since I use interfaces almost exclusively to accomplish this goal.  I added the folder Interfaces to the project and added a class called IRoeShader to the engine project.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;

namespace RoeEngine2.Interfaces
{
    public interface IRoeShader
    {  
        Effect BaseEffect
        {
            get;
        }

        bool ReadyToRender
        {
            get;
        }

        void Initialize(GraphicsDevice graphicsDevice);

        void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions);

        void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions, EffectPool effectPool);
    }
}

Simple enough, every shader needs to have an effect, indicate if it is ready to be rendered and be initialized.  So every class that inherits from IRoeShader will need to implement everything you see here.  This will guaruntee the engine can identify a shader effect from every other object in the engine, a nice thing in a huge and complex application.

Building an Effect Proxy Generator.

It might just be the WCF developer in me, but I simply love proxy classes.  There is just something wonderfully elegant about using code generators to create useful and powerful object classes.  That is why I loved this CodePlex project.  With a few simple adjustments we can adjust the code to include our IRoeShader interface.

Note:  Many developers with experience using XNA may want to use the generic Content Pipeline for their effects.  In a way we are using it here, just I would much rather have code that looks like effect.World = CameraManager.World, than effect.Parameter[“World”].SetValue(CameraManager.World).  To me it is much more natural to use Properties than Parameter setters. 

Create a new console project, I called mine ShaderEffectGenerator.

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Xna.Framework.Graphics;
using System.Windows.Forms;
using System.Text;

namespace ShaderEffectCodeGenerator
{
    internal class Program
    {
        // Fields
        private static List<string> effectParameters = new List<string>();
        private static StreamWriter streamWriter;
        private static int tabLevel = 0;

        // Methods
        private static int LoadEffectFromFile(string fullPath, out Effect effect, out byte[] windowsByteCode, out byte[] xboxByteCode)
        {
            try
            {
                Form form = new Form();
                PresentationParameters parameters = new PresentationParameters();
                GraphicsDevice device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware, form.Handle, parameters);
                CompiledEffect effect2 = Effect.CompileEffectFromFile(fullPath, null, null, CompilerOptions.None, Microsoft.Xna.Framework.TargetPlatform.Windows);
                windowsByteCode = effect2.GetEffectCode();
                effect = new Effect(device, effect2.GetEffectCode(), CompilerOptions.None, null);
                xboxByteCode = Effect.CompileEffectFromFile(fullPath, null, null, CompilerOptions.None, Microsoft.Xna.Framework.TargetPlatform.Xbox360).GetEffectCode();
            }
            catch (Exception exception)
            {
                ReportError(exception.ToString());
                effect = null;
                windowsByteCode = null;
                xboxByteCode = null;
                return -1;
            }
            return 0;
        }

        private static void Main(string[] args)
        {
            if (args.Length < 1)
            {
                WriteInstructions();
                ReportError("Please drag an .fx file into the app.");
            }
            else
            {
                Effect effect;
                byte&#91;&#93; buffer;
                byte&#91;&#93; buffer2;
                string path = args&#91;0&#93;;
                if (path.StartsWith("\"") && path.EndsWith("\""))
                {
                    path = path.Substring(0, path.Length - 1).Substring(1);
                }
                path = Path.GetFullPath(path);
                string str2 = Path.GetDirectoryName(path) + "/" + Path.GetFileNameWithoutExtension(path) + "Effect.cs";
                if (LoadEffectFromFile(path, out effect, out buffer, out buffer2) >= 0)
                {
                    if (File.Exists(str2))
                    {
                        File.Delete(str2);
                    }
                    string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
                    FileStream stream = new FileStream(str2, FileMode.Create);
                    using (StreamWriter writer = new StreamWriter(stream))
                    {
                        streamWriter = writer;
                        WriteUsingStatements(writer);
                        WriteLine("namespace RoeEngine2.Shaders");
                        WriteLine("{");
                        tabLevel++;
                        WriteLine(string.Format("public class {0}Effect : IRoeShader", fileNameWithoutExtension));
                        WriteLine("{");
                        tabLevel++;
                        WriteTechniqueEnumeration(effect);
                        streamWriter.WriteLine();
                        WriteRoeSpecificProperties(writer);
                        WriteLine("#region Effect Parameters");
                        writer.WriteLine();
                        foreach (EffectParameter parameter in effect.Parameters)
                        {
                            WriteEffectParameter(parameter);
                            writer.WriteLine();
                        }
                        WriteLine("#endregion");
                        writer.WriteLine();
                        WriteLine("#region Effect Techniques");
                        writer.WriteLine();
                        foreach (EffectTechnique technique in effect.Techniques)
                        {
                            WriteLine(string.Format("private EffectTechnique _{0}Technique;", technique.Name));
                            writer.WriteLine();
                        }
                        WriteLine("#endregion");
                        writer.WriteLine();
                        WriteLine("#region Initialize Methods");
                        writer.WriteLine();
                        WriteLine("///<summary>");
                        WriteLine("///Initializes the Effect from byte code for the given GraphicsDevice.");
                        WriteLine("///</summary");
                        WriteLine("///<param name=\"graphicsDevice\">The GraphicsDevice for which the effect is being created.</param>");
                        WriteLine("public void Initialize(GraphicsDevice graphicsDevice)");
                        WriteLine("{");
                        tabLevel++;
                        WriteLine("Initialize(graphicsDevice, CompilerOptions.None, null);");
                        tabLevel--;
                        WriteLine("}");
                        writer.WriteLine();
                        WriteLine("///<summary>");
                        WriteLine("///Initializes the Effect from byte code for the given GraphicsDevice and CompilerOptions.");
                        WriteLine("///</summary");
                        WriteLine("///<param name=\"graphicsDevice\">The GraphicsDevice for which the effect is being created.</param>");
                        WriteLine("///<param name=\"compilerOptions\">The CompilerOptions to use when creating the effect.</param>");
                        WriteLine("public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions)");
                        WriteLine("{");
                        tabLevel++;
                        WriteLine("Initialize(graphicsDevice, compilerOptions, null);");
                        tabLevel--;
                        WriteLine("}");
                        writer.WriteLine();
                        WriteLine("///<summary>");
                        WriteLine("///Initializes the Effect from byte code for the given GraphicsDevice, CompilerOptions, and EffectPool.");
                        WriteLine("///</summary");
                        WriteLine("///<param name=\"graphicsDevice\">The GraphicsDevice for which the effect is being created.</param>");
                        WriteLine("///<param name=\"compilerOptions\">The CompilerOptions to use when creating the effect.</param>");
                        WriteLine("///<param name=\"effectPools\">The EffectPool to use with the effect.</param>");
                        WriteLine("public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions, EffectPool effectPool)");
                        WriteLine("{");
                        tabLevel++;
                        WriteLine("_baseEffect = new Effect(graphicsDevice, byteCode, compilerOptions, effectPool);");
                        WriteLine("_readyToRender = true;");
                        writer.WriteLine();
                        foreach (string str4 in effectParameters)
                        {
                            WriteLine(string.Format("_{0}Param = _baseEffect.Parameters[\"{0}\"];", str4));
                        }
                        writer.WriteLine();
                        foreach (EffectTechnique technique2 in effect.Techniques)
                        {
                            WriteLine(string.Format("_{0}Technique = _baseEffect.Techniques[\"{0}\"];", technique2.Name));
                        }
                        tabLevel--;
                        WriteLine("}");
                        writer.WriteLine();
                        WriteLine("#endregion");
                        writer.WriteLine();
                        WriteLine("///<summary>");
                        WriteLine("///Sets the current technique for the effect.");
                        WriteLine("///</summary>");
                        WriteLine("///<param name=\"technique\">The technique to use for the current technique.</param>");
                        WriteLine(string.Format("public void SetCurrentTechnique({0}Effect.Techniques technique)", fileNameWithoutExtension));
                        WriteLine("{");
                        tabLevel++;
                        WriteLine("switch (technique)");
                        WriteLine("{");
                        tabLevel++;
                        foreach (EffectTechnique technique3 in effect.Techniques)
                        {
                            WriteLine(string.Format("case {0}Effect.Techniques.{1}:", fileNameWithoutExtension, technique3.Name));
                            tabLevel++;
                            WriteLine(string.Format("_baseEffect.CurrentTechnique = _{0}Technique;", technique3.Name));
                            WriteLine("break;");
                            writer.WriteLine();
                            tabLevel--;
                        }
                        tabLevel--;
                        WriteLine("}");
                        tabLevel--;
                        WriteLine("}");
                        writer.WriteLine();
                        WriteLine("#region Compiled Byte Code");
                        writer.WriteLine();
                        WriteLine("#if XBOX");
                        StringBuilder builder = new StringBuilder();
                        builder.Append("static readonly byte[] byteCode = { ");
                        foreach (byte num in buffer2)
                        {
                            builder.Append(num.ToString() + ",");
                        }
                        builder.Append(" };");
                        WriteLine(builder.ToString());
                        WriteLine("#else");
                        builder = new StringBuilder();
                        builder.Append("static readonly byte[] byteCode = { ");
                        foreach (byte num2 in buffer)
                        {
                            builder.Append(num2.ToString() + ",");
                        }
                        builder.Append(" };");
                        WriteLine(builder.ToString());
                        WriteLine("#endif");
                        writer.WriteLine();
                        WriteLine("#endregion");
                        tabLevel--;
                        WriteLine("}");
                        tabLevel--;
                        WriteLine("}");
                    }
                }
            }
        }

        private static void WriteUsingStatements(StreamWriter writer)
        {
            WriteLine("using System;");
            WriteLine("using Microsoft.Xna.Framework;");
            WriteLine("using Microsoft.Xna.Framework.Content;");
            WriteLine("using Microsoft.Xna.Framework.Graphics;");
            WriteLine("using RoeEngine2.Interfaces;");
            writer.WriteLine();
        }

        private static void WriteRoeSpecificProperties(StreamWriter writer)
        {
            WriteLine("private Effect _baseEffect;");
            WriteLine("///<summary>");
            WriteLine("///Gets the underlying Effect.");
            WriteLine("///</summary>");
            WriteLine("public Effect BaseEffect");
            WriteLine("{");
            tabLevel++;
            WriteLine("get { return _baseEffect; }");
            tabLevel--;
            WriteLine("}");
            writer.WriteLine();
            WriteLine("private bool _readyToRender = false;");
            WriteLine("///<summary>");
            WriteLine("///Is the shader ready to be rendered.");
            WriteLine("///</summary>");
            WriteLine("public bool ReadyToRender");
            WriteLine("{");
            tabLevel++;
            WriteLine("get { return _readyToRender; }");
            tabLevel--;
            WriteLine("}");
            writer.WriteLine();
        }       

        private static void WriteEffectParameter(EffectParameter param)
        {
            string item = param.Name;
            string str2 = String.Empty;
            string str3 = String.Empty;
            string str5 = String.Empty;
            switch (param.ParameterType)
            {
                case EffectParameterType.Bool:
                    str2 = "bool";
                    str3 = "Boolean";
                    break;
                case EffectParameterType.Int32:
                    str2 = "int";
                    str3 = "Int32";
                    break;
                case EffectParameterType.Single:
                    if ((param.RowCount > 1) && (param.ColumnCount > 1))
                    {
                        str2 = "Matrix";
                        str3 = "Matrix";
                    }
                    else if (param.ColumnCount > 1)
                    {
                        str2 = "Vector" + param.ColumnCount.ToString();
                        str3 = str2;
                    }
                    break;
                case EffectParameterType.String:
                    str2 = "string";
                    str3 = "String";
                    break;
                case EffectParameterType.Texture:
                    str2 = "Texture2D";
                    str3 = "Texture2D";
                    break;
                case EffectParameterType.Texture1D:
                    str2 = "Texture1D";
                    str3 = "Texture1D";
                    break;
                case EffectParameterType.Texture2D:
                    str3 = "Texture2D";
                    str3 = "Texture2D";
                    break;
                case EffectParameterType.Texture3D:
                    str2 = "Texture3D";
                    str3 = "Texture3D";
                    break;
                case EffectParameterType.TextureCube:
                    str2 = "TextureCube";
                    str3 = "TextureCube";
                    break;
                default:
                    return;
            }
            if (param.Elements.Count > 0)
            {
                str2 = str2 + "[]";
            }
            effectParameters.Add(item);
            string str4 = item.Substring(0, 1).ToUpper() + item.Substring(1);
            WriteLine(string.Format("private EffectParameter _{0}Param;", item));
            WriteLine(string.Format("public {0} {1}", str2, str4));
            WriteLine("{");
            tabLevel++;
            WriteLine("get");
            WriteLine("{");
            tabLevel++;
            WriteLine(string.Format("if (_{0}Param == null)", item));
            tabLevel++;
            WriteLine(string.Format("throw new Exception(\"Cannot get value of {0}; {0} EffectParameter is null.\");", str4));
            tabLevel--;
            if (param.Elements.Count > 0)
            {
                WriteLine(string.Format("return _{0}Param.GetValue{1}Array({2});", item, str3, param.Elements.Count));
            }
            else
            {
                WriteLine(string.Format("return _{0}Param.GetValue{1}();", item, str3));
            }
            tabLevel--;
            WriteLine("}");
            WriteLine("set");
            WriteLine("{");
            tabLevel++;
            WriteLine(string.Format("if (_{0}Param == null)", item));
            tabLevel++;
            WriteLine(string.Format("throw new Exception(\"Cannot set value of {0}; {0} EffectParameter is null.\");", str4));
            tabLevel--;
            WriteLine(string.Format("_{0}Param.SetValue(value);", item));
            tabLevel--;
            WriteLine("}");
            tabLevel--;
            WriteLine("}");
        }

        private static void WriteInstructions()
        {
            Console.WriteLine("The EffectCodeGenerator is a drop-in application. Simply drop in any .fx file to have");
            Console.WriteLine("the EffectCodeGenerator produce you a new class written in C# that enables easy use");
            Console.WriteLine("with your .fx file.");
        }

        private static void WriteLine(string line)
        {
            string str = string.Empty;
            for (int i = 0; i < tabLevel; i++)
            {
                str = str + "\t";
            }
            streamWriter.WriteLine(str + line);
        }

        private static void WriteTechniqueEnumeration(Effect effect)
        {
            WriteLine("public enum Techniques");
            WriteLine("{");
            tabLevel++;
            foreach (EffectTechnique technique in effect.Techniques)
            {
                WriteLine(string.Format("{0},", technique.Name));
            }
            tabLevel--;
            WriteLine("}");
        }
       
        private static void ReportError(string error)
        {
            Console.WriteLine("Error: {0}", error);
            Console.ReadKey(true);
        }
    }
}&#91;/sourcecode&#93;
 
<strong>ShaderManager class (Updated to use Dictionary)</strong> 

<strong></strong>

Note:  Thanks again to KaBaL, Please notice I changed the Manager namespace to Managers.  This is because there will be more than one. 

This class is our main container for everything that has to do with effects and shaders.  It includes some very simple principles that you may have seen from the Ramblings of a Hazy Mind series.  There are some minor adjustments, but I am not ashamed to say, the Ramblings series was a major contributer to this design principle.  Feel free to read the series, it is an excellent primer for many of the next articles in this series.

Add a new class to the Managers folder, I called mine ShaderManager.

Note:  Updated to use Dictionary thanks to leaf and KaBaL.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using System.Collections;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2.Shaders;

namespace RoeEngine2.Managers
{
    public class ShaderManager : GameComponent
    {
        private static Dictionary<string, IRoeShader> _shaders = new Dictionary<string, IRoeShader>();

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

        /// <summary>
        /// Create the shader Managers.
        /// </summary>
        /// <param name="game"></param>
        public ShaderManager(Game game)
            : base(game)
        {
        }

        /// <summary>
        /// Add a shader of type IRoeShader.
        /// </summary>
        /// <param name="newShader"></param>
        /// <param name="shaderLabel"></param>
        public static void AddShader(IRoeShader newShader, string shaderLabel)
        {
            if (shaderLabel != null && !_shaders.ContainsKey(shaderLabel))
            {
                _shaders.Add(shaderLabel, newShader);

                newShader.Initialize(EngineManager.Device);
            }
        }

        /// <summary>
        /// Get a shader of type IRoeShader.
        /// </summary>
        /// <param name="shaderLabel"></param>
        /// <returns></returns>
        public static IRoeShader GetShader(string shaderLabel)
        {
            if (shaderLabel != null && _shaders.ContainsKey(shaderLabel))
            {
                return _shaders[shaderLabel];
            }
            return null;
        }

        /// <summary>
        /// Create the shaders.
        /// </summary>
        public override void Initialize()
        {
            base.Initialize();

            foreach (IRoeShader shader in _shaders.Values)
            {
                if (!shader.ReadyToRender)
                {
                    shader.Initialize(EngineManager.Device);
                }
            }

            AddShader(new basicEffect(), "BasicEffect");

            _initialized = true;
        }
    }
}

There may be some readers that will ask why not use a generic list instead of a hashtable?  I am okay with either design, I chose a HashTable because I liked having instant access to each object rathre than getting an item out of a list by index.  If you are the kind of person that wants to explore deeper into engine design you might write test cases for each design.  It is very possible, a hashtable is sub optimal for the small number of shaders we will most likely be adding to the project. If you can prove it with numbers to support your findings, I will definitely redesign what you see here and give you credit for your work.

Using the ShaderManager GameComponent

Add the following code to the Properties section of RoeEngine2

private static ShaderManager _shaderManagers = null;

And in the RoeEngine2 Constructor

// Init shader Managers
_shaderManagers = new ShaderManager(this);
Components.Add(_shaderManagers);

//TODO include other inits here!

 
Encapsulating the BasicEffect

This might not be necessary, but since I would like to use the BasicEffect in some samples I will encapsulate it here.  This will most likely grow some in the future, it is here now for test cases.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework;
namespace RoeEngine2.Shaders
{
    public class basicEffect : IRoeShader
    {
        private BasicEffect _baseEffect;
        ///<summary>
        ///Gets the underlying Effect.
        ///</summary>
        public Effect BaseEffect
        {
            get { return _baseEffect; }
        }
        private bool _readyToRender = false;
        ///<summary>
        ///Is the shader ready to be rendered.
        ///</summary>
        public bool ReadyToRender
        {
            get { return _readyToRender; }
        }
        ///<summary>
        ///Initializes the Effect from byte code for the given GraphicsDevice.
        ///</summary
        ///<param name="graphicsDevice">The GraphicsDevice for which the effect is being created.</param>
        public void Initialize(GraphicsDevice graphicsDevice)
        {
            Initialize(graphicsDevice, CompilerOptions.None, null);
        }
        ///<summary>
        ///Initializes the Effect from byte code for the given GraphicsDevice and CompilerOptions.
        ///</summary
        ///<param name="graphicsDevice">The GraphicsDevice for which the effect is being created.</param>
        ///<param name="compilerOptions">The CompilerOptions to use when creating the effect.</param>
        public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions)
        {
            Initialize(graphicsDevice, compilerOptions, null);
        }
        ///<summary>
        ///Initializes the Effect from byte code for the given GraphicsDevice, CompilerOptions, and EffectPool.
        ///</summary
        ///<param name="graphicsDevice">The GraphicsDevice for which the effect is being created.</param>
        ///<param name="compilerOptions">The CompilerOptions to use when creating the effect.</param>
        ///<param name="effectPools">The EffectPool to use with the effect.</param>
        public void Initialize(GraphicsDevice graphicsDevice, CompilerOptions compilerOptions, EffectPool effectPool)
        {
            _baseEffect = new BasicEffect(graphicsDevice, effectPool);
            _readyToRender = true;
        }
    }
}

 
Optional TDD – Updated, no longer using InvalidOperationException

Add the following test cases to the RoeEngineTestCases class.

        [Test, Category("ShaderManager")]
        public void ShaderManagerCreated()
        {
            Assert.IsTrue(ShaderManager.Initialized);
            Assert.IsNotNull(ShaderManager.GetShader("BasicEffect"));
            Assert.AreEqual(typeof(basicEffect), ShaderManager.GetShader("BasicEffect").GetType());
        }

        [Test, Category("ShaderManager")]
        public void ShaderManagerAddGetFailed()
        {
            // It is always a good idea to test for Failures too!
            Assert.IsNull(ShaderManager.GetShader("testshader"));  
        }

Conclusion

In this article I introduced the IRoeShader, shaders, shader proxy generation and the ShaderManager.  I will be revisting this class from time to time, in a growing engine it is always necessary to Refactor your work.  Hopefully, you can use some of these design practices in other projects as well.

Your feedback is very important, please leave me a comments or suggestions.  I will respond as soon as possible.

January 14, 2008 Posted by | C#, GameComponent, TDD, XBOX360, XNA | 25 Comments

XNA Framework GameEngine Development. (Part 3, TDD)

Introduction

Welcome to Part 3 of the XNA Framework GameEngine Development series.  In this article, I will introduce the Test Driven Development piece of the game engine.  Better late than never.

There are plenty of articles covering TDD so I will not argue the benefits here.  I personally see TDD as just another tool for writing good code, starting early and writing test cases often is always “The Best Practice”.  The simple fact that I am introducing TDD in the 3rd article of the series indicates I am not an extremist.  I would be happy to entertain your thoughts and suggestions.

Creating a Unit Test Project (Updated to use Managers Namespace)

I chose to use NUnit as my unit tester, mainly because it is free and is what I use at work.  Also, please note, I do not use the Express Additions of Visual Studio, I have no experience setting up TDD in VS Express.  If you have questions or experiences please feel free to leave a comment.  Updated, KaBaL was kind enough to provide some code for the Visual Studio Team Suite test cases, please see below.

First, create a new windows class library project, I named mine RoeEngine2UnitTests.  You will need to reference your game engine project as well as, Microsoft.Xna.Framework, Microsoft.Xna.Framework.Game, and the nuit.framework.  Most likely you will also have a couple System references at this point, keep those as well.

Next, Rename or Delete/Add the Class1.cs file to RoeEngineTestCases.cs and add the following code.

 Note:  Updated, thanks to KaBaL for pointing out that I renamed the namespace Manager to Managers.

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

namespace RoeEngine2UnitTests
{
    [TestFixture]
    public class RoeEngineTestCases
    {
        private EngineManager _game = null;
        private const string _windowTitle = "UNITTEST";

        [TestFixtureSetUp]
        public void SetupTestFixture()
        {
            _game = new EngineManager(_windowTitle);
            EngineManager.Game = _game;
            _game.Activated += new EventHandler(game_Activated);
            _game.Run();
        }

        void game_Activated(object sender, EventArgs e)
        {
            _game.Exit();
        }

        [TestFixtureTearDown]
        public void TeardownTestFixture()
        {
            _game = null;
        }

        [Test]
        public void GameStartup()
        {
            Assert.IsNotNull(_game);
            Assert.IsNotNull(EngineManager.Game);
            Assert.IsNotNull(EngineManager.Device);
            Assert.IsNotNull(EngineManager.ContentManager);
            Assert.AreEqual(_windowTitle, EngineManager.WindowTitle.ToString());           
        }

        [Test]
        public void FpsCounterCreated()
        {
            Assert.IsNotNull(EngineManager.FpsCounter);
            Assert.AreEqual("0", EngineManager.FpsCounter.FPS.ToString());
        }
    }
}

Finally, we are ready to run our test cases.  Open up NUnit and navigate to your bin/Roeengine2UnitTests.dll file.  Run the project and voila! Updated… This is what makes the open source community so great.  The following was contributed by KaBaL.

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

namespace RoeEngine2UnitTest
{
    ///
    /// Summary description for UnitTest1
    ///
    [TestClass]
    public class UnitTest1
    {
        private EngineManager _game = null;
        private const string _windowTitle = "UNITTEST";

        [TestInitialize()]
        public void TestInitialize()
        {
            _game = new EngineManager(_windowTitle);
            EngineManager.Game = _game;
            _game.Activated += new EventHandler(game_Activated);
            _game.Run();
        }

        void game_Activated(object sender, EventArgs e)
        {
            _game.Exit();
        }

        [TestCleanup()]
        public void TestCleanup()
        {
            _game = null;
        }

        [TestMethod]
        public void GameStartup()
        {
            Assert.IsNotNull(_game);
            Assert.IsNotNull(EngineManager.Game);
            Assert.IsNotNull(EngineManager.Device);
            Assert.IsNotNull(EngineManager.ContentManager);
            //Assert.AreEqual(_windowTitle, EngineManager.WindowTitle.ToString());
        }

        [TestMethod]
        public void FpsCounterCreated()
        {
            Assert.IsNotNull(EngineManager.FpsCounter);
            Assert.AreEqual("0", EngineManager.FpsCounter.FPS.ToString());
        }

        public UnitTest1()
        {
            //
            // TODO: Add constructor logic here
            //
        }
    }
}

 


Conclusion

We have just set up a very basic testing environment for our engine.  Remember, all of this is optional, TDD may not be for everyone.  I strongly urge you to use it, but you know your skillsets better than I.

Please feel free to leave me any comments or suggestions/improvements to the code you see here.

January 10, 2008 Posted by | C#, TDD, XBOX360, XNA | 19 Comments