Tile Engine with Image Groups

Hello all,

I’m looking for a little basic advice about tile engines. I’ll first state that I have purchased Lime, and it’s nice, but might not be quite right for my current project, so I’m investigating a custom engine.

I’ve created a simple 2D array (table) for each layer in my map. Some layers are for visual elements, some for properties, etc. There can be multiple visual layers scrolling at different speeds for a parallax feel. I’ve implemented it with ImageSheets and ImageGroups. Here’s some simple code to demonstrate my algorithm:

[lua] local iv = {} – instance variables

– Set up the Image Sheet with the tile set
local options =
{
width = 64,
height = 64,
numFrames = 16,
sheetContentWidth = 256,
sheetContentHeight = 256
}
iv.tiles = graphics.newImageSheet( “images/tiles.png”, options )

– 2D table to hold a single layer of the map
iv.map = {}
iv.map[1] = { 1, 0, 0, 1, 0, 1, 1, 1, 0, 0 }
iv.map[2] = { 4, 7, 0, 10,5, 2, 1, 1, 0, 0 }
iv.map[3] = { 0, 1, 0, 4, 5, 9, 9, 2, 0, 0 }
iv.map[4] = { 0, 1, 0, 0, 0, 4, 6, 11, 0, 0 }
iv.map[5] = { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 }
iv.map[6] = { 0, 4, 5, 5, 5, 5, 7, 0, 0, 0 }
iv.map[7] = { 0, 0, 0, 12,0, 0, 1, 0, 0, 0 }
iv.map[8] = { 0, 12,0, 0, 0, 0, 10,5, 7, 0 }
iv.map[9] = { 5, 5, 3, 5, 7, 0, 1, 0, 1, 0 }
iv.map[10] = { 0, 0, 1, 0, 1, 0, 10,5, 2, 0 }

– Set up the Image Group with the current tile set
iv.map_group = display.newImageGroup( iv.tiles )

– The first layer in the map
iv.layer1 = {}

– Walk the table and add each tile to its spot in the layer (happens once per layer in setup)
local x, y
local i = 1
for y=1,#iv.map do
for x=1,#iv.map[1] do
local tile_number = iv.map[y][x]
if tile_number ~= 0 then
iv.layer1[i] = display.newImageRect( iv.tiles, tile_number, 64, 64 )
iv.layer1[i]:setReferencePoint( display.TopRightReferencePoint )
iv.layer1[i].x, iv.layer1[i].y = (x-1)*64, (y-1)*64
i = i + 1
end
end
end

– Moves each tile in a layer, one at a time, a delta in the x and y directions
local function move_all( layer, dx, dy )
local i
for i=1,#iv.layer do
iv.layer[i].x = iv.layer[i].x + dx
iv.layer[i].y = iv.layer[i].y - dy
end
end

– Sample move. This would actually be in listeners to move based on user input
move_all( layer1, 100, 100 )[/lua]

So my question is, does this look like the proper way to move a set of tiles around? It seems bizarre to me, because I am looping over every tile in the layer and moving it individually. I would have expected some sort of function to move an entire display group, but I didn’t see one. Can anyone tell me if I’m just missing something like that, or if anyone sees anything else that looks like I’m just crazy? I can see this getting a bit out of hand when maps get larger, say 256x256.

I would also like a way to hide all tiles at once, like group:setVisible(false) and then just turn on the tiles that are in the visible area to help with rendering speed. I’ll deal with this once I see how the performance is. Maybe Corona does this automatically with offscreen culling and it would realize not to draw the parts of the group that are off the screen?

Thanks all!
Nate
[import]uid: 118346 topic_id: 34999 reply_id: 334999[/import]

Here’s an optimization I use:

Put all your tiles in a displayGroup and just move that displayGroup around. This saves you from iterating over all the tiles. Then every frame check the top left and bottom right tile to see if they are still on the screen. If the top left tiles goes out of the top of your screen move the whole top row to the bottom of the screen. If the top left tile goes out of the left part of the screen move the whole left column to the right part of the screen. Likewise, if the bottom right tile goes out on the bottom of the screen, move the whole bottom row to the top of the screen. If the bottom right tile goes out of the right of the screen move the whole right column over to the left of the screen.

All of this is assuming that you are using just the number of tiles needed to cover the whole screen, and re-using the tiles that go out one part of the screen to pop-up on another part of the screen. You will also need to write some code to give tiles that move to a new spot the right image.

Another small hint: the easiest way to write this is to iterate over all tiles of the top row, then all tiles of the left column. I’ve had great performance with this, even in the days before imageGroups and Corona’s newer culling routines, and this on an old iPhone 3.

p.s. moving an entire displayGroup should be real easy, so it’s surprising to see you missed this. All you need to do is create a displayGroup before you create your tile images, insert them into the displayGroup after creating each tile and then just set the .x and .y of the group. [import]uid: 70134 topic_id: 34999 reply_id: 139216[/import]

@thomas6

Thanks for the thorough explanation and the nice tips! I will play around with them. I would have used display groups before image groups were added to Corona, but I wasn’t sure they would play nicely together. Have you tried using them both? I know you can’t put one image group inside another image group, but maybe you can put an image group inside a display group. That would be perfect if so.

Thanks!
Nate [import]uid: 118346 topic_id: 34999 reply_id: 139220[/import]

Don’t worry, they play together nicely. The imageGroups documentation in my opinion makes things harder by trying to simplify the explanation. The theory is simple: the less openGL-calls you make the faster the graphics go. To keep drawing images to the screen limited to one GL-call, you need to put all the images in one imageGroup. Basically this just says to openGL: these images all come from one texture, and these are all the coördinates of the separate images in this one texture, so you can draw all of them together (well, in reality not simultaneously but really fast one after the other). If your sprites come from a different imageGroup or a different texture you need to make an extra GL-call.

Extending from this, there is no reason why you can’t put images from an imageGroup inside a displayGroup. To be straight the best way to think of displayGroups is that they only serve a twofold purpose: 1) they define to order in which things are drawn, from back to front off the screen and 2) they apply a transformation matrix to all images so you can rotate or scale images together - and maybe 3) is that it’s practical to fade or mask images together. [import]uid: 70134 topic_id: 34999 reply_id: 139222[/import]

thomas6 has it exactly, but as I previously have tried to build a tile engine this way I’ll just say yes, you can put imageGroups in displayGroups (thomas6 describes the exact limitations). Additionally, while you can just directly set the x,y, apparently you can get some performance boost from using :translate(x,y) instead (providing you already know the delta values…)

Where thomas6’s approach to tiles makes sense is that in the 256x256 example (or worse) texture memory and moving the display groups becomes an extreme performance hog. I tried once and it took minutes (!) to load the app on my iPhone 4. The tile order solution (as per above) or the tile redraw solution (basically just change all tile frames if you move too far) are essential for making tilemaps of a reasonable, modern size.

I know someone else here had a tile engine nearing completion (and Jay has bought Lime to modernize it) but no idea when either will be commercially available. I’ve hammered on both of them to make sure what we’re talking about happens :slight_smile: [import]uid: 41884 topic_id: 34999 reply_id: 139225[/import]

@thomas6 @richard9

Thanks again guys. Great explanations! I’m doing some testing, with and without translate (:translate() appears faster) and with and without display groups (with is much faster). So when does Corona’s off-screen culling come in to play? Is it something automatic, or do you have to do something special? Right now I am moving a 128x128 tilemap around, in a display group, of 64x64 tiles around. It’s running great on my iPhone5, but I’m worried about other devices. I’m also wondering if it is doing the work of drawing the offscreen tiles or if Corona is stopping that from happening.

Thanks again!! [import]uid: 118346 topic_id: 34999 reply_id: 139278[/import]

Well the culling routine works, for sure (I’ve done a lot of 3GS/4 testing), it’s nowhere near as powerful as an actual low-tile solution. (For example, using an imageGroup solution when there’s 10000 tiles clearly improves framerate, but the fps is still bad and memory use is still through the roof. If you use one of the solutions mentioned where there isn’t much more than the tiles onscreen, this never becomes a problem.)

The other important thing is to use imageSheets (well, those are required for imageGroups) to pool all of your tiles onto a single sheet. It’s just a hell of a lot easier than using different files for every tile.

Are you saying 128x128px tiles? or 128x128 tiles (16k tiles)? The latter is probably going to run pretty badly on 3GS/4/iPad 1, and probably have significant load times on iPad 2/3. [import]uid: 41884 topic_id: 34999 reply_id: 139381[/import]

Well, obviously you must NEVER have more tiles than just enough to cover the entire screen plus one extra row and one extra column (that you need for scrolling purposes). Catering to different screen sizes makes this a bit trickier, but no more than a little. [import]uid: 70134 topic_id: 34999 reply_id: 139387[/import]

Thanks again guys! I am using 64x64 tiles on a 128x128 grid. 60 FPS on iPhone 5. But I’m doing no optimization. I insert the tiles from my image sheet into the display group and then just start moving the display group around via the translate() function. Should I also be determining which tiles are visible and setting isVisible to false to speed things up? Or dies corona realize which tiles are outside the viewport? [import]uid: 118346 topic_id: 34999 reply_id: 139392[/import]

  1. The iPhone 5 and iPad 4 will absolutely cover up performance issues simply due to the level of performance difference. There are tilemaps I’ve used with Lime that take 2m+ to load on an iPad 2 and crash an iPhone 4 that loaded with minimal pause on an iPad 3. Latest gen products are 2-4x times faster with 2x the memory so you really should consider trying to load your code on an older device and see what happens (I imagine iPad 2 will no longer be sold later this year, but the iPad mini will, and that’s using the iPad 2 performance profile)

  2. You should definitely look at the “perspective” library in the code sharing section of this website; it specializes at basically acting as a virtual camera which is perfect for your needs.

  3. Setting isVisible is probably where Corona’s internal culling is doing the work. I don’t remember any particular performance gain from changing that. The real problem is that (a) all of those tiles are in memory and (b) when you translate the group, you’re technically translating every tile within it, so at large tile map sizes this can be quite an intensive process per frame. (It’s faster than doing it manually since they are OpenGL calls, but still)

  4. Here is one of the code solutions I was alluding to.

It’s really quite clever and fast. The complexity trick for you is that you’ll have to manage your tiles as a regular data table and not so much as display objects because for any given tile onscreen you can never be completely sure what it corresponds to in the original tilemap. [import]uid: 41884 topic_id: 34999 reply_id: 139465[/import]

I have access to an iPad 1,2,3, and mini as well as iPhone 4 (mot S) and 5. I’ll be testing on those once I implement the ideas you recommended. The link to the old post is very interesting and I think I’ll be able to apply a lot if it to what in working on. I really appreciate the detailed explanations you guys have put the time into providing. [import]uid: 118346 topic_id: 34999 reply_id: 139579[/import]

Here’s an optimization I use:

Put all your tiles in a displayGroup and just move that displayGroup around. This saves you from iterating over all the tiles. Then every frame check the top left and bottom right tile to see if they are still on the screen. If the top left tiles goes out of the top of your screen move the whole top row to the bottom of the screen. If the top left tile goes out of the left part of the screen move the whole left column to the right part of the screen. Likewise, if the bottom right tile goes out on the bottom of the screen, move the whole bottom row to the top of the screen. If the bottom right tile goes out of the right of the screen move the whole right column over to the left of the screen.

All of this is assuming that you are using just the number of tiles needed to cover the whole screen, and re-using the tiles that go out one part of the screen to pop-up on another part of the screen. You will also need to write some code to give tiles that move to a new spot the right image.

Another small hint: the easiest way to write this is to iterate over all tiles of the top row, then all tiles of the left column. I’ve had great performance with this, even in the days before imageGroups and Corona’s newer culling routines, and this on an old iPhone 3.

p.s. moving an entire displayGroup should be real easy, so it’s surprising to see you missed this. All you need to do is create a displayGroup before you create your tile images, insert them into the displayGroup after creating each tile and then just set the .x and .y of the group. [import]uid: 70134 topic_id: 34999 reply_id: 139216[/import]

@thomas6

Thanks for the thorough explanation and the nice tips! I will play around with them. I would have used display groups before image groups were added to Corona, but I wasn’t sure they would play nicely together. Have you tried using them both? I know you can’t put one image group inside another image group, but maybe you can put an image group inside a display group. That would be perfect if so.

Thanks!
Nate [import]uid: 118346 topic_id: 34999 reply_id: 139220[/import]

Don’t worry, they play together nicely. The imageGroups documentation in my opinion makes things harder by trying to simplify the explanation. The theory is simple: the less openGL-calls you make the faster the graphics go. To keep drawing images to the screen limited to one GL-call, you need to put all the images in one imageGroup. Basically this just says to openGL: these images all come from one texture, and these are all the coördinates of the separate images in this one texture, so you can draw all of them together (well, in reality not simultaneously but really fast one after the other). If your sprites come from a different imageGroup or a different texture you need to make an extra GL-call.

Extending from this, there is no reason why you can’t put images from an imageGroup inside a displayGroup. To be straight the best way to think of displayGroups is that they only serve a twofold purpose: 1) they define to order in which things are drawn, from back to front off the screen and 2) they apply a transformation matrix to all images so you can rotate or scale images together - and maybe 3) is that it’s practical to fade or mask images together. [import]uid: 70134 topic_id: 34999 reply_id: 139222[/import]

thomas6 has it exactly, but as I previously have tried to build a tile engine this way I’ll just say yes, you can put imageGroups in displayGroups (thomas6 describes the exact limitations). Additionally, while you can just directly set the x,y, apparently you can get some performance boost from using :translate(x,y) instead (providing you already know the delta values…)

Where thomas6’s approach to tiles makes sense is that in the 256x256 example (or worse) texture memory and moving the display groups becomes an extreme performance hog. I tried once and it took minutes (!) to load the app on my iPhone 4. The tile order solution (as per above) or the tile redraw solution (basically just change all tile frames if you move too far) are essential for making tilemaps of a reasonable, modern size.

I know someone else here had a tile engine nearing completion (and Jay has bought Lime to modernize it) but no idea when either will be commercially available. I’ve hammered on both of them to make sure what we’re talking about happens :slight_smile: [import]uid: 41884 topic_id: 34999 reply_id: 139225[/import]

@thomas6 @richard9

Thanks again guys. Great explanations! I’m doing some testing, with and without translate (:translate() appears faster) and with and without display groups (with is much faster). So when does Corona’s off-screen culling come in to play? Is it something automatic, or do you have to do something special? Right now I am moving a 128x128 tilemap around, in a display group, of 64x64 tiles around. It’s running great on my iPhone5, but I’m worried about other devices. I’m also wondering if it is doing the work of drawing the offscreen tiles or if Corona is stopping that from happening.

Thanks again!! [import]uid: 118346 topic_id: 34999 reply_id: 139278[/import]

Well the culling routine works, for sure (I’ve done a lot of 3GS/4 testing), it’s nowhere near as powerful as an actual low-tile solution. (For example, using an imageGroup solution when there’s 10000 tiles clearly improves framerate, but the fps is still bad and memory use is still through the roof. If you use one of the solutions mentioned where there isn’t much more than the tiles onscreen, this never becomes a problem.)

The other important thing is to use imageSheets (well, those are required for imageGroups) to pool all of your tiles onto a single sheet. It’s just a hell of a lot easier than using different files for every tile.

Are you saying 128x128px tiles? or 128x128 tiles (16k tiles)? The latter is probably going to run pretty badly on 3GS/4/iPad 1, and probably have significant load times on iPad 2/3. [import]uid: 41884 topic_id: 34999 reply_id: 139381[/import]

Well, obviously you must NEVER have more tiles than just enough to cover the entire screen plus one extra row and one extra column (that you need for scrolling purposes). Catering to different screen sizes makes this a bit trickier, but no more than a little. [import]uid: 70134 topic_id: 34999 reply_id: 139387[/import]

Thanks again guys! I am using 64x64 tiles on a 128x128 grid. 60 FPS on iPhone 5. But I’m doing no optimization. I insert the tiles from my image sheet into the display group and then just start moving the display group around via the translate() function. Should I also be determining which tiles are visible and setting isVisible to false to speed things up? Or dies corona realize which tiles are outside the viewport? [import]uid: 118346 topic_id: 34999 reply_id: 139392[/import]

  1. The iPhone 5 and iPad 4 will absolutely cover up performance issues simply due to the level of performance difference. There are tilemaps I’ve used with Lime that take 2m+ to load on an iPad 2 and crash an iPhone 4 that loaded with minimal pause on an iPad 3. Latest gen products are 2-4x times faster with 2x the memory so you really should consider trying to load your code on an older device and see what happens (I imagine iPad 2 will no longer be sold later this year, but the iPad mini will, and that’s using the iPad 2 performance profile)

  2. You should definitely look at the “perspective” library in the code sharing section of this website; it specializes at basically acting as a virtual camera which is perfect for your needs.

  3. Setting isVisible is probably where Corona’s internal culling is doing the work. I don’t remember any particular performance gain from changing that. The real problem is that (a) all of those tiles are in memory and (b) when you translate the group, you’re technically translating every tile within it, so at large tile map sizes this can be quite an intensive process per frame. (It’s faster than doing it manually since they are OpenGL calls, but still)

  4. Here is one of the code solutions I was alluding to.

It’s really quite clever and fast. The complexity trick for you is that you’ll have to manage your tiles as a regular data table and not so much as display objects because for any given tile onscreen you can never be completely sure what it corresponds to in the original tilemap. [import]uid: 41884 topic_id: 34999 reply_id: 139465[/import]