次の方法で共有


Basic Game Engine Structure

I've seen a couple of posts on the forums asking what the best way to structure a game is. For what it's worth, here's a structure I've used in a few of my games. The design itself isn't too complicated, but it's pretty flexible, and will keep your code simpler in the long run. I think the Spacewars code uses a similar design, but I haven't looked at it in depth (slightly embarrassing.) I won't go into rendering engine structure; that could be a future post, if enough people ask. This is just a way of laying out all your GameComponents so that you're not constantly tripping over your own feet.

 
Edit:

Just a small disclaimer - a sample
has been released on the Creators Club website which uses this basic
design, but is more flexible. Plus, the source is available! As much as
it pains me to say it, it's probably a more useful resource than this
post.

 

The design isn't too technically complicated, but you should at least be familiar with what a state machine and a stack are, and how to use generics.

 

The Problem:

To understand why this structure is useful, I'll first explain the problem it's trying to solve. Imagine a game that has a menu and is pauseable. The game is going to have to keep track of a lot of different GameComponents, and is going to have be constantly fiddling with the Visible and Enabled properties on them, depending on what state the game is in. For example, when you go from the menu to the game, you're going to want to set Visible on all of the menu's UI elements to false. When you pause the game, you want to stop updating everything, but you may want to keep drawing it like normal. It doesn't take long for this kind of ad hoc structure to turn into a bug-prone code circus.

Say for example, you're writing the code that pauses your game.  There's about fifty different GameComponents in your game now, and you have to remember all of the ones that should be disabled when the pause key is pressed. What if you forget to disable the bad guys? Now you've got a bug where the player can pause the game, and everything pauses - everything except the bad guys, who happily carry on blasting your ship apart.

What we need is a better way to keep the GameComponents organized. How do we do it?

 

The Structure:

Basically, we'll turn the game into a sort of state machine. The whole design will center around two classes, GameScreen and GameScreenManager. A GameScreen is a grouping of GameComponents, and roughly corresponds to the current state the game is in: at the menu, waiting to start a game, playing the game, paused, etc. For this example, we'll have the MenuScreen, PlayScreen, and PauseScreen.

Each of the screens keeps the GameComponents it needs in a GameComponentCollection, just like Framework.Game does. GameScreen also has Update and Draw functions that call Update and Draw on their components.

The GameScreenManager is in charge of keeping track of all the different GameScreen. Internally, this is implemented as a stack of active screens.

Here's the structure we have so far (sorry about the shoddy diagram; VS2005's class designer is cool, but I'm not very good at it. There should be a "has-a" association between GameScreenManager and GameScreen - the ActiveGameScreens stack.)

Ok, so why a stack? At first glance, it seems like an odd way to organize your screens. It allows for some pretty cool behavior to be implemented simply, though. Let's say, for example, that when the user pauses the game, you just want to stop everything from updating, and put an image over the top of everything that says "Paused." With a stack, that's easy: you can just push a new PauseScreen onto the stack. When the user unpauses, pop the PauseScreen back off again. To implement the "everything still draws, but nothing updates" functionality, we'll add two new properties to GameScreen. BlocksDraw says that no screens under this one can draw, and BlocksUpdate does the same thing for update.

Now, we'll give GameScreenManager Update and a Draw functions. In his Update, he'll go through the stack of ActiveGameScreens from top to bottom, calling Update on everyone until he finds someone that BlocksUpdate.

Draw is a bit more complicated, since we want things on the top of the stack to draw after things on the bottom. Bear with me here, think of pause again: we want to draw the game screen, then the pause screen over the top. In order to get this behavior we have to draw from bottom to top. But, in order to know what to draw we have to go through the stack from top to bottom, looking for BlocksDraw. So, we'll go through twice: Draw could be implemented using a temporary list called screensToDraw. First go through the stack from top to bottom, adding to screensToDraw until you find one that BlocksDraw. Then go through screensToDraw backwards drawing all the screens. Iterating through the screens twice is not likely to cause any kind of noticeable performance hit: it's hard to imagine cases where you'd have more than around five screens on the stack.

 List<GameScreen> screensToDraw = new List<GameScreen>();
foreach (GameScreen screen in ActiveGameScreens)
{
    screensToDraw.Add( screen );
    if (screen.BlocksDraw)
        break;
}

for (int i = screensToDraw.Count - 1; i <= 0; i--)
{
    screensToDraw[i].Draw( gameTime );
}

 

Different Screens:

It works for more than just pausing, too. You could split your HUD and main game into two separate screens, making their code easier to understand. You could go even further, splitting your HUD into separate elements, and then give the user control over what UI elements they want to see without completely muddying up your code.

In a first person shooter, you might have a console that slides down and takes up most of the screen. This could be a screen with BlocksDraw and BlocksUpdate both set to false. A full screen in game menu (the kind you hit esc to get to) would be a screen with BlocksDraw and BlocksUpdate both set to true. BlocksDraw is set to true for this one because there's no point in rendering the whole game if the menu is just going to draw over it anyway.

 

Switching States:

If we give GameScreens a reference back to the GameScreenManager, they can access the ActiveGameScreens stack in order to move the game from one state into another. To replace one screen with another, like a MenuScreen might want to, just pop the current one off, then push the new one on. We saw above how pause would work: PlayScreen watches for the pause key, and pushes on a new PauseScreen. PauseScreen watches for pause button, and pops itself off. GameScreenManager's constructor takes a GameScreen for the startup screen.

 

Initialization - A Complication:

When a game is first created, the graphics device is created, and then the base Framework.Game.Initialize is called. This function then calls Initialize on all of the GameComponents in the Components collection. Internally, the DrawableGameComponent.Initialize function does some magic to make DrawableGameComponent.GraphicsDevice and LoadGraphicsContent work. Obviously, those are important, so we need Initialize to be called on all the DrawableGameComponents.

However, since our screen's components aren't in Game.Components, Game won't call Initialize for us: we're going to need to do it ourselves. We'll add an Initialize function to GameScreen, which will call Initialize on all of its components. Now, every time you create a GameScreen, remember to call Initialize on it - and make sure the graphics device is ready when you do so!

It all sounds complicated, but just remember to Initialize your GameScreens, and create your GameScreenManager in your game's Initialize, not the constructor. Even if you don't understand that part above, if you do those two things you'll be fine.

 

Final Design:

Again, sorry about the diagram; There should be a "has-a" association between GameScreenManager and GameScreen - the ActiveGameScreens stack.

 

Potential Improvements:

To reduce stress on the garbage collector, advanced users might want to keep a cache of screens somewhere, rather than instantiating fresh ones every time. Take it easy on the garbage collector, and your performance will thank you. smile_regular

Comments

  • Anonymous
    December 13, 2006
    You beat me to it. I was thinking about doing something on this (it comes up a lot in game dev forums), but yours is probably better written than what I could have done. :) For what it's worth, I'm linking to this from my blog.

  • Anonymous
    December 14, 2006
    I did come up with an idea for one improvement - if a window's BlockDraw is set, the ones below it could be drawn greyed out. This would require a little change to the draw, probably overloading it with a flag to specify whether to draw regularly or greyed out.

  • Anonymous
    December 15, 2006
    Very good article. Maybe one of the search processes for the Draw call can be erased, at least partially, by taking advantage of the update method. I mean when the update method goes through the stack in a top-down manner it also verifies whether the screen must be drawed or not. To ways to follow from here: either you let that search to "scan" all screens objects in the stack regardless whether a BlockUpdate was found or you stop when the BlockUpdate was found and continue the search in the draw method in case the BlockDraw was not yet found. Another approach would be using "events". Again, very good series of articles.

  • Anonymous
    December 22, 2006
    The comment has been removed

  • Anonymous
    January 16, 2007
    The comment has been removed

  • Anonymous
    February 19, 2007
    The comment has been removed

  • Anonymous
    February 19, 2007
    The comment has been removed

  • Anonymous
    March 03, 2007
    I came across this a little while ago and have been watching the progress, Game Maker Studio is a 2D

  • Anonymous
    April 10, 2007
    nickspacek:  I am wondering about the same thing.  I guess there is only one way to find out - try it!

  • Anonymous
    April 22, 2007
    for (int i = screensToDraw.Count - 1; i <= 0; i--){    screensToDraw[i].Draw( gameTime );} this needs to be: for (int i = screensToDraw.Count - 1; i >= 0; i--){    screensToDraw[i].Draw( gameTime );} note the change of <= to >= thanks, Ziggy

  • Anonymous
    May 09, 2008
    I came across this a little while ago and have been watching the progress, Game Maker Studio is a 2D editor and Engine for Tile based RPG Systems. In the month of January the system was released in CTP for us all to try and see how it goes. Included on

  • Anonymous
    August 27, 2010
    @zygote: he's iterating backwards through the collection. Your change would cause it to never run, because i will always be greater than 0 as long as screensToDraw has items in it. <= 0 is correct because it will stop after it reaches the item at index 0, the first one.

  • Anonymous
    April 18, 2011
    A simple implementation: https://gist.github.com/925477 Sincerely, Kaji