Running on Empty

The few things I know, I like to share.

XNA Framework GameEngine Development. (Part 20, Plug-in Manager)

Introduction

Welcome to Part 20 of the XNA Framework GameEngine Development series.  This is the first article of my new game engine series Roe3. 

This article will focus on creating a plug-in architecture for game components and managers.

Creating the plug-in socket

While writing this series I found a few instances when I wanted to be able to turn off a component or manager in order to test or even to create a game that didn’t use all of bells and whistles that the engine had to offer.  I normally would go into the base engine code and remove the components rebuild and move on.  Of course this works, but it is not exactly a very elegant solution.

So I created a configuration management system that would allow me to instantiate managers and components using an xml file that looks something like the following.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <components>
    <component value="RoeEngine3.GameComponents.Cursor"/>
    <component value="RoeEngine3.GameComponents.FpsCounter"/>
    <component value="RoeEngine3.GameComponents.DebugOverlay"/>
  </components>
  <managers>
    <manager value="RoeEngine3.Managers.AudioManager"/>
    <manager value="RoeEngine3.Managers.ScreenManager"/>
  </managers>
</configuration>

Much nicer!  In this example there are 3 components and 2 managers that will be created as the engine starts up… without the need to recompile the engine.  Don’t want to show a cursor or fpscounter?  Simply remove the component from the config.

So all we need to do is create an xml reader for this file, something like the following.

namespace RoeEngine3.Managers
{
    [XmlType("configuration")]
    public sealed class ConfigurationManager
    {
        public static ConfigurationManager Instance { get; private set; }

        static ConfigurationManager()
        {
            if(Instance == null)
            {
                string file = @"Managers.config";

                if(!File.Exists(file))
                {
                    throw new FileNotFoundException();
                }

                using(StreamReader reader = new StreamReader(file))
                {
                    XmlSerializer serializer = new XmlSerializer(typeof(ConfigurationManager));
                    Instance = (ConfigurationManager)serializer.Deserialize(reader);
                }
            }
        }

        [XmlArray("managers"), XmlArrayItem("manager")]
        public Managers Managers { get; set; }

        [XmlArray("components"), XmlArrayItem("component")]
        public Components Components { get; set; }
    }

    public sealed class Components : KeyedCollection<string, Component>
    {
        protected override string GetKeyForItem(Component item)
        {
            return item.Value;
        }
    }

    public sealed class Managers : KeyedCollection<string, Manager>
    {
        protected override string GetKeyForItem(Manager item)
        {
            return item.Value;
        }
    }

    [XmlType("component")]
    public sealed class Component
    {
        [XmlAttribute("value")]
        public string Value { get; set; }
    }

    [XmlType("manager")]
    public sealed class Manager
    {
        [XmlAttribute("value")]
        public string Value { get; set; }
    }
}

Now we have a list of Managers and Components that we want to activate in the engine.  So in our base engine class we need to write some prep code for our managers and components.

        private void PrepareComponents()
        {
            foreach (Component component in ConfigurationManager.Instance.Components)
            {
                GameComponent baseComponent = ComponentActivator.CreateInstance(Type.GetType(component.Value)) as GameComponent;
                EngineManager.Game.Components.Add(baseComponent);
            }
        }
       
        private void PrepareManagers()
        {    
            foreach (Manager manager in ConfigurationManager.Instance.Managers)
            {
                GameComponent baseManager = ManagerActivator.CreateInstance(Type.GetType(manager.Value)) as GameComponent;              
                Components.Add(baseManager);
            }
        }
        protected override void Initialize()
        {
            PrepareManagers();
            PrepareComponents();

     ... // OTHER INITIALIZE STUFF GOES HERE
 }

Turn on the Power

All that is left for us to do is activate the managers and components.  Just need to create some reflection magic to invoke them.

    public sealed class ManagerActivator
    {
        public static object CreateInstance(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("Manager Type");
            }

            ConstructorInfo info = type.GetConstructor(new Type[] { typeof(Game) });

            if (info == null)
            {
                return null;
            }

            object instance;

            try
            {
                instance = info.Invoke(new object[] { EngineManager.Game });
            }
            catch (Exception ex)
            {
                if (ex is SystemException)
                {
                    throw;
                }
                instance = null;
            }
            return instance;
        }
    }

    public sealed class ComponentActivator
    {
        public static object CreateInstance(Type type)
        {
            return ManagerActivator.CreateInstance(type);
        }
    }

A few side notes here, if you notice the GetConstructor object passes in a Type array of type Game.  This is because all GameComponents have a constructor parameter of the Game.  Your components will obviously need to subscribe to the correct GetConstructor types.

Conclusion

In this article I outlined a simple method of creating a pluggable interface for components and managers.  This interface will allow us to turn on and off components and managers as we need them without writing additional code.

December 17, 2008 Posted by | C#, GameComponent, XBOX360, XNA | 22 Comments

XNA Framework GameEngine Development. (Part 14, Adding Physics)

Introduction

Welcome to Part 14 of the XNA Framework GameEngine Development series.  This is the first article in the Physics sub-section and will deal mainly on integrating physics into the exisiting engine.  I am not a physics expert and will not go into the details of how dynamic objects interact.  Rather my goal is to integrate an existing physics engine into up until now can only be described as a wrapper for the XNA rendering engine.

part14.jpg

Reader Feedback

I had intended to use the Newton Physics Wrapper written by TamedTornado, but thanks to feedback from (kt) I moved over to the BulletX physics engine.  I do read your comments and want them, take the time to make a suggestion and I will work to make it a part of the engine.  I do want to give everybody an oppurtunity to get the best start for making games, I don’t know all the answers, so I find your insights very valuable.  We all get better games in return.

Release

Here is the source code for this article.  Please feel free to use this code, make improvements if you like, but share what you do.  Drop me a line or comment if you do make something with this engine, I would love to see it.

Shapes

The only major new concept that we have to pay attention to for physics is the concept of shapes.  Every object that we want to have in the physics engine must define a shape.  The BulletX engine goes out of its way to provide many useful shapes for our game objects.  In this example I have solely used the Box shape, but in future demos I will use others.

PhysicsManager

The PhysicsManager is the first internal class GameComponent of the engine.  That means the “game” will never be able to use its classes.  This is an encapsulation technique that will protect a very busy class from being misused.  As a result, the PhysicsManager is tied very closly to the SceneGraphManager (and will be tied to other scene managers in the future).  So what does the physics manager do for us?

First, it defines the World using properties such as gravity, the axis aligned bounding box (aabb) algorithm, as well as collision data.  Quite a lot of information here, most of which is just intializing the stuff that makes BulletX work.

Second, it contains the CreateRigidBody method.  This is the single most important method in the class and the one you will use most often as a game developer.  This method takes an IRoeBulletPhysics object as a parameter adds it to the world and returns a world index to the object passed in as an out parameter.  This method does all the setup work for getting objects into the BulletX engine, not that it is hard, it just isn’t code I want to write all the time.

Finally, the GetObject method allows you to grab the CollisionObject from the world by index.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using RoeEngine2.GameComponents;
using RoeEngine2.Interfaces;
using XnaDevRu.BulletX;
using XnaDevRu.BulletX.Dynamics;
using RoeEngine2.SceneObject;

namespace RoeEngine2.Managers
{
    internal class PhysicsManager : GameComponent
    {
        private static int _collisionIndex = 0;
        private static CollisionDispatcher _collisionDispatcher;
        private static OverlappingPairCache _broadPhase;
        private static SequentialImpulseConstraintSolver _solver;
        private static DiscreteDynamicsWorld _world;
        public static DiscreteDynamicsWorld World
        {
            get { return _world; }
        }

        private static bool _useSweepAndPrune;
        public static bool UseSweepAndPrune
        {
            get { return _useSweepAndPrune; }
            set { _useSweepAndPrune = value; }
        }

        private static bool _drawDebugger;
        public static bool DrawDebugger
        {
            get { return _drawDebugger; }
            set { _drawDebugger = value; }
        }

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

        /// 
        /// Create the Physics Managers.
        /// 
        /// 
        public PhysicsManager(Game game)
            : base(game)
        {
            Enabled = true;
            ResetPhysics();
        }

        public static void ResetPhysics()
        {
            _collisionDispatcher = new CollisionDispatcher();

            if (_useSweepAndPrune)
            {
                Vector3 worldAabbMin = new Vector3(-10000, -10000, -10000);
                Vector3 worldAabbMax = new Vector3(10000, 10000, 10000);
                const int maxProxies = 32766;
                _broadPhase = new AxisSweep3(worldAabbMin, worldAabbMax, maxProxies);
            }
            else
            {
                _broadPhase = new SimpleBroadphase();
            }
            _solver = new SequentialImpulseConstraintSolver();
            _world = new DiscreteDynamicsWorld(_collisionDispatcher, _broadPhase, _solver);

            _world.Gravity = new Vector3(0, -10.0f, 0);

            GC.Collect();
        }

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

            XnaDebugDraw debugDrawer = new XnaDebugDraw(EngineManager.Device);
            debugDrawer.DebugMode = DebugDrawModes.DrawAabb | DebugDrawModes.DrawContactPoints;
            _world.DebugDrawer = debugDrawer;

            _initialized = true;
        }

        public static void Draw(GameTime gameTime)
        {
            if (_drawDebugger)
                ((XnaDebugDraw)_world.DebugDrawer).Update(CameraManager.ActiveCamera.View, CameraManager.ActiveCamera.Projection);
            _world.StepSimulation(1.0f / 60.0f, 1);
        }

        /// 
        /// Add a new shape to the physics manager.
        /// 
        /// Body mass, if 0 body is static.
        /// Starting body matrix transform.
        /// Body shape
        /// Index into the Physics World collection
        /// Created Rigid Body
        private static void CreateRigidBody(float mass, Matrix startMatrix,
                                                 CollisionShape newShape, CollisionOptions options,
                                                 out int worldIndex)
        {
            bool isDynamic = (mass > 0.0f);

            Vector3 localInertia = new Vector3();
            if (isDynamic)
                newShape.CalculateLocalInertia(mass, out localInertia);

            //using motionstate is recommended, it provides interpolation capabilities, and only synchronizes 'active' objects
            DefaultMotionState myMotionState = new DefaultMotionState(startMatrix, Matrix.Identity);
            RigidBody body = new RigidBody(mass, myMotionState, newShape, localInertia, 0.0f, 0.0f, 0.5f, 0.0f);

            _world.AddRigidBody(body);

            worldIndex = _collisionIndex;
            _collisionIndex++;

            body.CollisionFlags |= options;
        }

        /// 
        /// Add a new shape to the physics manager.
        /// 
        /// Body mass, if 0 body is static.
        public static void CreateRigidBody(IRoeBulletPhysics iRoeBulletPhysics)
        {
            int index;
            RoeSceneObject obj = (RoeSceneObject)iRoeBulletPhysics;
            CreateRigidBody(iRoeBulletPhysics.Mass, Matrix.CreateTranslation(obj.Position) * Matrix.CreateFromQuaternion(obj.Rotation),
                            iRoeBulletPhysics.CollisionShape, iRoeBulletPhysics.CollisionOptions,
                            out index);
            iRoeBulletPhysics.WorldIndex = index;
        }

        public static CollisionObject GetObject(int index)
        {
            if (_world.CollisionObjectsCount > index)
            {
                return _world.CollisionObjects[index];
            }
            return null;
        }
    }
}

Interface

As usual I like to use interfaces to define base classes for game objects.  Here is the new IRoeBulletPhysics interface.

using System;
using System.Collections.Generic;
using System.Text;
using XnaDevRu.BulletX.Dynamics;
using XnaDevRu.BulletX;

namespace RoeEngine2.Interfaces
{
    /// 
    /// Every RoeSceneObject that implements this interface will be added to the physics manager
    /// 
    public interface IRoeBulletPhysics : IRoeSceneObject
    {
        CollisionOptions CollisionOptions
        {
            get;
            set;
        }

        int WorldIndex
        {
            get;
            set;
        }
       
        CollisionShape CollisionShape
        {
            get;
            set;
        }

        float Mass
        {
            get;
            set;
        }
    }
}

RoeSceneObject

Just some small changes here, mainly to do with the World matrix.  We pretty much need to give control over world transform to the physics engine, here is how to do it.

        public virtual Matrix World
        {
            get
            {
                if (ReadyToRender)
                {
                    if (this is IRoeBulletPhysics)
                    {
                        Matrix world = Matrix.Identity;
                        CollisionObject obj = PhysicsManager.World.CollisionObjects[((IRoeBulletPhysics)this).WorldIndex];
                        RigidBody body = RigidBody.Upcast(obj);

                        if (body != null && body.MotionState != null)
                        {
                            DefaultMotionState myMotionState = (DefaultMotionState)body.MotionState;
                            return Matrix.CreateScale(Scale) * myMotionState.GraphicsWorldTransform;
                        }
                        else
                        {
                            return Matrix.CreateScale(Scale) * obj.WorldTransform;
                        }
                    }
                }
                return Matrix.CreateScale(_scale) * Matrix.CreateFromQuaternion(_rotation) * Matrix.CreateTranslation(_position);
            }
        }

Box.cs

I know this isn’t very interesting, but it is a start.  In any good physics demo, you get to see spheres or boxes falling and colliding.  So we need to make a Box class.

using System;
using System.Collections.Generic;
using System.Text;
using RoeEngine2.Interfaces;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using XnaDevRu.BulletX;
using RoeEngine2.Managers;
using RoeEngine2.Models;
using XnaDevRu.BulletX.Dynamics;

namespace RoeEngine2.SceneObject.StandardObjects
{
    public class Box : RoeSceneObject, IRoeLoadable, IRoeUpdateable, IRoeOcclusion, IRoeBulletPhysics
    {
        public VertexPositionColor[] points = new VertexPositionColor[8];
        public int[] index = new int[24];

        private string _occlusionModelName;
        public string OcclusionModelName
        {
            get { return _occlusionModelName; }
            set { _occlusionModelName = value; }
        }

        private OcclusionQuery _query = new OcclusionQuery(EngineManager.Device);
        public OcclusionQuery Query
        {
            get { return _query; }
        }

        private bool _occluded = false;
        public bool Occluded
        {
            get { return _occluded; }
            set { _occluded = value; }
        }

        private bool _culled = false;
        public bool Culled
        {
            get { return _culled; }
            set { _culled = value; }
        }

        private CollisionOptions _collisionOptions;
        public CollisionOptions CollisionOptions
        {
            get { return _collisionOptions; }
            set { _collisionOptions = value; }
        }

        private int _worldIndex;
        public int WorldIndex
        {
            get { return _worldIndex; }
            set { _worldIndex = value; }
        }

        private CollisionShape _collisionShape;
        public CollisionShape CollisionShape
        {
            get { return _collisionShape; }
            set { _collisionShape = value; }
        }

        private float _mass = 0.0f;
        public float Mass
        {
            get { return _mass; }
            set { _mass = value; }
        }

        private bool _boundingBoxCreated;
        public bool BoundingBoxCreated
        {
            get { return _boundingBoxCreated; }
        }

        public BoundingBox _boundingBox;
        public BoundingBox BoundingBox
        {
            get { return _boundingBox; }
        }

        public Box()
        {
           
        }

        public Box(Vector3 newPosition)
        {
            Position = newPosition;
        }

        public BoundingBox GetBoundingBoxTransformed()
        {
            Vector3 min, max;
            min = _boundingBox.Min;
            max = _boundingBox.Max;
           
            min = Vector3.Transform(_boundingBox.Min, World);
            max = Vector3.Transform(_boundingBox.Max, World);

            return new BoundingBox(min, max);
        }

        public void Update(GameTime gameTime)
        {
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            IRoeModel model = ModelManager.GetModel(ModelName);
            if (model != null && model.ReadyToRender && !ReadyToRender)
            {
                Matrix[] transforms = new Matrix[model.BaseModel.Bones.Count];
                model.BaseModel.CopyAbsoluteBoneTransformsTo(transforms);

                _boundingBox = new BoundingBox();

                foreach (ModelMesh mesh in model.BaseModel.Meshes)
                {
                    if (!BoundingBoxCreated)
                    {
                        _boundingBox = BoundingBox.CreateMerged(_boundingBox, BoundingBox.CreateFromSphere(mesh.BoundingSphere));
                    }
                }
                _boundingBoxCreated = true;

                Vector3 min, max;
                min = BoundingBox.Min * new Vector3(0.6f, 0.6f, 0.6f);
                max = BoundingBox.Max * new Vector3(0.6f, 0.6f, 0.6f);

                _boundingBox = new BoundingBox(min, max);

                points[0].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[1].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[2].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Max.Z);
                points[3].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Max.Z);

                points[4].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[5].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[6].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Max.Z);
                points[7].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Max.Z);

                _collisionShape = new BoxShape(Scale / 2.0f);
                SceneGraphManager.AddPhysics(this);
               
                ReadyToRender = true;
            }
        }

        public void LoadContent()
        {
            RoeModel model = new RoeModel("Content/Models/Box");

            ModelManager.AddModel(model, "boxmodel");
            this.ModelName = "boxmodel";
            this.OcclusionModelName = "boxmodel";

            index[0] = 0;
            index[1] = 1;
            index[2] = 1;
            index[3] = 2;
            index[4] = 2;
            index[5] = 3;
            index[6] = 3;
            index[7] = 0;

            index[8] = 4;
            index[9] = 5;
            index[10] = 5;
            index[11] = 6;
            index[12] = 6;
            index[13] = 7;
            index[14] = 7;
            index[15] = 4;

            index[16] = 0;
            index[17] = 4;
            index[18] = 1;
            index[19] = 5;
            index[20] = 2;
            index[21] = 6;
            index[22] = 3;
            index[23] = 7;

        }

        public void UnloadContent()
        {
           
        }

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

            using (VertexDeclaration declaration = new VertexDeclaration(EngineManager.Device, VertexPositionColor.VertexElements))
            {
                EngineManager.Device.RenderState.PointSize = 1.0f;
                EngineManager.Device.VertexDeclaration = declaration;

                IRoeShader shader = ShaderManager.GetShader("basic");

                if (shader.ReadyToRender)
                {
                    BasicEffect effect = shader.BaseEffect as BasicEffect;
                    effect.DiffuseColor = Color.Red.ToVector3();
                    effect.View = CameraManager.ActiveCamera.View;
                    effect.Projection = CameraManager.ActiveCamera.Projection;
                    effect.World = World;

                    effect.Begin();

                    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                    {
                        pass.Begin();
                        EngineManager.Device.DrawUserIndexedPrimitives(
                                            PrimitiveType.LineList, points,
                                            0,
                                            8,
                                            index,
                                            0,
                                            12);
                        pass.End();
                    }
                    effect.End();
                }
            }
        }

        public override void DrawCulling(GameTime gameTime)
        {
            Occluded = false;
            if (ReadyToRender && !Culled)
            {
                _query.Begin();
                IRoeModel model = ModelManager.GetModel(_occlusionModelName);
                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 = World;
                            effect.View = CameraManager.ActiveCamera.View;
                            effect.Projection = CameraManager.ActiveCamera.Projection;
                        }
                        mesh.Draw();
                    }
                }
                _query.End();

                while (!_query.IsComplete)
                {

                }

                if (_query.IsComplete && _query.PixelCount == 0)
                {
                    Occluded = true;
                }
            }
        }
    }
}

The Rest

Pretty much now you have the basics of physics in the engine.  Now all you need to do is add some logic to the game to add some boxes and objects and watch them collide.

Here is my gameclass Initalizer to get you started.

        /// 
        /// Load graphics content for the game.
        /// 
        public override void LoadContent()
        {
            SceneGraphManager.DrawDebugger = true;

            Box box = new Box();
            box.Position = new Vector3(0, -10, 0);
            box.Scale = new Vector3(10, 10, 10);
            box.Mass = 0.0f;
            SceneGraphManager.AddObject(box);

            for (int j = -2; j < 3; j++)
            {
                box = new Box(new Vector3(j * 2, 0, 0));
                box.Scale = new Vector3(2, 2, 2);
                box.Mass = 1.0f;
                SceneGraphManager.AddObject(box);

                for (int i = 1; i < 10; i++)
                {
                    box = new Box(new Vector3(j * 2f, i * 3.0f, 0));
                    box.Scale = new Vector3(0.5f, 0.5f, 0.5f);
                    box.Mass = 10.0f;
                    SceneGraphManager.AddObject(box);
                }
            }
            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();
        }

Conclusion

In this article I introduced physics into the engine.  This is a point of demarcation between a rendering engine and a true to life game engine, by including multiple core components working in unison.  From now on this series can truely be called a game engine.

Please leave a suggestion or comment.  I value what you have to say and read every comment, and sometimes make improvements based on what you have to say.

February 26, 2008 Posted by | C#, GameComponent, XBOX360, XNA | 6 Comments

XNA Framework GameEngine Development. (Part 13, Occlusion Culling and Frustum Culling)

Introduction

Welcome to Part 13 of the XNA Framework GameEngine Development series.  Yet another slight departure from my planned series.  This article will focus on culling, both using the Frustum and OcclusionQuery.  I know I covered Culling in Part 12, but going back to look at that article I realized I was falling sadly short on explaination.  So rather than confuse readers by going back to make changes in previous articles, I will simply explain sections of code and release code with each sample.  Hopefully, this new format will be beneficial to readers.

part13.jpg

Release

If you enjoy these articles please leave a comment, here is the sourcecode.  Eventually, I am going to run out of things to talk about, your feedback is very important to me and helps formulate future articles.

Built-in Frustum Culling

The frustum is an imaginary trapezoid that defines the area that a camera can view.  In general this trapezoid can be defined using a near plane, the far plane, and the field of view.  There are plenty of articles that you can find online that describe the frustum in far more elegant ways than I can, but I think of the frustum as the following.

Imagine you are looking out of a window in your room, in real physical terms, the distance you are sitting from that window is the same as the near plane distance.  If you moved closer to the window your near plane distance would be smaller, easy enough. 

The far plane is a little harder to describe, it is the farthest viewable distance that you can see.  It literally is a wall that sits out in space and covers up everything beyond.  Finally, there is the field of View, this is how wide your periferal vision extends.  Anybody who has looked through a telescope, camera lens, or binoculars should notice this.

So what does this mean to a game engine?  Quite simply if we dont see it in our frustum, we dont need to draw it.  Modern video cards do this very well, they simply do not render pixels that lay outside of the frustum, you should know however, the draw call is sent to the video card though. 

This is a very important distiction to make, depending on the video card to cull objects outside of the view frustum is not enough.  Why send instructions to the video card if it is just going to not draw an object?  So, we cull the items that never are going to be drawn before they go to the video card.

part13-frustum-culling.jpg

Frustum Culling

So the whole point of frustum culling is to reduce the number of objects we send to the video card to be drawn.  This is usually done by testing simple geometry for inclusion in the scene, enter the BoundingBox and BoundingSphere.  A BoundingBox or BoundingSphere is a very simple mathematical shape that we can use to test for inclusion in the frustum.  These shapes are just large enough to completely enclose the entire object.  See image below (Notice, the bounding box is a perfect square, even though the top and bottom sides of the random object do not touch the edge).

 part13-bounding-box.jpg

Now that we have a bounding box or sphere, we can get to some serious work.  See the image below for the new comparison.

part13-frustum-culling-bounding-box.jpg

Wow!  If you are thinking we just reduced the number of objects sent to the video card in this scene by half, you are correct. 

Obviously, this sort of culling is very important for scene optimization, so how do we get this magic to work in the Engine?

First, create an interface so that we can identify Cullable SceneObjects

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

namespace RoeEngine2.Interfaces
{
    /// <summary>
    /// Every RoeSceneObject that implements this interface will be culled using its bounding object.
    /// </summary>
    public interface IRoeCullable : IRoeSceneObject
    {
        bool DrawBoundingBox
        {
            get;
            set;
        }

        bool Culled
        {
            get;
            set;
        }

        bool BoundingBoxCreated
        {
            get;
        }

        BoundingBox BoundingBox
        {
            get;
        }

        BoundingBox GetBoundingBoxTransformed();
    }
}

Next, implement DrawCulling in the Node.cs class.

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

Do the same in the SceneObjectNode.cs class, but this time do some work if the sceneobject is cullable.

        public override void DrawCulling(GameTime gameTime)
        {
            if (SceneObject is IRoeCullable)
            {
                ((IRoeCullable)SceneObject).Culled = false;
                if (CameraManager.ActiveCamera.Frustum.Contains(((IRoeCullable)SceneObject).GetBoundingBoxTransformed()) == ContainmentType.Disjoint)
                {
                    ((IRoeCullable)SceneObject).Culled = true;
                }
                else
                {
                    SceneObject.DrawCulling(gameTime);
                }
            }
        }

Implement DrawCulling in the SceneGraphManager.cs as well.  I am also going to implement a static int so we can indicate if anything has been culled.

 public static int culled = 0;
        
 public static void DrawCulling(GameTime gameTime)
        {
            culled = 0;
            occluded = 0;
            _root.DrawCulling(gameTime);
        }

Finally in RoeSceneObject add a virtual so we can override it with cullable objects and draw culling objects.

        public virtual void DrawCulling(GameTime gameTime)
        {

        }

That is pretty much all there is to do for culling.  The real work is done by the SceneObjectNode class.  The line CameraManager.ActiveCamera.Frustum.Contains(((IRoeCullable)SceneObject).GetBoundingBoxTransformed()) == ContainmentType.Disjoint essentially is testing to see if the cullable object is intersecting/within the frustum and simply marking the object as culled if it is not.  Now we can test for culled when we draw.

Add the following code to SceneObjectNode.cs

        public override void Draw(GameTime gameTime)
        {
            if (SceneObject is IRoeCullable && ((IRoeCullable)SceneObject).Culled)
            {
                SceneGraphManager.culled++;
            }         
            else
            {
                SceneObject.Draw(gameTime);
            }
        }

Excellent, that is all there is to do for frustum culling.

Occlusion Culling

Now this is a little bit more complex form of culling, but it is a very important concept to learn for good culling and portal scene management.  Occlusion is what happens when one object passes in front of another object.  In reality the object that was occluded still exists and the same is true in our engine, accept in our engine we no longer have to draw the complex geometry of the occluded object.

This is usually accomplished by drawing less geometrically complex and somewhat larger scale object.  We are obviously drawing more geometry per object, but hopefully less geometry overall by doing this simple test.  In this engine, I will accomplish this task in two “passes”.  First a pass of simple geometry to test for occlusion, and second the final geometry for the scene.

I start by creating a simple interface to identify Occlusion sceneobjects

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

namespace RoeEngine2.Interfaces
{
    /// <summary>
    /// Test if an object is occluded in the scene.
    /// </summary>
    public interface IRoeOcclusion : IRoeCullable, IRoeSceneObject
    {
        string OcclusionModelName
        {
            get;
            set;
        }

        OcclusionQuery Query
        {
            get;
        }

        bool Occluded
        {
            get;
            set;
        }

        void DrawCulling(GameTime gameTime);
    }
}

Notice IRoeOcclusion also implements IRoeCullable, this is very important, you NEVER EVER want to Occlusion cull something that is already Frustum culled.  A simple rule to live by.

Since occlusion culling is a geometry based technique, we also need to implement an object modle for the occluder.  Finally, we want to force the object to implement the DrawCulling override.

IT IS VERY IMPORTANT TO NOTE, when doing occlusion culling your objects must be sorted by their distance to the camera.  You want to draw objects nearer to the camera first, this will reduce overdraw in the scene and make your graphics card very happy.  Also, occlusion culling just doesn’t work if you dont.  So we have to make our Node class an IComparable, I wont explain all of this, if you have questions about what is going on feel free to use MSN.

Node.cs should implement IComparable and add the following code

        int IComparable.CompareTo(object obj)
        {
            SceneObjectNode node1 = (SceneObjectNode)this; 
            SceneObjectNode node2 = (SceneObjectNode)obj;

            if (node1.SceneObject.Distance < node2.SceneObject.Distance)
                return -1;
            else if (node1.SceneObject.Distance > node2.SceneObject.Distance)
                return 1;
            else
                return 0;
        }

        public void Sort()
        {
            _nodes.Sort();
        }

In the SceneGraphManager.DrawCulling method add the following code

        public static void DrawCulling(GameTime gameTime)
        {
            _root.Sort();
            culled = 0;
            occluded = 0;
            _root.DrawCulling(gameTime);
        }

That is bascially all we really have to do to implement Occlusion culling, well all accept the real work.  Now we have to test for occlusion.  The very basic implementation looks something like this.

      OcclusionQuery query = new OcclusionQuery(EngineManager.Device);

      query.Begin();
      // Drawing simple objects, bounding areas, etc.
      query.End();

      // Do additional work here to provide enough time for the GPU to complete query execution.
      
      // Draw additional models
      if (query.IsComplete == true && query.PixelCount > )
      {
      // A non-zero pixel count means some of the low res model is visible
      // so let's draw the real version of it
      }

That isn’t so bad.  so now we can test occluders and simply not draw an object if it is occluded.  We can accomplish that task like this.

        public override void Draw(GameTime gameTime)
        {
            if (SceneObject is IRoeCullable && ((IRoeCullable)SceneObject).Culled)
            {
                SceneGraphManager.culled++;
            }
            else if (SceneObject is IRoeOcclusion && ((IRoeOcclusion)SceneObject).Occluded)
            {
                SceneGraphManager.occluded++;
            }            
            else
            {
                SceneObject.Draw(gameTime);
            }
        }

Please feel free to look at the code linked at the top of this page.  I will no longer add every line of code I write to the blog.

Conclusion

In this article I introduced Frustum and Occlusion Query culling.  These techniques are used to reduce the number of complex geomotry draw calls that we need to make to the graphics card.

I very much enjoy your comments, if you found something here that was helpful or interesting please leave some feedback.  Hope you are enjoying reading these articles as much as I am creating them.

February 18, 2008 Posted by | C#, GameComponent, XBOX360, XNA | 10 Comments

XNA Framework GameEngine Development. (Part 12, Culling and Chase Camera)

Introduction

Welcome to Part 12 of the XNA Framework GameEngine Development series.  This article is a slight departure from my original plan of showing a SkyDome game object.  Rather, I decided to go back and clean up several of the classes I have already created as well as include some culling in the SceneGraph.

part12.jpg

Interfaces

I removed the IRoeDrawable Interface from the project.  I figure all RoeSceneObjects should be drawable.

IRoeSimplePhysics.cs – allows an object to maintain some simple physical updates.

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

namespace RoeEngine2.Interfaces
{
    /// <summary>
    /// Adds simple physics and reset to an RoeSceneObject
    /// </summary>
    public interface IRoeSimplePhysics : IRoeSceneObject
    {
        Vector3 Up
        {
            get;
        }

        Vector3 Right
        {
            get;
        }

        float RotationRate
        {
            get;
            set;
        }

        float Mass
        {
            get;
            set;
        }

        float ThrustForce
        {
            get;
            set;
        }

        float DragFactor
        {
            get;
            set;
        }

        Vector3 Velocity
        {
            get;
            set;
        }

        void Reset();
    }
}

IRoeAcceptInput.cs – each object that inherits this interface will be able to Handle game input.

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

namespace RoeEngine2.Interfaces
{
    /// <summary>
    /// Allows a RoeSceneObject to handle input.
    /// </summary>
    public interface IRoeAcceptInput : IRoeSceneObject
    {
        void HandleInput(GameTime gameTime, Input input);
    }
}

IRoeCullable.cs – allows an object to be culled by the SceneManager.

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

namespace RoeEngine2.Interfaces
{
    public interface IRoeCullable : IRoeSceneObject
    {
        bool BoundingBoxCreated
        {
            get;
        }

        BoundingBox BoundingBox
        {
            get;
        }

        BoundingBox GetBoundingBoxTransformed();
    }
}

IRoeUpdateable.cs – Added GameTime to the update method

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

namespace RoeEngine2.Interfaces
{
    /// <summary>
    /// Allows an RoeSceneObject to be Updated
    /// </summary>
    public interface IRoeUpdateable : IRoeSceneObject
    {
        void Update(GameTime gameTime);
    }
}

Class changesNode.cs – Added HandleInputMethod also clar nodes after they are unloaded

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

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 HandleInput(GameTime gameTime, Input input)
        {
            _nodes.ForEach(
               delegate(Node node)
               {
                   node.HandleInput(gameTime, input);
               });
        }

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

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

        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);
               });
        }
    }
}

SceneObjectNode.cs – Added HandleInput and Culling

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

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 HandleInput(GameTime gameTime, Input input)
        {
            if (SceneObject is IRoeAcceptInput)
                ((IRoeAcceptInput)SceneObject).HandleInput(gameTime, input);
        }

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

        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 is IRoeCullable)
            {
                if (CameraManager.ActiveCamera.Frustum.Contains(((IRoeCullable)SceneObject).GetBoundingBoxTransformed()) == ContainmentType.Disjoint)
                {
                    SceneGraphManager.culled++;
                }
                else
                {
                    SceneObject.Draw(gameTime);
                    SceneObject.DrawNodesBoundingBox(gameTime);
                }
            }
            else
                SceneObject.Draw(gameTime);
        }
    }
}

RoeSceneObject.cs – added bounding box drawing and World matrix

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
    {
        public VertexPositionColor[] points = new VertexPositionColor[8];
        public int[] index = new int[24];

        private bool _readyToRender = false;
        /// <summary>
        /// Is this object ready to render?
        /// </summary>
        public bool ReadyToRender
        {
            get { return _readyToRender; }
            set { _readyToRender = 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 Matrix World
        {
            get
            {
                return Matrix.CreateScale(this.Scale) *
                       Matrix.CreateFromQuaternion(this.Rotation) *
                       Matrix.CreateTranslation(this.Position);
            }
        } 

        public virtual void Draw(GameTime gameTime)
        {
            if (ReadyToRender)
            {
                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 = World;
                            effect.View = CameraManager.ActiveCamera.View;
                            effect.Projection = CameraManager.ActiveCamera.Projection;
                        }
                        mesh.Draw();
                    }
                }
            }
        }

        public void DrawNodesBoundingBox(GameTime gameTime)
        {
            if(ReadyToRender)
            {
                using (VertexDeclaration declaration = new VertexDeclaration(EngineManager.Device, VertexPositionColor.VertexElements))
                {
                    EngineManager.Device.RenderState.PointSize = 1.0f;
                    EngineManager.Device.VertexDeclaration = declaration;

                    IRoeShader shader = ShaderManager.GetShader("basic");

                    if (shader.ReadyToRender)
                    {
                        BasicEffect effect = shader.BaseEffect as BasicEffect;
                        effect.DiffuseColor = Color.Red.ToVector3();
                        effect.View = CameraManager.ActiveCamera.View;
                        effect.Projection = CameraManager.ActiveCamera.Projection;
                        effect.World = World;

                        effect.Begin();

                        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                        {
                            pass.Begin();
                            EngineManager.Device.DrawUserIndexedPrimitives<VertexPositionColor>(
                                                PrimitiveType.LineList, points,
                                                0,
                                                8,
                                                index,
                                                0,
                                                12);
                            pass.End();
                        }
                        effect.End();
                    }
                }
            }
        }
    }  
}

CameraManager.cs – Now supports FirstPersonCamera specifically.

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 FirstPersonCamera(), CameraNumber._default);
            SetActiveCamera(CameraNumber._default);
            AddCamera(new FirstPersonCamera(), 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(gameTime);
        }

        /// <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;
            }
        }    
    }
}

SceneGraphManager.cs – Added support for HandleInput

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

namespace RoeEngine2.Managers
{
    public class SceneGraphManager : GameComponent
    {
        public static int culled = 0;

        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)
        {
            Enabled = true;
            _root = new Node();
        }

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

        public static void HandleInput(GameTime gameTime, Input input)
        {
            _root.HandleInput(gameTime, input);
        }

        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
            _root.Update(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);
        }
    }
}

New ClassesShip.cs – This is just for the test case in the example

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

namespace Kynskavion.GameObjects.Ship
{
    public class Ship : RoeSceneObject, IRoeLoadable, IRoeUpdateable, IRoeSimplePhysics, IRoeCullable
    {
        public Vector3 Up
        {
            get { return Vector3.Up; }
        }

        public Vector3 Right
        {
            get { return Vector3.Right; }
        }

        private float _rotationRate = 1.0f;
        public float RotationRate
        {
            get { return _rotationRate; }
            set { _rotationRate = value; }
        }

        public float Mass
        {
            get
            {
                throw new Exception("The method or operation is not implemented.");
            }
            set
            {
                throw new Exception("The method or operation is not implemented.");
            }
        }

        public float ThrustForce
        {
            get
            {
                throw new Exception("The method or operation is not implemented.");
            }
            set
            {
                throw new Exception("The method or operation is not implemented.");
            }
        }

        public float DragFactor
        {
            get
            {
                throw new Exception("The method or operation is not implemented.");
            }
            set
            {
                throw new Exception("The method or operation is not implemented.");
            }
        }

        public Vector3 Velocity
        {
            get
            {
                throw new Exception("The method or operation is not implemented.");
            }
            set
            {
                throw new Exception("The method or operation is not implemented.");
            }
        }

        private bool _boundingBoxCreated;
        public bool BoundingBoxCreated
        {
            get { return _boundingBoxCreated; }
        }

        public BoundingBox _boundingBox;
        public BoundingBox BoundingBox
        {
            get { return _boundingBox; }
        }

        public Ship()
        {

        }

        public Ship(Vector3 newPosition)
        {
            Position = newPosition;
        }

        public BoundingBox GetBoundingBoxTransformed()
        {
            Vector3 min, max;
            min = _boundingBox.Min;
            max = _boundingBox.Max;

            min = Vector3.Transform(_boundingBox.Min, Matrix.CreateTranslation(Position));
            max = Vector3.Transform(_boundingBox.Max, Matrix.CreateTranslation(Position));

            return new BoundingBox(min, max);
        }

        public void Update(GameTime gameTime)
        {
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
            Rotation = Quaternion.Multiply(Rotation, Quaternion.CreateFromAxisAngle(Vector3.UnitZ, _rotationRate * elapsed));

            IRoeModel model = ModelManager.GetModel(ModelName);
            if (model != null && model.ReadyToRender && !ReadyToRender)
            {
                Matrix[] transforms = new Matrix[model.BaseModel.Bones.Count];

                _boundingBox = new BoundingBox();

                foreach (ModelMesh mesh in model.BaseModel.Meshes)
                {
                    if (!BoundingBoxCreated)
                    {
                        _boundingBox = BoundingBox.CreateMerged(_boundingBox, BoundingBox.CreateFromSphere(mesh.BoundingSphere));
                    }
                }
                _boundingBoxCreated = true;

                Vector3 min, max;
                min = BoundingBox.Min;
                max = BoundingBox.Max;

                _boundingBox = new BoundingBox(min, max);

                points[0].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[1].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[2].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Max.Z);
                points[3].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Max.Z);

                points[4].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[5].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[6].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Max.Z);
                points[7].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Max.Z);

                ReadyToRender = true;
            }
        }

        public void LoadContent()
        {
            RoeModel model = new RoeModel("Content/Models/Ship");

            ModelManager.AddModel(model, "ship");
            this.ModelName = "ship";

            index[0] = 0;
            index[1] = 1;
            index[2] = 1;
            index[3] = 2;
            index[4] = 2;
            index[5] = 3;
            index[6] = 3;
            index[7] = 0;

            index[8] = 4;
            index[9] = 5;
            index[10] = 5;
            index[11] = 6;
            index[12] = 6;
            index[13] = 7;
            index[14] = 7;
            index[15] = 4;

            index[16] = 0;
            index[17] = 4;
            index[18] = 1;
            index[19] = 5;
            index[20] = 2;
            index[21] = 6;
            index[22] = 3;
            index[23] = 7;
        }

        public void UnloadContent()
        {
           
        }

        public void Reset()
        {
            RotationRate = 0.0f;
        }
    }
}

ShipMain.cs – Same Thing as Ship, but this also accepts input

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

namespace Kynskavion.GameObjects.ShipMain
{
    public class ShipMain : RoeSceneObject, IRoeLoadable, IRoeUpdateable, IRoeSimplePhysics, IRoeAcceptInput, IRoeCullable
    {
        public Vector3 Direction;

        private Vector3 _up = Vector3.Up;
        public Vector3 Up
        {
            get { return _up; }
        }

        private Vector3 _right;
        public Vector3 Right
        {
            get { return _right; }
        }

        private float _rotationRate = 1.0f;
        public float RotationRate
        {
            get { return _rotationRate; }
            set { _rotationRate = value; }
        }

        private float _mass = 1.0f;
        public float Mass
        {
            get { return _mass; }
            set { _mass = value; }
        }

        private float _thrustForce = 24000.0f;
        public float ThrustForce
        {
            get { return _thrustForce; }
            set { _thrustForce = value; }
        }

        private float _dragFactor = 0.97f;
        public float DragFactor
        {
            get { return _dragFactor; }
            set { _dragFactor = value; }
        }

        private Vector3 _velocity;
        public Vector3 Velocity
        {
            get { return _velocity; }
            set { _velocity = value; }
        }

        private Matrix _world;
        public override Matrix World
        {
            get { return _world; }
        }

        private bool _boundingBoxCreated;
        public bool BoundingBoxCreated
        {
            get { return _boundingBoxCreated; }
        }

        public BoundingBox _boundingBox;
        public BoundingBox BoundingBox
        {
            get { return _boundingBox; }
        }

        public ShipMain()
        {
            Reset();
        }

        public ShipMain(Vector3 newPosition)
        {
            Position = newPosition;
            Reset();
        }

        public BoundingBox GetBoundingBoxTransformed()
        {
            Vector3 min, max;
            min = _boundingBox.Min;
            max = _boundingBox.Max;

            min = Vector3.Transform(_boundingBox.Min, Matrix.CreateTranslation(Position));
            max = Vector3.Transform(_boundingBox.Max, Matrix.CreateTranslation(Position));

            return new BoundingBox(min, max);
        }

        public void Update(GameTime gameTime)
        {
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            IRoeModel model = ModelManager.GetModel(ModelName);
            if (model != null && model.ReadyToRender && !ReadyToRender)
            {
                Matrix[] transforms = new Matrix[model.BaseModel.Bones.Count];
                model.BaseModel.CopyAbsoluteBoneTransformsTo(transforms);

                _boundingBox = new BoundingBox();

                foreach (ModelMesh mesh in model.BaseModel.Meshes)
                {
                    if (!BoundingBoxCreated)
                    {
                        _boundingBox = BoundingBox.CreateMerged(_boundingBox, BoundingBox.CreateFromSphere(mesh.BoundingSphere));
                    }
                }
                _boundingBoxCreated = true;

                Vector3 min, max;
                min = Vector3.Transform(BoundingBox.Min, Matrix.CreateTranslation(Position));
                max = Vector3.Transform(BoundingBox.Max, Matrix.CreateTranslation(Position));

                _boundingBox = new BoundingBox(min, max);

                points[0].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[1].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Min.Z);
                points[2].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Min.Y, _boundingBox.Max.Z);
                points[3].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Min.Y, _boundingBox.Max.Z);

                points[4].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[5].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Min.Z);
                points[6].Position = new Vector3(_boundingBox.Max.X, _boundingBox.Max.Y, _boundingBox.Max.Z);
                points[7].Position = new Vector3(_boundingBox.Min.X, _boundingBox.Max.Y, _boundingBox.Max.Z);

                ReadyToRender = true;
            }
        }

        public void LoadContent()
        {
            RoeModel model = new RoeModel("Content/Models/Ship");

            ModelManager.AddModel(model, "ship");
            this.ModelName = "ship";

            index[0] = 0;
            index[1] = 1;
            index[2] = 1;
            index[3] = 2;
            index[4] = 2;
            index[5] = 3;
            index[6] = 3;
            index[7] = 0;

            index[8] = 4;
            index[9] = 5;
            index[10] = 5;
            index[11] = 6;
            index[12] = 6;
            index[13] = 7;
            index[14] = 7;
            index[15] = 4;

            index[16] = 0;
            index[17] = 4;
            index[18] = 1;
            index[19] = 5;
            index[20] = 2;
            index[21] = 6;
            index[22] = 3;
            index[23] = 7;
        }

        public void UnloadContent()
        {
           
        }

        public void Reset()
        {
            Position = new Vector3(0, 0, 0);
            Direction = Vector3.Forward;
            _up = Vector3.Up;
            _right = Vector3.Right;
            Velocity = Vector3.Zero;
        }

        public void HandleInput(GameTime gameTime, Input input)
        {
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            if (ReadyToRender)
            {
                Vector2 rotationAmount = -input.CurrentGamePadState.ThumbSticks.Left;
                if (input.CurrentKeyboardState.IsKeyDown(Keys.W))
                    rotationAmount.Y = -1.0f;
                if (input.CurrentKeyboardState.IsKeyDown(Keys.S))
                    rotationAmount.Y = 1.0f;
                if (input.CurrentKeyboardState.IsKeyDown(Keys.D))
                    rotationAmount.X = -1.0f;
                if (input.CurrentKeyboardState.IsKeyDown(Keys.A))
                    rotationAmount.X = 1.0f;
                // Determine thrust amount from input
                float thrustAmount = input.CurrentGamePadState.Triggers.Right;
                if (input.CurrentKeyboardState.IsKeyDown(Keys.Space))
                    thrustAmount = 1.0f;

                // Scale rotation amount to radians per second
                rotationAmount = rotationAmount * RotationRate * elapsed;

                // Correct the X axis steering when the ship is upside down
                if (Up.Y < 0)
                    rotationAmount.X = -rotationAmount.X;

                // Create rotation matrix from rotation amount
                Matrix rotationMatrix =
                    Matrix.CreateFromAxisAngle(Right, rotationAmount.Y) *
                    Matrix.CreateRotationY(rotationAmount.X);

                // Rotate orientation vectors
                Direction = Vector3.TransformNormal(Direction, rotationMatrix);
                _up = Vector3.TransformNormal(Up, rotationMatrix);

                // Re-normalize orientation vectors
                // Without this, the matrix transformations may introduce small rounding
                // errors which add up over time and could destabilize the ship.
                Direction.Normalize();
                Up.Normalize();

                // Re-calculate Right
                _right = Vector3.Cross(Direction, Up);

                // The same instability may cause the 3 orientation vectors may
                // also diverge. Either the Up or Direction vector needs to be
                // re-computed with a cross product to ensure orthagonality
                _up = Vector3.Cross(Right, Direction);

                // Calculate force from thrust amount
                Vector3 force = Direction * thrustAmount * ThrustForce;

                // Apply acceleration
                Vector3 acceleration = force / Mass;
                Velocity += acceleration * elapsed;

                // Apply psuedo drag
                Velocity *= DragFactor;

                // Apply velocity
                Position += Velocity * elapsed;

                // Reconstruct the ship's world matrix
                _world = Matrix.Identity;
                _world.Forward = Direction;
                _world.Up = Up;
                _world.Right = Right;
                _world.Translation = Position;
            }
        }
    }
}

FirstPersonCamera.cs – The original camera, just now encapsulated in its own class

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

namespace RoeEngine2.GameComponents
{
    public class FirstPersonCamera : Camera
    {
        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;      

        private float _pitch = 0.0f;       

        /// <summary>
        /// Set the position in 3d space.
        /// </summary>
        /// <param name="newPosition"></param>
        public override 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 override 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 override 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 override 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);
        }

        public override void Update(GameTime gameTime)
        {
            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));
        }
    }
}

ChaseCamera.cs – A camera that chases an object in an over the should view.

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

namespace RoeEngine2.GameComponents
{
    public class ChaseCamera : Camera
    {
        private bool _springEnabled = true;
        public bool SpringEnabled
        {
            get { return _springEnabled; }
            set { _springEnabled = value; }
        }
    
        private Vector3 _position;
        public Vector3 Position
        {
            get { return _position; }
        }

        private Vector3 _velocity;
        public Vector3 Velocity
        {
            get { return _velocity; }
        }

        private float _stiffness = 1800.0f;
        public float Stiffness
        {
            get { return _stiffness; }
            set { _stiffness = value; }
        }

        private float _damping = 600.0f;
        public float Damping
        {
            get { return _damping; }
            set { _damping = value; }
        }

        private float _mass = 50.0f;
        public float Mass
        {
            get { return _mass; }
            set { _mass = value; }
        }

        private Vector3 _chasePosition;
        public Vector3 ChasePosition
        {
            get { return _chasePosition; }
            set { _chasePosition = value; }
        }

        private Vector3 _chaseDirection;
        public Vector3 ChaseDirection
        {
            get { return _chaseDirection; }
            set { _chaseDirection = value; }
        }

        private Vector3 _up = Vector3.Up;
        public Vector3 Up
        {
            get { return _up; }
            set { _up = value; }
        }    

        private Vector3 _desiredPositionOffset = new Vector3(0, 2.0f, 2.0f);
        public Vector3 DesiredPositionOffset
        {
            get { return _desiredPositionOffset; }
            set { _desiredPositionOffset = value; }
        }

        private Vector3 _desiredPosition;
        public Vector3 DesiredPosition
        {
            get { return _desiredPosition; }
            set { _desiredPosition = value; }
        }

        private Vector3 _lookAtOffset = new Vector3(0, 2.8f, 0);
        public Vector3 LookAtOffset
        {
            get { return _lookAtOffset; }
            set { _lookAtOffset = value; }
        }

        private Vector3 _lookAt;
        public Vector3 LookAt
        {
            get { return _lookAt; }
        }

        public override void Update(GameTime gameTime)
        {
            UpdatePosition();

            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

            Vector3 stretch = _position - _desiredPosition;
            Vector3 force = -_stiffness * stretch - _damping * _velocity;

            Vector3 acceleration = force / _mass;
            _velocity += acceleration * elapsed;

            _position += _velocity * elapsed;

            UpdateMatrices();
        }

        private void UpdatePosition()
        {
            // Construct a matrix to transform from object space to worldspace
            Matrix transform = Matrix.Identity;
            transform.Forward = ChaseDirection;
            transform.Up = Up;
            transform.Right = Vector3.Cross(Up, ChaseDirection);

            // Calculate desired camera properties in world space
            _desiredPosition = ChasePosition +
                Vector3.TransformNormal(DesiredPositionOffset, transform);
            _lookAt = ChasePosition +
                Vector3.TransformNormal(LookAtOffset, transform);
        }

        private void UpdateMatrices()
        {
            View = Matrix.CreateLookAt(Position, LookAt, Vector3.Up);

            Frustum = new BoundingFrustum(Matrix.Multiply(View, Projection));
            ReflectedFrustum = new BoundingFrustum(Matrix.Multiply(ReflectedView, Projection));
        }

        public override void Reset()
        {
            UpdatePosition();

            _velocity = Vector3.Zero;

            _position = _desiredPosition;

            UpdateMatrices();
        }
    }
}

Updates to GameScreensGameplayScreen.cs

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;
using RoeEngine2.Shaders;
using RoeEngine2.Texures;
using Kynskavion.GameObjects.Ship;
using Microsoft.Xna.Framework.Graphics;
using Kynskavion.GameObjects.ShipMain;

namespace Kynskavion.GameScreens
{
    class GameplayScreen : GameScreen
    {
        private static double delta = 0.0;
        private static ShipMain shipMain = new ShipMain();

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

            ChaseCamera camera = new ChaseCamera();
            camera.DesiredPositionOffset = new Vector3(0.0f, 2000.0f, 3500.0f);
            camera.LookAtOffset = new Vector3(0.0f, 150.0f, 0.0f);

            camera.NearPlane = 10.0f;
            camera.FarPlane = 1000000.0f;

            CameraManager.AddCamera(camera, CameraManager.CameraNumber._3);
            CameraManager.SetActiveCamera(CameraManager.CameraNumber._3);
            CameraManager.SetAllCamerasProjectionMatrix((float)EngineManager.Device.Viewport.Width / EngineManager.Device.Viewport.Height);
        }

        /// <summary>
        /// Load graphics content for the game.
        /// </summary>
        public override void LoadContent()
        {
            Random rnd = new Random();
            for (int i = -10000; i <= 10000; i += 5000)
            {
                for (int j = -10000; j <= 10000; j += 5000)
                {
                    for (int k = -10000; k <= 10000; k += 5000)
                    {
                        Ship ship = new Ship(new Vector3(i, j, k));

                        ship.RotationRate = rnd.Next(-10, 10);
                        SceneGraphManager.AddObject(ship);
                    }
                }
            }

            SceneGraphManager.AddObject(shipMain);

            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;

            ChaseCamera camera = (ChaseCamera)CameraManager.ActiveCamera;

            camera.ChasePosition = shipMain.Position;
            camera.ChaseDirection = shipMain.Direction;
            camera.Up = shipMain.Up;
        }

        public override void HandleInput(GameTime gameTime, Input input)
        {
           
            if (input.PauseGame)
            {
                ScreenManager.AddScreen(new PauseMenuScreen());
            }
            else
            {
                SceneGraphManager.HandleInput(gameTime, input);
                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);

            string message = "FPS:  " + EngineManager.FpsCounter.FPS.ToString() + "  Culled:  " + SceneGraphManager.culled.ToString();

            // Center the text in the viewport.
            Vector2 textPosition = new Vector2(10, 20);

            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();
        }
    }
}

ConclusionIn this article I introduced many new concepts.  First, I introduced the chase camera.  This camera option is an over the shoulder view camera, it will follow an object around at a nice viewing distance. Next, I demonstrated culling in the SceneGraphManager.  Culling is accomplished view bounds checking with the camera frustum and the bounding box of an object.  Finally, I demonstrated all of these concepts in a simple fly through game.

Looking forward to your comments and suggestions.

February 15, 2008 Posted by | C#, GameComponent, XBOX360, XNA | 7 Comments

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 8, Multi-Threading GameComponents)

Introduction

Welcome to Part 8 of the XNA Framework GameEngine Development series.  In this article, I will be revisiting the TextureManager and the ShaderManager GameComponents to add in Multi-Threading.  Since we want to be able to load our game as quickly as possible it makes sense to use all the CPUs available to us.  For now I will focus on Background Loading, a future article will discuss Locking and Thread-Safety.

Background Loading template, just a sample

ThreadStart threadStarter = delegate
{
    // Do Work Here
};
Thread loadingThread = new Thread(threadStarter);
loadingThread.Start();

Yup, it is just that easy.

ShaderManager.cs – Multi-Threaded Loading

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;
using System.Threading;

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

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

        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);

                if (_initialized)
                {
                    ThreadStart threadStarter = delegate
                    {
                        newShader.Initialize(EngineManager.Device);
                        _shadersLoaded++;
                    };
                    Thread loadingThread = new Thread(threadStarter);
                    loadingThread.Start();
                }
            }
        }

        /// <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();

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

            ThreadStart threadStarter = delegate
            {
                foreach (IRoeShader shader in _shaders.Values)
                {
                    if (!shader.ReadyToRender)
                    {
                        shader.Initialize(EngineManager.Device);
                        _shadersLoaded++;
                    }
                }
            };
            Thread loadingThread = new Thread(threadStarter);
            loadingThread.Start();

            _initialized = true;
        }
    }
}

TextureManager.cs – Multi-Threaded Loading

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;
using System.Threading;

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)
                {
                    ThreadStart threadStarter = delegate
                    {
                        newTexture.LoadContent();
                        _texturesLoaded++;
                    };
                    Thread loadingThread = new Thread(threadStarter);
                    loadingThread.Start();
                }
            }
        }

        /// <summary>
        /// Remove a texture from memory
        /// </summary>
        /// <param name="textureName"></param>
        public static void RemoveTexture(string textureName)
        {
            if (textureName != null && _textures.ContainsKey(textureName))
            {                
                if (_initialized)
                {
                    ThreadStart threadStarter = delegate
                    {
                        _textures[textureName].UnloadContent();
                        _textures.Remove(textureName);
                        _texturesLoaded--;
                    };
                    Thread loadingThread = new Thread(threadStarter);
                    loadingThread.Start();
                }                
            }
        }

        /// <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();

            ThreadStart threadStarter = delegate
            {
                foreach (IRoeTexture texture in _textures.Values)
                {
                    if (!texture.ReadyToRender)
                    {
                        texture.LoadContent();
                        _texturesLoaded++;
                    }
                }
            };
            Thread loadingThread = new Thread(threadStarter);
            loadingThread.Start();

            _initialized = true;
        }
    }
}

Conclusion

In this article, I introduced Multi-threading concepts into the engine.  Background Loading is the easiest threading template that the engine has to offer.  Granted, we have not put enough content into the game yet to really tell a difference between single and multithreading options.  If you use your system watch tool and have a multi core or processor system, you will see all CPUs sharing the “load” of loading your game.

I look forward to discussing your thoughts and requests for future articles.

February 1, 2008 Posted by | C#, GameComponent, XBOX360, XNA | 12 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[i].Update(this, isSelected, gameTime);
            }
        }

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

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

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

            ScreenManager.SpriteBatch.Begin();

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

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

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

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

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

            titlePosition.Y -= transitionOffset * 100;

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

            ScreenManager.SpriteBatch.End();
        }
    }
}

MessageBoxScreen.cs

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

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

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

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

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

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

            IsPopup = true;

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

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

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

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

                ExitScreen();
            }
        }

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

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

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

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

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

            ScreenManager.SpriteBatch.Begin();

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

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

            ScreenManager.SpriteBatch.End();
        }
    }  
}

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

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

Conclusion

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

I look forward to your comments and suggestions.

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

Follow

Get every new post delivered to your Inbox.