Observer Pattern

From GPWiki
Jump to: navigation, search

The Observer shows how to separate problem areas and create a loose event driven object relationship.

Definition

Problem

Imagine that you are making a first person shooter 3D action game like Half-Life. You have a main character that you are controlling, and you have a console that you can type commands in. The game should feature multiple people online gameplay using a dedicated server. To maximize the capacity of the server, it should not display graphics on screen but use all resources to handle game logic and networking. It should, however, keep a log of ingame actions to create statistics and check for cheaters.

The game will need to have the following features: Multiple input modes (moving your character and writing commands), ability to disable graphical routines, an action logging system, easy implementation of a multiplayer mode.

How would you go about making a flexible and stable architecture for that system? The Observer pattern and the Model-View-Controller architecture, an extension of the Observer pattern, teaches how to create such a dynamic architecture.

Context

  1. An object (the subject) register events.
  2. One or more objects (observers) want to know when an event occurs.

Solution

  1. Define an observer interface type that observer classes must implement.
  2. The subject holds a collection of observer objects.
  3. The subject has a method that allows for adding observer objects to the collection.
  4. Notify all the observer objects when an event occurs.


Observer pattern:

Formal class diagram of the Observer pattern

The Subject holds the raw data, and the Observer displays the data.


Model-View-Controller architecture:

Formal class diagram of the Model-View-Controller architecture

The Model holds the raw data and logic, the Controller invokes methods of the Model, and the View displays the data in some way.


Conceptually the Model-View-Controller architecture can be explained using a more concrete example. Below is a standard living room sound system, illustrated as a Model-View-Controller system.

Illustration of a sound system
  • Controller: The remote control.
    The remote control controls the stereo system and is able to change the volume, the track being played etc..
  • Model: The stereo system.
    The sound system reads data from the CD, processes the data according to the selected bass, discant and midtone levels and pass the processed data to its output ports.
    It knows nothing of the presence of the remote control. For all it knows, there might not be a remote control or there might be several. However, it still reacts to commands passed from controls.
    The model does not know about output devices either, it just broadcasts data to those willing to listen.
  • View: The speaker and head phones.
    Both the speaker and head phones use the audio data to vibrate and thus create sound, but it would also be possible to plug in a device that analysed or recorded the audio data. This works even though the sound system knows nothing about the type of device that is plugged in.

Example Usage - Event Based

Let's try applying the Model-View-Controller architecture to a FPS game system like the one described above, but highly simplified.

Class diagram of example game system
Sequence diagram for GameEngine.modifyWorldData()


Pseudo code:

interface Input
{
    void handleInput();
}
 
class TexturalInput implements Input
{
    private GameEngine ge;
 
    public TexturalInput( GameEngine ge )
    {
        this.ge = ge;
    }
 
    public void handleInput()
    {
        // If we've got an input
        ge.modifyWorldData( "Text input" );
    }
}
 
// For the sake of simplicity, I'll like the graphical classes out but
// the functionality is basically the same as its textural counterpart
class GraphicalInput implements Input
{
    private GameEngine ge;
 
    public GraphicalInput( GameEngine ge ) { /* Code */ }
    public void handleInput() { /* Code */ }
}
 
class GameEngine
{
    private List<Display> displays;
    private String worldData;
 
    public GameEngine()
    {
        displays = new List<Display>();
        worldData = "Nothing";
    }
 
    public void attach( Display d )
    {
        d.setGameEngine( this );
        displays.add( d );
    }
 
    public void updateAll()
    {
        for ( Display d : displays ) {
            d.update();
        }
    }
 
    public void modifyWorldData( String newData )
    {
        worldData = newData;
        updateAll();
    }
    public String getWorldData() { return "WORLD DATA: " + worldData; }
}
 
interface Display
{
    void setGameEngine( GameEngine ge );
    void update();
    void draw();
}
 
class TexturalDisplay implements Display
{
    private GameEngine ge;
 
    public void setGameEngine( GameEngine ge )
    {
        this.ge = ge;
    }
 
    public void update()
    {
        draw();
    }
 
    public void draw()
    {
        print( ge.getWorldData() );
    }
}
 
class GraphicalDisplay implements Display
{
    private GameEngine ge;
 
    public void setGameEngine( GameEngine ge ) { /* Code */ }
    public void update() { /* Code */ }
    public void draw()   { /* Code */ }
}

Test:

GameEngine ge = new GameEngine();
ge.attach( new TexturalDisplay );
 
TexturalInput ti = new TexturalInput( ge );
ti.handleInput();

Output:

WORLD DATA: Text input

Here I call the handleInput() method of TexturalInput manually for the sake of the example. Normally you would hook the input classes up with the controller handling routines. Let's go back and see how our system could provide a framework for all the required features of the game. The game system need;

  • Multiple input modes - CHECK. We can have different methods of input like TexturalInput and GraphicalInput.
  • Ability to disable graphical routines - CHECK. If we don't add a Display class, nothing is outputted.
  • An action logging system - CHECK. The TexturalDisplay class is in effect a logging system outputting game data to a stream.
  • Easy implementation of a multiplayer mode - CHECK. Sort of checked, at least. If we make the input classes more abstract - to take written or numerical commands instead of checking for controller input, for example - then making the system networked would "just" mean sending the commands to the server as well as to the local input class.

Example Usage - Passive Controller

The Observer pattern or the Model-View-Controller architecture isn't limited to large frameworks of classes such as a game system, but can also be used on single classes. In this example we'll try to model a military tank using the MVC architecture. We'll use a somewhat different approach compared to the last example, because we'll let our model class read the data in our controller class instead of having the controller class invoke methods on the model.


Below the tank class system is illustrated. Notice that the controller class has pretty abstract (accessor) methods, and not methods related directly to certain keys or buttons. This increases the flexibility of the system, as supporting another physical controller would be made much easier because it can use the same input interface. The run() method of the tank class symbolizes a single frame update for the tank in a game loop.

Class diagram for example game system
Sequence diagram for Tank.run()


Pseudo code:

class TankController
{
    public float getSpeed() { /* Code */ }
    public float getTurretAngle() { /* Code */ }
}
 
class Tank
{
    private List<View> views;
    private TankController control;
 
    public Tank()
    {
        views = new ArrayList<View>();
        control = new TankController();
    }
 
    public void attach( View v )
    {
        views.add( v );
    }
 
    public String getTankData() 
    {
        return "Tank data here";
    }
 
    public void run()
    {
        newTankSpeed = currentTankSpeed + control.getSpeed();
        newTurretAngle = currentTurretAngle + control.getTurretAngle();
 
        updateAll();
    }
 
    public void updateAll()
    {
        for ( View v : views )
        {
            v.update();
        }
    }
}
 
interface View
{
    void update();
    void draw();
}
 
class TankView implements View
{
    private Tank tank;
 
    public TankView( Tank t )
    {
        tank = t;
    }
 
    public void update()
    {
        draw();
    }
 
    public void draw()
    {
        String tankData = t.getTankData();
        print( "Data: " + tankData );
        // draw tank
    }
}

Test:

Tank t = new Tank();
t.attach( new TankView( t ) );
 
t.run();

Output:

Data: Tank data here

Using the Observer pattern and the Model-View-Architecture require typing in some more code than you'd otherwise have to, but it pays off tenfold later in development. It'll provide better stability, extendability and maintainability. It's one of the most widely recognized computer science design patterns around, so use it and save yourself a lot of trouble.

Additional Information