Design Patterns and Video Games

AWT GUI Facade (5): Load a level

In this article, I propose to use the Visitor Pattern to easily load a level. The GUI facade in the previous post is used to display the level. This pattern allows (among other things) to easily browse a data structure to extract information. In our case, it is an XML file created by "Tiled Map Editor" which is analyzed to load a level in memory.

This post is part of the AWT GUI Facade series

I start by creating a level in XML with Tiled:

Edit a level with Tiled Map Editor

You have to choose in the left panel Tile layer format as "XML". This produces an XML file that looks like this:

<map version="1.0" orientation="orthogonal" renderorder="left-up" width="16" height="16" tilewidth="16" tileheight="16" nextobjectid="1">
 <tileset firstgid="1" name="advancewars-tileset1" tilewidth="16" tileheight="16" tilecount="256">
  <image source="advancewars-tileset1.png" width="256" height="256"/>
 </tileset>
 <tileset firstgid="257" name="advancewars-tileset2" tilewidth="16" tileheight="16" tilecount="256">
  <image source="advancewars-tileset2.png" width="256" height="256"/>
 </tileset>
 <layer name="Ground" width="16" height="16">
  <data>
   <tile gid="41"/>
   <tile gid="41"/>
   <tile gid="41"/>
   <tile gid="41"/>
   <tile gid="41"/>
   <tile gid="41"/>
   <tile gid="41"/>
   <tile gid="41"/>
   ...
   <tile gid="41"/>
  </data>
 </layer>
 <layer name="Objects" width="16" height="16">
  <data>
   <tile gid="0"/>
   <tile gid="0"/>
   <tile gid="0"/>
   <tile gid="0"/>
   <tile gid="0"/>
   ...
   <tile gid="0"/>
   <tile gid="0"/>
   <tile gid="0"/>
  </data>
 </layer>
</map>

We first have a

tag with global properties, then two tags that define the tile sets used (one for the background, the other for the objects), and finally two tags that define the tiles for two layers (one for the background, the other for the objects).

To decode this file, I propose to use the XML parser included in the standard Java library. It is based on the Visitor pattern, and allows you to easily work on tags without having to worry about character decoding issues.

Visitor Pattern

The Visitor pattern can be presented as follows:

Visitor Pattern

The IElement interface represents the elements of the structure. In this example, there are two possible types of objects: ElementA and ElementB. The interface requires a method like accept() which takes as argument a visitor who implements the IVisitor interface. There can be several ways to visit the structure, and so many other methods similar to accept(). Parameters can also be used to influence the traversal.

The IVisitor interface is implemented by the user who wants to traverse the structured data. In general, the interface methods correspond to the different types of elements that compose it: in this example, they are the ElementA and ElementB classes. It is quite possible to imagine other cases that may interest a user, such as being at the beginning or at the end of an element. In all cases, the accept() method calls the methods of the IVisitor interface according to the cases encountered during its course. Within these methods, the user is free to view and modify the data.

Load a level

To read and store the level information, I define a Level class that contains this information in attributes:

public class Level
{
    private ArrayList<String> tilesetImages = new ArrayList();
    private int tileWidth;
    private int tileHeight;

    private int width;
    private int height;
    private int[][][] level;

    private int tilesetWidth;
    private int tilesetHeight;
    private int x;
    private int y;
    ...

To decode the file, I define a LevelLoader class that implements org.xml.sax.helpers.DefaultHandler. The LevelLoader class is the equivalent of the Visitor class in the diagram above, and DefaultHandler is the equivalent of IVisitor. I'm only interested in the startElement() method, equivalent to the visitElementA() and visitElementB() methods in the diagram above. This method is called whenever the parser encounters a new opening tag:

public class LevelLoader extends DefaultHandler {

    public void startElement (String uri, String localName,
                          String qName, Attributes attributes)
       throws SAXException {
        if (qName.equals("tileset")) {
            if (tilesetImages.isEmpty()) {
                tileWidth = Integer.parseInt(attributes.getValue("tilewidth"));
                tileHeight = Integer.parseInt(attributes.getValue("tileheight"));
            }
        }
        else if (qName.equals("image")) {
            if (tilesetImages.isEmpty()) {
                tilesetWidth = Integer.parseInt(attributes.getValue("width")) / tileWidth;
                tilesetHeight = Integer.parseInt(attributes.getValue("height")) / tileHeight;
            }
            tilesetImages.add(attributes.getValue("source"));                
        }
        else if (qName.equals("layer")) {
            if (level == null) {
                width = Integer.parseInt(attributes.getValue("width"));
                height = Integer.parseInt(attributes.getValue("height"));
                level = new int[height][width][3];                    
            }
            x = 0;
            y = 0;
        }
        else if (qName.equals("tile")) {
            int id = Integer.parseInt(attributes.getValue("gid"));
            if (id != 0) {
                if (id >= 257) {
                    level[x][y][2] = 1;
                    id -= 256;
                    level[x][y][0] = id % tilesetWidth - 1;
                    level[x][y][1] = id / tilesetWidth - 1;
                }
                else {
                    level[x][y][0] = id % tilesetWidth - 1;
                    level[x][y][1] = id / tilesetWidth;
                }
            }
            x ++;
            if (x >= width) {
                x = 0;
                y ++;
                if (y > height) {
                    throw new SAXException("Erreur dans le fichier");
                }
            }
        }
    }
}

The method is a long discussion based on the tag encountered, whose name is placed in the qName argument. For the "tileset" and "image" cases the information relating to a set of tiles is decoded. For the "layer" case, the level is initialized, and for "tile", the information relating to a tile is stored. The x and y attributes are used to store the coordinates of the next tile to be decoded.

Finally, I define a load() method in the Level class that uses the LevelLoader class to load the level:

public boolean load(String fileName) {
    try {
        SAXParserFactory spf = SAXParserFactory.newInstance();
        SAXParser saxParser = spf.newSAXParser();
        XMLReader xmlReader = saxParser.getXMLReader();
        xmlReader.setContentHandler(new LevelLoader());
        URL fileURL = this.getClass().getClassLoader().getResource(fileName);
        xmlReader.parse(fileURL.toString());
        return true;
    }
    catch(Exception ex) {
        return false;
    }
}

The Level class also has accessors/mutators (getters/setters) not listed here.

Display the level

To display the level, I use the GUI facade from the previous post:

Level level = new Level();
if (!level.load("advancewars-map1.tmx")) {
    JOptionPane.showMessageDialog(null, "Error when loading advancewars-map1.tmx", "Error", JOptionPane.ERROR_MESSAGE);
    return;
}

int scale = 2;

Layer backgroundLayer = gui.createLayer();
backgroundLayer.setTileSize(level.getTileWidth(),level.getTileHeight());
backgroundLayer.setTexture(level.getTilesetImage(0));
backgroundLayer.setSpriteCount(level.getWidth()*level.getHeight());
for(int y=0;y<level.getHeight();y++) {
    for(int x=0;x<level.getWidth();x++) {
        int index = x + y * level.getWidth();
        backgroundLayer.setSpriteLocation(index, new Rectangle(scale*x*level.getTileWidth(), scale*y*level.getTileHeight(), scale*level.getTileWidth(), scale*level.getTileHeight()));
        if (level.getTileset(x, y) == 0) {
            Rectangle tile = new Rectangle(level.getTile(x, y), new Dimension(1,1));
            backgroundLayer.setSpriteTexture(index, tile);
        }
    }
}

Layer groundLayer = gui.createLayer();
groundLayer.setTileSize(level.getTileWidth(),level.getTileHeight());
groundLayer.setTexture(level.getTilesetImage(1));
groundLayer.setSpriteCount(level.getWidth()*level.getHeight());
for(int y=0;y<level.getHeight();y++) {
    for(int x=0;x<level.getWidth();x++) {
        int index = x + y * level.getWidth();
        groundLayer.setSpriteLocation(index, new Rectangle(scale*x*level.getTileWidth(), scale*(y-1)*level.getTileHeight(), scale*level.getTileWidth(), scale*2*level.getTileHeight()));
        if (level.getTileset(x, y) == 1) {
            Rectangle tile = new Rectangle(level.getTile(x, y), new Dimension(1,2));
            groundLayer.setSpriteTexture(index, tile);
        }
    }
}

gui.createWindow("Load a level with the Visitor Pattern",
    scale*level.getTileWidth()*level.getWidth(),
    scale*level.getTileHeight()*level.getHeight());

The code of this post can be downloaded here:

awtfacade05.zip

To compile: javac com/learngameprog/awtfacade05/Main.java
To run: java com.learngameprog.awtfacade05.Main

Contents - Next: Game Loop pattern