Created
September 27, 2011 10:02
-
-
Save paulhoux/1244739 to your computer and use it in GitHub Desktop.
AS3 sample that compares standard Euler integration to the performance of a Runge-Kutta-4 integrator. Based on an article by Glenn Fiedler: http://gafferongames.com/game-physics/integration-basics/ .
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package nl.cowlumbus.integrator | |
{ | |
/** An object with a position, velocity and acceleration. */ | |
public class BallisticsIntegrator extends Object | |
{ | |
private const force:Number = 10; | |
private const mass:Number = 1; | |
private var position:Number = 0; | |
private var velocity:Number = 0; | |
private const acceleration:Number = force/mass; | |
public function BallisticsIntegrator() | |
{ | |
// no initialization necessary | |
} | |
/** Returns a copy of the instance. */ | |
public function clone():BallisticsIntegrator { | |
var clone:BallisticsIntegrator = new BallisticsIntegrator(); | |
clone.position = this.position; | |
clone.velocity = this.velocity; | |
return clone; | |
} | |
/** Returns precise, accurate values based on ballistic formulas. Use it as a reference. | |
* @param state - initial state of the object | |
* @param t - time (in seconds) since initial state | |
*/ | |
public static function accurate(state:BallisticsIntegrator, t:Number):BallisticsIntegrator { | |
var result:BallisticsIntegrator = state.clone(); | |
result.position = state.velocity * t + 0.5 * state.acceleration * t * t; | |
result.velocity = state.acceleration * t; | |
return result; | |
} | |
/** Performs standard Euler integration on the object, calculating the new state of the object | |
* by advancing the current state in time. | |
* @param state - current state of the object | |
* @param elapsed - time (in seconds) to advance the object | |
* @param samplesPerSecond - more samples means higher accuracy, but at greater computational cost. | |
*/ | |
public static function euler(state:BallisticsIntegrator, elapsed:Number, samplesPerSecond:int=1):BallisticsIntegrator { | |
// calculate necessary number of simulation steps (fractional number) | |
var steps:Number = samplesPerSecond * elapsed; | |
// round to the nearest integer (using int() is MUCH faster than Math.floor()) | |
var rounded:int = int(steps); | |
// note: rounding the steps will make you 'lose' time (i.e. (steps - rounded) / samplesPerSecond), | |
// which should be added to 'elapsed' on the next update if you want to compensate for that. | |
// To keep this sample easy to understand, I have omitted this functionality. | |
// perform Euler integration using fixed time step | |
var result:BallisticsIntegrator = state.clone(); | |
for(var i:int=0;i<rounded;++i) { | |
result = BallisticsIntegrator.evaluate(result, result, 1.0 / samplesPerSecond); | |
} | |
return result; | |
} | |
/** Performs Runge-Kutta 4 integration on the object, calculating the new state of the object | |
* by advancing the current state in time. This is a very efficient, very high-precision | |
* algorithm that should be used instead of the euler method. | |
* @param state - current state of the object | |
* @param elapsed - time (in seconds) to advance the object | |
*/ | |
public static function integrate(state:BallisticsIntegrator, elapsed:Number):BallisticsIntegrator { | |
var a:BallisticsIntegrator = state.clone(); | |
var b:BallisticsIntegrator = evaluate(state, a, 0.5 * elapsed); | |
var c:BallisticsIntegrator = evaluate(state, b, 0.5 * elapsed); | |
var d:BallisticsIntegrator = evaluate(state, c, elapsed); | |
var result:BallisticsIntegrator = state.clone(); | |
result.position += (a.velocity + 2.0*(b.velocity + c.velocity) + d.velocity) / 6.0 * elapsed; | |
result.velocity += (a.acceleration + 2.0*(b.acceleration + c.acceleration) + d.acceleration) / 6.0 * elapsed; | |
return result; | |
} | |
/** Calculates the new state of the object by advancing the current state in time. | |
* @param state - current state of the object | |
* @param derived - used by the RK4 integrator, simply pass current state if not used | |
* @param elapsed - time (in seconds) to advance the object | |
*/ | |
private static function evaluate(state:BallisticsIntegrator, derived:BallisticsIntegrator, elapsed:Number):BallisticsIntegrator { | |
var result:BallisticsIntegrator = state.clone(); | |
result.position += derived.velocity * elapsed; | |
result.velocity += derived.acceleration * elapsed; | |
return result; | |
} | |
public function toString():String { | |
return "(Position:" + position + ", Velocity:" + velocity + ")"; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package nl.cowlumbus.integration | |
{ | |
import flash.display.Sprite; | |
import flash.events.Event; | |
import flash.external.ExternalInterface; | |
import flash.system.Capabilities; | |
import flash.utils.getTimer; | |
[SWF(width=1024, height=768, backgroundColor=0x000000, frameRate=30)] | |
public class IntegrationSample extends Sprite | |
{ | |
private var mEuler:BallisticsIntegrator = new BallisticsIntegrator(); | |
private var mRunge:BallisticsIntegrator = new BallisticsIntegrator(); | |
private var mStartMs:int = 0; // for actual time, use: getTimer(); | |
private var mTimeMs:int = 0; // for actual time, use: getTimer(); | |
public function IntegrationSample() | |
{ | |
this.addEventListener(Event.ADDED_TO_STAGE, onAddedToStage); | |
this.addEventListener(Event.REMOVED_FROM_STAGE, onRemovedFromStage); | |
} | |
private function onAddedToStage(event:Event):void { | |
this.addEventListener(Event.ENTER_FRAME, onEnterFrame); | |
} | |
private function onRemovedFromStage(event:Event):void { | |
this.removeEventListener(Event.ENTER_FRAME, onEnterFrame); | |
} | |
private function onEnterFrame(event:Event):void { | |
var t:int = 0; | |
// determine elapsed time (in milliseconds) since last update | |
var elapsedMs:int = 1000; // for actual time, use: getTimer() - mTime; | |
var elapsedSeconds:Number = elapsedMs * 0.001; | |
// keep track of current time | |
mTimeMs += elapsedMs; | |
// determine total time passed since the application started | |
var totalSeconds:Number = (mTimeMs - mStartMs) * 0.001; | |
IntegrationSample.debug("simulation time: " + totalSeconds + " seconds"); | |
// calculate actual accurate values as a reference | |
var actual:BallisticsIntegrator = BallisticsIntegrator.accurate( new BallisticsIntegrator(), totalSeconds ); | |
IntegrationSample.debug("- actual: " + actual.toString()); | |
// perform Euler integration | |
// note: increase 'samplesPerSecond' to increase the accuracy of the | |
// Euler integrator. You will notice that the error becomes smaller, | |
// but at the cost of a huge increase in computation time. | |
// Even at very large values it will not become more accurate | |
// than the (much faster) RK4 integrator. | |
t = getTimer(); | |
mEuler = BallisticsIntegrator.euler(mEuler, elapsedSeconds, 1); | |
IntegrationSample.debug("- euler : " + mEuler.toString() + " took " + (getTimer() - t) + " milliseconds"); | |
// perform RK4 integration | |
t = getTimer(); | |
mRunge = BallisticsIntegrator.integrate(mRunge, elapsedSeconds); | |
IntegrationSample.debug("- rk4 : " + mRunge.toString() + " took " + (getTimer() - t) + " milliseconds"); | |
// stop after 10 seconds of simulation time | |
if(totalSeconds >= 10) | |
this.removeEventListener(Event.ENTER_FRAME, onEnterFrame); | |
} | |
/** | |
* Sends a debug message to the console window of Flash Builder and your browser. | |
*/ | |
public static function debug(text:String, ...arguments):void { | |
trace(text); | |
if (Capabilities.playerType != "StandAlone") { | |
ExternalInterface.call("console.log", text); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Instructions on how to use this sample:
Run this sample in Debug mode and examine the messages in the Output window. You will notice that Euler integration does not give the correct results and the error accumulates over time. To reduce the error, you can increase the 'samplesPerScond' in line 58 of 'IntegrationSample.as' to somthing like 1000 or even a million. This takes a lot more time to compute and still isn't nearly as accurate as the RK4 integrator.