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
[import]uid: 5712 topic_id: 5681 reply_id: 305681[/import]

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 [import]uid: 6645 topic_id: 5681 reply_id: 19595[/import]

+1 for this :slight_smile: [import]uid: 5833 topic_id: 5681 reply_id: 19730[/import]

+1 [import]uid: 12088 topic_id: 5681 reply_id: 22554[/import]

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. [import]uid: 12108 topic_id: 5681 reply_id: 22564[/import]

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.
[import]uid: 5712 topic_id: 5681 reply_id: 22579[/import]

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? [import]uid: 12108 topic_id: 5681 reply_id: 22589[/import]

By culling I mean the calculation if an object is outside the visible area and set it to be inVisible. [import]uid: 5712 topic_id: 5681 reply_id: 22604[/import]

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). [import]uid: 9422 topic_id: 5681 reply_id: 22657[/import]

another related previous feature request:
http://developer.anscamobile.com/forum/2010/12/15/separate-physics-world-display-world

[import]uid: 6645 topic_id: 5681 reply_id: 23430[/import]

(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”):

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)  


ADDITION: In another thread jmp noticed that the image got downsized when I posted it, so the tiles you need to use are 41x41 and the quadrant is (41*8) rather than 512 (64*8) [import]uid: 12108 topic_id: 5681 reply_id: 26394[/import]

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)

[lua]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)[/lua]

In order to use this in a game for anything other than just a scrolling background, you would probably want to package it into a module and also provide a few more helper methods like determining the tile coordinates which correspond to a particular screen location (e.g. “which tile is the user’s finger touching?”)
[import]uid: 32962 topic_id: 5681 reply_id: 26658[/import]

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. [import]uid: 12108 topic_id: 5681 reply_id: 26665[/import]

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 [lua]isVisible=false[/lua] 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 [import]uid: 6645 topic_id: 5681 reply_id: 26671[/import]

p120ph37:

just one thing… you’re creating an anonymous function closure every time:

[lua]map.forTilesInView(function(t, x, y)[/lua]

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

[lua]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[/lua]

re: the performance difference… try this
[lua]local t = os.clock()
local i
local total = 0

local function fn(n)
total=total+n
end

local function doSomething(f)
f(5)
end

for i=1, 1000000, 1 do

– uncomment one of these two methods and compare times
–doSomething(function(n) total=total+n end)
–doSomething(fn)
end
print(os.clock()-t)[/lua] [import]uid: 6645 topic_id: 5681 reply_id: 26682[/import]

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 [import]uid: 12108 topic_id: 5681 reply_id: 26686[/import]

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. [import]uid: 32962 topic_id: 5681 reply_id: 26692[/import]

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. [import]uid: 12108 topic_id: 5681 reply_id: 26732[/import]

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…
[import]uid: 32962 topic_id: 5681 reply_id: 26854[/import]

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.

[import]uid: 6645 topic_id: 5681 reply_id: 26866[/import]