XNA Game Engine Tutorial Series #9: ComponentPredicate and Materials

In this tutorial we are going to create a component predicate, a class used to determine whether or not a component will be used for an action. This is useful for things like special drawing, for example when drawing shadows we only want shadow casters to draw when creating the shadow map.

Side note: I apologize ahead of time but nothing cool is going to happen during this tutorial. By the end of this tutorial, you will have exactly the same results. But they will be better. :)

// Provides a way for the engine to let the game tell it what to
// draw, instead of the somewhat limited ComponentType
public abstract class ComponentPredicate
{
    // Decides whether the specified component is eligible for the
    // action being done
    public abstract bool IsComponentEligible(Component Component);
}

// Basic ComponentPredicate that uses ComponentType to determine
// what components to use
public class ComponentTypePredicate : ComponentPredicate
{
    // The type of component to accept
    ComponentType Type;

    // Accepts a ComponentType to use to determine what
    // components will be accepted
    public ComponentTypePredicate(ComponentType Type)
    {
        this.Type = Type;
    }

    public override bool IsComponentEligible(Component Component)
    {
        if (Type == ComponentType.Both)
        {
            // If the render type is both, we will use all 2D or
            // 3D components
            if (Component is I2DComponent || Component is I3DComponent)
                return true;
        }
        else if (Type == ComponentType.Component2D)
        {
            // If the render type is 2D, we will only use 2D
            // components
            if (Component is I2DComponent)
                return true;
        }
        else if (Type == ComponentType.Component3D)
        {
            // If the render type is 3D, we will only use 3D
            // components
            if (Component is I3DComponent)
                return true;
        }
        else
        {
            // Otherwise, we will use every component regardless of type
            return true;
        }

        // If the conditions have not been met, we will not use the
        // component
        return false;
    }
}

Now we can update GameScreen to use it:

// Draw the screen and its components. Accepts a ComponentPredicate
// to tell us what kind of components to draw.
public virtual void Draw(ComponentPredicate DrawPredicate)
{
    // Temporary list of components to draw
    List<Component> drawing = new List<Component>();

    foreach (Component component in Components.InDrawOrder)
        if (DrawPredicate.IsComponentEligible(component))
            drawing.Add(component);

    // Keep a list of components that are 2D so we can draw them on top
    // of the 3D components
    List<Component> defer2D = new List<Component>();

    foreach (Component component in drawing)
        if (component.Visible && component.Initialized)
            // If the component is visible
            if (component is I2DComponent)
                // If it is 2D, wait to draw
                defer2D.Add(component);
            else
                // otherwise, draw immediately
                component.Draw();

    // Draw 2D components
    foreach (Component component in defer2D)
        component.Draw();
}

And finally update Engine to use it:

// ComponentType to render
public static void Draw(GameTime gameTime, ComponentPredicate DrawPredicate)
{
    // Update the time, create a temp list
    Engine.GameTime = gameTime;
    List<GameScreen> drawing = new List<GameScreen>();

    // Clear the back buffer
    GraphicsDevice.Clear(Color.CornflowerBlue);

    // Populate the temp list if the screen is visible
    foreach (GameScreen screen in GameScreens)
        if (screen.Visible)
            drawing.Add(screen);

    // BlocksDraw and OverrideDrawBlocked logic
    for (int i = GameScreens.Count - 1; i >= 0; i--)
        if (GameScreens[i].BlocksDraw)
        {
            if (i > 0)
                for (int j = i - 1; j >= 0; j--)
                {
                    if (!GameScreens[j].OverrideDrawBlocked)
                        drawing.Remove(GameScreens[j]);
                }

            break;
        }

    // Draw the remaining screens
    foreach (GameScreen screen in drawing)
        if (screen.Initialized)
            screen.Draw(DrawPredicate);
}

public static void Draw(GameTime gameTime, ComponentType DrawType)
{
    Draw(gameTime, new ComponentTypePredicate(DrawType));
}

Now run it and you should see that nothing changed at all. At least, hopefully you do. If not, something went wrong.

The second part of this tutorial is on materials. A material defines how an object should be drawn. They define properties like specularity, texture, color, emmisive light, bump map, etc. The materials we will make right now only define texture, but they can be expanded in the future. Here is the base Material class:

// Base Material class keeps track of an effect and handles basic
// properties for it
public class Material : Component
{
    // The effect to handle
    Effect effect;

    public virtual Effect Effect
    {
        get { return effect; }
        set { effect = value; }
    }

    // Sets up the material from the values embedded in a MeshPart
    public virtual Material CreateFromMeshPart(ModelMeshPart MeshPart)
    {
        this.effect = MeshPart.Effect;
        return this;
    }

    // Set World, View, and Projection values
    public virtual void Prepare3DDraw(Matrix World)
    {
        effect.Parameters["World"].SetValue(World);
        effect.Parameters["View"].SetValue(
            Engine.Services.GetService<Camera>().View);
        effect.Parameters["Projection"].SetValue(
            Engine.Services.GetService<Camera>().Projection);
    }
}

Now we will expand this to make a material that supports texturing called TexturedMaterial:

// Material that handles a texture
public class TexturedMaterial : Material
{
    // The texture to keep track of
    Texture2D texture;

    // Set accesor sets the texture to the underlying effect
    public virtual Texture2D Texture
    {
        get { return texture; }
        set { texture = value; SetTextureToEffect(Effect, texture); }
    }

    public override Effect Effect
    {
        get { return base.Effect; }
        set
        {
            SetTextureToEffect(value, GetTextureFromEffect(base.Effect));
            base.Effect = value;
        }
    }

    // Override gets the texture from the MeshPart
    public override Material CreateFromMeshPart(ModelMeshPart MeshPart)
    {
        base.CreateFromMeshPart(MeshPart);
        this.texture = GetTextureFromEffect(MeshPart.Effect);
        return this;
    }

    // Method to get texture from various types of effect
    internal static Texture2D GetTextureFromEffect(Effect Effect)
    {
        if (Effect == null)
            return null;

        if (Effect is BasicEffect)
            return ((BasicEffect)Effect).Texture;
        else if (Effect.Parameters["Texture"] != null)
            return Effect.Parameters["Texture"].GetValueTexture2D();

        return null;
    }

    // Method to set the texture to various types of effect
    internal static void SetTextureToEffect(Effect Effect, Texture2D Texture)
    {
        if (Effect == null)
            return;

        if (Effect is BasicEffect)
            ((BasicEffect)Effect).Texture = Texture;
        else if (Effect.Parameters["Texture"] != null)
            Effect.Parameters["Texture"].SetValue(Texture);
    }
}

The final material we will make is called MeshPartMaterial. This class can be used by classes like Actor to handle the material properties of a ModelMeshPart, contained in a Model:

// Handles setting effect and texture to a MeshPart
public class MeshPartMaterial : TexturedMaterial
{
    ModelMeshPart meshPart;

    public ModelMeshPart MeshPart
    {
        get { return meshPart; }
        set { meshPart = value; value.Effect = Effect; }
    }

    public override Effect Effect
    {
        get { return base.Effect; }
        set { SetTextureToEffect(value, GetTextureFromEffect(base.Effect)); base.Effect = value; MeshPart.Effect = value; }
    }

    public override Material CreateFromMeshPart(ModelMeshPart MeshPart)
    {
        base.CreateFromMeshPart(MeshPart);
        this.meshPart = MeshPart;
        return this;
    }
}

Now we can update the Actor class to use MeshPartMaterial:

// In the top of the class:

public Dictionary<ModelMeshPart, Material> Materials = new Dictionary<ModelMeshPart, Material>();
// In Setup()

foreach (ModelMesh mesh in Model.Meshes)
    foreach (ModelMeshPart part in mesh.MeshParts)
        Materials.Add(part, new MeshPartMaterial().CreateFromMeshPart(part));
// In the Draw() method:

// Loop through meshes and effects and set them up to draw
foreach (ModelMesh mesh in model.Meshes)
{
    foreach (ModelMeshPart part in mesh.MeshParts)
        Materials[part].Prepare3DDraw(world);

    // Draw the mesh
    mesh.Draw();
}

Now if you hit F5 and spawn a box (left click if you have been following the tutorials in order), you should see.. exactly the same thing. I know, not very exciting, but at least it’s working! We’ll get to more advanced stuff later.

Now lets do the same thing for Terrain:

// Material for a Terrain object
public class TerrainMaterial : TexturedMaterial
{
    public TerrainMaterial(Texture2D Texture)
        : base()
    {
        this.Effect = new BasicEffect(Engine.GraphicsDevice, null);
        this.Texture = Texture;

        SetupEffect();
    }

    // Setup BasicEffect
    private void SetupEffect()
    {
        BasicEffect basicEffect = (BasicEffect)this.Effect;

        basicEffect.Texture = Texture;
        basicEffect.TextureEnabled = true;
        basicEffect.EnableDefaultLighting();
        basicEffect.DirectionalLight0.Direction = new Vector3(1, -1, 1);
        basicEffect.DirectionalLight0.Enabled = true;
        basicEffect.AmbientLightColor = new Vector3(0.3f, 0.3f, 0.3f);
        basicEffect.DirectionalLight1.Enabled = false;
        basicEffect.DirectionalLight2.Enabled = false;
        basicEffect.SpecularColor = new Vector3(0, 0, 0);
    }

    public override void Prepare3DDraw(Matrix World)
    {
        base.Prepare3DDraw(World);

        Engine.GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
        Engine.GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;
    }
}

All this does right now is keep track of the texture and create the effect, but we will extend it later on for effect like multitexturing. Now change terrain:

// Replaces Effect and Texture properties

// Material
public TerrainMaterial Material;
// Remove the following from Setup()

basicEffect = new BasicEffect(Engine.GraphicsDevice, null);
SetupEffect();

// Add to Setup()

Material = new TerrainMaterial(Texture);
private float[,] CreateTerrain(Texture2D heightMap, Texture2D texture)

// Becomes

private float[,] CreateTerrain(Texture2D heightMap)

// Remove from CreateTerrain()

this.texture = texture;
// Update the Draw() method

// Draw the terrain
public override void Draw()
{
    // Require the camera
    Camera camera = Engine.Services.GetService<Camera>();
    if (camera == null)
        throw new Exception("The engine services does not contain a "
        + "camera service. The terrain requires a camera to draw.");

    // Set effect values
    Material.Prepare3DDraw(MathUtil.CreateWorldMatrix(
        Position, Rotation, Scale));

    // Get width and height
    int width = heightData.GetLength(0);
    int height = heightData.GetLength(1);

    // Terrain uses different vertex winding than normal models,
    // so set the new one
    Engine.GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace;

    // Start the effect
    Material.Effect.Begin();

    // For each pass..
    foreach (EffectPass pass in Material.Effect.CurrentTechnique.Passes)
    {
        // Begin the pass
        pass.Begin();

        // Draw the terrain vertices and indices
        Engine.GraphicsDevice.Vertices[0].SetSource(terrainVertexBuffer, 0,
            VertexPositionNormalTexture.SizeInBytes);
        Engine.GraphicsDevice.Indices = terrainIndexBuffer;
        Engine.GraphicsDevice.VertexDeclaration = myVertexDeclaration;
        Engine.GraphicsDevice.DrawIndexedPrimitives(
            Microsoft.Xna.Framework.Graphics.PrimitiveType.TriangleStrip,
            0, 0, width * height, 0, width * 2 * (height - 1) - 2);

        // End the pass
        pass.End();
    }

    // End the effect
    Material.Effect.End();

    // Set the vertex winding back
    Engine.GraphicsDevice.RenderState.CullMode =
        CullMode.CullCounterClockwiseFace;
}

Now, run it, and again you should see the same thing you did before. Once, again, thrilling, but we’ll make it cooler in the future.

Thats it for this tutorial!

Download the Code for this Chapter
Back to Game Engine Tutorial Series

23 Responses to “XNA Game Engine Tutorial Series #9: ComponentPredicate and Materials” »

  1. Comment by Ilya Ostrovskiy — December 20, 2008 @ 8:53 am

    Hmmm, it seems as though something is wrong with the grass texture. I actually had this in the previous tutorial also, but I thought maybe it would be fixed in this one.

    http://i44.tinypic.com/2expl4i.png

    That’s what happens. Only one corner is shown, the rest is some colorful-lines on the side as seen, and the rest is that yucky green color. Any ideas what could wrong?

  2. Comment by Ilya Ostrovskiy — December 20, 2008 @ 8:56 am

    addendum to the previous: What I meant was the terrain itself actually appears, however only one corner of the grass is rendered properly. The green scanline things only appear on the two sides in the image see, and the yucky green color fills the rest.

  3. Comment by Ilya Ostrovskiy — December 20, 2008 @ 9:06 am

    Addendum to the addendum:
    I tried it with a bitmap texture, and the outcome is the same. One corner gets rendered right, the rest are exactly like the previous. Any ideas?

  4. Comment by Ilya Ostrovskiy — December 20, 2008 @ 9:33 am

    Addendum to the addendum of the addendum: I just reran it and it seems to work? I did screw around with some lighting, so maybe that fixed it.

    Great tuts!

  5. Comment by Sean — December 20, 2008 @ 2:28 pm

    It sounds like you had some texture wrap settings messed up. On certain texture wrap modes, if a texture is outside the range of [0,1] (like the entire terrain except for one corner), it will just stretch the outermost pixel of the texture to cover the rest of the terrain (ie, the ugly yellow). If it’s set to Wrap, it will wrap the texture over the whole terrain:

    Engine.GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
    Engine.GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;

    If you used a SpriteBatch or possibly BasicEffect, this would have been fiddled with.

  6. Comment by hey — December 20, 2008 @ 5:58 pm

    It keeps giving me an error in the TexturedMaterial class that value.Parameters["Texture"].SetValue(texture); is not assigned to. I even downloaded your source code.

  7. Comment by James — December 21, 2008 @ 5:13 am

    value.Parameters["Texture"].SetValue(texture); Will not work with the provided code as it tries to set a Parameter of the effect which is null.

    if you chuck

    if (value.Parameters["Texture"] != null)

    before that line it will fix your problem.

  8. Comment by Sean James — December 21, 2008 @ 11:03 am

    Or you could use SetTextureToEffect(), and make sure the Effect has a parameter called Texture.

  9. Comment by Johnson — December 27, 2008 @ 4:17 am

    hello sean

    first, say sorry, my engish is poor.

    I adding a simple class Font to engine, inherit Component and I2DComponent, called SpriteBatch.DrawString() to drawing font in override Draw().

    in test game, in LoadContent() ,

    Font font = new Font();
    Engine.Services.AddService(typeof(Font), font);

    but color parameter of SpriteBatch.DrawString() causative BasicEffect of SetupEffect() in TerrainTexture bring bug.

    same with Ilya Ostrovskiy say

    please help me, thank you.

  10. Comment by Johnson — December 27, 2008 @ 8:56 am

    I add code

    Engine.GraphicsDevice.RenderState.DepthBufferEnable = true;
    Engine.GraphicsDevice.RenderState.AlphaBlendEnable = true;
    Engine.GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
    Engine.GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;
    Engine.GraphicsDevice.RenderState.AlphaBlendEnable = true;

    remove bug,hehe

  11. Comment by Johnson — December 27, 2008 @ 8:59 am

    is

    this.graphics.GraphicsDevice.RenderState.AlphaTestEnable = true;

  12. Comment by Johnson — December 27, 2008 @ 9:04 am

    I m fool, sorry ,is

    Engine.GraphicsDevice.RenderState.DepthBufferEnable = true;
    Engine.GraphicsDevice.RenderState.AlphaTestEnable = true;
    Engine.GraphicsDevice.SamplerStates[0].AddressU = TextureAddressMode.Wrap;
    Engine.GraphicsDevice.SamplerStates[0].AddressV = TextureAddressMode.Wrap;
    Engine.GraphicsDevice.RenderState.AlphaBlendEnable = true;

    my engish is poor,hehe

  13. Comment by Jay — January 7, 2009 @ 10:03 am

    I get an error when trying to add the Box we used earlier. In MeshPartMaterial it says: “if (value.Parameters["Texture"] != null)” in line
    “set { SetTextureToEffect(value, GetTextureFromEffect(base.Effect)); base.Effect = value; MeshPart.Effect = value; }”

    Specifically its the “MeshPart.Effect = value” that causes problems. It seems that the Meshpart isnt set to anything before we try and assign it an effect… Am i missing something?

  14. Pingback by EGGEngine Dev Blog » Blog Archive » Component Predicate System — January 15, 2009 @ 2:51 pm

    [...] http://innovativegames.net/blog/2008/12/19/engine-tutorial-9/ [...]

  15. Comment by Pixelstudio — February 10, 2009 @ 8:10 am

    If you ask me this :
    Materials[part].Prepare3DDraw(world);

    should be :
    Materials[part].Prepare3DDraw(transforms[mesh.ParentBone.Index] * world);

    when you dont all model positions are at zero :)

  16. Comment by ram — February 16, 2009 @ 5:33 am

    how to add animation library to this engine.when i tried to add it, it was not displaying the animation models.But it is displaying the static models. Am i missing something?
    Thanks

  17. Comment by Jordan — April 21, 2009 @ 8:53 am

    @Ram
    See the comment before you- I think that’s what you want. ie. The world matrix needs to be combined with the bone transformations…

  18. Comment by Joshua — May 25, 2009 @ 10:35 pm

    I created a FrameRateDisplay component that inherits from Component and I2DComponent and overrides the Update and Draw methods to calculate and print the framerate on the screen.

    The problem I’m having is that the terrain is not being drawn correctly when I draw text to the screen to display the framerate. If I comment out the DrawString method, everything is fine. It’s only when I draw the text on the screen that I see anything wrong.

    Could you possibly post the correct code for displaying text (or any other sprite) on the screen with the terrain rendering correctly behind it?

    Thank you.

  19. Comment by Joshua — May 28, 2009 @ 8:20 pm

    Update: I read through these comments again and it appears I was having the same issue as Johnson was. All is well now. Thanks for the great tuts!

  20. Comment by MaiKai — May 31, 2009 @ 9:19 am

    Any problem rewriting the draw code like this?

    public virtual void Draw(ComponentPredicate DrawPredicate)
    {
    //Temporary lists
    List drawing = new List();
    List defer2d = new List();

    foreach (Component Component in Components.InDrawOrder)
    {
    if (Component.Visible && Component.Initialized)
    {
    if (DrawPredicate.IsComponentEligible(Component)
    {
    if (Component is I2dComponent)
    defer2d.Add(Component);
    else
    drawing.Add(Component);
    }
    }
    }

    //Now Draw
    foreach (Component Component in drawing)
    Component.Draw();
    foreach (Component Component in defer2d)
    Component.Draw();

    }

  21. Comment by Jay — February 13, 2012 @ 3:18 am

    Hello,

    I am trying to follow your tutorial, but I get an error in the ” Prepare3DDraw ” methode at the lines :

    effect.Parameters["View"].SetValue(Engine.Services.GetService().View);
    effect.Parameters["Projection"].SetValue(Engine.Services.GetService().Projection);

    The error tells me the object is not set to an instance on an object.

    My Camera seems to be well initialized, but I can’t find the problem.

    All ideas are welcome.

    Thank you.

  22. Comment by Khorum — February 17, 2012 @ 1:39 pm

    @Jay and all others converting to XNA 4.0

    I ran into the same problem when trying to draw the box actor, and figured it was due to BasicEffect changing in XNA 4.0. Inside of the Material Class, try to replace your Draw method with this:

    // Set the World, View, and Projection Matrix values
    public virtual void Prepare3DDraw(Matrix World)
    {
    if (Effect is BasicEffect)
    {
    ((BasicEffect)Effect).World = World;
    ((BasicEffect)Effect).View = Engine.Services.GetService<Camera>().View;
    ((BasicEffect)Effect).Projection = Engine.Services.GetService<Camera&gt().Projection;
    }
    else
    {
    effect.Parameters["World"].SetValue(World);
    effect.Parameters["View"].SetValue(Engine.Services.GetService<Camera&gt().View);
    effect.Parameters["Projection"].SetValue(Engine.Services.GetService<Camera&gt().Projection);
    }
    }

    This might not work in future tutorials, but until we replace this with a better method, it seems to work for now. BasicEffect no longer has Parameters, instead they are just used as BasicEffect.World, BasicEffect.View, etc.

    Cheers for the excellent tutorials. Can’t wait to add serialization :(

  23. Comment by Khorum — February 17, 2012 @ 1:40 pm

    Missed a few semicolons for the >’s

    // Set the World, View, and Projection Matrix values
    public virtual void Prepare3DDraw(Matrix World)
    {
    if (Effect is BasicEffect)
    {
    ((BasicEffect)Effect).World = World;
    ((BasicEffect)Effect).View = Engine.Services.GetService<Camera>().View;
    ((BasicEffect)Effect).Projection = Engine.Services.GetService<Camera>().Projection;
    }
    else
    {
    effect.Parameters["World"].SetValue(World);
    effect.Parameters["View"].SetValue(Engine.Services.GetService<Camera>().View);
    effect.Parameters["Projection"].SetValue(Engine.Services.GetService<Camera>().Projection);
    }
    }

RSS feed for comments on this post. TrackBack URI

Leave a comment