Proposal: Tilemap changes

@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

I really appreciate your help

I’ve merged TilemapData into Tilemap, I agree that a tighter API (in general) is better overall.

I just need to finish the CairoTilemap and do a little work for DOM mode, and I think we’ll be in business :shipit:

As long as you store your Tilesets in an array, then I guess you could make that work. Instead of a Tile object, you’d have a set of floats, one of which would be an index in the Tileset array, and one of which would be a tile ID. (Then you’d have x, y, width, height, etc.)

You certainly could use abstracts here, but you’d still need a separate array to store the abstracts. Each abstract would need a reference to the Float32Array, plus an index in that array, so you’re still looking at an array of objects.

If you’re really willing to complicate the code, each abstract could store only the index, which would mean the array of abstracts would be an Array<Int>. The drawback is that you’d have to pass the Float32Array to every single one of the abstract’s functions.

All in all, I think it would be one big headache without much benefit. Is the array of Tile objects really that much of a bottleneck?

Nice!

Bit slow on my reply, but here are my thoughts on the OP… (will read through replies and post again)

Overall: Is there a need to have a fixed size for the Tilemap on creation? and could it be amended on the fly ? ((var tilemap = new Tilemap (800, 600);))
I’m used to using the Sprite displayobject on stage when using tilesheets, Sprite isn’t restricted by bounds, and makes managing window size changes and device orientation change fairly trivial, rather than having to recreate the Tilemap object whenever a viewing method/window changes.

option 1, ok, couldn’t a tileset be assumed to be a tilelayer ? or are they currently seperated so that two layers could be created from the same bitmapData (at a loss of performance) ?
option 2, imho the developer should retain control over the draw order of the tiles, rather than them being resorted internally, if tiles overlap and are internally moved to earler or later draw calls, their may be undesired order effects.
Also, Setting the tile ID manually seems unnecessary when unique ID’s are required and their actual value isn’t important. imho it would be better to have the tileset control the ID’s when tiles are added.
option 3, am not sure i understand when the bitmapdata would be passed to the GPU during this method? i understood it required passing early, rather than if a new bitmapdata was used for the first time mid program flow.
option 4, i would say unneeded complexity, the point of the API is batch rendering and performance. anything which unneccesarily slows the redering should be ditched.

edit: ok caught up now

My thoughts as i read through,

Personaly i dont like the idea of the TileData object.
It could in future include offset, centrepoint, hit rectangle information etc. But unless the Tilemap requires use of these variables, they’re unnecessay within the class, and could easily be added by the developer when they extend the class if required within their code.
I also dont like the appearance of encouraging new developers to use multiple bitmapdata, and Tilemap rendering, if they’re unaware that it’s a batch process, and dont know what that is, and when it should be used.

If Tileset is being kept, Option B
The incluusion of Tileset.getRect(id:Int):Rectangle; appears instantly very useful for me!. very useful when falling back to manually rendering using bitmapdata.draw calls on targets that cant or i dont want using GPU batch rendering.

1 Like

Could there be a faster method to create TileMap? Like this

var myTileMap = new Tilemap(_params1,_params2,...,"tiles.png");

and then add it to the stage like this

addChild(myTileMap);

myTileMap.x = this.location.in.X;
myTileMap.WhichTileImage[2ndTile.Or.So];
myTileMap.others = others;

No need to do this manually. Tilemap automatically falls back when necessary.

This also explains why the size is fixed. The Flash target copies tiles onto one big bitmap, and the big bitmap needs a fixed size. On other targets, you can draw beyond the bounds of the Tilemap, so you can just ignore width and height if you aren’t targeting Flash.

You’re right that it doesn’t need any hit rectangle information, but it would definitely use the offset and rotation point.

Agreed, but as loudo pointed out, sometimes you can’t avoid using multiple. I think the best solution is to update BunnyMark to demonstrate how to batch tiles:

//Like wabbit_alpha.png, except it contains two rabbits.
var bitmapData = Assets.getBitmapData ("assets/wabbits_alpha.png");

var tileset = new Tileset (bitmapData);
tileset.addRect (new Rectangle(0, 0, 26, 37));
tileset.addRect (new Rectangle(26, 0, 26, 37));

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

for (i in 0...100) {
    var bunnyID:Int = Std.int (Math.random () * tileset.tileCount));
    var bunny:Bunny = new Bunny (bunnyID);
    bunny.x = 0;
    bunny.y = 0;
    bunny.speedX = Math.random () * 5;
    bunny.speedY = (Math.random () * 5) - 2.5;
    bunnies.push (bunny);
    tilemap.addTile (bunny);
}

Actually, I guess it’s still pretty obvious how to use multiple bitmaps. Ideally, we want to force users to look it up. Then when they ask about it, we can explain why it’s slower, and maybe help them do it the right way.

Maybe what we need to do is add the Tileset functions to Tilemap, so that you never have to interact with the Tileset class as long as you only have one tileset.

var bitmapData = Assets.getBitmapData ("assets/wabbits_alpha.png");

//When you pass a BitmapData object to Tilemap's constructor,
//it creates a Tileset object from that data, and sets it as the default.
var tilemap = new Tilemap (800, 600, bitmapData);

//The addRect and getRect functions are passed through to the
//default Tileset.
tilemap.addRect (new Rectangle(0, 0, 26, 37));
tilemap.addRect (new Rectangle(26, 0, 26, 37));

addChild (tilemap);

Hello. Thanks for your work in the Tilemap api. I’ve made some test and it’s ok on flash but there are some bugs in other targets.

I’m currently working on a Tileset extension that allow to automatically create rectangles for each tiles in the atlas.
For now I have Sparrow and Spriter atlases working. I will create a git repo soon.

It allows to positionate correctly the Tile depending on the original image and not on the trimmed one (Spriter works like that) :

Tile.x = x + tileset.getSize(Tile.id).x;