I continue the extension of the graphic facade (see previous article), with here the addition of a very classic form of drawing for video games: tiles. These make it possible to compose an image using small images called tiles. There are several types of tiles, I propose in this article the simplest case with rectangular tiles.
This post is part of the AWT GUI Facade series.
For the implementation, the approach that I propose to follow is similar to the one used for images: we use a factory (Factory Method Pattern) to instantiate layers that allow drawing with tiles. On the GUIFacade main interface side, I add the following two methods, one to create a layer, and the other to draw it:
For layers (Layer interface), I propose to manage the tiles with one texture image by layer, and with tiles of a fixed size. In addition, the rendering of tiles can be memorized by the layer: in doing so, it is sufficient to indicate which tiles should be displayed in which places, and then the layer will display these tiles continuously. These renderings are called in this article sprites, so as not to confuse them with tiles (e.g., a tile is just a small picture, and a sprite is a small picture displayed at a specific location on the screen).
At this stage of design, layers able to renders independently may seem unnecessary: we could continuously consult the game data for rendering. Later, when it will be necessary to make multi-threaded synchronization, independent layers are a blessing. In addition, for low-level implementations (such as OpenGL), it allows you to use vertex buffers, which greatly speeds up rendering. These properties can be obtained via an interface like the following one:
- The getTileWidth() and getTileHeight() methods return the size of the tiles (for example, 16 by 16 pixels);
- The getTextureWidth() and getTextureHeight() methods return the number of tiles in the texture image, in width and height;
- The setTileSize() method is used to set the size of tiles;
- The setTexture() method is used to define and load the texture image;
- The setSpriteCount() method is used to set the number of sprites managed by the layer;
- The setSpriteTexture() method is used to define the tile used by a sprite. The tile rectangle defines the used tile: they are coordinates in tiles, and not in pixels. For example, if the tile size is 16×16 pixels, and the tile rectangle is (3,4,1,2), then the tile location in pixels is (3 * 16,4 * 16,1 * 16,2 * 16) = (48,64,16,32). By convention, if the rectangle argument is null, then the sprite is disabled;
- The setSpriteLocation() method is used to define the location where the sprite is drawn. The rectangle is in pixels, and may be larger or smaller than the tiles used, to achieve a zoom effect.
AWT Implementation
For the implementation, we still use the AWT library, with the AWTLayer class (the methods of the Layer interface are not repeated):
- The tileWidth, tileHeight, textureWidth, and textureHeight attributes define the size of the tiles and the texture image;
- The texture attribute contains the texture image;
- The texture and location arrays contain all sprite information: tiles to use and render locations;
- The draw() method draws the entire layer.
Most methods are accessors / mutators (getters / setters) whose implementation is very simple. The draw () method is less obvious:
public void draw(Graphics graphics) {
for (int i = 0; i < locations.length; i++) {
if (textures[i] != null) {
graphics.drawImage(texture,
locations[i].x,
locations[i].y,
locations[i].x + locations[i].width,
locations[i].y + locations[i].height,
textures[i].x * tileWidth,
textures[i].y * tileHeight,
(textures[i].x+textures[i].width) * tileWidth,
(textures[i].y+textures[i].height) * tileHeight,
null);
}
}
}
All sprites are traversed (l.2), if a sprite is well defined (l.3), we draw an image (l.4-13). Lines 5 to 8 define the rendering location, in pixels: it is the coordinates of the upper left corner and the lower right corner of the rectangle. Lines 9 to 12 define a rectangle in the texture image: all coordinates are multiplied by tile size (tileWidth, tileHeight).
Use case
To illustrate this facade with tile drawing, I propose to consider two layers: one for the background, and the other for the buildings. In both cases, we consider the same tile size of 16 × 16 pixels, and the same area of rendering (the level of the game) of 17 × 17 tiles. In addition, a scale factor is used to zoom tiles. These parameters are placed in variables:
int scale = 2;
int tileWidth = 16;
int tileHeight = 16;
int levelWidth = 17;
int levelHeight = 17;
For the background layer, each sprite uses only one tile, and we always use the same one:
Layer backgroundLayer = gui.createLayer();
backgroundLayer.setTileSize(tileWidth,tileHeight);
backgroundLayer.setTexture("advancewars-tileset1.png");
backgroundLayer.setSpriteCount(levelWidth*levelHeight);
for(int y=0;y<levelHeight;y++) {
for(int x=0;x<levelWidth;x++) {
int index = x + y * levelWidth;
backgroundLayer.setSpriteLocation(index,
new Rectangle(scale*x*tileWidth, scale*y*tileHeight, scale*tileWidth, scale*tileHeight));
backgroundLayer.setSpriteTexture(index,
new Rectangle(new Point(7,0), new Dimension(1,1)));
}
}
Lines 1-4 instantiate and initialize the layer. Then, for each cell of the level (l, 5-6), we associate a sprite (l, 7). To achieve this association, it is necessary to define a unique number for each cell of the level: to do this, the cells are numbered from left to right then from top to bottom. The rendering area of each sprite is defined in lines 8-9, which is a simple scaling: everything is multiplied by the tileWidth, tileHeight, and the global scale. Finally, the tile is selected in lines 10-11, with the 8th column of the first line of the texture Point (7,0) and the size of a tile Dimension (1,1)
The following layer is used to display buildings, with the particularity that these have a height of two tiles in height:
Layer groundLayer = gui.createLayer();
groundLayer.setTileSize(tileWidth,tileHeight);
groundLayer.setTexture("advancewars-tileset2.png");
groundLayer.setSpriteCount(2);
groundLayer.setSpriteLocation(0,
new Rectangle(scale*8*tileWidth, scale*6*tileHeight,
scale*tileWidth, scale*2*tileHeight)
);
groundLayer.setSpriteTexture(0,
new Rectangle(new Point(0,2), new Dimension(1,2)));
groundLayer.setSpriteLocation(1,
new Rectangle(scale*8*tileWidth, scale*7*tileHeight,
scale*tileWidth, scale*2*tileHeight)
);
groundLayer.setSpriteTexture(1,
new Rectangle(new Point(0,4), new Dimension(1,2)));
Lines 1-4 instantiate the layer and initialize its properties: tile size, texture image, and sprite numbers (2). Lines 6 through 11 define the properties of the first sprite (index 0): its render location is at cell (8,6) of the grid (l. 7), its render size equals a tile width and two heights of tiles (l. 8). Its tiles are at coordinates (0,2) in the texture and use a tile of width one and height two (l. 11). Lines 13 to 18 define the properties of the second sprite: it is drawn on the cell just below, and its tiles start at the coordinates (0,4) in the texture. If all goes well, the top of the second building sprite should cover the bottom of the first:
Finally, in the game’s main loop, displaying layers is as easy as displaying an image:
if (gui.beginPaint()) {
gui.drawLayer(backgroundLayer);
gui.drawLayer(groundLayer);
gui.endPaint();
}
The code of this article can be downloaded here:
Some improvements have been made compared to the version of the previous article: it uses a canvas and limits the number of images per second. These improvements are purely related to the problems of video games, and does not change the concepts presented above.
To compile: javac com/learngameprog/awtfacade04/Main.java
To run: java com.learngameprog.awtfacade04.Main