In the process of figuring out my game engine, I've decided to create an open discussion about the methods I've found to achieve certain visual effects so that others can learn from my experiments and I can learn from others' responses.

Before I get into anything, I want to point out that my engine utilizes a document stack of container movieclips. By that I mean I have a seperate container for all interactive objects, a seperate container for players, a seperate container for background graphics, a seperate container for buildings, etc. Each container serves as a layer that is composited into the final frame at render time. With that in mind, let's continue.

1. Residual Frames

A specific goal I had from the beginning was the ability to render residual frames. This is an effect that is typically used in fighting or action games during special moves or power ups. Although a bit of work can make this staple of eye candy look original, it generally tends to look like this:




My first approach to this effect was the assumption that I could simply use a seperate BitmapData surface to copy the position of affected objects and composite that surface into each rendered frame like everything else, however, this would not address two particular issues. First, it would retain all frames rendered until the effect was turned off, rather than only retaining a specified number of frames. Second, in a multiplayer situation, the residual frames would render relative to the visible region of the screen, not world coordinates of the game level itself, so not every one would see the same thing at render time. I devised the following method to address these issues, which holds up well at 60 fps on my mashed up machine:

Code:
// New Document: frame 1, main timeline
import flash.display.BitmapData
import flash.geom.ColorTransform
import flash.geom.Point
import flash.geom.Rectangle

var myRoot:MovieClip = this;
var playerMC:MovieClip = myRoot.createEmptyMovieClip("playerMC", myRoot.getNextHighestDepth());
var renderMC:MovieClip = myRoot.createEmptyMovieClip("renderMC", myRoot.getNextHighestDepth());
var renderPoint:Point = new Point(0, 0);
var startP:Point = new Point(-1, -1);
var endP:Point;
var renderW:Number = Stage.width;
var renderH:Number = Stage.height;
var residualBuffer:Number = 8; // legal values are 1 thru 255
var residualFade:ColorTransform = new ColorTransform(1, 1, 1, 1, 0, 0, 0, -Math.round(256 / (residualBuffer + 1)));
var residualSurface:BitmapData = new BitmapData(renderW, renderH, true, 0x00000000);
var renderSize:Rectangle = new Rectangle(0, 0, renderW, renderH);

function testResidual() {
	// reduce render quality to improve framerate
	if(myRoot._quality != "LOW") myRoot._quality = "LOW";
	// remove our player from video memory
	if(playerMC._visible) playerMC._visible = false;
	// erase previous line
	playerMC.clear();

	// start drawing new line
		playerMC.lineStyle(10, 0xffffff, 100);
		if((startP.x < 0) && (startP.y < 0)) {
			startP.x = Math.round(Math.random() * renderW);
			startP.y = Math.round(Math.random() * renderH);
		} else {
			startP.x = endP.x;
			startP.y = endP.y;
		}
		endP = new Point(Math.round(Math.random() * renderW), Math.round(Math.random() * renderH));
		playerMC.moveTo(startP.x, startP.y);
		playerMC.lineTo(endP.x, endP.y);
	// finished drawing new line
	
	// fade residual frames
	residualSurface.colorTransform(renderSize, residualFade);
	// append a current frame
	residualSurface.draw(playerMC);
	
	// clean up before rendering
	renderMC.clear();
	// start rendering current frame
		renderMC.lineStyle(0, 0x000000, 0);
		renderMC.moveTo(0, 0);
		renderMC.beginBitmapFill(residualSurface);
			renderMC.lineTo(renderW, 0);
			renderMC.lineTo(renderW, renderH);
			renderMC.lineTo(0, renderH);
			renderMC.lineTo(0, 0);
		renderMC.endFill();
	// finished rendering current frame
}

// render once per frame
myRoot.onEnterFrame = testResidual;
The way this works is pretty straight forward. Each frame of animation is drawn to residualSurface, then the entire contents of the BitmapData are manipulated using a ColorTransform so that their alpha channel is reduced. Older frames appear more transparent and eventually disappear because the effect is cumulative. By controlling the rate of dissipation, the number of visible residual frames can be accurately constrained.

This demo is a basic primer on the technique, but it doesn't give any real indication of its limitations or practical use. A bit of rewriting and additional logic is required to properly test this effect. Below is a beefed up version, complete with full commentation:

Code:
// New Document: frame 1, main timeline

// import the classes we need
import flash.display.BitmapData
import flash.geom.ColorTransform
import flash.geom.Matrix
import flash.geom.Point
import flash.geom.Rectangle

// allow or disallow residual frame rendering
var ALLOW_RESIDUAL:Boolean = true;
// numerics
var worldW:Number = Stage.width;
var worldH:Number = Stage.height;
var charW:Number = 64;
var charH:Number = 96;
var residualBuffer:Number = 8;

// the behavior of our character
var behavior:Object = {maxSpeed : 10, moveX : 0, moveY : 0};

// clean reference to _root
		var myRoot:MovieClip = this;
// a container for all elements of our world
		var world_mc:MovieClip = myRoot.createEmptyMovieClip("world_mc", myRoot.getNextHighestDepth());
// a surface to render our frames on
		var render_mc:MovieClip = myRoot.createEmptyMovieClip("render_mc", myRoot.getNextHighestDepth());
// a container for all elements of our character
		var char_mc:MovieClip = world_mc.createEmptyMovieClip("char_mc", world_mc.getNextHighestDepth());
// a surface to render our character's residual frames on
		var residual_mc:MovieClip = char_mc.createEmptyMovieClip("residual_mc", char_mc.getNextHighestDepth());
// a surface to render our character's current frame on
		var graphics_mc:MovieClip = char_mc.createEmptyMovieClip("frame_mc", char_mc.getNextHighestDepth());

// objects used in rendering residual frames
var residualFader:ColorTransform = new ColorTransform(1, 1, 1, 1, 0, 0, 0, -Math.round(256 / (residualBuffer + 1)));
var residualMatrix:Matrix;			// to be initialized later
var residualRegion:Rectangle;		// ...
var residualSurface:BitmapData;	// ...
var renderSurface:BitmapData = new BitmapData(worldW, worldH, false, 0xff000000);

// the events of our demo
myRoot.onLoad = init;
myRoot.onEnterFrame = drawFrame;

// initilization code
function init() {
	initRender();
	initRegion();
	initSurface();
	initChar();
};
// optimize rendering
function initRender() {
	// reduce render quality to improve framerate
	myRoot._quality = "LOW";
	// free up video memory by hiding the contents of our world
	world_mc._visible = false;
};
// calculate surface area needed for residual frames
function initRegion() {
	residualRegion = new Rectangle(0,
																 0,
																 charW + (2 * (residualBuffer * behavior.maxSpeed)),
																 charH + (2 * (residualBuffer * behavior.maxSpeed)));
};
// allocate memory for surface area of residualRegion
function initSurface() {
	residualSurface = new BitmapData(residualRegion.width, residualRegion.height, true, 0x00000000);
};
// draw the content of our character
function initChar() {
	graphics_mc.clear();
	graphics_mc.lineStyle(0, 0xc0c0c0, 100);
	graphics_mc.moveTo(0, 0);
	graphics_mc.beginFill(0xc0c0c0, 100);
		graphics_mc.lineTo(charW, 0);
		graphics_mc.lineTo(charW, charH);
		graphics_mc.lineTo(0, charH);
		graphics_mc.lineTo(0, 0);
	graphics_mc.endFill();
	residual_mc._x = -(residualBuffer * behavior.maxSpeed);
	residual_mc._y = -(residualBuffer * behavior.maxSpeed);
};
// process keypresses independent of Key.addListener() event handlers
function getInput() {
	// assume no key is pressed
	behavior.moveX = 0;
	behavior.moveY = 0;
	if(Key.isDown(Key.LEFT)) {
		if(char_mc._x - behavior.maxSpeed >= 0) {
			// enable left movement
			behavior.moveX = -1;
		}
	}
	if(Key.isDown(Key.RIGHT)) {
		if(worldW - (char_mc._x + charW + behavior.maxSpeed) >= 0) {
			// enable up movement
			behavior.moveX = 1;
		}
	}
	if(Key.isDown(Key.UP)) {
		if(char_mc._y - behavior.maxSpeed >= 0) {
			// enable right movement
			behavior.moveY = -1;
		}
	}
	if(Key.isDown(Key.DOWN)) {
		if(worldH - (char_mc._y + charH + behavior.maxSpeed) >= 0) {
			// enable down movement
			behavior.moveY = 1;
		}
	}
	// toggle rendering of residual frames
	ALLOW_RESIDUAL = !Key.isToggled(Key.CAPSLOCK);
};
// process the movement flags and update character position on screen
function moveChar() {
	char_mc._x += behavior.moveX * behavior.maxSpeed;
	char_mc._y += behavior.moveY * behavior.maxSpeed;
};
function drawFrame() {
	// build translation matrix
	residualMatrix = new Matrix(1, 0, 0, 1, residualBuffer * behavior.maxSpeed, residualBuffer * behavior.maxSpeed);
	// once-per-frame code
		getInput();
		moveChar();
		// translate the residual frames rendered up until this point
		residualSurface.scroll(-behavior.moveX * behavior.maxSpeed, -behavior.moveY * behavior.maxSpeed);
		// apply fade to residual frames rendered up until this point
		residualSurface.colorTransform(residualRegion, residualFader);
		// conditionally allow current frame to be added to residual frames
		(ALLOW_RESIDUAL) ? residualSurface.draw(graphics_mc, residualMatrix) : null;

		// clean up before compositing
		residual_mc.clear();
		// composite the residual frames into the final frame
		residual_mc.lineStyle(0, 0x000000, 0);
		residual_mc.moveTo(0, 0);
		residual_mc.beginBitmapFill(residualSurface);
			residual_mc.lineTo(residualRegion.width, 0);
			residual_mc.lineTo(residualRegion.width, residualRegion.height);
			residual_mc.lineTo(0, residualRegion.height);
			residual_mc.lineTo(0, 0);
		residual_mc.endFill();
	
		// clean up before capturing
		renderSurface.fillRect(new Rectangle(0, 0, worldW, worldH), 0xff000000);
		// capture the current composite
		renderSurface.draw(world_mc);
	
		// clean up before rendering
		render_mc.clear();
		// render the current final frame
		render_mc.lineStyle(0, 0x000000, 0);
		render_mc.moveTo(0, 0);
		render_mc.beginBitmapFill(renderSurface);
			render_mc.lineTo(worldW, 0);
			render_mc.lineTo(worldW, worldH);
			render_mc.lineTo(0, worldH);
			render_mc.lineTo(0, 0);
		render_mc.endFill();
	// end of once-per-frame code
};
It is important to note that while the first demonstration can be run using 255 residual frames with no perceptible bogging, this second demonstration begins to bog very noticably with a residual frame count of only 32 (on my junky machine, at least). While a strict rectangle is far from being anything remotely like a fully animated character, you get the idea.

The first demo can be used to generate fog-like effects by setting the alpha value of playerMC.lineStyle() to 1 and changing the alpha channel transformation in residualFade to return a positive value instead of a negative one. Unfortunately, this takes time to generate anything very interesting, but perhaps using a for() loop might be handy in churning out a randomized haze layer in no time.

More soon.