Grid Theme Guide (part 2)

So in this part, we’ll first complete the challenges from the first one, then look at including support for LED controllers, for MAME autoconfiguration and some minor animation / transition.

(spoiler, end result looks like this:)

1. Add a background image, and make it so that the cursor blends better with the text.

First the image, in the gridle() function add this at the bottom:

 bgimage = load_image("background.png");
 resize_image(bgimage, VRESW, VRESH);
 show_image(bgimage);

This will load an image, stretch it to the size of your screen, and then display it. However, there are very few images that actually fit all screen sizes and orientations. Another option would be to tile the image, but there are complications — to be absolutely compatible with not-ancient-but-at-least-decade-old video hardware, the image must be a power of two. The following ’tile’ will be used in another guide later on, so we might as well start using it here:

Save this image, store it in the gridle theme folder. Note here that everytime you request a resource (be it an image, or a movie or something) the themepath is checked first, and if there is no match, the resourcepath will be tried (comp. science nerds, give a shout-out to ‘two-tiered hierarchical namespacing!’).

Now we tile the thing, change the loading to:

 
    switch_default_texmode( TEX_REPEAT, TEX_REPEAT );
    bgimage = load_image("background.png");
    resize_image(bgimage, VRESW, VRESH);
    image_scale_txcos(bgimage, VRESW / 32, VRESH / 32);
    show_image(bgimage);
    switch_default_texmode( TEX_CLAMP, TEX_CLAMP );

The ’32’ happens to be half the width and height of the tile. Now you get something like:

Two problems, now the “replacement text” is barely visible, and the cursor is hidden.
Find the line that looks like:

rvid = render_text( [[\ffonts/default.ttf,96 ]] .. romset );

and change it to:

rvid = render_text( [[\#000088\ffonts/default.ttf,96 ]] .. romset );

(there are a lot more formatting codes to the render_text function, including multiple fonts / colours per line, bold, italic and tabs). Now the text is dark blue, which has a high contrast to a light background.

Now we need to do something about the cursor, one thing to play around with would be drawing the border to an image, with a hollowed out (transparent) middle section, make one dark version and a light version, load both, set one as the cursor and add the other as a frame to that cursor (image_framesetsize to 2, set_image_as_frame and then image_active frame in the clock_pulse function) to make it flash.

Or, a simpler version, remove the cursor-vid and instead brighten the currently selected cell. So delete all references to cursorvid, and replace

show_image(vid);

in build_grid with

blend_image(vid, 0.3);

Now, all the images suddenly look very dim and blends into the background.

just above the move_cursor function we add:

function blend_gridcell(val, dt)
    local cursor_row = math.floor(cursor / ncw);
    gridcell_vid = grid[ cursor_row ][ cursor - cursor_row * ncw ]; 
   
    if (gridcell_vid) then
	    instant_image_transform(gridcell_vid);
	    blend_image(gridcell_vid, val, dt);
    end
end

some explaining, note that in the blend_image function, there’s a new argument added.
for all image transforms (expire, blend, rotate, scale, move) you can add a timed delay which will animate the vid in question. These can be mixed and chained together. So the blend_image here will actually fade over the course of (dt) clock pulses.

The instant_image_transform will forcefully stop all animations, so that we know we won’t add an animation to another (useful here to stop a dragging fade effect if the user would switch back and forth between the same two items very rapidly).

function move_cursor( ofs )
 blend_gridcell(0.3, 10);
 ... -- rest of the function as usual
 blend_gridcell(1.0, 10);
end

There, now the slot which is currently selected will be fully opaque while the rest will be faded.

Next part of the challenge,
2. Add support for a sound-effect when the cursor moves (play_sample).

This one is really easy, the difficult part is rather finding sound-effects you like, want and (for redistribution) got the rights to. For testing, just grab something of say, and copy to your gridle theme folder under the name click.wav.
Then, alter the UP/DOWN/LEFT/RIGHT of the iodispatch table, have the play_sample call, like:

iodispatch["MENU_UP"] = function(iotbl) play_sample("click.wav"); move_cursor( -1 * ncw); end

3. Add support for changing the grid- size in-game via a configurable key.
First we need a new label, add it to the keyconf_create list at the end as
“rGROW_ICONS” and “rSHRINK_ICONS”.

add new iodispatch routines:

 iodispatch["GROW_ICONS"] = function(iotbl) resize_grid(32); end
 iodispatch["SHRINK_ICONS"] = function(iotbl) resize_grid(-32); end

add a new function:

function resize_grid(step)
 local new_cellw = cell_width; local new_cellh = cell_height;

 -- find the next grid size that would involve a density change
 repeat
    new_cellw = new_cellw + step;
 until math.floor(VRESW / (new_cellw + hspacing)) ~= ncw;

 repeat
    new_cellh = new_cellh + step;
 until math.floor(VRESH / (new_cellh + vspacing)) ~= nch;

-- safety checks
 if (new_cellw < 64 or new_cellw > VRESW * 0.75) then return; end
 if (new_cellh < 64 or new_cellh > VRESH * 0.75) then return; end

 cell_width = new_cellw;
 cell_height = new_cellh;

 local currgame = pageofs + cursor;
 local new_ncc = math.floor( VRESW / (new_cellw + hspacing) ) * math.floor( VRESH / (new_cellh + vspacing) );
 pageofs = math.floor( currgame / new_ncc ) * new_ncc;
 cursor = currgame - pageofs;
 if (cursor < 0) then cursor = 0; end

-- remove the old grid
 erase_grid();
 build_grid(cell_width, cell_height);
end

This function increases or decreases cell size until we find cell dimensions that would require a higher or lower cell density on the grid. Then we calculate what game is currently selected, figures out which page and position this will have in the new grid, finally deletes the old grid and build a new.

The last challenge presented, the marquee/flier is either trivial (just change folder name on a failed resource) or requires a bit more of the theme (which will be on the list for next time). Instead, we’ll add delayed video playback. Delayed meaning that we will wait a set number of clock pulses before trying to replace the current vid with a movie_playback version.

At the end of move_cursor, where we added the blend_gridcell(1.0, 10) we’ll also add:

    local game = games[cursor + pageofs + 1];
    setname = game and game.setname or nil;

    if (movievid) then
        expire_image(movievid, 40);
        blend_image(movievid, 0.0, 40);
        audio_gain(movieaid, 0.0, 40);
    end

-- look for a movie, if one exists, enable a timer that we'll check for later
    if (setname and resource( "movies/" .. setname .. ".avi")) then
        movievid, movieaid = load_movie( "movies/" .. setname .. ".avi" );
        if (movievid and movieaid and movievid ~= BADID and movieaid ~= BADID) then
            print(movieaid);
            audio_gain(movieaid, 0.0);
            move_image(movievid, x, y);
            hide_image(movievid);
            order_image(movievid, 3);
            resize_image(movievid, cell_width, cell_height);
            movietimer = 10;
        end
    else
        moviefile = "";
        movietimer = nil;
    end

Note that the frameserver supports many more video formats (since it’s ffmpeg that is responsible for the decoding), but the current resource() API doesn’t allow wildcard matching, that will be fixed in the next release, same goes for another few safe-guard checks that aren’t possible at the moment.

What this code does is that if there already is a movie playing, we gently kill it while fading out its audiostream (less harsh on the ears). We then look for a new movie, if found, we load it and set a timer in which we want it to start playing back. We also place it ontop of the snapshot and size it accordingly. Note that the hide_image call is a work-around for a bug where movies (unlike images) start visible when loaded.

Finally, we need the timer to start the playback as well.

function gridle_clock_pulse()
    if (movietimer) then
        movietimer = movietimer - 1;
        if (movietimer <= 0) then
            play_movie(movievid);
            blend_image(movievid, 1.0, 10);
            audio_gain(movieaid, 1.0, 40);
            movietimer = nil;
        end
    end
end

When the timer runs out, we remove it, fade in the audio and start movie playback.
Left for this guide is LED/Mame config and some minor animation tweaks.

There is a ledconf script that works very similar to the keyconf, but now we need to specify a few more labels for it to be useful.

All scripts that uses keyconfig take a few command-line options, most notably:
players=num
buttons=num
keyname=filename
nomodifier
forcekeyconf

Keyname switches the default config file to have a different filename
The forcekeyconf tells the script to delete and override any preexisting config file.

If we don’t have any players set (default 0) we won’t get any useful labels to map to LEDs. In the top of the gridle function, add two system load commands after the system_load keyconf line.

 system_load("scripts/keyconf_mame.lua")();
 system_load("scripts/ledconf.lua")();

Now replace the block after keyconfig.iofun = gridle_input so that it looks like:

 if (keyconfig.active == false) then
  gridle_input = function(iotbl) 
   if (keyconfig:input(iotbl) == true) then
    keyconf_tomame(keyconfig, "_mame/cfg/default.cfg");
    ledconfig = ledconf_create( keyconfig:labels() );
    if (ledconfig.active == false) then
     gridle_input = ledconfig_iofun;  
    else 
     gridle_input = keyconfig.iofun;
    end
   end
  end
 else
  ledconfig = ledconf_create( keyconfig:labels() );
   if (ledconfig.active == false) then
    gridle_input = ledconfig_iofun;
   end
 end

Just like with the old keyconf block, this new one is a bit messy. In essence, it replaces the current input handler with that of keyconf. When keyconf has working keybindings, it tries to save these in themes/gridle/_mame/cfg
Make sure that folder exists (we havn’t created it before) or else you won’t get a default.cfg stored.
Thereafter, it repeats the process from ledconfig, which reuses the labels that come from keyconfig to assign LEDs to each label. If no usable LED controller is found, it simply returns.
Similarly to keyconf, ledconf takes a few command-line arguments:
ledname=name
ledlabels=label1,label2,label3
forceledconf

the ledname command is analog to keyname, the ledlabels is a comma-separated list of labels to configure, these are in addition to the ones passed on from keyconf.
Still, the configured leds are not lit yet. To fix this, simply add:

    if (game and ledconfig) then
    	ledconfig:toggle(game.players, game.buttons);
    end

At the end of move_cursor. Now for some fine-tuning. The grid background may be nice, but the lines overlapping the faded screenshots are a bit of a distraction, lets fix that. In the build_grid function, after the blend_image(vid, 0.3); add:

 local whitebg = fill_surface(cell_width, cell_height, 255, 255, 255);
 order_image(whitebg, 1);
 show_image(whitebg);
 link_image(whitebg, vid);
 image_mask_clear(whitebg, MASK_SCALE);
 image_mask_clear(whitebg, MASK_ORIENTATION);
 image_mask_clear(whitebg, MASK_OPACITY);

This will create a new empty white cell, link it to the corresponding screenshot at the same cell location and place it beneath this, then mask so the linked image won’t inherit blending, or orientation. This vid will be automatically freed when its parent expires.

Just for fun, add this to the top of the file:

vertex_shader = [[
uniform int n_ticks;

void main(void)
{
    gl_TexCoord[0] = gl_MultiTexCoord0;
    gl_TexCoord[0].s = gl_TexCoord[0].s + fract(n_ticks / 64.0);
    gl_TexCoord[0].t = gl_TexCoord[0].t + fract(n_ticks / 64.0);
    gl_Position = ftransform();
}]];

fragment_shader = [[
#version 120
uniform sampler2D tex;

void main() {
    vec4 color = texture2D(tex, gl_TexCoord[0].st);
	gl_FragColor = color;
}
]];

and when loading background:

 bgimage = load_image("background.png");
 image_program(bgimage, vertex_shader, fragment_shader);

There, now the background is animated.

Until the next version of Arcan, it is not possible to load an image asynchronously, so there’s currently nothing we can do about the delays when all the imagery is loaded. Next guide will be worked on when 0.1.3 have been released. Some challenges to play around with until then:

1. Add some fades and transitions to erase_grid (like the ones in the video) and switch each time erase is called.

2. Figure out a way to switch between a list-view (dishwater theme) and the grid.

3. Add a “zoom” button that will enhance the currently playing video so that an enlarged version is shown in some area of the screen that doesn’t occlude the part where the cursor is.

4. Add a popup- menu that allows for different filters of the gamelist (players, buttons, genre, target, favorites etc.)

5. Add a new button and feature for randomly selecting a game, bonus points if it looks like it is quickly scanning through pages hunting for the random game rather than going there directly.

(and if there’s any OpenAL / Win32 whiz out there, try to figure out the bug where sound gets messed up if running in windowed mode and moving the window while a video is playing) 😉

As per usual, the pastebin can 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:

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