Object culling from the render process when not in content area

Hi Ansca,

for performance reasons it would be great if Corona could cull objects from the rendering process if they are outside the visible area.

Doing this in Lua with a lot of Objects just kills performance right there.

Usage? Drawing huge maps easily

Cheers
Michael Hartlef

http://www.whiteskygames.com
http://www.twitter.com/mhartlef

hi,

I had another request to go with this one, but I believe the two are related quite closely:

(optional) removal of display object from physics object / creating non-visible physics shapes

thanks
j

+1 for this :-)

+1

Doing this in Lua with a lot of Objects just kills performance right there.

I'm surprised culling objects with Lua kills performance. When I have time (hahaha) I should test that out.

I am not talking about one two objects. If you have over 25-50, then the fun starts. Lua can't be as fast as something natively coded in C/C++.
It only makes sence that Corona as a render engine culls objects from the render process if they are not in the visible area.

I know what you're talking about, I'm just surprised. 50 still doesn't sound like a lot to me, but I haven't actually tried this yet.

Incidentally, by "culling" are you talking about something more complex than setting isVisible to false when the object moves outside the screen?

By culling I mean the calculation if an object is outside the visible area and set it to be inVisible.

fyi, in my own case I created a VERY simple culling routine in LUA that sets display objects that are outside the visible area to alpha = 0. It iterates through a couple hundred objects, and I assumed it would be too slow to help with framerate, but to my surprise it brought the framerate back up to 60 fps (it was hovering around 45 without the culling routine). And yes, I'm talking about on an actual phone, not just the simulator.

So don't write off LUA for that purpose until you try it (although I fully support Ansca making something that does this under the hood automagically. It would be even faster and we need all the performance we can get).

(cross-post from the subscribers area because I figure others might want to see this)

Alright I finally got around to testing out the tilemap performance in Corona and writing my own visibility culling sample. I've been meaning to do this for almost a month when I first saw some people chirping about this problem.

In summation, I made a map with 1600 tiles at 64x64 pixels per tile, and I optimized the code so that the map scrolls around easily on my iPhone 3Gs.

--

First off, I tried displaying a big tilemap (like 40x40) without making any attempt to optimize things just to see how it performs; I hadn't tried that before so I wanted to see for myself. As you all are pointing out, it performs like a pig. At first I was confused what the complaints were about because it seemed fine in the simulator, but then I tried it on device and it was terrible.

So then I whipped up a modified version of my hitTestObjects function from Code Exchange to use for visibility culling. The modification was to simply check the screen boundaries instead of the boundaries of any specific display object.

Then I tried looping through all the tiles to check visibility. That helped a little but it was still chugging badly, so I needed to optimize the code further.

I split up the map into 8x8 quadrants and checked visibility for those quadrants instead of the individual tiles. That really boosted performance and I got up to 1600 tiles (40x40) with performance that was still pretty good. The reason this ran faster is that instead of calling isVisible() 1600 times I only had to call it 25 times.

If I wanted an even bigger map I could optimize further, probably by nesting quadrants within quadrants to reduce the number of calls to isVisible(), but 1600 tiles was already plenty for me to demonstrate this approach. Setting up the quadrants procedurally for a tool like Lime could be tricky, but the basic idea isn't that complicated.

Here's the code and tileset for my sample (incidentally, if you want to see how it performs without the culling, just delete the first dash in the line "---visibility culling optimized w/quadrants"):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
local function isVisible(obj)
        local screen = display.getCurrentStage().contentBounds
        local bounds = obj.contentBounds
        local left = bounds.xMin <= screen.xMin and bounds.xMax > screen.xMin
        local right = bounds.xMin >= screen.xMin and bounds.xMin <= screen.xMax
        local up = bounds.yMin <= screen.yMin and bounds.yMax >= screen.yMin
        local down = bounds.yMin >= screen.yMin and bounds.yMin <= screen.yMax
        return (left or right) and (up or down)
end
 
--tiles are 64x64 in 576x1216 image for 9x19=171 tiles
local sprite = require("sprite")
local tileSheet = sprite.newSpriteSheet("tiles.png", 64, 64)
local tileSet = sprite.newSpriteSet(tileSheet, 1, 171)
 
--init random function
local randomseed = math.randomseed
local random = math.random
local time = os.time
randomseed(time())
random()        --bug in first use
 
--setup tile map w/quadrants
local map = display.newGroup()
for i = -2, 2 do
        for j = -2, 2 do
                local quad = display.newGroup()
                quad.x = i * 512
                quad.y = j * 512
                map:insert(quad)
                
                --tiles in quadrant
                for i = 0, 7 do
                        for j = 0, 7 do
                                local tile = sprite.newSprite(tileSet)
                                tile.currentFrame = random(171)
                                tile.x = i * 64
                                tile.y = j * 64
                                quad:insert(tile)
                        end
                end
        end
end
 
--move tile map
local prevX, prevY
local function drag(event)
        if event.phase == "began" then
                prevX = event.x
                prevY = event.y
        elseif event.phase == "moved" then
                local dX = event.x - prevX
                local dY = event.y - prevY
                map.x = map.x + dX
                map.y = map.y + dY
                prevX = event.x
                prevY = event.y
                
                ---[[visibility culling optimized w/quadrants
                for i = 1, map.numChildren do
                        if isVisible(map[i]) then
                                map[i].isVisible = true
                        else
                                map[i].isVisible = false
                        end
                end
                --]]
        end
end
map:addEventListener("touch", drag)

Here is an alternate implementation of a draggable tile map which supports almost unlimited map sizes while still remaining responsive on my 3GS. (tested with 160,000 tiles)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
require("sprite")
 
--tiles are 64x64 in 576x1216 image for 9x19=171 tiles
local map = display.newGroup()
map.xTiles = 400
map.yTiles = 400
map.tiles = {} -- array to hold tile ids without actually creating display objects yet
map.tileWidth = 64
map.tileHeight = 64
map.tileSheet = sprite.newSpriteSheet("tiles.png", map.tileWidth, map.tileHeight)
map.tileSet = sprite.newSpriteSet(map.tileSheet, 1, 171)
map.xTilesInView = math.ceil((display.viewableContentWidth - 1) / map.tileWidth) + 1
map.yTilesInView = math.ceil((display.viewableContentHeight - 1) / map.tileHeight) + 1
map.xScroll = 0
map.yScroll = 0
 
--populate the group with just enough objects to cover the entire screen
for y = 0, map.yTilesInView - 1 do
        for x = 0, map.xTilesInView - 1 do
                local tile = sprite.newSprite(map.tileSet)
                tile:setReferencePoint(display.TopLeftReferencePoint)
                tile.x = x * map.tileWidth
                tile.y = y * map.tileHeight
                tile.isVisible = false -- everything starts hidden
                map:insert(tile)
        end
end
 
-- iterate over the visible tiles with a callback
function map.forTilesInView(f)
        for y = 0, map.yTilesInView - 1 do
                for x = 0, map.xTilesInView - 1 do
                  local tx = math.floor(map.xScroll / map.tileWidth) + x;
                  local ty = math.floor(map.yScroll / map.tileHeight) + y;
                        f(map[map.xTilesInView * y + x + 1], tx, ty)
                end
        end
end
 
-- align and update the display object grid based on the current scroll position
function map.updateDisplay()
  -- align the display object grid
  map.x = -(map.xScroll % map.tileWidth)
  map.y = -(map.yScroll % map.tileHeight)
  -- update the tile contents
  map.forTilesInView(function(t, x, y)
    if(x >= 0 and x < map.xTiles and y >= 0 and y < map.yTiles) then
                        -- tile is inside the map
                        t.isVisible = true
                        t.currentFrame = map.tiles[y * map.xTiles + x + 1]
                else
                        -- tile is off the edge of the map
                        t.isVisible = false
                end
        end)
end
 
--init random function
math.randomseed(os.time())
math.random()        --bug in first use
 
--randomly populate tile ids (normally this data would be loaded from a file)
for y = 0, map.yTiles - 1 do
        for x = 0, map.xTiles - 1 do
          map.tiles[#map.tiles + 1] = math.random(171)
        end
end
 
-- center the map and display the visible tiles
map.xScroll = map.xTiles * map.tileWidth / 2
map.yScroll = map.yTiles * map.tileHeight / 2
map.updateDisplay()
 
--move tile map
local prevX, prevY
local function drag(event)
        if event.phase == "began" then
                prevX = event.x
                prevY = event.y
        elseif event.phase == "moved" then
                map.xScroll = map.xScroll + prevX - event.x
                map.yScroll = map.yScroll + prevY - event.y
                map.updateDisplay()
                prevX = event.x
                prevY = event.y
        end
end
map:addEventListener("touch", drag)

Nice! What I wrote was just a first step for someone else to optimize further, but I didn't expect someone to step up to the plate so quickly.

Can you give a brief overview of how your approach works? I'll probably figure it out by reading the code, but pointers on what to look for would help.

ADDITION: It looks like what you are doing is instead of making sprites for every tile you only make a screenful of sprites and then change their frame as you scroll around. Is that right? I must be missing something, because you wouldn't need to set isVisible.

hi,

thanks for the contribution, this is very helpful

however... do note that this method is not really applicable when your map is partly comprised of physical objects (as Lime offers etc). Because physics objects are tied into display objects. you'd have to also destroy and recreate a physical body for every tile moving on/off screen. Maybe this is doable, but I don't know what the performance would be like. Maybe this is something someone could investigate next

jhocking.. from what I can tell the only time isVisible=false is being set is when you reach the edges of the map.... you can currently scroll past the edges of the map, so therefore you don't want sprites there

thanks
j

p120ph37:

just one thing... you're creating an anonymous function closure every time:

map.forTilesInView(function(t, x, y)

i think this might cause slight memory/performance issues.. i changed it to this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function map.visTest(t,x,y)
 if(x >= 0 and x < map.xTiles and y >= 0 and y < map.yTiles) then
                        -- tile is inside the map
                        t.isVisible = true
                        t.currentFrame = map.tiles[y * map.xTiles + x + 1]
                     
                else
                        -- tile is off the edge of the map
                        t.isVisible = false
                        
                end
end
 
-- iterate over the visible tiles with a callback
function map.forTilesInView(f)
        for y = 0, map.yTilesInView - 1 do
                for x = 0, map.xTilesInView - 1 do
                  local tx = math.floor(map.xScroll / map.tileWidth) + x;
                  local ty = math.floor(map.yScroll / map.tileHeight) + y;
                        f(map[map.xTilesInView * y + x + 1], tx, ty)
                end
        end
end
 
-- align and update the display object grid based on the current scroll position
function map.updateDisplay()
  -- align the display object grid
  map.x = -(map.xScroll % map.tileWidth)
  map.y = -(map.yScroll % map.tileHeight)
  -- update the tile contents
  map.forTilesInView(map.visTest)
end

from what I can tell the only time isVisible=false is being set is when you reach the edges of the map

oh that makes sense

re: isVisible - bingo. You could also designate some special tile as your edge-of-the-world filler, or you could stop scrolling when you reach the edge of the map.

re: closure function - yes, I create an anonymous function every drag event. One anonymous function per drag event is not enough delay to add up to much, but abstracting the that out into a named function of its own isn't a bad idea, especially since it might be useful in other cases too.

re: tiles with physics - yes, physics properties of an object cannot be changed once it is instantiated, so you would need to modify the updateDisplay method to check if the current tile has changed physics properties and delete and recreate it if it has rather than just flipping the sprite frame.

I would try to separate the physics objects from other display objects. In most games a lot of the background graphics don't have physics properties, so his scrolling approach would work great for those. Then because the display is so optimized you could take a different approach (I haven't thought about this part yet) for the physics objects.

If you wanted a different approach specifically targeted at a sparsely-populated grid as you might expect in a physics layer, you could do something like this:

- Create an array (technically a table indexed numerically) with one position per grid location as with the tile ids in my previous example.
- Only populate those indexes which actually contain a physics object. (most indexes will be nil)
- Whenever the map scrolls, use similar math (perhaps with a slightly larger window) to get the list of indexes in this physics object array which fall within or near the current viewport.
- Compare this list of physics objects to the list of physics objects from last frame.
- If any are newly missing (moved out of range), remove them from the display.
- If any are newly present (moved into range), add them to the display.
- Go through all displayed physics objects and set their .x and .y properties.
- Save this list of objects for calculating the delta next frame.

Though slightly less efficient than my original map scrolling implementation, this would be a more general solution, allowing for not only the scrolling tiles, but also for physics objects, special objects with user interaction features (touch events), non-standard shapes for objects (a tree which overhangs several nearby terrain tiles), objects which are not entirely grid-aligned (could specify an offset from the grid location where the object "lives"), multiple objects in the same grid square, etc.

If I feel terribly inspired I may write this...

hi,

please note my other post as well
http://developer.anscamobile.com/forum/2010/11/22/scrolling-large-non-tiled-world-objects

one potential issue is physics objects that are created/destroyed dynamically but need to maintain state (if the user returns to that area) and also respond in a way that makes sense even if they are off screen. I know a lot of games will probably reset properties of an object once it falls outside of a certain area, especially larger games or there would be a lot of constant data to maintain.

however i'm still just wondering if suddenly changing a physics object onscreen might lead to unexpected collision behaviour.

thanks for your input. I hope you can share some more examples, or maybe even contribute to what Graham is working on with Lime, since it allows for modular functionality

regards
j.

@p120ph37: Are you a paid subscriber? That's required for Code Exchange, and your tile-map approach would be a great addition there.

@jhocking

I am not yet - I've been working on a couple of projects in Corona and will pay the fee when I'm ready to publish the first. I did look into posting to the Code Exchange page a while ago for my bitmap font stuff, and when I do subscribe, that will probably be the first one I post.

oh you did the bitmap font code, that was pretty good too

I did the Glyph Designer compatible one, not the TextCandy one.

The bitmap code is actually working really well for me in one of my projects. I don't have any projects using the tile map code yet.

How would you go about doing this if you wanted to use single image tiles instead of a tilemap, AND in addition to that use the output data from the Physicseditor for each of the tiles?

I am trying to put together a maze level using single tiles (one tile for a left corner, one tile for a right corner, one tile for a left wall etc), then using the data from the Physics editor, and putting together the main menu, level 1, level 2 etc using Director.

Could someone please give me an example for just a few tiles, to get me started. Right now programming 28x28 tiles just as is with Corona is out of the question, my iPhone is about to die when I load the level.

views:2335 update:2011/9/18 10:09:28
corona forums © 2003-2011