Learning 18: Grid planes and advanced animation
This page describes grid planes and how to use them.
How grid planes work
Up to now, you've been working with a single layer of beads, each assigned a single color and alpha transparency. Animation and layering effects required you to keep track of the background color of the playing field, changing and restoring it to make objects appear to move "over" or "under" objects on the grid.
To make these common tasks easier, Perlenspiel supports grid planes. This powerful feature lets you work with multiple planes of bead color, as visualized by the diagram below.
Plane properties and behavior
1. All beads have at least one plane, zero (0), which is the default plane affected by all calls to PS.color(), PS.alpha() and PS.imageBlit(), and all sprite commands.
2. Planes are numbered 0, 1, 2 ... n, with higher-numbered planes appearing "above" lower-numbered planes.
3. Individual beads can use any number of planes, each with its own color and alpha transparency. Planes don't have to be numbered sequentially; it's okay for a bead to have color assigned to planes 0, 3 and 42, with no planes in between.
4. By default, planes above zero (0) are fully transparent (alpha = 0). You must explicitly call PS.alpha() to make beads in planes 1 and higher visible.
5. If a color plane uses transparency (alpha < 255), the color that "shows through" is the effective color of all lower planes combined, including transparency.
6. All of a bead's planes share the same PS.scale(), PS.radius(), PS.data(), PS.exec(), PS.visible() and PS.active() assignments.
7. If visible, a bead's borders and glyph always appear above all color planes.
8. If a bead border uses transparency (alpha < 255), the color that "shows through" the border is the background color of the grid, not the bead's planes or background color.
9. If a bead glyph uses transparency (alpha < 255), the color that "shows through" the glyph is the effective color of all lower planes combined, including transparency.
10. If visible, a bead's background color — controlled by PS.bgColor() and PS.bgAlpha() — always appears outside and around all color planes.
11. If a bead's background color uses transparency (alpha < 255), the color that "shows through" is the background color of the grid.
By default, the commands that change the color of beads — PS.color(), PS.alpha() and PS.imageBlit(), as well as the sprite commands to be described later — operate on grid plane 0.
The PS.gridPlane() command lets you specify which grid plane all subsequent calls to these commands will affect.
The optional plane parameter can be any positive integer. Values less than zero are clamped to zero. Non-integral values are floored.
If plane is PS.DEFAULT, the default plane (0) is used. If plane is PS.CURRENT or not supplied, the active plane is not changed.
// EXAMPLE
// Color bead 0, 0 to red using default plane (0)
PS.color( 0, 0, PS.COLOR_RED );
// Change active plane to 1
PS.gridPlane( 1 );
// Set plane 1 to opaque and assign a new color
// This hides the red plane under the blue
PS.alpha( 0, 0, PS.ALPHA_OPAQUE );
PS.color( 0, 0, PS.COLOR_BLUE );
// Set plane 1 back to transparent
// to see the red plane again
PS.alpha( 0, 0, PS.ALPHA_TRANSPARENT );
Using planes
The general technique for using planes is simple.
1. Call PS.gridPlane() to select the plane you want to change.
2. By default, planes above zero (0) are fully transparent (alpha = 0). If necessary, call PS.alpha() to make sure the beads you're changing will be visible.
3. Use PS.color(), PS.alpha(), PS.imageBlit() or the appropriate sprite commands to make changes to the current plane.
4. Repeat.
Usage notes
1. Once you start using grid planes, it's essential to know which plane is currently active. Rather than saving/restoring the active plane, it's usually much easier to explicitly call PS.gridPlane() before changing the color and/or alpha of related beads.
2. To hide a bead plane, use PS.gridPlane() to select the plane, and then call PS.alpha() on the relevant beads with an alpha value of 0 (fully transparent).
3. A call to PS.gridSize() deletes all existing grid planes, except plane zero (0).
4. Although there's no formal limit to the number of planes you can use, engine performance suffers as the number of planes goes up. If speed is important, use as few planes as possible.
Demonstration
This demo is based on the Goldgrabber game developed in the Coding strategies tutorial. In this version, four grid planes are used:
Plane 0 is for the floor and walls. Because the grabber and gold will be placed in their own planes, we no longer have to worry about restoring the floor color when the grabber moves over it, so it's now colored in random shades of dark gray.
Plane 1 is used for the gold. Since the gold's color is the only color used in this plane, we initialize the entire plane to that color, and change only the alpha transparency when gold is placed or removed.
Plane 2 is the grabber's. Again, the grabber's color is the only color used in this plane, so we initialize the entire plane to that color, changing only the alpha when the grabber is placed or moved.
Plane 3 is used to display a set of eight red arches. Because this plane is higher than the others, any elements in other planes that occupy the same locations will be hidden underneath.
The map array contains the layout of the playing area. As before, it's used both to draw the map, steer the grabber and keep track of where gold has been placed.
A neat trick: When G.drawMap() finds an arch, it replaces the arch location in the map array with floor, making it a legal place for the grabber to walk, and also a legal place to place gold. If some gold pieces appear to be missing when you play, it's because they are hidden under an arch! The scoring message and sound will alert you when the grabber finds gold under an arch.
var G; // establish global namespace
( function () {
// Private variables and functions
var grabX = 0; // current x-position
var grabY = 0; // current y-position
var score = 0; // current score
var goldCount = 0; // number of pieces grabbed
var goldMax = 10; // max number of gold pieces
var goldValue = 10; // value of each gold piece
var wallColor = PS.COLOR_BLACK;
var archColor = 0x800000; // medium red
var goldColor = PS.COLOR_YELLOW;
var grabColor = PS.COLOR_GREEN;
// Establish planes for geography, gold & grabber
var floorPlane = 0;
var goldPlane = 1;
var grabPlane = 2;
var archPlane = 3; // so grabber goes "under" arches
// 15 x 15 map array
// 0 = wall, 1 = floor, 2 = arch
var map = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 0,
0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0,
0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0,
0, 1, 0, 2, 0, 1, 1, 1, 1, 1, 0, 2, 0, 1, 0,
0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0,
0, 1, 0, 2, 0, 1, 1, 1, 1, 1, 0, 2, 0, 1, 0,
0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0,
0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0,
0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 0,
0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
];
// Private variables and functions
// This is where G is defined as an object,
// and its properties initialized
G = {
width : 15, // width of grid
height : 15, // height of grid
// drawMap()
// Scans the map data and draws geography,
// randomly placing gold and grabber
drawMap : function () {
var ptr, x, y, data, val, i, loc;
ptr = 0; // init data pointer
for ( y = 0; y < G.height; y += 1 ) {
for ( x = 0; x < G.width; x += 1 ) {
data = map [ ptr ]; // get map data
if ( data === 0 ) { // wall?
PS.gridPlane( floorPlane );
PS.color( x, y, wallColor );
}
else if ( data === 1 ) { // floor?
PS.gridPlane( floorPlane );
// Set floor to random gray
val = ( PS.random(32) - 1 ) + 128;
PS.color( x, y, val, val, val );
}
else if ( data === 2 ) { // arch?
map[ ptr ] = 1; // change to floor
PS.gridPlane( archPlane );
PS.alpha( x, y, PS.ALPHA_OPAQUE );
PS.color( x, y, archColor );
}
ptr += 1; // update pointer
}
}
// Set entire grabber plane to gold
// Only alpha will need to be manipulated
PS.gridPlane( goldPlane );
PS.color( PS.ALL, PS.ALL, goldColor );
// Randomly place gold pieces on gold plane
// Some pieces may end up beneath arches!
for ( i = 0; i < goldMax; i += 1 ) {
do {
x = PS.random( 15 ) - 1;
y = PS.random( 15 ) - 1;
loc = ( y * G.width ) + x;
data = map[ loc ]; // get map data
} while ( data !== 1 ); // must be floor
map[ loc ] = 4; // place gold in data
PS.alpha( x, y, PS.ALPHA_OPAQUE );
}
// Set entire grabber plane to green
// Only alpha will need to be manipulated
PS.gridPlane( grabPlane );
PS.color( PS.ALL, PS.ALL, grabColor );
// Randomly place grabber on floor
do {
x = PS.random( 15 ) - 1;
y = PS.random( 15 ) - 1;
loc = ( y * G.width ) + x;
data = map[ loc ]; // get map data
} while ( data !== 1 ); // must be floor
grabX = x; // save location
grabY = y;
PS.alpha( x, y, PS.ALPHA_OPAQUE );
},
// move( x, y )
// Attempts to move grabber relative to
// current position
move : function ( x, y ) {
var nx, ny, loc, data;
// Calculate proposed new position
nx = grabX + x;
ny = grabY + y;
// Is new location off the grid?
// If so, return without moving
if ( ( nx < 0 ) || ( nx >= G.width ) ||
( ny < 0 ) || ( ny >= G.height ) ) {
return;
}
// Is there a wall in that location?
// If map data = 0, it's a wall
// so return without moving
// If data = 4, it's gold, so update score
loc = ( ny * G.width ) + nx;
data = map[ loc ];
if ( data === 0 ) {
return;
}
if ( data === 4 ) {
map[ loc ] = 1; // Change gold to floor
PS.gridPlane( goldPlane );
PS.alpha( nx, ny, PS.ALPHA_TRANSPARENT );
PS.audioPlay( "fx_coin7" );
score += goldValue;
goldCount += 1;
if ( goldCount >= goldMax ) {
PS.statusText( "You win with $" +
score + "!" );
PS.audioPlay( "fx_tada" );
}
else {
PS.statusText( "Score = $" + score );
}
}
// Legal move, so erase grabber's current
// location and move grabber
PS.gridPlane( grabPlane );
PS.alpha ( grabX, grabY, PS.ALPHA_TRANSPARENT );
PS.alpha( nx, ny, PS.ALPHA_OPAQUE ); // new pos
grabX = nx;
grabY = ny;
PS.audioPlay( "fx_click" );
}
};
}() );
PS.init = function( system, options ) {
PS.gridSize( G.width, G.height ); // init grid
PS.border( PS.ALL, PS.ALL, 0 ); // no borders
G.drawMap(); // draws walls
PS.audioLoad( "fx_click" );// preload sounds
PS.audioLoad( "fx_coin7" );
PS.statusText( "Use arrow/WASD keys to grab gold" );
};
PS.keyDown = function( key, shift, ctrl, options ) {
switch ( key ) {
case PS.KEY_ARROW_UP:
case 119: // lower-case w
case 87: // upper-case W
{
G.move( 0, -1 );
break;
}
case PS.KEY_ARROW_DOWN:
case 115: // lower-case s
case 83: // upper-case S
{
G.move( 0, 1 );
break;
}
case PS.KEY_ARROW_LEFT:
case 97: // lower-case a
case 65: // upper-case A
{
G.move( -1, 0 );
break;
}
case PS.KEY_ARROW_RIGHT:
case 100: // lower-case d
case 68: // upper-case D
{
G.move( 1, 0 );
break;
}
}
};
Return value
PS.gridPlane() returns an integer indicating the currently active grid plane.
PS.ERROR is returned if an error occurs.
Next: Sprites