love2d · lua

Bootstrapper Lua Library – Overengineered Love2D

In the games I am writing in Love2D, I tend to use various sorts of “managers” to cache things for me.

One of the more notable managers I use is an image repository. I want to simply do the following:

--loads the library

local images = require "game.images"

--caches the images

images.load() -- during love.load

--retrieves an image

local img = images.get("imageName") -- during love.draw

Typically, I don’t lazy load my images, but I could easily configure my images.lua file to do so.

I find myself faced with a number of these managers, however. I’ll have one for images, one for sound effects, one for music, one for options, one for image regions, and so on.

What I like to use for this is a meta-manager called a bootstrapper, which each of my managers will automatically add themselves to, and then during love.load, I only have to call bootstrapper.load().

So let us overengineer this solution. I’ve got my current implementation for bootstrapper:

local bootstrapper = {}

local loaders={}

function bootstrapper.add(loader)
 assert(type(loader)=="function","bootstrapper.add - loader must be a function")
 --TODO: idempotency?
 table.insert(loaders,loader)
end

function bootstrapper.load()
 for _,v in ipairs(loaders) do
 v()
 end
end

return bootstrapper

And I want to add a few more bits of functionality:

  • I only want calls to bootstrapper.add to work before a call to bootstrapper.load.
  • I want to enforce only one call to bootstrapper.load.
  • I want calls to bootstrapper.add to be idempotent, as in my TODO
  • Do unit tests
  • Make bootstrapper use a proxy object, to keep it from being subverted by client code

If that all seems like a bit of overkill for something like a bootstrapper, then you are going to not like this series of articles.

The changes to the main part of the code are minor:

--bootstrapper library object
--initialize to empty object, as is my habit
local bootstrapper = {}

--static data for this library is hidden from the outside
--starts as empty table
--gets changed to nil after a call to bootstrapper.load
local loaders={}

--bootstrapper.add
--adds a function to the bootstrapper
--requires that the loader parameter is a function
--will throw an exception when called after bootstrapper.load
function bootstrapper.add(loader)
  assert(type(loader)=="function","bootstrapper.add - loader must be a function")
  assert(type(loaders)=="table","bootstrapper.add - cannot call after bootstrapper.load hase been called")
 
 --idempotency...
 --if the loader already exists in loaders, don't re-add it and just return
 for _,v in ipairs(loaders) do
   if v == loader then
     return
   end
 end
 
 table.insert(loaders,loader)
end

function bootstrapper.load()
 assert(type(loaders)=="table","bootstrapper.load - cannot call multiple times")
 
 --perform the load actions
 for _,v in ipairs(loaders) do
   v()
 end
 
 --eliminate loaders to keep from being re-called
 loaders=nil
end

I decided to make use of the static variable loaders instead of having a separate boolean flag for whether or not load has been called. There was no reason to hold on to that array after the call anyway.

Unit tests were a wholly different thing. Firstly, I don’t always want to run unit tests. I do when I’m developing, but not in release code. Of course, the scope of my activity today does not include any sort of unit testing framework, so I decided to at least mock out what I needed, with a plan to augment it later.

--runTests library
--This will eventually be a require that returns a function, so that I can turn off unit tests in one place
local runTests = function(f) f() end

runTests(function()  
  --save off old static state 
  local oldLoaders = loaders 
  loaders = {} 
  local start = os.clock() 

  --bootstrapper.add with non-function 
  local status, err = pcall(function() bootstrapper.add(nil) end) 
  assert(not status) 

  local counter = 0 
  local called1 = false 
  local called2 = false 

  function f1()  
    called1 = true 
    counter = counter + 1  
  end 
  function f2()  
    called2 = true 
    counter = counter + 1  
  end 

  --bootstrapper.add with function 
  bootstrapper.add(f1) 
  assert(#loaders==1) 
  --bootstrapper.add with same function 
  bootstrapper.add(f1) 
  assert(#loaders==1) 
  --bootstrapper.add with second function 
  bootstrapper.add(f2) 
  assert(#loaders==2) 
  --bootstrapper.load 
  bootstrapper.load() 
  assert(loaders==nil) 
  assert(called1) 
  assert(called2) 
  assert(counter==2) 

  --bootstrapper.add after load 
  status, err = pcall(function() bootstrapper.add(f1) end) 
  assert(not status) 
  --bootstrapper.load after load 
  status, err = pcall(function() bootstrapper.load() end) 
  assert(not status) 

  local elapsed = os.clock()-start 
  print(string.format("bootstrapper unit tests - %f",elapsed)) 
  --restore original static state 
  loaders = oldLoaders 
end)

I set up my runTests function to call whatever function is passed to it. Eventually, this can be a library that I load instead, and I can configure that library to either run or not run the functions it passed depending on whether or not I am in “release” mode.

Before you go out and comment something about “pcall is inefficient!”, let me give you the results of these unit tests on each run of my application:

ss03-unittests

That’s right… the unit tests do not take up enough time to actually register time passing.

Finally, I make a protected proxy using a metatable:

local mt = { 
  __index=function (t,k)  
    return bootstrapper[k] 
  end, 
  __newindex=function(t,k,v) end,  
  __metatable="bootstrapper"
}
local proxy = {}
setmetatable(proxy,mt)

return proxy

From what I read, this is all I need to do in order to protect bootstrapper from casual changes from outside code.

It occurs to me that this is going to be a common pattern I want to use, and so a more generic version of this code is likely to wind up in a utility library.

So, this concentration on making a robust bootstrapper seems to fly in the face of how I normally develop code. Almost invariably, I will recommend that one writes exactly the code that one needs, and once it is “good enough” then stop.

Actually, that is exactly what I am doing. The main difference is that this code has been deemed useful enough (by me) to be part of the framework I use in other games going forward, which means it needs to be library code – as bulletproof as I can make it.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s