Friday, September 9, 2011

Game Level Architecture

Today, I will walk you through some common pitfalls I see game programmers make when designing their game level architectures. By the end of this post, you will know how to write an architecture that can support hundreds of game levels.

Inheritance
Inheritance is a cool feature of Object-Oriented programming languages, but too often programmers treat it as a one-size-fits all solution for every architecture problem they encounter.

Where an Inheritance Solution Fails, Hard
Flixel uses Objects called FlxStates to segment different sections of the game into seperate ActionScript class files that the programmer writes. One state runs at a time, and switching states destroys the old FlxState before switching to the new FlxState. This system is great for writing things like menu screens,  where each screen sits in a different ActionScript class file.

// file TitleScreen.as
public class TitleScreen extends FlxState
{
     // ...
}
// file CreditsScreen.as
public class CreditsScreen extends FlxState
{
     // ...
}


However, you would never want game levels to extend FlxStates.


public class Level1 extends FlxState
{
     // ...
}

Doing this would pack the following into the same class
  • Level Data
  • Gameplay Logic (win/lose conditions)


Is this a problem? Not if your game is only one level.  :D


Now consider Level 2:

public class Level2 extends FlxState
{
     // ...
}

Writing Level 2 this way means that you will need to copy/paste your gameplay logic from Level 1 into Level 2. Sound less than ideal? Now imagine that your game has hundreds of levels. There is no way any sane person is going to copy/paste the same gameplay code hundreds of times. 

Another way you can try to write Level 2 is like this:

public class Level2 extends Level1
{
     // ...
}

Of course, this is equally insane for a different reason. Yes, you can inherit the gameplay logic from Level 1, but you will also inherit all the Level Data from Level 1, which you probably don't care about. Apply this scheme to hundreds of objects, where "Level3 extends Level 2", etc. and your Level99 is going to be obnoxiously inflated from the previous 98 Levels. Some other problems:
  • Every level must have the same gameplay logic 
    • (or you must haphazardly override functions all over the place)
  • Any change you make to Level 1 will affect every level that inherits from it.

A Better Solution
Consider the following:

public class GameLevel extends FlxState
{
     // Level Data
     // Gameplay Logic
}
public class Level1 extends GameLevel
{
     // ...
}
public class Level2 extends GameLevel
{
     // ...
}

This is actually a reasonable Game Level Hierarchy, but we still have a problem where the GameLevel contains both Level Data and Gameplay Logic. This means that we can't have different win/lose conditions for different levels. Is there a way to separate the Level Data from the Gameplay Logic? Absolutely.


The Best Solution
public class PlayState extends FlxState
{
     private var gameLevel:GameLevel;
     // Gameplay Logic
}
public class GameLevel
{
     // Level Data
}
public class Level1 extends GameLevel
{
     // ...
}
public class Level2 extends GameLevel
{
     // ...
}

This is the Game Level Architecture that I use in Bullet Time Ninja. Here, the PlayState creates a GameLevel, knows about all the gameLevel's data, and runs it's gameplay logic on it. What's cool about this architecture, is that you can easily write another class that has different Gameplay Logic, but uses the same levels. One possibility:
  • PlayState: the player wins if he or she reaches the goal at the end of the level
  • CollectState: the player wins if all jewels in the level are collected
  • DestroyEnemiesState: the player wins when all enemies in the level are killed
BAM. Your game levels are totally reusable for lots of cool things now.


Transitioning Between Levels
In its simplest form, all that you really need to know to transition from one level to the next is:
  • An array that contains all the levels*
  • The current level number that you're on
*For performance reasons, don't store actual object instances of your levels in an array. ActionScript 3 allows you to store Class objects in an array. That way you can do something like this:

public var classReference:Class = Level1;

// later...
public var myLevel:GameLevel = new classReference(); 
With an array of Classes, you save a lot of memory, since you only need to instantiate one GameLevel at a time.

I recommend making the array and current level number "public static variables" for easy access. Then, the Gameplay Logic in your PlayState class can simply read the data decide what level to pick next. When your levels system gets more complicated, you can begin writing a LevelManager class.


Next Steps
The game level architecture I have described supports hundreds of levels, but it also requires writing a new class for each level. The next step is to store your levels as data files, and have one class in your game import the data file and set up a game level. Perhaps I can discuss this in a future blog post.


Summary
  • Game levels should only contain data about that level
  • Objects that extend FlxState should only contain Gameplay Logic, and read data from the level
  • Let your Gameplay Logic determine what level to jump to next

Questions or comments? Feel free to post a reply below.

Your partner in science,
-Greg

2 comments:

  1. For what it's worth, this is the structure the DAME editor uses if you export it to Flixel. I've got autogenerated BaseLevel.as and Level_Level0.as files.

    I suspect it's a bad idea to modify the Level_* files directly to add level-specific knowledge, though, because that means when you re-export the level from DAME you'll risk overwriting that data unless you remember to check "Export to CSV only".

    ReplyDelete
  2. I've just started my first Flixel project, so my experience is very limited. Still, I think that instead of that class per level solution that "next step" is "the best solution". I have just one class for all the levels, since I don't see much use for having separate classes for each level. The class just reads an embedded XML file based on the level the player is currently on, and it seems to be working quite well.

    Any way, I like your game. It's more innovative than most games, it has a nice ninja feeling and it offers an appropriate challenge!

    ReplyDelete