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() end function gridle_input(iotbl) end function gridle_clock_pulse() end
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 end function cell_coords(x, y) return (0.5 * borderw) + x * (cell_width + hspacing), (0.5 * borderh) + y * (cell_height + vspacing); end 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); show_image(vid); grid[row][col] = vid; cellcount = cellcount + 1; end end end
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() system_load("scripts/keyconf.lua")(); keyconfig = keyconf_create(0, { "rMENU_ESCAPE", "rMENU_LEFT", "rMENU_RIGHT", "rMENU_UP", "rMENU_DOWN", "rMENU_SELECT" } ); keyconfig.iofun = gridle_input; if (keyconfig.active == false) then gridle_input = function(iotbl) if (keyconfig:input(iotbl) == true) then gridle_input = keyconfig.iofun; end end end iodispatch["MENU_ESCAPE"] = function(iotbl) shutdown(); end build_grid(64, 64); end -- 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 iotbl.active) then -- only work with keyPRESS not RELEASE for ind,val in pairs(restbl) do if (iodispatch[val]) then iodispatch[val](restbl); end end end end
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); end 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); end -- add these two lines to the bottom of the build_grid function resize_image(cursorvid, width + 2 * hspacing, height + 2 * vspacing); move_cursor(0);
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"; shutdown(); end -- 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"); end if (rvid == BADID) then rvid = render_text( [[\ffonts\default.ttf,96 ]] .. romset ); end return rvid; end
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); show_image(vid); grid[row][col] = vid; cellcount = cellcount + 1; end end
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 delete_image(grid[row][col]); grid[row][col] = nil; end end end end
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; end -- wrap around on overflow if (pageofs_cur + cursor >= #games) then pageofs_cur = 0; cursor = 0; end 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; end if (cursor < 0 or cursor >= #games - pageofs_cur) then cursor = #games - pageofs_cur - 1; end end end 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 erase_grid(); pageofs = pageofs_cur; build_grid(cell_width, cell_height); -- reuse previous cellw/h end end
There, now we should have something that looks and behaves something like:
For you lazy people, the full pastebin can be found at:
Gridle.lua@pastebin
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.