Grid- Theme Guide (part 1)

To more intimately familiarize you with the immediate-to-inner workings of Arcan (that almost sounds filthy), we’ll have a little series where we build a theme from scratch.

In the first part, we’ll get the pure basics up and running, meaning a grid with screenshots. In coming parts, we’ll add more and more bells and whistles to the whole thing. This assumes some pretty basic programming background for things to make sense, other than that, copy paste, modify and play around to try and figure out what happens.

For starters, we’ll need to let the engine know about the theme, so create a subfolder in the theme folder, called gridle (for windows; c:\program files\arcan\themes in OSX, ~/.arcan/themes or, in Linux, either the /usr/local/share/arcan/themes, in ~/.arcan/themes or wherever you set the ENV variable ARCAN_THEMEPATH to (we can even change this as a command-line argument).

In this folder, /gridle, create a new file in your favorite code editor called ‘gridle.lua’.

Now we’ll add a few (empty) entry-points to it that the arcan FE knows about;

function gridle()

function gridle_input(iotbl)

function gridle_clock_pulse()

In short, gridle() will be called as soon as the engine has finished setting up, gridle_input whenever a key is pressed, joystick or mouse is moved, and clock_pulse will be called at a fixed rate of about 25Hz (the actual rate is determined at compile-time, but is exposed as the CLOCK global variable).

You should now be able to launch the theme using simply gridle as the command-line argument, and a nice empty large window will appear.

First of all, we’ll set up some kind of static imagery, and worry about games, screenshots and so on a bit later. Since the user might have varying video resolution and aspect ratio (vertical or horizontally oriented screen), and we want to accomodate as large a crowd as possible, we’ll try and make this flexible.

grid = {};      -- table to store references to all video objects
vspacing = 4;   -- used to indicate how many pixels we want
hspacing = 4;   -- between our grid cells
cellcount = 0;  -- # operator is wonky when you 0-index tables..

function gridle()
    build_grid(64, 64);  -- for testing

function cell_coords(x, y)
 return (0.5 * borderw) + x * (cell_width + hspacing), (0.5 * borderh) + y * (cell_height + vspacing);

function build_grid(width, height)
--  figure out how many full cells we can fit with the current resolution
    ncw = math.floor(VRESW / (width + hspacing));
    nch = math.floor(VRESH / (height + vspacing));
    ncc = ncw * nch;
    cell_width = width;
    cell_height = height;

--  figure out how much "empty" space we'll have to pad with
    borderw = VRESW % (width + hspacing);
    borderh = VRESH % (height + vspacing);

    for row=0, nch-1 do
        grid[row] = {};
        for col=0, ncw-1 do
            local vid = fill_surface(width, height, 0,
             col * (255.0 / ncw), row * (255.0 / nch));
            move_image(vid, cell_coords(col, row));
            order_image(vid, 2);
            grid[row][col] = vid;
            cellcount = cellcount + 1;

Try it out and you’ll get something that looks like:

To go any further, we actually need to handle some input. This is a decently sized subject on its own, so to help you along, there’s a support script already included in the program (the fabled keyconf.lua).

iodispatch = {}; -- we add this as a jump table for labels to handlers
cursor = 0;  -- offset which keeps track on which greid cell is currently selected

function gridle()

  keyconfig = keyconf_create(0, { "rMENU_ESCAPE", "rMENU_LEFT", "rMENU_RIGHT", "rMENU_UP", "rMENU_DOWN", "rMENU_SELECT" } );
  keyconfig.iofun = gridle_input;
  if ( == false) then
   gridle_input = function(iotbl)
    if (keyconfig:input(iotbl) == true) then
     gridle_input = keyconfig.iofun;

 iodispatch["MENU_ESCAPE"]  = function(iotbl) shutdown(); end
 build_grid(64, 64);

-- now we flesh out the old gridle_input function, so that it 
-- looks up the current input event in the keyconfig, and if it matches
-- one or several labels, looks them up in the dispatch table and try to
-- execute them.

function gridle_input(iotbl)
 local restbl = keyconfig:match(iotbl);
 if (restbl and then -- only work with keyPRESS not RELEASE
  for ind,val in pairs(restbl) do
   if (iodispatch[val]) then

What these new lines does is that they;
a. (line 5) load the support script and make the keyconf_create function available in the global namespace.

b. (line 7) let the support script set up a new configuration session, designed for 0 players (can be overridden through command-line arguments), and a list of labels that we want to have defined. The r prefix specifies that the options is Required. Keyconf will also forcefully insert a MENU_ESCAPE label.

c. (lines 8..15) remap the current input handler to an anonymous function which feeds the input to keyconf until keyconf says that it has finished configuration. Afterwards, the input handler is reset to the original one (the empty gridle_input we defined at the beginning of this guide).

Now we may have input configured (and if you launch and complete a full configuration, it’ll be stored and reloaded next time, so you might only get the dialog once. If you want to redo the configuration, remove the themes/gridle/keysym.lua, alternatively add the forcekeyconf argument to the command-line after the themename.

We also added a little handler (the MENU_ESCAPE) label, that will shut down the front-end whenever the button bound to ESCAPE is pressed.

next up is creating some handlers for the rest of the labels we defined, along with a cursor indicating which grid cell is selected.

cursor = 0; -- add to the list of global variables at the top

-- at the end of function griddle(), we change things to:
 iodispatch = {};
 iodispatch["MENU_UP"]      = function(iotbl) move_cursor( -1 * ncw); end
 iodispatch["MENU_DOWN"]    = function(iotbl) move_cursor( ncw ); end
 iodispatch["MENU_LEFT"]    = function(iotbl) move_cursor( -1 ); end
 iodispatch["MENU_RIGHT"]   = function(iotbl) move_cursor( 1 ); end
 iodispatch["MENU_ESCAPE"]  = function(iotbl) shutdown(); end
 iodispatch["MENU_SELECT "] = function(iotbl) end
 build_grid(64, 64);

function move_cursor( ofs )
 cursor = cursor + ofs;

-- clamp cursor to the current grid
 if (cursor >= cellcount) then cursor = cellcount - 1; end
 if (cursor < 0) then cursor = 0; end

-- calculate the position and move the cursor
 local x,y = cell_coords(math.floor(cursor % ncw), math.floor(cursor / ncw));
 move_image(cursorvid, x - hspacing, y - hspacing);

 -- add these two lines to the bottom of the build_grid function
 resize_image(cursorvid, width + 2 * hspacing, height + 2 * vspacing);

So now we have a cursor that can be moved around between the different cells, what we’re lacking is content. For this step, we assume that the database is populated already (i.e. the included space / dishwater themes are working).

To get a list of available games, we have a function called list_games that takes a table of filter arguments. An empty table will yield a list of the entire database, whileas you can fill in a bunch of number fields (year, input, players, buttons) and strings (title, genre, subgenre, target with the % character working as a wildcard match). To keep things simple though, we’ll just add two things;

  -- add this to the beginning of the gridle function
  games = list_games( {} );
  if (#games == 0) then
   error "No games found";

  -- and replace the iodispatch["MENU_SELECT"] with this:
  iodispatch["MENU_SELECT"] = function(iotbl)  launch_target( games[ Math.random(1, #games) ].title, LAUNCH_EXTERNAL ); end

Now when you press the button bound to MENU_SELECT, it should try to launch a random game. Great, getting somewhere. Left to do for this part of the guide is to:

a. somehow associate screenshots, flyers, etc. with each grid-cell.
b. manage paging, meaning that when there are more games than can be shown in the current-grid.
c. a minor visual tuneup or two.

Then there are a few open challenges until the next part of this guide for you to play around with. But first, lets deal with a and b.

function get_image(romset)
    local rvid = BADID;
    if resource("screenshots/" .. romset .. ".png") then
        rvid = load_image("screenshots/" .. romset .. ".png");
    if (rvid == BADID) then
        rvid = render_text( [[\ffonts\default.ttf,96 ]] .. romset );
    return rvid;

The function tries to find a png image in either themes/gridle/screenhots or resources/screenhots with a namn matching the romset. If one cannot be found,
we, instead, just write the name of the romset as a text string.

Now, replace the loop in build_grid with something like:

    for row=0, nch-1 do
        grid[row] = {};
        for col=0, ncw-1 do
            local gameno = (row * ncw + col + 1);
            if (games[gameno] == nil) then break; end
            local vid = get_image(games[gameno]["setname"]);
            resize_image(vid, cell_width, cell_height);
            move_image(vid,cell_coords(col, row));
            order_image(vid, 3);
            grid[row][col] = vid;
            cellcount = cellcount + 1;

And try it out. Assuming you have the screenshot folder in order, you should get something like ( a little depending on the arguments to build_grid ):

Now, however, the game you select still isn’t launched. Fix this by replacing the MENU_SELECT handler:

 iodispatch["MENU_SELECT"] = function(iotbl) if (games[cursor + 1]) then
  launch_target( games[cursor + 1].title, LAUNCH_EXTERNAL); end end

Still, we have the paging issue (aka. the mother of all fence-post problems).
We’ll start by way of cleaning up the grid each time a page-swap occurs. Even though LUA garbage collects some resources when they are not used, this is not possible with all of the ones we work with, particularly the ones that arcan exports. That’s why we have to explicitly call delete_image(vid) or (for a delayed version) expire_image(vid, timer).

function erase_grid()
    for row=0, ncw-1 do
     for col=0, new-1 do
      if (grid[row][col]) then
       grid[row][col] = nil;

Now for the big one, we have to change move_cursor() to take into account all different kinds of weird situations e.g. wrapping around (from last page to first, and from first page to last) and half-full pages in wrapping.

function move_cursor( ofs )
    local pageofs_cur = pageofs;
	cursor = cursor + ofs;

-- paging calculations
	if (ofs > 0) then -- right/forward
		if (cursor >= ncc) then -- move right or "forward"
			cursor = cursor - ncc;
			pageofs_cur = pageofs_cur + ncc;

		-- wrap around on overflow
		if (pageofs_cur + cursor >= #games) then
			pageofs_cur = 0;
			cursor = 0;
	elseif (ofs < 0) then -- left/backward
		if (cursor < 0) then -- step back a page
			pageofs_cur = pageofs_cur - ncc;
			cursor = ncc - ( -1 * cursor);

			if (pageofs_cur < 0) then -- wrap page around
				pageofs_cur = math.floor(#games / ncc) * ncc;

			if (cursor < 0 or cursor >= #games - pageofs_cur) then
				cursor = #games - pageofs_cur - 1;

	local x,y = cell_coords(math.floor(cursor % ncw), math.floor(cursor / ncw));
	move_image(cursorvid, x - hspacing, y - vspacing);

-- reload images of the page has changed
	if (pageofs_cur ~= pageofs) then
		pageofs = pageofs_cur;
		build_grid(cell_width, cell_height); -- reuse previous cellw/h

There, now we should have something that looks and behaves something like:

For you lazy people, the full pastebin can be found at:

Until next time, here are a few mini-challenges to play around with;
1. Add a background image, and make it so the cursor blends better with the text.
2. Add support for a sound-effect when the cursor moves (play_sample).
3. Add support for changing the grid- size in-game via a configurable key.
4. Include Flyers and Marquees as either user switchable options or a fallback when screenshots cannot be found.

This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

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

You are commenting using your 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 )

Connecting to %s