October 28, 2012

Design Patterns in Game Programming (I): Observer / MVC

Design patterns are well established in business software, but there is no reason to not benefit from using some in game programming. After all, programming is programming.

Design patterns are often either loved or hated but neither position should have its place in professional software development. You shouldn't work with a pattern check list at hand and integrate them without reason. And if you do not like patterns, there are most likely more of them hidden in your very own programming work that you are aware of.
They are also a means of communication and common language and knowledge between developers.

Patterns are not restricted to programming, but can be found in many other domains. Human behaviour often follows established patterns as do created novels, movies and songs.

 
The observer is probably - after the infamous singleton - the best known design pattern in software development.

Here is a simple but useful example.
Let's design a game which has the following objects:
  • a scene to represent the whole game world full of acting characters and the environment
  • several views for the visual part, like the main game view, HUD view for statistics, a debug view 
  • controller for the game logic
Whenever the controller decides to modify the world, like for instance spawn another evil monster or remove it again after being killed by the player, the changes must be reflected by all views.
First, the controller modifies the scene. Then, we make the views passively observe the game world.

public interface IPuppetLifecycleListener {
    void onPuppetAdded(Puppet puppet);
    void onPuppetRemoved(Puppet puppet);
}


Each observer implements this interface and gets called whenever characters are added to or removed from the game scene.
For example, we have two views: a main game view and an additional small radar sub view to let the player see at a glimpse where all the enemies are hiding.

public class TileMapView implements IPuppetLifecycleListener {
    private List spritePuppets;
   
    @Override
    public void onPuppetAdded(Puppet puppet) {       
       SpritePuppet viewPuppet = 

          puppetHouse.createViewPuppet(puppet);
       ...
    }
      
   
    @Override
    public void onPuppetMoved(Puppet puppet) {

       ...
    }

public class OverlayHudView implements IPuppetLifecycleListener {
    private ListViewPuppetCast radarPuppets;
   
    @Override
    public void onPuppetAdded(Puppet puppet) {       
        if (puppet.getActorClass().equals(ActorClasses.BIONIC)) {
            rivalCount++;
            RadarPuppet radarPuppet = new RadarPuppet(puppet);
            radarPuppets.addPuppet(radarPuppet);
        }
    }


    @Override
    public void onPuppetMoved(Puppet puppet) {

       ...
    }
    ....
}


Both view types have their own kinds of visual puppets. For the tile map are sprites created, for the radar view small filled circles are drawn.

The publisher of any puppet event is the game scene which holds a list of observers:

public class GameScene {
    private PuppetCast puppets;

    private List<IPuppetLifecycleListener > puppetLifecycleListeners;
 

    public GameScene () {          
        puppets = new PuppetCast();
        puppetLifecycleListeners = new ArrayList<>();
    }

 

    public void addPuppetLifecycleListener(IPuppetLifecycleListener listener) {
         puppetLifecycleListeners.add(listener);
    }

 

    public Puppet addPuppet(Puppet puppet) {
        puppets.add(puppet);

        
        for (int i = 0; i < puppetLifecycleListeners.size(); i++) {
            IPuppetLifecycleListener listener = puppetLifecycleListeners.get(i);
            listener.onPuppetAdded(puppet);    

        } 
     ....
}

Very simple and very useful. Actually, this is is a model-view-controller implementation for games.
Benefits are:
  • observers can take it easy and only get active when called
  • on each event individual additional actions can happen like playing animations or sounds
  • dependencies are reduced, no global object sets are referenced throughout the code base
  • the internal representation of the game scene is hidden from the views
  • only one clearly defined place to handle each external event
Here, no dependency is pointing from the scene to the views. The scene does not know - and does not need to know - whoever cares about its puppet events. The scene could even run without any views at all in a distributed multiplayer environment.
Thus, the complete scene package can be independent of the view package. Note, that to accomplish that, all observer interfaces are placed into the scene package and not into the view package.

The same technique can be applied for the other way round, for handling UI events created in views:
  • views allow observers to register for UI events
  • view interfaces are placed into the view package and are implemented by controller classes
  • thus, views do not need dependencies to their controllers

Observers can be added once at game start or during level changes.

Class sketch:


MVC roles:
  • model: ClientScene, Puppet, PuppetListener
  • controller: SceneController
  • view: SceneView, TileMapView, SpritePuppet, OverlayHudView, RadarPuppet

No comments:

Post a Comment