Proposal: Tilemap changes

Hi everyone!

The Tilemap API is in beta, and as more developers have started to use it, we have started to get some feedback on things that people prefer and don’t prefer about the API.

Change for it’s own sake is not ideal, but we are early enough in this process that I would like to talk about whether we should consider making some changes—API changes now are going to be easier than if we wait.

This is an example of how the Tilemap API might look in a project:

var bitmapData = Assets.getBitmapData ("tiles.png");
var tileset = new Tileset (bitmapData);
tileset.addTileRect (new Rectangle (0, 0, 100, 100));

var tilemap = new Tilemap (800, 600);

var tilemapLayer = new TilemapLayer (tileset);
var tile = new Tile (0);

tilemapLayer.addTile (tile);
tilemap.addLayer (tilemapLayer);
addChild (tilemap);

There is one Tileset per BitmapData, you can add tile rectangles to the tileset, which generates new tile IDs. Anyone who has used drawTiles or another batch tiling API should recognize this portion.

The Tilemap object is an OpenFL DisplayObject, which is later added to the display list. This is what controls where and how all of our tiles will be drawn. The current API allows for any number of TilemapLayer objects, which contain Tile instances. Each one of these Tile instances holds a tile ID, x, y, scale, rotation or other draw information needed when perform the final render.

TilemapLayer is locked to one Tileset only, and represents one draw call on the GPU. If you need more than one Tileset, you will need to separate into multiple layers, increasing the number of draw calls.

Here is one alternative:

var bitmapData = Assets.getBitmapData ("tiles.png");
var tileset = new Tileset (bitmapData);
tileset.addTileRect (new Rectangle (0, 0, 100, 100));

var tilemap = new Tilemap (800, 600);
var tile = new Tile (tileset, 0);
tilemap.addTile (tile);
addChild (tilemap);

In this example, TilemapLayer has been removed, each Tile needs to have both a Tileset and a tile ID value. This complicates the code internally, but allows for the batching mechanism of splitting into one or more draw calls to be done internally, and not passed onto the user. It also lends itself to shorter code, though perhaps with some lost flexibility?

Here is one more alternative:

var bitmapData = Assets.getBitmapData ("tiles.png");
var tileData = new TileData (bitmapData, new Rectangle (0, 0, 100, 100));

var tilemap = new Tilemap (800, 600);
var tile = new Tile (tileData);
tilemap.addTile (tile);
addChild (tilemap);

This extends the concept a bit further, avoiding a Tileset object to use TileData objects instead. Then a Tile needs a TileData (similar to how Bitmap requires BitmapData) but there are no longer tile IDs, and there is no need for an added Tileset object, as we already have BitmapData to begin with.

Lastly, here is one more concept:

var bitmapData = Assets.getBitmapData ("tiles.png");
var tileData = new TileData (bitmapData, new Rectangle (0, 0, 100, 100));

var tilemap = new Tilemap (800, 600);
var tile = new Tile (tileData);
tilemap.addTile (tile);
addChild (tilemap);

var tileGroup = new TileGroup ();
var tile2 = new Tile (tileData);
tile2.x = 100;
tileGroup.addTile (tile2);
tilemap.addGroup (tileGroup);

Finally, if there is a benefit to having tile layers, or other forms of built-in grouping within the API, we could consider allowing Tilemap to define a default group internally, but for us to be able to add other groups as well, manually. This might be unneeded complexity, or we might consider allowing a TileGroup to have it’s own transformation, slowing the render a bit, but adding convenience for rotating or scaling a set of tiles together.

…or we might be happy as it is, it’s simple and works similar to other APIs in other libraries. We’re open to your feedback, and appreciate your help on this.

Thank you!

3 Likes

I think the main objective is to use one of this and keep it.

Personally, I prefer the current implementation.

Do you have any article or post about the benefits of tilemaps for someone that comes from HaxeFlixel and never used this before?

On my mind I still confuse this with the tiled tilemaps :stuck_out_tongue:

I think you’re on the right track with option 2, but you could make it simpler.

My main concern is, I don’t see any reason for the TilemapLayer and Tileset classes to exist. There’s no benefit to having TilemapLayers if they don’t improve performance, and if there’s only one layer per Tilemap, that means there’s only one Tileset.

That’s a 1:1:1 ratio, so why not merge everything into the Tilemap class?

var bitmapData = Assets.getBitmapData ("tiles.png");
var tilemap = new Tilemap (800, 600, bitmapData);
tilemap.addTileRect (new Rectangle (0, 0, 100, 100));

var tile = new Tile (0);
tilemap.addTile (tile);
addChild (tilemap);

It doesn’t even complicate the internal code. In fact, it arguably simplifies it, since there’s so much less to keep track of. Here’s what it looks like:

I tested BunnyMark with this implementation, and (after fixing the expected compile errors) it worked in Flash, Neko, Windows, and HTML5.

@player_03 Oh, that’s a smart approach!

I have just finished implementing first steps towards a new implementation. I agree that simplicity is best. A user can build their own constructs around the API to create the layers or other structure they want for their specific use-case. I think it is best for Tilemap to be a bit more straightforward.

This is how it works right now:

var bitmapData = Assets.getBitmapData ("tiles.png");
var tileData = new TileData (bitmapData, new Rectangle (0, 0, 100, 100));

var tilemap = new Tilemap (800, 600);

var tile = new Tile (tileData);
tilemap.addTile (tile);
addChild (tilemap);

This does add a TileData object, but long-term, this may include offset, center point or hit rectangle information. If we make tile data smarter, we will not have to redesign any APIs. I think someone could do tileset:Array<TileData> if they wanted, but this would not be forced.

One benefit of this approach is that we can mix-and-match tiles that use different bitmapData values. This is not as good for performance, but provides some added flexibility if you have objects that do use different source BitmapData.

@ilMareGames I agree that it is better to finalize an API, but we have gotten a lot of new feedback since OpenFL 4 was released. I think we need to consider the feedback we have received on how simple it is to get started with the API, and how flexible, before we cement it too solidly. Now is probably the best time to make major changes if we are going to make them.

@Tembac Tilemap is meant to be similar to Tiled tilemaps, the idea is similar to a bitmap, but instead of bits, you have tiles, or quads as they are may be called in 3D graphics programming. The idea is taking the concept of a Tiled map, a Bunnymark sample, a Mario game or other projects which use many small images (often from the same large bitmap sources) and render efficiently. This is different than the display list (which is not meant for millions of objects) and are square graphics, not single pixels, like BitmapData does.

1 Like

Hmm… I was going to complain about TileData, but now that you mention it, center point information is a good idea. Perhaps it could also include default rotations/scales/etc.

I still want to raise two issues. First, as you said, switching bitmap data mid-loop is bad for performance, and defeats the point of Tilemap. Second, it looks like this loop skips the final tile.

My suggestion is, don’t have the TileData track bitmap data. Use my implementation, but replace addTileRect() with addTileData(). Actually, hang on while I go try that…

@player_03 thanks, I think it should be fixed now :slight_smile:

In a perfect world, I think we would use only one BitmapData, but I think this approach would actually perform pretty well. You have one buffer that is uploaded, then you have as many draw calls as you have bitmapData changes. An optimized Tilemap would have one draw call, but a “lazy” developer could still get better performance than a traditional Bitmap approach

Except now it lumps an extra tile in with the previous group. If tileData.bitmapData != cacheBitmapData, then it’ll use the wrong bitmap for one tile.

I’ve written this sort of loop a couple times in the past, and as far as I know, the simplest working implementation is to handle the final group outside the loop. The alternative is to run the loop for an extra iteration, but then you have to be careful about out-of-bounds errors.

Is there any reason this couldn’t happen with Bitmap objects in the display list?

I guess that helps, but I don’t like the idea of Tilemap encouraging bad practices. Tilemap should make it as easy as possible to do things the right way.

First, you shouldn’t have to type in “bitmapData” each time you make a new TileData. This can be solved with a simple convenience function, plus a default bitmap data for the Tilemap.

Second, if you have an animation spanning frames 10-60, it would be much easier to update an integer than a reference. I think the best solution to this is to go back to indexing. TileData is basically a glorified Rectangle, so you may as well put it in an array like the old __rects array.

Well, sometimes you can’t do anything you want with your graphics so you need a flexible framework to make it works.
I need to use more than one atlas in a frame. I’m stuck with it because of my big atlases. Also, consider having just one atlas of 4096x4096 but now you need to split it to support an old hardware that can use only 2048x2048 texture atlas. With what you suggest you will also need to make a lot of changes because you will not have any openfl api to do that. With @singmajesty last suggestion, same code (…almost, you will just have to change how you create the tiles…)! Hourra! \o/

Well it depends how many drawcalls it needs. I did’t notice performance drop while raising some drawcalls in my game. Don’t be too only-one-drawcall-focused

@singmajesty if you want to clear the RAM memory of a BitmapData once it is loaded in the GPU, how do you do that in openfl 4.0? Where did the BitmapData.dumpBits() goes?

I have a more general question on tilemaps (apologies if this is not the right place).

In the future are you likely to make tilemaps allow input of SVG images rather than bitmaps? (Or is this possible already and I just don’t know about it?) This would allow a nice mix of classic 2d presentation with better graphical fidelity.

I’ve heard from multiple reliable sources that reducing draw calls is one of the best ways to speed up OpenGL rendering. I’m not saying there should only ever be one call; I just want to avoid a hundred calls every frame.

Before you say that’s unrealistic, consider how BunnyMark would look in this implementation. (This is simplified slightly, but it has the important parts.)

var bitmapData = Assets.getBitmapData ("assets/wabbit_alpha.png");
var tileData = new TileData (bitmapData, bitmapData.rect);

var tilemap = new Tilemap (800, 600);
addChild (tilemap);

for (i in 0...100) {
    var bunny:Bunny = new Bunny (tileData);
    bunny.x = 0;
    bunny.y = 0;
    bunny.speedX = Math.random () * 5;
    bunny.speedY = (Math.random () * 5) - 2.5;
    bunnies.push (bunny);
    layer.addTile (bunny);
}

Now consider how someone new to OpenFL might extend that code:

var brownBitmapData = Assets.getBitmapData ("assets/wabbit_alpha.png");
var whiteBitmapData = Assets.getBitmapData ("assets/white_wabbit_alpha.png");
var brownTileData = new TileData (brownBitmapData, brownBitmapData.rect);
var whiteTileData = new TileData (whiteBitmapData, whiteBitmapData.rect);

var tilemap = new Tilemap (800, 600);
addChild (tilemap);

for (i in 0...100) {
    var tileData:TileData = Math.random () > 0.5 ? brownTileData : whiteTileData;
    var bunny:Bunny = new Bunny (tileData);
    bunny.x = 0;
    bunny.y = 0;
    bunny.speedX = Math.random () * 5;
    bunny.speedY = (Math.random () * 5) - 2.5;
    bunnies.push (bunny);
    layer.addTile (bunny);
}

See the problem? Since it randomly alternates between BitmapData objects, you end up with hundreds of draw calls when you only need two. (Thousands, after you add enough bunnies.)

Tilemap is meant to be intuitive and efficient. But the intuitive solution fails horribly, and a new programmer won’t know why.

Why not just make a second Tilemap? It shouldn’t be any harder to do than switching to Joshua’s version.

The difference is, if you keep adding tiles from both atlases as time goes on, the two-Tilemap implementation will only use two draw calls, while the two-BitmapData-in-one-Tilemap implementation will use an ever-increasing number.


Maybe this could be solved by organizing the Tile objects based on which BitmapData they use. It’s important to preserve the order of TileData objects, since you need to refer to them by index. But you never refer to Tiles by index, so why not sort them?

Once they’re sorted, there won’t be more than one draw call per BitmapData. And if you implement it properly, the sorting won’t impact performance significantly.

On the other hand, this complicates the code, and it still doesn’t encourage the use of texture atlases.

Consider A as your method (one tilemap per atlas) and B the last implementation of Joshua (Tile that references an atlas).

  1. 2 drawcalls for A and B. For B, Tilemap need to draw in the z-order, and need to batch all Tiles that use the same atlas surrounded by other atlases.

  2. 3 drawcalls for A and B. You can’t batch all the brown rabbits together because they’re overlapping. Also A need 3 Tilemap.

  3. 4 drawcalls for A and B. You can’t batch anything because of overlapping. Also A need 4 Tilemap.

Now consider the ease of sorting your tiles by z-index. With B, you just need to order your Tiles in the correct order within your Tilemap using Tilemap.addTileAt. With A, you have to order correctly your Tiles within Tilemap + to order correctly each Tilemap between them. And you will need a variable number of Tilemap depending of your overlapping. That’s not user friendly at all.

@player_03 to address your concern about ease-of-use, and trying to send the right direction in regards to batch use, I think we need to consider bringing Tileset back into the equation, even if it isn’t the only way to do Tilemap, a “one Tileset, one BitmapData” approach could reduce typing (if you manually key in geometry) while also providing some rails

We have a couple choices if we choose to keep Tileset:

Option A

var bitmapData = Assets.getBitmapData ("tiles.png");
var tileset = new Tileset (bitmapData);
tileset.addRect (new Rectangle (0, 0, 100, 100));

var tile = new Tile (tileset.tileData[0]);
var tilemap = new Tilemap (800, 600);
tilemap.addTile (tile);
addChild (tilemap);

Option B

var bitmapData = Assets.getBitmapData ("tiles.png");
var tileset = new Tileset (bitmapData);
tileset.addRect (new Rectangle (0, 0, 100, 100));

var tile = new Tile (tileset, 0);
var tilemap = new Tilemap (800, 600);
tilemap.addTile (tile);
addChild (tilemap);

Option C

var bitmapData = Assets.getBitmapData ("tiles.png");
var tileset = new Tileset (bitmapData);
tileset.addRect (new Rectangle (0, 0, 100, 100));

var tile = new Tile (0);
var tilemap = new Tilemap (tileset, 800, 600);
tilemap.addTile (tile);
addChild (tilemap);

Option D

var bitmapData = Assets.getBitmapData ("tiles.png");
var tileset = new Tileset (bitmapData);
tileset.addRect (new Rectangle (0, 0, 100, 100));

var tile = tileset.createTile (0);
var tilemap = new Tilemap (tileset, 800, 600);
tilemap.addTile (tile);
addChild (tilemap);

There are others, but these cases help illustrate some core differences in how this could be applied.

In Option A, Tileset generates TileData objects when you add rectangles to it. You use TileData when creating a Tile instance.

In Option B, we remove the TileData type, and use Tileset + Int ID values to define which tile we are drawing. Option C extends this idea by forcing a default Tileset per Tilemap, making the tileset property optional for tiles (you would still be allowed to define a tileset value to mix multiple tilesets). Option D would be an example of not allowing new Tile, but instead using the Tileset to generate Tile instances.

Any feedback is appreciated!

You’re right. If you want z-ordering, then the only way to reduce draw calls is to use a single source bitmap. Sorting the tiles would mess up the z-order, as would splitting into layers or multiple Tilemaps. And the depth buffer isn’t an option with semitransparent images.

Maybe so, but I’m not a big fan of any of your examples. If we’re going to bring back Tileset, it should either add functionality, simplify the API, or encourage best practices. Currently it’s just as easy to abuse this as it is to abuse the TileData implementation, and TileData required fewer lines of code.

My suggestion is to make the Tileset class import spritesheets from various common formats (Starling, Texture Packer, etc.). This counts as both adding functionality and encouraging best practices.

As for simplifying the API, well… the main thing we can do is make Tilesets optional. You should be able to skip Tileset entirely if you don’t need it (because you have only one sprite, or because you have your own spritesheet importer).

So here’s my proposed implementation:

  1. Tilemap keeps track of the canonical list of TileData objects, just like before.
  2. Each TileData object includes a reference to a BitmapData object and a Rectangle, just like before.
  3. The renderer works like before.
  4. A Tileset generates a list of TileData objects, either by manually adding rectangles, by importing data from Texture Packer, or based on a formula.
  5. The Tileset constructor takes a reference to the Tilemap. During the constructor, the Tileset appends its generated list to the Tilemap's canonical list. It also saves the old length of the Tilemap's list.
  6. If you want a certain Tile object to display a specific frame, you have two or three options:
    1. Use an “absolute” index from the Tilemap's list.
    2. Use a “relative” index from a Tileset's list. This requires a reference to the Tileset in question. The Tileset uses the saved offset to turn this into an absolute index.
    3. Depending on the Tileset and the data it imported, you may be able to get a frame by name.

Sample usage:

var tilemap = new Tilemap (800, 600);

//No need for a Tilesheet if you don't want one.
tilemap.addBitmapData (Assets.getBitmapData ("wabbit_alpha.png"));

var wabbitTile = new Tile (0);
tilemap.addTile (wabbitTile);

//But if you do, it's easy enough to use.
var bitmapData = Assets.getBitmapData ("tiles.png");
var starlingData = Assets.getText ("tiles.xml");
var tileset = new StarlingTileset (tilemap, bitmapData, starlingData);

var moonTile = new Tile(tileset.getFrameByName ("moon"));
tilemap.addTile (moonTile);

addChild (tilemap);

Hi @player_03, thank you for the feedback! At the risk of going over this ad nauseum, we’ve been discussing this on Slack, and I think that we might be getting closer to a good implementation.

I think we need to consider the simple “fire and go” approach, somewhat like the addBitmapData sample you have. I think that we also need to consider the mental process one might go through when building a Tilemap. I think that Tileset is one example of this – on the mental road, Tileset is where you might begin, with “I have a set of tiles, I need to define their coordinates in the larger whole” and flow from there.

TilemapData

I have another implementation that more closely emulates Bitmap/BitmapData, and may consider all the points we’re raising.

Tilemap (similar to Bitmap) is pretty simple.

class Tilemap extends DisplayObject {
	
	public var pixelSnapping:PixelSnapping;
	public var smoothing:Bool;
	public var tilemapData (default, set):TilemapData;
	
	public function new (tilemapData:TilemapData = null, pixelSnapping:PixelSnapping = AUTO, smoothing:Bool = false);
	
}

The real logic follows in TilemapData

class TilemapData {
	
	public var height (default, null):Int;
	public var numTiles (default, null):Int;
	public var tileset:Tileset;
	public var width (default, null):Int;
	
	public function new (width:Int, height:Int, tileset:Tileset = null);
	
	public function addTile (tile:Tile):Tile;
	public function addTiles (tiles:Array<Tile>):Array<Tile>;
	public function addTileAt (tile:Tile, index:Int):Tile;
	public function contains (tile:Tile):Bool;
	public function getTileAt (index:Int):Tile;
	public function getTileIndex (tile:Tile):Int;
	public function removeTile (tile:Tile):Tile;
	public function removeTileAt (index:Int):Tile;
	public function removeTiles (beginIndex:Int = 0, endIndex:Int = 0x7fffffff):Void;
	
}

There are no layers, it is flat. We can use the display list to layer multiple tilemaps, or you can layer virtually and flatten when you get here. Unlike the previous approaches, note that the width and height are specific to TilemapData, not Tilemap

You could use it in a compact form:

var tilemap = new Tilemap (new TilemapData (800, 600));

or you can set a default tileset value:

var tilemapData = new TilemapData (800, 600, tileset);
var tilemap = new Tilemap (tilemapData);

This is what the Tileset object looks like right now. I agree, I think that this could grow to support more data per tile if we need it (like center points), as well as automation for images that use consistent width and height per tile, with spacing.

class Tileset {
	
	public var bitmapData (default, set):BitmapData;
	
	public function new (bitmapData:BitmapData, rects:Array<Rectangle> = null);
	
	public function addRect (rect:Rectangle):Int;
	public function getRect (id:Int):Rectangle;
	
}

If you have a default tileset, then you do not need to define “tileset” per tile, you can define only the ID value. If you specifically add a tileset value to a Tile, you can use multiple tilesets in one map, but you can see that it’s more convenient to use one tileset for the whole thing.

var tileset = new Tileset (Assets.getBitmapData ("tiles.png"), [ new Rectangle (0, 0, 100, 100) ]);
var tilemapData = new TilemapData (800, 600, tileset);
var tilemap = new Tilemap (tilemapData);

tilemapData.addTile (new Tile (0));
tilemapData.addTile (new Tile (0, 100, 100));

addChild (tilemap);

The alternative option is to not use a default tileset, and to define one per tile. This is supported, but is clearly not as simple to write as using only one tileset, so both use-cases are handled, while the single tileset approach is preferred by convention, I believe:

var tileset = new Tileset (Assets.getBitmapData ("tiles.png"));
tileset.addRect (new Rectangle (0, 0, 100, 100));

var tileset2 = new Tileset (Assets.getBItmapData ("tiles2.png"), 
tileset2.addRect (new Rectangle (0, 0, 100, 100));

var tilemapData = new TilemapData (800, 600);
var tilemap = new Tilemap (tilemapData);

var tile = new Tile (0);
tile.tileset = tileset;

var tile2 = new Tile (0, 100, 100);
tile2.tileset = tileset2;

tilemapData.addTile (tile);
tilemapData.addTile (tile2);

addChild (tilemap);

For reference, this is what the Tile type looks like :slight_smile:

class Tile {
	
	public var data:Dynamic;
	public var id (default, set):Int;
	public var matrix:Matrix;
	public var rotation (get, set):Float;
	public var scaleX (get, set):Float;
	public var scaleY (get, set):Float;
	public var tileset (default, set):Tileset;
	public var x (get, set):Float;
	public var y (get, set):Float;
	
	
	public function new (id:Int = 0, x:Float = 0, y:Float = 0, scaleX:Float = 1, scaleY:Float = 1, rotation:Float = 0);
		
}

I’m not quite happy with your suggestion for TilemapData. There are common use cases for displaying multiple identical copies of a bitmap, but not for displaying multiple identical copies of a TilemapData.

Unless you can think of such a use case, I’d suggest merging those two classes back into one. As-is, you’re using “bitmaps work that way” as an excuse to make tilemaps slightly harder to use.

Why not both? If tile.tileset is non-null, use that. Otherwise, fall back to tilemap.tileset. This saves you some typing if you only have one tileset, but it isn’t hard to add more if you want to.


Everything else here looks pretty good.

You’re right – it’s worth considering whether TilemapData and Tilemap should be merged back into one. I also am wondering if there are any changes we can consider to change our tile data to be more of a big buffer – abstracts wouldn’t work (I don’t think) because we need both a tilemapData and a tile index in order to do anything useful

That’s actually how this works right now – I think it’s maybe a “best of both worlds” scenario

The one thing I thought of is, you could apply different shaders to each Tilemap. On the other hand, if you’re an advanced enough user to use shaders, you’re advanced enough to shallow-clone a Tilemap:

@:privateAccess tilemap2.__tiles = tilemap1.__tiles;

That’s one (admittedly ugly) line of code in a rare case, which saves a line of code in all other cases. If you wanted to make it prettier, you could always provide a clone() method.

Are you talking about the Tile class, the array of Tile objects, or the TileData (Rectangle) objects?

I would assume it’s the array, but I don’t see why that needs a reference to a TilemapData object. Did you mean Tileset?

Great! Sorry, I didn’t check the repo first.

Yeah, I meant Tileset, part of me feels it would be interesting if we really did have a big Float32Array that all the tiles were in, but I’m not sure that would actually run faster or better