Game State Management and DrawableGameComponent

Game State Management and DrawableGameComponent

  • Comments 10

I used the Game State Management sample as a basis for my AppWeek game. I also used several samples that are implemented as a DrawableGameComponent (bloom, lensflare, 3D particles).

Herein lies a dilemma:

  • Components are global to the entire game. You control what order they draw in, but cannot do complex interleaved things like drawing part of one component, then part of another, then back to the first.

  • The Game State Management sample implements a more dynamic scheme using a stack of screen objects. This allows it to pop up one screen over the top of another, and to implement transitions between screens.

I wanted my components to be local to the GameplayScreen, not global to the entire game. I wanted to draw gameplay objects, then particles, then apply the bloom filter, draw the lensflare, and then draw any other popup screens or transition effects over the top of all that. When you pause the game, I don't want particles or bloom appearing on top of the pause menu!

One solution would be to host components directly inside the GameplayScreen, rather than registering them with the main game. I could write my own code to maintain a list of components, taking care to call their Initialize, Update, and Draw methods at the right times and in the right order. This would be elegant and powerful, but also a lot of subtle code to write, and I was in a hurry.

I chose an easier approach. I registered my components globally with the main game, but set their Visible property to false so the system would not attempt to draw them. Then my GameplayScreen looked up the components it was interested in, and manually called their Draw method at the appropriate time.

Here's how I created the components in my Game constructor:

    Components.Add(new LensFlareComponent(this) { Visible = false });
    Components.Add(new BloomComponent(this) { Visible = false });

I added a couple of fields to my GameplayScreen:

    LensFlareComponent lensFlare;
    BloomComponent bloom;

I added this method to GameplayScreen (using LINQ extension methods, so you need the System.Linq namespace for it to compile):

    T FindComponent<T>()
    {
        return ScreenManager.Game.Components.OfType<T>().First();
    }

GameplayScreen.LoadContent looks up the components it is interested in:

    lensFlare = FindComponent<LensFlareComponent>();
    bloom = FindComponent<BloomComponent>();

Now GameplayScreen.Draw can manually draw these components in the appropriate order:

    level.Sky.Draw(camera.View, camera.Projection);

    DrawCat(cats[0], Color.Red);
    DrawCat(cats[1], Color.Blue);

    DrawNameLabels();

    bloom.Draw(gameTime);

    lensFlare.LightDirection = camera.LightDirection;
    lensFlare.View = camera.View;
    lensFlare.Projection = camera.Projection;
    lensFlare.Draw(gameTime);

    DrawHud();

Tada! Bloom and lensflare rendering is now local to the GameplayScreen, and correctly positioned underneath the pause menu.

  • Nice trick. I've actually been manually converting a bunch of GameComponents to unique types because of the drawing hassles you describe - I hadn't ever thought of making them invisible and manually issuing the Draw calls.

    I'll have to see if I can apply this to my game; might save me some effort.

  • I've also hit the problems you describe, and though what you've done is great in a rush. It feels like a hack to me.

    There really should be a library for this. I feel a new project coming on. "XNAContrib" anyone? :P

  • I have tried it. It works fine. And it is simple like all great things. Thanks for sharing

  • Thanks for posting this.  I've been struggling with GamestateManagement and using Game Components.

    One additional problem you run into is if you add your Game Components in GamePlayScreen.cs, every time you cycle back to the main menu from your game and then go back to the game, you re-add those components to your components collection.  If you check your component count, you'll see that number just grow every time you exit to main menu and back to game.  I would assume, over time, that would lead to a crash.  One solution is to Clear all components when you unload content in GamePlayScreen.cs.  But that cleared components I did not want to clear.  So now I manually remove all components that I added in GamePlayScreen but again....it's just a pain.

  • That's great stuff Shawn, do you think you'll be able to update the GSM sample with everything you've discovered.  I know the APPWeek stuff is usually private but I think this update should be ok?

    Thanks

  • Simon: I guess this comes down to how much we want the sample to be just a starting framework, versus how much we want to polish it into something that can be used as-is. The tension here is between making it as robust and powerful as possible, but not adding so much complexity that people can't understand it enough to build their own stuff on top of it. I keep going back and forth as to which goal I think is more important!

  • Nice stuff!  I've been inspired and am putting together my own mashup this week during my students' test week.

    I had an issue getting the Bloom and Lens Flare components to play well together though.  Using something similar to what you have above, by doing this

               DrawModel(shipModel, ship.World);

               DrawModel(groundModel, Matrix.Identity);

               // Bloom the entire scene that we've just drawn.

               bloom.Draw(gameTime);

               lensFlare.View = camera.View;

               lensFlare.Projection = camera.Projection;

               lensFlare.Draw(gameTime);

    The lens flare component doesn't get blocked out when a 3D object covers the light source.

    Doing the following:

    DrawModel(shipModel, ship.World);

               DrawModel(groundModel, Matrix.Identity);

               lensFlare.View = camera.View;

               lensFlare.Projection = camera.Projection;

               lensFlare.Draw(gameTime);

               // Bloom the entire scene that we've just drawn.

               bloom.Draw(gameTime);

    ... blurs the lens flare as well, but gives allows the flare and light source to be blocked when an object comes between that and the camera.

    Am I missing something or is there no elegant fix?  I'm thinking it's the former. :)

  • Hey Alvin, I actually ran into the exact same issue!

    The problem is that the lensflare draw method actually does two unrelated things: first it checks visibility with the occlusion query, then it draws the flares. We want to draw the flares over the top of the bloom, but the visibility query needs to take place before the bloom rendering, as bloom will destroy the depth buffer data that it depends on to detect occlusion.

    My solution was to split up the LensFlareComponent.Draw method, so I could call the first part of it (up to and including the UpdateOcclusion method) before bloom, and then the remaining part (DrawGlow and DrawFlares) after the bloom.

  • Hey Shawn,

    Thanks! Nice fix.  Got it up and running as you suggested.  Looks sweet!

    Cheers!

  • The reason there is no FindComponent already is that components are not supposed to reference each other directly.  If you really need a component to be accessible from other components, you should create an interface containing only the things you want accessible, and add it as a service; see gamedev.stackexchange.com/.../13763

    A class-instance can be both a component and a service.

Page 1 of 1 (10 items)
Leave a Comment
  • Please add 1 and 5 and type the answer here:
  • Post