Friday, November 18, 2011

Let's Make a Game Engine for XNA 4.0r, Part 4, RenderComponent and CameraComponent

In Part 3 of this series we covered BaseEntity and BaseComponent. Entities and Components are really at the heart of gameplay in any object/entity and component-based game engine, for example, in LEGO Universe we had over 110 different types of components, including a scripting component that allowed scripts to do all kinds of custom gameplay. In this post (Part 4) we will create our first two components, RenderComponent, and CameraComponent, and to start with we'll keep them extremely simple. Remember, our first milestone goal is just to render a cube on the screen with the engine.

RenderComponent
Ok, let's start with RenderComponent. First, make sure you derive RenderComponent from BaseComponent:
public class RenderComponent : BaseComponent

And the only two objects this class holds for now is a RenderDescription (which we'll get to see in Part 5), and a Matrix that defines the scale of the render mesh (although we won't implement scale until after the first milestone):
private RenderDescription description;
private Matrix scaleMatrix = Matrix.Identity;
Notice we don't have accessors for these. The only time you want an accessor in a component is for data you want other users to be able to access or alter from outside the component. But if we make a script component giving access to these variables is as easy as making an accessor or function.

Next you'll see that derived components, like derived managers, must handle GetName():
protected override String GetName()
{
    return "Render";
}

RenderComponent will start out with two constructors, one in which the user can create a render component based on the name of an asset in the content pipeline, and another in which the user can create a render component based on a geometric primitive:
public RenderComponent(BaseEntity ParentEntity, String ModelName) :
    base(ParentEntity)
{            
    Initialize();

    LoadModel(ModelName);
}

public RenderComponent(BaseEntity ParentEntity, GeometricPrimitiveType primitiveType) :
    base(ParentEntity)
{
    Initialize();

    LoadPrimitive(primitiveType);
}
Notice the component pass the 'ParentEntity' reference to the base class, which is BaseComponent. BaseComponent stores that reference, and when the derived component calls base.Initilize(), within its own Initialize() method, then BaseComponent attaches the component to the Entity.

You can see based on which constructor is called we use a function called LoadModel() and pass in the name of the model, or another called LoadPrimitive() and pass in the type of primitive, here are those methods:
private void LoadModel(String modelName)
{
    description.model = this.Parent.Manager.Content.Load<model>(modelName);
}

private void LoadPrimitive(GeometricPrimitiveType primitiveType)
{
    switch (primitiveType)
    {
        case GeometricPrimitiveType.Cube:
            description.geoPrim = new CubePrimitive(this.Parent.Manager.Game.GraphicsDevice);
            break;
        case GeometricPrimitiveType.Sphere:
            description.geoPrim = new SpherePrimitive(this.Parent.Manager.Game.GraphicsDevice);
            break;
        case GeometricPrimitiveType.Cylinder:
            description.geoPrim = new CylinderPrimitive(this.Parent.Manager.Game.GraphicsDevice);
            break;
        case GeometricPrimitiveType.Torus:
            description.geoPrim = new TorusPrimitive(this.Parent.Manager.Game.GraphicsDevice);
            break;
        case GeometricPrimitiveType.Teapot:
            description.geoPrim = new TeapotPrimitive(this.Parent.Manager.Game.GraphicsDevice);
            break;
        default:
            throw new Exception("LoadPrimitive does not handle this type of GeometricPrimitive. Was a new primitive type made and not handled here?");
    }
}
Within LoadModel we utilitize the ContentManager to load our asset based on its name. ContentManager can be access from a component by going through the Entity, then SceneManager. And within LoadPrimitive we use an enum of a primitive type to decide which primitive to load. I will go over the GeometricPrimitives briefly in Part 5, there will be no need to go into them in full detail, as I didn't write them, they are extracted from a Microsoft XNA Sample that you can use if you wish to learn about them.

In our Update() method we make sure our render mesh's worldTransform is updated just in case the position or rotation of the entity changed. Please note that this is not the most efficient way to do this, what would be more efficient is to have components that care about changes to position or rotation listen for those changes, and when a change occurs then update the worldTransform. Maybe I'll do this in a later version of the engine. The worldTransform is a matrix that holds the scale, rotation, and translation (position) of a mesh. This is needed by the shaders to properly render the mesh:
public override void Update(GameTime gameTime)
{
    description.worldTransform = this.scaleMatrix * this.Parent.rotation * Matrix.CreateTranslation(this.Parent.position);
}

And finally is our Draw method. Draw is called by XNA's base Game class, and we pass it through to the RenderManager, which calls Draw on all entities to request all of the RenderDescriptions they may have. Having already seen RenderManager in a previous section I won't explain what it does with those RenderDescriptions. But RenderDescriptions are covered in Part 5.
public override void Draw(GameTime gameTime, List renderDescriptions)
{
    // Could do frustum culling here if we wanted to be more efficient.
        // We could go a step further and gather the entities within sections of an octree that are
        // currently within the view frustum, then skip the frustum check here.

    renderDescriptions.Add(description);
}

And that's it for RenderComponent. Here's the entire file:
using System;
using System.Collections.Generic;
using System.Text;

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

using SimpleGameEngine.Entity;
using SimpleGameEngine.Render;
using SimpleGameEngine.GeometricPrimitives;

namespace SimpleGameEngine.Components
{
    public class RenderComponent : BaseComponent
    {
        private RenderDescription description;
        private Matrix scaleMatrix = Matrix.Identity;

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

        public RenderComponent(BaseEntity ParentEntity, String ModelName) :
            base(ParentEntity)
        {            
            Initialize();

            LoadModel(ModelName);
        }

        public RenderComponent(BaseEntity ParentEntity, GeometricPrimitiveType primitiveType) :
            base(ParentEntity)
        {
            Initialize();

            LoadPrimitive(primitiveType);
        }

        protected override void Initialize()
        {
            description = new RenderDescription();

            description.worldTransform = this.scaleMatrix * this.Parent.rotation * Matrix.CreateTranslation(this.Parent.position);

            base.Initialize();
        }

        private void LoadModel(String modelName)
        {
            description.model = this.Parent.Manager.Content.Load<Model>(modelName);
        }

        private void LoadPrimitive(GeometricPrimitiveType primitiveType)
        {
            switch (primitiveType)
            {
                case GeometricPrimitiveType.Cube:
                    description.geoPrim = new CubePrimitive(this.Parent.Manager.Game.GraphicsDevice);
                    break;
                case GeometricPrimitiveType.Sphere:
                    description.geoPrim = new SpherePrimitive(this.Parent.Manager.Game.GraphicsDevice);
                    break;
                case GeometricPrimitiveType.Cylinder:
                    description.geoPrim = new CylinderPrimitive(this.Parent.Manager.Game.GraphicsDevice);
                    break;
                case GeometricPrimitiveType.Torus:
                    description.geoPrim = new TorusPrimitive(this.Parent.Manager.Game.GraphicsDevice);
                    break;
                case GeometricPrimitiveType.Teapot:
                    description.geoPrim = new TeapotPrimitive(this.Parent.Manager.Game.GraphicsDevice);
                    break;
                default:
                    throw new Exception("LoadPrimitive does not handle this type of GeometricPrimitive. Was a new primitive type made and not handled here?");
            }
        }

        public override void Update(GameTime gameTime)
        {
            description.worldTransform = this.scaleMatrix * this.Parent.rotation * Matrix.CreateTranslation(this.Parent.position);
        }

        public override void Draw(GameTime gameTime, List<RenderDescription> renderDescriptions)
        {
            // Could do frustum culling here if we wanted to be more efficient.
                // We could go a step further and gather the entities within sections of an octree that are
                // currently within the view frustum, then skip the frustum check here.

            renderDescriptions.Add(description);
        }
    }
}

CameraComponent
Next up we have CameraComponent, which at this time does virtually nothing special. You see, to render a basic camera we need position, rotation, and aspect ratio. Position and rotation are part of all entities, existing within BaseEntity, so for now that just leaves aspect ratio as the only thing we need the camera component for. But later on it will do more advanced things.

Rather than go over each part of such a simple class, I will post the entire file here. The only thing worth noting is that is has an aspectRatio variable which is initializes to be based on the dimensions of the viewport/window.

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

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

using SimpleGameEngine.Entity;
using SimpleGameEngine.Render;

namespace SimpleGameEngine.Components
{
    public class CameraComponent : BaseComponent
    {
        private float aspectRatio;
        public float AspectRatio
        {
            get { return aspectRatio; }
        }

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

        public CameraComponent(BaseEntity ParentEntity) :
            base(ParentEntity)
        {            
            Initialize();
        }

        protected override void Initialize()
        {            
            // Default aspect ratio is that of the viewport/window
            aspectRatio = this.Parent.Manager.Game.GraphicsDevice.Viewport.AspectRatio;

            base.Initialize();
        }
    }
}

And that's it for Part 4. In Part 5 we're going to cover everything that remains for the first milestone for this engine, after which point you should be at the same point as if you had downloaded the engine yourself.

No comments:

Post a Comment