Thursday, November 17, 2011

Let's Make a Game Engine for XNA 4.0r, Part 2, SceneManager and MainGame

The last post (Part 1 of this series) was about creating a BaseManager and RenderManager class. If you remember, when the RenderManager renders the scene it needs to get the list of entities to render, and those entities belong the the SceneManager. In order to get to the SceneManager we'll need to go through the MainGame class.

MainGame
So let's begin with the MainGame class (Main.cs). For now there are only two objects in this class, the GraphicsDeviceManager and the list of Managers. The list of Managers is the list that includes SceneManager, RenderManager, and others we will need in the future. The way a scripter or programmer will access the Managers is by name, which is a String, so our list of Managers will be a Dictionary with Strings as the key, and the Manager itself as the value. So let's create those objects and accessors for them:

private GraphicsDeviceManager graphics;
public GraphicsDeviceManager Graphics
{
    get { return this.graphics; }
    set { this.graphics = value; }
}

private Dictionary<String, BaseManager> managers;
public Dictionary<String, BaseManager> Managers
{
    get { return this.managers; }            
}

Next we'll create a constructor:
public MainGame()
{
    this.graphics = new GraphicsDeviceManager(this);
    this.Content.RootDirectory = "Content";      

    this.managers = new Dictionary<string, BaseManager>();    
}

We have to initialize the GraphicsDeviceManager here, before our parent class (Microsoft.Xna.Framework.Game) calls Initialize(). We have to call base.Initialize() within our handler of Initialize() (code below), otherwise we'll never get a call to LoadContent(), however, if we do not have a GraphicsDeviceManager we'll also never get a call to LoadContent().

Here's the order that XNA's base Game class calls things (or at least the part we care about for now):
Constructor, Initialize, LoadContent (if base.Initialize is called on the base class, and only if a GraphicsDeviceManager is loaded), Update, Draw, Update, Draw, etc...., UnloadContent (as game is closed down).

Anyway, here's the handler to the virtual Initialize method:
protected override void Initialize()
{
    SceneManager sceneManager = new SceneManager(this);
    RenderManager renderManager = new RenderManager(this);

    base.Initialize();
}

You can see we create the two Managers that we need. Notice we're passing them the reference to this MainGame instance. If you remember from the RenderManager class, it will take that reference, and during RenderManager's Initialize method it calls to BaseManager's Initialize method which registers that manager with MainGame. So it only takes a single line here to create a Manager instance and have it register with the MainGame. In order for BaseManager to register with MainGame, it needs a method within MainGame that allows it to do so. We'll call that method 'AddManager', and here it is:

public void AddManager(BaseManager manager)
{
    BaseManager checkManager = null;
    if (managers.TryGetValue(manager.Name, out checkManager))
    {
        throw new Exception("Manager type " + manager.Name + " already exists within the game engine");
    }

    managers.Add(manager.Name, manager);
}
We do a check to see if a Manager by the same name has already been registered with MainGame. This shouldn't happen, but better safe than sorry. Efficiency shouldn't be a problem here anyway, this method is only called a few times each time your game runs. The methods you really look at for optimization should be called either many times per frame, or are very time consuming methods.

The reason we add managers to a Dictionary is so that we can access them by name later on, so we'll need a method that lets us do that:
public BaseManager GetManager(String managerName)
{
    BaseManager manager = null;
    if (!managers.TryGetValue(managerName, out manager))
    {
        // Manager wasn't found
    }

    return manager;
}

And the rest of our methods are called from the base Game class and we simply pass them on to all registered Managers:
protected override void LoadContent()
{
    foreach (KeyValuePair<String, BaseManager> pair in managers)
    {
        (pair.Value).LoadContent();
    }
}

protected override void UnloadContent()
{
    foreach (KeyValuePair<String, BaseManager> pair in managers)
    {
        (pair.Value).UnloadContent();
    }
}

protected override void Update(GameTime gameTime)
{
    foreach (KeyValuePair<String, BaseManager> pair in managers)
    {
        (pair.Value).Update(gameTime);
    }
}

protected override void Draw(GameTime gameTime)
{
    foreach (KeyValuePair<String, BaseManager> pair in managers)
    {
        (pair.Value).Draw(gameTime);
    }

    base.Draw(gameTime);
}
You must register all Managers during MainGame's Initialize, because otherwise you will never get the call to LoadContent, because as you can see if only sends it to Managers that are registered.

And here is the full file:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

using SimpleGameEngine.Scene;
using SimpleGameEngine.Render;

namespace SimpleGameEngine
{
    /// 
    /// This is the main type for your game
    /// 
    public class MainGame : Microsoft.Xna.Framework.Game
    {
        private GraphicsDeviceManager graphics;
        public GraphicsDeviceManager Graphics
        {
            get { return this.graphics; }
            set { this.graphics = value; }
        }

        private Dictionary<String, BaseManager> managers;
        public Dictionary<String, BaseManager> Managers
        {
            get { return this.managers; }            
        }

        public MainGame()
        {
            this.graphics = new GraphicsDeviceManager(this);
            this.Content.RootDirectory = "Content";      

            this.managers = new Dictionary<string, BaseManager>();    
        }

        protected override void Initialize()
        {
            SceneManager sceneManager = new SceneManager(this);
            RenderManager renderManager = new RenderManager(this);

            base.Initialize();
        }

        public void AddManager(BaseManager manager)
        {
            BaseManager checkManager = null;
            if (managers.TryGetValue(manager.Name, out checkManager))
            {
                throw new Exception("Manager type " + manager.Name + " already exists within the game engine");
            }

            managers.Add(manager.Name, manager);
        }

        public BaseManager GetManager(String managerName)
        {
            BaseManager manager = null;
            if (!managers.TryGetValue(managerName, out manager))
            {
                // Manager wasn't found
            }

            return manager;
        }

        protected override void LoadContent()
        {
            foreach (KeyValuePair<String, BaseManager> pair in managers)
            {
                (pair.Value).LoadContent();
            }
        }

        protected override void UnloadContent()
        {
            foreach (KeyValuePair<String, BaseManager> pair in managers)
            {
                (pair.Value).UnloadContent();
            }
        }

        protected override void Update(GameTime gameTime)
        {
            foreach (KeyValuePair<String, BaseManager> pair in managers)
            {
                (pair.Value).Update(gameTime);
            }
        }

        protected override void Draw(GameTime gameTime)
        {
            foreach (KeyValuePair<String, BaseManager> pair in managers)
            {
                (pair.Value).Draw(gameTime);
            }

            base.Draw(gameTime);
        }
    }
}

SceneManager
That leaves us with the SceneManager class. The SceneManager is in charge or running the scene, and all of the entities that exist within it. For now the SceneManager only has one object it is in charge of, and that is the list of entities. Just like with the Managers list in MainGame, the entity list is also a Dictionary with Strings as the key, and the entity as the value. Every entity has a name, and that is how they're located...just like the Managers. So let's start with our one object, its accessor, and an accessor to the ContentManager (which is not a class we've created, it's part of XNA's Game class):
private Dictionary<String, BaseEntity> entities;        
public Dictionary<String, BaseEntity> Entities
{
    get { return entities; }
}

public ContentManager Content
{
    get { return Game.Content; }
}

Also, just like RenderManager, SceneManager needs to handle the abstract GetName() method declared by BaseManager, this allows BaseManager to register SceneManager by its name with MainGame:
protected override String GetName()
{
    return "Scene";
}

And we'll need our constructor and Initialize methods, just like RenderManager, except in the case of SceneManager we need to create the instance of the Dictionary for the entities:
public SceneManager(MainGame Game)
            : base(Game)
{
    this.entities = new Dictionary();

    Initialize();
}

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

The LoadContent function is where we load up the Root entity for the engine. There should always be one entity in a Scene otherwise you'll have no entity to act as a camera. So we will create an entity, and give it a CameraComponent, and then add it to the scene. You'll get to see the functionality behind most of this in Part 3 of this series:
public override void LoadContent()
{
    // We always have at least one entity in the world. We'll create that now and then
    // make it our main camera for the game.
    BaseEntity entity = new BaseEntity(this, EngineCommon.RootEntityName);

    // The component will automatically attach to the entity that we pass into it
    CameraComponent camComp = new CameraComponent(entity);

    // Now we add the entity to the scene
    AddEntityToScene(entity);

    // ===================================================================================
    // TEMPORARY!!!!!!!!!!!!!!!!!!!!!!!!!!
    BaseEntity testCube = new BaseEntity(this, "Cube");
    testCube.position = new Vector3(0, 0, -10);

    RenderComponent rendComp = new RenderComponent(testCube, GeometricPrimitiveType.Cube);

    AddEntityToScene(testCube);
    // ===================================================================================
}
There's a lot going on here, lets run down exactly what is happening.

  • We create a BaseEntity, and pass it a reference to this SceneManager, and we also pass it a String for its name. In this case it's the root entity for the game so we give it the default root entity name. NOTE: BaseEntities do not automatically register with the SceneManager, this is because they're not complete until they have all of their components loaded onto them, and if we decide to make this engine multi-threaded then adding it to the list before it has components could cause it to be run without components.
  • We create a CameraComponent, and pass it a reference to the BaseEntity that it belongs to. All components will automatically attach the a BaseEntity. We'll see later that Component are just like many other classes, during their initialization they ball BaseComponent which registers them with an Entity.
  • And now that our BaseEntity has all of the components it needs we can add it to the scene by calling AddEntityToScene (which we'll see soon in code below).
  • Notice the large comment that says "TEMPORARY" here. This code is only here during this milestone, as a way to easily load up a cube so we can see that we're successfully rendering something.
  • We create the cube, naming it "Cube", and we update its position to be at (0, 0, -10). Because our camera we loaded is using its default rotation (Matrix::Identity), and means its facing directly down the -Z axis, so putting the cube here should put it 10 units in front of the camera.
  • We create a RenderComponent (which we'll see in Part 3 or 4), and assign it to our BaseEntity, and we create it with a GeometricPrimitiveType of Cube (we'll see this in part 3 or 4 as well).
  • And now that the entity is ready with its component we add it to the scene.
Whew...now that we're done with that let's continue. All Managers can handle the Update method, which is called each frame by MainGame, so we'll handle it, and pass the Update to all entities (and eventually only entities that need updates each frame):
public override void Update(GameTime gameTime)
{
    foreach (KeyValuePair<string, baseentity&rt; pair in entities)
    {
        (pair.Value).Update(gameTime);
    }
}

Just like with the MainGame class and how it adds Managers and allows users to retrieve them by name, SceneManager does the same thing with Entities:
public void AddEntityToScene(BaseEntity entity)
{
    BaseEntity checkEntity = null;
    if (entities.TryGetValue(entity.Name, out checkEntity))
    {
        throw new Exception("An entity named " + entity.Name + " already exists.");
    }

    entities.Add(entity.Name, entity);
}

public void RemoveEntityFromScene(BaseEntity entity)
{
    BaseEntity checkEntity = null;
    if (!entities.TryGetValue(entity.Name, out checkEntity))
    {
        throw new Exception("No entity named " + entity.Name + " exist in the scene to be removed.");
    }

    entities.Remove(entity.Name);
}

public BaseEntity GetEntity(String entityName)
{
    BaseEntity entity = null;
    if (!entities.TryGetValue(entityName, out entity))
    {
        // Entity wasn't found
    }

    return entity;
}
And you can see we've added a RemoveEntityFromScene method. We aren't using it yet, but it will remove the entity from the scene, but not delete the entity. If we were to multithread this engine (and maybe we will) and we wanted to add/remove component from it after it was loaded, we would make sure to lock the entity list, then remove the entity from the list, unlock the list, lock the entity, add/remove the necessary components, unlock the entity, lock the entity list, readd the entity to the list, unlock the list. Yea...fun stuff. Long story short we would need a method to remove the entity while it was having its components altered, or some other way to lock it, removing it might not be ideal because it could blip out of view.

And that's it for the SceneManager, here's the full file:
using System;
using System.Collections.Generic;
using System.Text;

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

using SimpleGameEngine.Entity;
using SimpleGameEngine.Components;
using SimpleGameEngine.GeometricPrimitives;

namespace SimpleGameEngine.Scene
{
    public class SceneManager : BaseManager
    {        
        private Dictionary<String, BaseEntity> entities;        
        public Dictionary<String, BaseEntity> Entities
        {
            get { return entities; }
        }        

        public ContentManager Content
        {
            get { return Game.Content; }
        }

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

        public SceneManager(MainGame Game)
            : base(Game)
        {
            this.entities = new Dictionary<string, BaseEntity>();

            Initialize();
        }

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

        public override void LoadContent()
        {
            // We always have at least one entity in the world. We'll create that now and then
            // make it our main camera for the game.
            BaseEntity entity = new BaseEntity(this, EngineCommon.RootEntityName);

            // The component will automatically attach to the entity that we pass into it
            CameraComponent camComp = new CameraComponent(entity);

            // Now we add the entity to the scene
            AddEntityToScene(entity);

            // ===================================================================================
            // TEMPORARY!!!!!!!!!!!!!!!!!!!!!!!!!!
            BaseEntity testCube = new BaseEntity(this, "Cube");
            testCube.position = new Vector3(0, 0, -10);

            RenderComponent rendComp = new RenderComponent(testCube, GeometricPrimitiveType.Cube);

            AddEntityToScene(testCube);
            // ===================================================================================
        }

        public override void Update(GameTime gameTime)
        {
            foreach (KeyValuePair<String, BaseEntity> pair in entities)
            {
                (pair.Value).Update(gameTime);
            }
        }

        public void AddEntityToScene(BaseEntity entity)
        {
            BaseEntity checkEntity = null;
            if (entities.TryGetValue(entity.Name, out checkEntity))
            {
                throw new Exception("An entity named " + entity.Name + " already exists.");
            }

            entities.Add(entity.Name, entity);
        }

        public void RemoveEntityFromScene(BaseEntity entity)
        {
            BaseEntity checkEntity = null;
            if (!entities.TryGetValue(entity.Name, out checkEntity))
            {
                throw new Exception("No entity named " + entity.Name + " exist in the scene to be removed.");
            }

            entities.Remove(entity.Name);
        }

        public BaseEntity GetEntity(String entityName)
        {
            BaseEntity entity = null;
            if (!entities.TryGetValue(entityName, out entity))
            {
                // Entity wasn't found
            }

            return entity;
        }
    }
}

In Part 3 we will go over BaseEntity, and BaseComponent.

No comments:

Post a Comment