Desktop Java

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.

Toni Epple

Anton is a consultant worldwide for a wide variety of companies, ranging from startups to Fortune 500 companies, in many areas, including finance institutions and aerospace. His main interest is Client side development, and he has authored books and numerous articles on this topic. He is a member of the NetBeans Dream Team and a Oracle Java Champion. In 2013 he was elected as a JavaONE Rockstar, in 2014 he received a Duke’s Choice Award for his work on DukeScript.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Lê Tiến Dũng
Lê Tiến Dũng
3 years ago

how to install Library supporting TileMap

բալջետ
բալջետ
2 years ago

i do not know good sir but i do enjoy java

download.jpg
Joe Foster
Joe Foster
1 year ago

You need to specify your problem and provide a minimum complete and verifiable example or no one can help you.

Back to top button