Writing a Tile Engine in JavaFX

With the advent of embedded versions of JavaFX, our framework has become more interesting for game development, since we now can target small consumer devices like tablets & smartphones. So I decided to do a little more experimenting with JavaFX for writing Games. This time I wanted to use Canvas to have more control over the rendering in order to be able to optimize the performance on smaller devices. These are my experiences when writing a Tile Engine.

What’s a Tile Engine?

Back in the early days game consoles & computers had very limited resources. So in order to have games with thousands of large screens developers needed to come up with a method to store the screens in a format other than a bitmap per screen. So Tile Engines were invented that can generate large screens from a limited set of re-usable smaller graphics (Tiles). This saves ram and improves rendering performance.

TileMaps

The instructions how to generate the screen are stored in TileMaps. Those maps are typically organized as a 2-dimensional matrix of Tile ids. Usually the tiles are organized in layers to allow for a simple Z-ordering and more flexibility in combining graphics with different backgrounds. Usually TileMaps also support storing of meta data, for example if certain tiles are blocked, or spawn points for enemies.

A TileMap with several layers created with the

TileSets

The tiles referenced in the map are usually stored in TileSets that consist of a single bitmap and meta information about how to divide it into tiles. Here’s an example of such an image from opengameart.com, a site that hosts game assets with Open Source Licences. In my examples I use some of these graphics.

A typical TileSet Image sized 1024 x 1024 (^2 = good for graphics cards)

ObjectGroups

One additional feature of the TMX format are Object Layers. These special layers can be used to define freeform shapes and polylines and assign properties to them. The basic idea behind that is that we can use them to define areas where Sprites are created (spawnpoints), exits, portals, and non-rectangular collision shapes. It’s up to the creator of the TileEngine or the developer who builds games with it to define how to handle the ObjectGroups. I’m planning to use them extensively, and they are a very nice extension point for declaratively defining the gameplay. You can e.g. use them to define animations, skript dialogs, etc..

Spawnpoints defined in the

Workflow, Tools & Formats

The idea of tilemaps also allows for a nice workflow. Graphic designers can create the assets and game designers can import them into a level editor like “Tiled” and design the levels via drag & drop. The maps are stored in a machine readable TileMap format. Tiled for example uses the TMX Map format for storing the TileMap. That’s a very simple XML format, that can then be loaded by the TileEngine. For my implementation I decided to use the TMX Format, so I can use “Tiled” for designing the levels.

Implementation in JavaFX

For the implementation I decided to use JavaFX Canvas immediate mode rendering instead of the retained mode rendering when using individual Nodes. This gives me a bit more control for optimizing the performance on small devices like the Raspberry Pi.

Reading TMX/TSX files

The first thing we need is a way to read the TileMap (TMX) and TileSet (TSX) files. With JAXB it’s pretty simple to create a TileMapReader that can create POJOs from a file. So if you use the Engine you simply call:

TileMap map = TileMapReader.readMap(“path/to/my/map.tmx”);

The Camera

Since in most games the TileMaps will be larger than the screen, only a part of the Map is rendered. Usually the map is centered on the hero. You can do that by simply tracking the map position of upper left corner of the screen. We refer to this as our Camera position. The position is then updated from the hero’s position just before the TileMap is rendered like this:

// the center of the screen is the preferred location of our hero

double centerX = screenWidth / 2;

double centerY = screenHeight / 2;

cameraX = hero.getX() - centerX;

cameraY = hero.getY() - centerY;

We just need to make sure the camera doesn’t leave the tilemap:

// if we get too close to the borders

if (cameraX >= cameraMaxX) {

cameraX = cameraMaxX;

}

if (cameraY >= cameraMaxY) {

cameraY = cameraMaxY;

}

Rendering the TileMap using Canvas

Then it’s really easy to render the tiles. we simply loop through the layers and ask the tilemap to render the correct image at the current position. First we need to find out which tiles are currently visible, and the offset, since our hero moves pixel by pixel, not tile by tile:

// x,y index of first tile to be shown

int startX = (int) (cameraX / tileWidth);

int startY = (int) (cameraY / tileHeight);

// the offset in pixels

int offX = (int) (cameraX % tileWidth);

int offY = (int) (cameraY % tileHeight);

Then we loop through the visible layers and draw the tile:

for (int y = 0; y < screenHeightInTiles; y++) {

for (int x = 0; x < screenWidthInTiles; x++) {

// get the tile id of the tile at this position

int gid = layer.getGid((x + startX) + ((y + startY) * tileMap.getWidth()));

graphicsContext2D.save();

// position the graphicscontext for drawing

graphicsContext2D.translate((x * tileWidth) - offX, (y * tileHeight) - offY);

// ask the tilemap to draw the tile

tileMap.drawTile(graphicsContext2D, gid);

// restore the old state

graphicsContext2D.restore();

}

}

The TileMap will then find out which Tileset the Tile belongs to and ask the TileSet to draw it to the Context. Drawing itself is as simple as finding the correct coordinates in your TileSets Image:

public void drawTile(GraphicsContext graphicsContext2D, int tileIndex) {

int x = tileIndex % cols;

int y = tileIndex / cols;

// TODO support for margin and spacing

graphicsContext2D.drawImage(tileImage, x * tilewidth, y* tileheight, tilewidth, tileheight, 0, 0, tilewidth, tileheight);

}

Game Loop. So we can simplify it to:

The Game Loop is again very simple. I’m using a TimeLine and a KeyFrame to fire a pulse for the game at a certain framerate (FPS):

final Duration oneFrameAmt = Duration.millis(1000 / FPS);

final KeyFrame oneFrame = new KeyFrame(oneFrameAmt,

new EventHandler() {

@Override

public void handle(Event t) {

update();

render();

}

});

TimelineBuilder.create()

.cycleCount(Animation.INDEFINITE)

.keyFrames(oneFrame)

.build()

.play();

Sprites

Every call to update in the TileMapCanvas loops through all Sprites and updates them. Basic Sprites currently contain one TileSet with a walkcycle like this:

Since sprites typically have a lot of transparent space around them, in order to have some extra room for animated behavior like like swinging a sword, I decided to allow to add a MoveBox and a CollisionBox for convenience. The CollisionBox can be used to define an area where our hero can be hurt. The MoveBox should be placed around the legs, so it can pass in front of forbidden tiles while the upper body is overlapping the tile. The blueish area around our “hero” is the sprite boundary:

https://www.youtube.com/watch?v=08H6LZkcqXw

Sprites can also have a timed Behavior. On every update the Sprite loops through it’s behaviors and checks if it’s time to fire. If so it’s “behave” method is called. If we have an enemy, like the skeleton in the sample app, we can add it’s AI here. Our Skeleton has for example a very simple behavior to make it follow our hero. It also checks for collision and causes damage to our hero like that:

monsterSprite.addBehaviour(new Sprite.Behavior() {

@Override

public void behave(Sprite sprite, TileMapCanvas playingField) {

if (sprite.getCollisionBox().intersects(hero.getCollisionBox())) {

hero.hurt(1);

}

}

});

The default interval is a second. If you need other intervals you can set them. Behaviors are reusable, different sprites can share the same Behavior instance. Behaviors are similar to KeyFrames, and I’m currently also using them to time the Animations (increase the tile index for the next render call).

ObjectGroupHandler

As mentioned in the beginning ObjectGroups are handy extension points. In my example game I use them for defining the spawn points of our hero and the monsters. Currently you simply add an ObjectGroupHandler which in turn uses the information in the ObjectGroup to create the Hero and Monster sprites and add Behavior to them:

class MonsterHandler implements ObjectGroupHandler {

Sprite hero;

@Override

public void handle(ObjectGroup group, final TileMapCanvas field) {

if (group.getName().equals('sprites')) {

for (TObject tObject : group.getObjectLIst()) {

if (tObject.getName().equals('MonsterSpawner')) {

try {

double x = tObject.getX();

double y = tObject.getY();

TileSet monster = TileMapReader.readSet('/de/eppleton/tileengine/resources/maps/BODY_skeleton.tsx');

Sprite monsterSprite = new Sprite(monster, 9, x, y, 'monster');

monsterSprite.setMoveBox(new Rectangle2D(18, 42, 28, 20));

field.addSprite(monsterSprite);

monsterSprite.addBehaviour(new Sprite.Behavior() {

@Override

public void behave(Sprite sprite, TileMapCanvas playingField) {

if (sprite.getCollisionBox().intersects(hero.getCollisionBox())) {

hero.hurt(1);

}

}

});

}

Putting it all together

To create a sample game all you need to do is create TileMaps, TileSets, one or more ObjectGroupHandler(s) to create the Sprites and add Behavior, and you’re ready to play:

// create the world

TileMap tileMap = TileMapReader.readMap('/de/eppleton/tileengine/resources/maps/sample.tmx');

// initialize the TileMapCanvas

TileMapCanvas playingField = new TileMapCanvas(tileMap, 0, 0, 500, 500);

// add Handlers, can also be done declaratively.

playingField.addObjectGroupHandler(new MonsterHandler());

// display the TileMapCanvas

StackPane root = new StackPane();

root.getChildren().add(playingField);

Scene scene = new Scene(root, 500, 500);

playingField.requestFocus();

primaryStage.setTitle('Tile Engine Sample');

primaryStage.setScene(scene);

primaryStage.show();

That was the starting point of my Tile Engine. In the meantime it has evolved a bit into a more general purpose 2D-engine, so also Sprites that are not using TileSets and Layers that are freely rendered are supported. But it works pretty well so far.
 

Reference: Writing a Tile Engine in JavaFX from our JCG partner Toni Epple at the Eppleton blog.

Related Whitepaper:

Java Essential Training

Author David Gassner explores Java SE (Standard Edition), the language used to build mobile apps for Android devices, enterprise server applications, and more!

The course demonstrates how to install both Java and the Eclipse IDE and dives into the particulars of programming. The course also explains the fundamentals of Java, from creating simple variables, assigning values, and declaring methods to working with strings, arrays, and subclasses; reading and writing to text files; and implementing object oriented programming concepts. Exercise files are included with the course.

Get it Now!  

Leave a Reply


7 − = six



Java Code Geeks and all content copyright © 2010-2014, Exelixis Media Ltd | Terms of Use | Privacy Policy
All trademarks and registered trademarks appearing on Java Code Geeks are the property of their respective owners.
Java is a trademark or registered trademark of Oracle Corporation in the United States and other countries.
Java Code Geeks is not connected to Oracle Corporation and is not sponsored by Oracle Corporation.

Sign up for our Newsletter

20,709 insiders are already enjoying weekly updates and complimentary whitepapers! Join them now to gain exclusive access to the latest news in the Java world, as well as insights about Android, Scala, Groovy and other related technologies.

As an extra bonus, by joining you will get our brand new e-books, published by Java Code Geeks and their JCG partners for your reading pleasure! Enter your info and stay on top of things,

  • Fresh trends
  • Cases and examples
  • Research and insights
  • Two complimentary e-books