Using metatables to remove strictness of tile retrieval

You can cause an error to occur when you try to retrieve the tile for a location in the world where a tile doesn’t exist, in the case that you are converting transform positions to correspond to the grid of your tilemap.

Also, storing tile indices to a lua object and retrieving them from there is faster than frequently calling tilemap.get_tile(), given the tilemap doesn’t change or changes infrequently.

Using the __index metamethod, we can construct the stored tilemap data to avoid throwing an error for trying to retrieve nonexistent tiles without needing to explicitly check whether the coordinates we are querying is within the bounds of our stored data.

local function get_tiles(url, layer)
    local tiles = {}
    setmetatable(tiles, { 
        __index = function(table,key)
            return {}
        end
    })
    local x, y, w, h = tilemap.get_bounds(url)
    for m = x,w do
        tiles[m] = {}
        for n = y,h do
            tiles[m][n] = tilemap.get_tile(url, layer, m, n)
        end
    end
    return tiles
end

Assigning the result of this call as a value acts as 2D storage of that tilemap, represented as tables of tables. After constructing the data in this way, we can retrieve the tilesource index safely even outside the bounds of our tilemap after conversion.

-- store tilemap data
my_tiles = get_tiles('#','my_tile_layer')

-- retrieve tile
print(my_tiles[x][y]) --[[ --> the index of the tile,
      or nil if out of bounds
]]
4 Likes

I’m sure it doesn’t matter in the grand scheme of things, but to me this seems extremely over-engineered and inefficient.

If you’re getting a tile that doesn’t exist it:

  1. Checks ‘my_tiles’…[x] does not exist.
  2. Gets the metatable for ‘my_tiles’ and checks it…[x] does not exist in the metatable either.
  3. Calls the metatable’s __index()
  4. __index() creates a new empty table and returns it.
  5. Access [y] (which we know doesn’t exist)…we finally end up with nil.

Instead of doing something so elaborate and “clever”, you can just do this:

local function getTile(x, y)
	local column = tiles[x]
	if column then
		return column[y]
	end
end

Simple, readable, and efficient. Anyone else (or yourself when you forget and come back to this code) can read and understand it easily.

Of course this hard-codes ‘tiles’ into the function, but you could easily tweak it to take the map as another argument, or put it on the map table and use it as a ‘method’.


I did performance test these with a big loop, and it was not as significant as I expected. My function was only 11% faster than your metatable technique, and if I put the function on the map as a method, that negated the gains.

However I also tested with the JIT turned off, and then the metatable technique was 3.7x to 5.1x slower, so if you’re targeting iOS, Switch, or HTML5, you might want to think about it. (Yay LuaJIT! :partying_face: )

5 Likes

Good to know! You’re probably right that it’s over-engineered, and with that in mind as long as you’re below the threshold of the performance consideration becoming a problem for the target platform, it would be more of a style thing?

The question at that point being whether you want your data access code to look similar to these

getTile(x,y) -- from hardcoded data storage
getTile(map,x,y) -- taking the map as an argument
map.getTile(x,y) -- as a 'method'

or like these

tiles[x][y]
tiles.map[x][y] -- putting your maps in keyed containers

My thought process was actually exactly to avoid including it as a ‘method’ as you mentioned, and it also did another thing which I personally found to be intuitive from my understanding in addition to that ‘no-method’ condition → If I’m getting tiles frequently and not including the access as a method, wherever I needed to get a tile I would need to either include the access functionality somehow or have it globally scoped and then localized which is just another thing to keep track of.

How would I go about doing this myself? I’m still pretty green and learning without much guidance, where can I learn a bit more about benchmarking and stuff like that? What’s the best way to go about it in the context of testing for Defold?

It occurs to me that in the original function I wrote, instead of returning a newly created table I can instead create an empty one in the same scope as tiles and return the reference to that instead, would that be a potential improvement in any way?

3 Likes

Note that several platforms don’t actually allow JIT itself: iOS, Switch, PS4.
We’ll still use the LuaJIT interpreter though, as it’s still faster than vanilla Lua.

3 Likes