Wednesday, November 16, 2011

Let's Make a Game Engine for XNA 4.0r, Part 1, BaseManager and RenderManager

Getting even a simple game engine running is no small task, but once you've created one you'll start to have a grasp over all of the different techniques and systems that come together to make one. Our first milestone (version 0.1) will be to create a couple of simple hierarchies of Main Game Interface -> System Managers, and Entity -> Components, create methods of easily communicating between all systems, and to create a simple primitive shape and render it on the screen.

There's two ways that I recommend that you continue:
1.) Download version 0.1, and have it open side by side with your own project, and write it yourself as you follow along on this blog.
2.) Download version 0.1, and have it open and use it as a reference as you follow along on this blog.

Method #1 is probably more thorough, and as you type through everything you're probably more likely to grasp the engine architecture and be familiar with it.
Method #2 will save you a lot of time but you may have to glance through things a few times before you're comfortable with it.

Whichever way you choose I recommend downloading the project, because I won't be explaining every single line of code on the blog, only the parts worth mentioning.

Alright, lets begin, at the beginning, of course.

Create a Windows XNA project (this engine may or may not work for the Xbox 360 as well, I do not currently have a subscription to test it). I named mine 'SimpleGameEngine'. I then changed the name of the main project to 'GameEngine', and the name of the content project 'EngineContent'. Name yours whatever you would like, but I will refer to them from here on by these names. Additionally I named my Xna.Game class MainGame and changed the name of the file to Main.cs.

The first thing we're going to focus on is the SceneManager and RenderManager classes, and the class that all Managers derive from, BaseManager. Create a class named 'BaseManager.cs' and place it directly within the GameEngine project. Create 'SceneManager' and 'RenderManager' folders. After making those folders, within the 'SceneManager' folder add a new class called 'SceneManager.cs', and within the 'RenderManager' folder add a new class called 'RenderManager.c's. After doing so my folder structure looked something like this:


BaseManager
Ok let's start with the BaseManager class. This is a simple abstract class that contains a couple things that all Manager classes must have, like a name and reference to the MainGame, and some common virtual functions.

Here's the variable and accessors for BaseManager.
private MainGame game;
public MainGame Game
{
    get { return this.game; }
}
        
public String Name
{
    get { return this.GetName(); }
}
Notice the accessor for 'name' calls a virtual function 'GetName()'. This is because the name of the manager is determined by the derived class, not this base class.

Here's the definition for the 'GetName' function:
// Name of the manager
protected abstract String GetName();
This is an abstract function so all derived classes must handle it.

Alright let's get the constructor made. At the very least we'll need the reference to MainGame given to use when the Manager is created, so that's the only variable needed for this base class:
public BaseManager(MainGame game)
{
     this.game = game;
}

All derived Manager classes will call Initialize upon being constructed, this give the base class a change to register this Manager class with the MainGame.
protected virtual void Initialize()
{
    this.game.AddManager(this);
} 
We make this protected so it cannot be called outside of a derived Manager class, and we make it virtual so that when it's called by the derived class it gives that class a chance to handle Initialization before calling the BaseManager.

And the rest of the class is just virtual methods that are defined to be passed to derived classes:

public virtual void LoadContent() {}
public virtual void UnloadContent() {}
public virtual void Update(GameTime gameTime) {}
public virtual void Draw(GameTime gameTime) {}

Here's the code for the entire BaseManager class:
using System;
using System.Collections.Generic;
using System.Text;

using Microsoft.Xna.Framework;

namespace SimpleGameEngine
{
    abstract public class BaseManager
    {
        private MainGame game;
        public MainGame Game
        {
            get { return this.game; }
        }
        
        public String Name
        {
            get { return this.GetName(); }
        }

        public BaseManager(MainGame game)
        {
            this.game = game;
        }

        public virtual void LoadContent() {}
        public virtual void UnloadContent() {}
        public virtual void Update(GameTime gameTime) {}
        public virtual void Draw(GameTime gameTime) {}

        protected virtual void Initialize()
        {
            this.game.AddManager(this);
        }        

        // Name of the manager
        protected abstract String GetName();
    }
}

RenderManager
The first thing we'll do is make RenderManager derive from BaseManager:
public class RenderManager : BaseManager

Now, we need to handle the abstract method 'GetName' from the base class. If we ever want to look for the RenderManager in our engine we'll be able to do it by name, that is what this function with help with. We want the user to be able to look for it by the name "Render". So our function is this:
protected override String GetName()
{
    return "Render";
}

Ok we still need to create a constructor for this class. Because the BaseManager requires a MainGame, we need it as well so we can pass it through. And in the case of this class it's all we'll need:
public RenderManager(MainGame game)
    : base(game)
{            
    Initialize();
}
As I said in 'BaseManager', all Managers call Initialize() during construction. If you don't handle anything within this derived class during initialization then you don't technically need to create a function handler for it, but there's a good chance we'll need it eventually so let's just add it:
protected override void Initialize()
{
    base.Initialize();
}

Because the RenderManager is manager of all things rendering I put an accessor here to the Xna.Game class' GraphicsDeviceManager and Graphics Device:
public GraphicsDeviceManager Graphics
{
    get { return this.Game.Graphics; }
    set { this.Game.Graphics = value; }
}

public GraphicsDevice GraphicsDevice
{
    get { return this.Game.GraphicsDevice; }
}

I also moved the SpriteBatch instance out of MainGame
private SpriteBatch spriteBatch;
public SpriteBatch SpriteBatch
{
    get { return spriteBatch; }
}

The render manager has a single BasicEffect that is used for rendering (at least at this point in time). So we'll create the variable for it, and a function to load the effect:
private BasicEffect effect;

private void LoadBasicEffect()
{
    effect = new BasicEffect(this.GraphicsDevice);
    effect.EnableDefaultLighting();
}

Try and keep your variables at the top/bottom of your code, and your functions on the other side. Don't mix them. The samples I'm posting just show the relationship between them, it doesn't imply they'll go next to each other in the final product.

We'll also want a String variable to hold the name of the currently rendering camera.
private String currentCameraEntityName = EngineCommon.RootEntityName;

'EngineCommon' and 'RootEntityName' are defined in another file we'll get to soon. Don't expect your code to compile until we reach the first milestone.

LoadContent is called on all Managers when the MainGame begins its LoadContent phase. We'll want to handle that function to load up our BasicEffect and SpriteBatch:
public override void LoadContent()
{
    LoadBasicEffect();

    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(this.GraphicsDevice);
}

And finally we're left with the Draw function. Pay close attention to this part because it involves some engine architecture discussion.



  • The MainGame class (which we haven't filled out yet) holds a list of all managers, of which there are currently just SceneManager and RenderManager.
  • SceneManager holds all of the entities loaded into the scene.
  • Each entity can hold multiple components, but only one per type.
  • CameraComponent is just one of any number of components we may eventually have, it is an example of a component that an entity can have.

So back to the Draw() function. What we want to do during the draw phase is get all entities in the scene, grab their render descriptions, and grab the camera, and use the camera's properties to render with. So how do we get all the information from within the RenderManager? The arrows in the above image represent the areas we need to go. As we just said above, the SceneManager holds all entities, so we need to get the entities from there, and the CameraComponent on the current rendering camera will have camera-specific information we need.

As we'll see later, the MainGame class lets us access any Manager class by its name, which in the case of the SceneManager is "scene". Once we have access to the SceneManager it in turn has a method to let us access all an entity by name. We use the camera's name (which we'll learn about later) to find the entity that is currently serving as the camera. SceneManager also has the functionality to let us grab all entities in the scene, so we grab that list as well.

And now based on all of that, we can render the scene:
public override void Draw(GameTime gameTime)
{
    SceneManager sceneMgr = this.Game.GetManager("Scene") as SceneManager;
    if (null == sceneMgr)
    {
        throw new Exception("Scene manager not registered properly to the game engine");
    }

    BaseEntity cameraEntity = sceneMgr.GetEntity(currentCameraEntityName);
    if (null == cameraEntity)
    {
         throw new Exception("A camera entity must always exist if we are trying to render a scene");
    }

    CameraComponent camComp = cameraEntity.GetComponent("Camera") as CameraComponent;
    if (null == camComp)
    {
        throw new Exception("An entity was designated as a camera but never given a camera component");
    }

    this.GraphicsDevice.Clear(Color.CornflowerBlue);

    List<renderdescription> renderDescriptions = new List<renderdescription>();

    // Get all entities in the scene
    Dictionary<string, baseentity> entities = sceneMgr.Entities;

    foreach (KeyValuePair<string, baseentity> pair in entities)
    {
        (pair.Value).Draw(gameTime, renderDescriptions);                
    }

    foreach (RenderDescription desc in renderDescriptions)
    {
        if (null != desc.model)
        {
            // Copy any parent transforms.
            Matrix[] transforms = new Matrix[desc.model.Bones.Count];
            desc.model.CopyAbsoluteBoneTransformsTo(transforms);

            // Draw the model. A model can have multiple meshes, so loop.
            foreach (ModelMesh mesh in desc.model.Meshes)
            {
                // This is where the mesh orientation is set, as well 
                // as our camera and projection.
                foreach (BasicEffect effect in mesh.Effects)
                {                            
                    effect.World = desc.worldTransform;
                    effect.View = Matrix.CreateLookAt(cameraEntity.position,
                        Vector3.Zero, Vector3.Up);
                    effect.Projection = Matrix.CreatePerspectiveFieldOfView(
                        MathHelper.ToRadians(45.0f), camComp.AspectRatio, 1.0f, 5000.0f);
                }
                // Draw the mesh, using the effects set above.
                mesh.Draw();
            }
        }
        else if (null != desc.geoPrim)
        {
            // Set our vertex declaration, vertex buffer, and index buffer.
            this.Game.GraphicsDevice.SetVertexBuffer(desc.geoPrim.VertexBuffer);
            this.Game.GraphicsDevice.Indices = desc.geoPrim.IndexBuffer;
                    
            foreach (EffectPass effectPass in effect.CurrentTechnique.Passes)
            {
                effect.World = desc.worldTransform;
                effect.View = Matrix.CreateLookAt(cameraEntity.position,
                          (cameraEntity.position + cameraEntity.rotation.Forward), Vector3.Up);
                effect.Projection = Matrix.CreatePerspectiveFieldOfView(
                          MathHelper.ToRadians(45.0f), camComp.AspectRatio, 1.0f, 5000.0f);
            
                effectPass.Apply();

                int primitiveCount = (desc.geoPrim.Indices.Count / 3);

                this.Game.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0,
                                             desc.geoPrim.Vertices.Count, 0, primitiveCount);

            }
        }
    }
}

You can see here we get the scene manager and store it (and make sure it's valid). We grab the camera entity as well, and then specifically grab its camera component because that has the aspect ratio information we'll need.

We then create a list of render descriptions and then call Draw() on all entities, passing the list to each entity. As we'll see later entities that have render components simply add to that list their own model or primitive.

One we have the list of render descriptions we simply go through each one in the list and render the model or the primitive.

Here's the code for the entire RenderManager:
using System;
using System.Collections.Generic;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

using SimpleGameEngine.Scene;
using SimpleGameEngine.Entity;
using SimpleGameEngine.Components;

namespace SimpleGameEngine.Render
{
    public class RenderManager : BaseManager
    {
        public GraphicsDeviceManager Graphics
        {
            get { return this.Game.Graphics; }
            set { this.Game.Graphics = value; }
        }        

        public GraphicsDevice GraphicsDevice
        {
            get { return this.Game.GraphicsDevice; }
        }

        private SpriteBatch spriteBatch;
        public SpriteBatch SpriteBatch
        {
            get { return spriteBatch; }
        }

        private BasicEffect effect;

        private String currentCameraEntityName = EngineCommon.RootEntityName;

        protected override String GetName()
        {
            return "Render";
        }

        public RenderManager(MainGame game)
            : base(game)
        {            
            Initialize();
        }

        protected override void Initialize()
        {
            base.Initialize();
        }

        public override void LoadContent()
        {
            LoadBasicEffect();

            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(this.GraphicsDevice);
        }

        private void LoadBasicEffect()
        {
            effect = new BasicEffect(this.GraphicsDevice);
            effect.EnableDefaultLighting();
        }

        public override void Draw(GameTime gameTime)
        {
            SceneManager sceneMgr = this.Game.GetManager("Scene") as SceneManager;
            if (null == sceneMgr)
            {
                throw new Exception("Scene manager not registered properly to the game engine");
            }

            BaseEntity cameraEntity = sceneMgr.GetEntity(currentCameraEntityName);
            if (null == cameraEntity)
            {
                throw new Exception("A camera entity must always exist if we are trying to render a scene");
            }

            CameraComponent camComp = cameraEntity.GetComponent("Camera") as CameraComponent;
            if (null == camComp)
            {
                throw new Exception("An entity was designated as a camera but never given a camera component");
            }

            this.GraphicsDevice.Clear(Color.CornflowerBlue);

            List<renderdescription> renderDescriptions = new List<renderdescription>();

            // Get all entities in the scene
            Dictionary<string, baseentity> entities = sceneMgr.Entities;

            foreach (KeyValuePair<string, baseentity> pair in entities)
            {
                (pair.Value).Draw(gameTime, renderDescriptions);                
            }

            foreach (RenderDescription desc in renderDescriptions)
            {
                if (null != desc.model)
                {
                    // Copy any parent transforms.
                    Matrix[] transforms = new Matrix[desc.model.Bones.Count];
                    desc.model.CopyAbsoluteBoneTransformsTo(transforms);

                    // Draw the model. A model can have multiple meshes, so loop.
                    foreach (ModelMesh mesh in desc.model.Meshes)
                    {
                        // This is where the mesh orientation is set, as well 
                        // as our camera and projection.
                        foreach (BasicEffect effect in mesh.Effects)
                        {                            
                            effect.World = desc.worldTransform;
                            effect.View = Matrix.CreateLookAt(cameraEntity.position,
                                Vector3.Zero, Vector3.Up);
                            effect.Projection = Matrix.CreatePerspectiveFieldOfView(
                                MathHelper.ToRadians(45.0f), camComp.AspectRatio,
                                1.0f, 5000.0f);
                        }
                        // Draw the mesh, using the effects set above.
                        mesh.Draw();
                    }
                }
                else if (null != desc.geoPrim)
                {
                    // Set our vertex declaration, vertex buffer, and index buffer.
                    this.Game.GraphicsDevice.SetVertexBuffer(desc.geoPrim.VertexBuffer);
                    this.Game.GraphicsDevice.Indices = desc.geoPrim.IndexBuffer;
                    
                    foreach (EffectPass effectPass in effect.CurrentTechnique.Passes)
                    {
                        effect.World = desc.worldTransform;
                        effect.View = Matrix.CreateLookAt(cameraEntity.position,
                            (cameraEntity.position + cameraEntity.rotation.Forward), Vector3.Up);
                        effect.Projection = Matrix.CreatePerspectiveFieldOfView(
                            MathHelper.ToRadians(45.0f), camComp.AspectRatio,
                            1.0f, 5000.0f);
                        effectPass.Apply();

                        int primitiveCount = (desc.geoPrim.Indices.Count / 3);

                        this.Game.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0,
                                                             desc.geoPrim.Vertices.Count, 0, primitiveCount);

                    }
                }
            }
        }
    }
}

In part 2, we go over the SceneManager and MainGame classes.

No comments:

Post a Comment