A Framework for Screens & Logic

I’m going to dive into some software architecture for a little bit and describe the system that I’m using for overall interactions and screens in our game. This is the first of two posts on this topic.

In developing many games, one problematic thing I’ve seen is that developers or teams start out by rolling some high performance 3D rendering system, and the first time they run the game, they boot right into some 3D world with a few models to control and drive or shoot or whatever. They pat themselves on the back, and they pull up a clever console so that they can load a different level or model or racetrack, and then cheer because they think they’re 90% of the way done to building a great game.

They’re wrong.

There are so many other things that go into making a game, and certainly into making a game that is user friendly.  These things are often forgotten until near the end of the production cycle, which is why there are countless games with terrible audio, music, menus, transitions and so on. It’s why I chose to solve this problem up front (even though I do have a console) and develop a good system for switching between game modes, as well as segmenting my display and logic functionality.  Here’s what I did.

Because this game is built off of Adobe’s AS3/Flash, I knew I had to handle three very specific events in my top level Game Loop: Frame Enter, Key Up, and Key Down. Everything else we can handle separately. When the game starts up, it sets up listeners for those events at the highest level (the stage) and assigns methods in my base game engine to respond to those events.

Next, we set up an interface: IDisplayLayer. This interface defines a contract of a set of methods for any “mode” of the game, whether it’s a menu, a settings screen, a game mode or a minigame. Every IDisplayLayer exposes the following methods:

  • onKeyDown(keycode)
  • onKeyUp(keycode)
  • onEnterFrame(deltaT)
  • show(previous)
  • hide()
  • load()

There are a few others, but they’re not that important for this discussion.

Any game modes are classes that implement the IDisplayLayer interface. I’ll get into their internal logic below, but from the point of view of the game engine, all it needs to know about is which IDisplayLayer is currently active, which it tracks in the variable m_currentDisplay. Because the base engine is handling keyboard input and frame updates, our handlers for those events simply call m_currentDisplay.onEnterFrame() with the current time difference from the last frame. The same goes for keyboard events.

The great part about this is that when we switch game modes, we automatically pause that mode. An inactive IDisplayLayer no longer receives frame ticks, nor responds to keyboard input, because the engine doesn’t pass those events along. You would not believe the number of poorly written codebases where seemingly random things would happen while typing, all because some invisible screen was still responding to keyboard input. This avoids that entirely.

The other methods, show() and hide(), allow us to do cleanup or setup as we switch between IDisplayLayers.  Flash uses a nested hierarchy of DisplayObjects (or a “scene tree”), where each object is a child of the stage, or another object. If a higher level object is made invisible, or deactivated, all it’s children are also invisible. Thus, for each display layer, the engine creates a parent sprite that the display layer attaches to inside of the load() method. The engine can then hide that display by setting the parent sprite visible property to false, or show it by setting visible to true. When we switch displays, first we call m_currentDisplay.hide(), which gives the display a chance to do any housekeeping for going away, like finishing up an animation (see next week’s post). It also sets the parent sprite visibility for m_currentDisplay to false.  Then the engine calls show() on the new IDisplayLayer that we want to show, which sets the parent sprite visibility to true as well as doing any return housekeeping, and then set m_currentDisplay to be the new display layer.

public function display(newLayer:IDisplayLayer):void
{
    Debug.assert(newLayer != null,"EngineBase:display() - parameter 'newLayer' was null");

    if(m_currentDisplayLayer)
    {
        m_currentDisplayLayer.hide();
    }

    newLayer.show(m_currentDisplayLayer); // Pass in the old layer in case it needs to know
    m_currentDisplayLayer = newLayer;
}

What about setting up those IDisplayLayers internally? We want to separate out the display from the logic for each game mode, because we don’t want the complexity of how the world is displayed to be closely coupled with the underlying data that represents it. We may want to start with numbers or text for a display, and then move to animated shapes, but none of that should affect the underlying data representation.

We also want to ensure that these classes are loosely coupled. If we make changes in any one file, we shouldn’t have to recompile a whole bunch of other files. So we need to minimize the dependencies between classes. Luckily, Interfaces and Factories come to the rescue!

Each display mode gets a concrete implementation class, let’s say GameDisplay, that implements the IDisplayLayer interface so that the game engine can interact and communicate with it. GameDisplay is responsible for all non-keyboard input (mouse interaction), graphics, audio, running animations, etc. but it doesn’t really understand anything about the underlying game mode logic. It has two responsibilities: First, handle input, and second, show pretty things on the screen (as well as play nice sounds).

GameLogic is a separate class that does everything GameDisplay doesn’t do, and nothing that it does. GameLogic tracks the game state, maybe the position of pieces on a board, it understands the logic of valid and invalid moves, scoring, and so on, but it doesn’t care about what these things look like. In fact, one could write several different GameDisplays that interact with the same GameLogic, and these would just be different ways of visualizing the game. Likewise, one could create multiple GameLogics for the same GameDisplay, maybe like different game modes, and they would just run different rules.

So it’s good to separate these two out. And while it’s tempting to simply create a GameLogic right in the constructor for GameDisplay, that’s a path fraught with disaster.

private var m_logic:IGameLogic;

public function GameDisplay()
{
    m_logic = new GameLogic(this); // DON'T DO THIS
}

Why? Well, compiling is still fast, but if you just create the logic directly, every change in GameLogic will force a recompile to GameDisplay. Also, every public function in GameLogic will be exposed to GameDisplay, and you may not want that. GameLogic may need to communicate with other classes via public methods, but some new developer to the project may not know which methods are okay and which are verboten, creating complexity, confusion, and the opportunity for bugs.

So we’re a little clever about this. Some may say too clever. However, we’ll solve all these problems with two more Interfaces, and a Factory Design Pattern. First, we’ll figure out all the methods in GameLogic that GameDisplay needs to call. We’ll put those methods in the IGameLogic Interface. Next we’ll figure out all the methods in GameDisplay that the logic needs to call back into, and we’ll put those into IGameDisplay. GameLogic will implement the IGameLogic interface, and GameDisplay will implement both the IGameDisplay and the IDisplayLayer interfaces.  Now, we can make the constructor to GameLogic take an IGameDisplay as a parameter, and pass in this when we instantiate it in GameDisplay, returning an IGameLogic reference.

public class GameLogic implements IGameLogic
public class GameDisplay implements IGameDisplay, IDisplayLayer

But isn’t that what I stated not to do above? It is, because GameDisplay still needs a reference to GameLogic in order to construct it, when all we want GameDisplay to have is a reference to the interface IGameLogic. Enter the Factory.

public class LogicFactory
{
    public static function BuildLogic(display:IGameDisplay):IGameLogic
    {
        return new ICEHackerGameLogic(display);
    }
}

Now, GameDisplay constructs it’s reference to IGameLogic like this:

private var m_logic:IGameLogic;

public function ICEHackerGameDisplay()
{
    m_logic = LogicFactory.BuildLogic(this,target);
}

No GameDisplay is entirely decoupled from the concrete class of GameLogic.

Five Classes - New Page

This seems like a bit of complexity to get some straightforward functionality, but it winds up paying off in testability and maintainability. The first part allows us to quickly spin up new game modes by developing a class that implements the IDisplayLayer interface, and we know our engine can handle switching to and from this screen with ease, and also without breaking any existing screens. Likewise, we can create new game modes that are easily maintainable and decouple logic from display simply and quickly, while defining straightforward methods of communication between those modes.

Next week, I’ll talk about the animation system that we use, and how it solves some clever problems.

One comment

Post a comment

You may use the following HTML:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>