|
-
Qwaizang:syntax_entity_
[F8] [Disc] 2D Effects Rendering
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.
Qwai•zang \kwî-'zan\ n [origin unknown] 1 : abstract designer, esp. of complex real-time experiments, c. 21st century
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|