Ballistics 3
http://johnblackburne.blogspot.com/2012/01/trig-free-rotation-blending.html
http://johnblackburne.blogspot.com/2012/01/varying-rotation-speed.html
http://johnblackburne.blogspot.com/2012/01/variable-speed-rotation-with-complex.html
http://johnblackburne.blogspot.com/2012/02/quadratic-curve-through-three-points.html
// forked from John_Blackburne's Trig-free ballistics 2
// forked from John_Blackburne's Trig-free ballistics
// forked from John_Blackburne's New ballistic demo
package
{
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Shape;
import flash.display.Sprite;
import flash.events.Event;
import flash.geom.Matrix;
import flash.events.MouseEvent;
import flash.text.TextField;
[SWF(backgroundColor="#000000", frameRate="30")]
public class Main extends Sprite {
// gravity: positive as y measured downwards.
// Adjusted for the frame rate
private var fGravity:Number = 10 / stage.frameRate;
// dimensions of all game objects
private const fSize:Number = 8.0;
// gun position, target initial position
private const fGunX:Number = 250.0;
private const fGunY:Number = 350.0;
private const fInitX:Number =400.0;
private const fInitY:Number =400.0;
// speed and speed squared
private const fSpeed:Number= 10.0;
private const fSpeed2:Number= fSpeed * fSpeed;
// the angle of rotation per frame
private var stepAngle:Number;
private var fMinEaseAngle:Number;
// game objects
private var gun:Bitmap;
private var ball:Shape, target:Shape;
private var txt:TextField;
// target position
private var tx:Number, ty:Number;
// the following variable pairs represent complex numbers
// (r1, i1), the current rotation/aim direction
private var r1:Number, i1:Number;
// (r2, i2), the target aim direction
private var r2:Number, i2:Number;
// (rDelta, iDelta), fixed speed rotation per frame
private var rDelta:Number, iDelta:Number;
// (rDelta2, iDelta2), smalle rotation
private var rDelta2:Number, iDelta2:Number;
// (rStep, iStep), rotation per frame + direction
private var rStep:Number, iStep:Number;
// (rStep2, iStep2), small rotation per frame + direction
private var rStep2:Number, iStep2:Number;
// last and target angle, used to work out the rotation time
private var fAngle1:Number, fAngle2:Number;
// times for rotating, shooting and the current time
private var iTime1:Number, iTime2:Number, iTime:Number;
// whether to rotate at fixed speed (with or without ease-in)
// or in a fixed time
private var iSpeedMode:int;
// set when easing in
private var bEasing:Boolean;
private const eConstant:int = 0;
private const eFixedTime:int = 1;
private const eEaseIn:int = 2;
private const eNumModes:int = 3;
private const aModeStr:Vector.<String> = Vector.<String>([
"fixed rotation speed",
"fixed rotation time",
"fixed rotation speed with ease-in",
]);
private const aModeChangeStr:Vector.<String> = Vector.<String>([
"\nfixed speed rotation enabled",
"\nvariable speed rotation enabled",
"\nfixed speed rotation with ease-in enabled",
]);
private const aColours:Vector.<int> = Vector.<int>([
0x804040,
0x408040,
0x404080,
])
// initialise variables, start events
public function Main():void {
MakeThings();
r1 = r2 = 0;
i1 = i2 = -1;
fAngle1 = fAngle2 = 0;
RotateGun(r1, -i2);
init();
// default speed is thirty degrees/second, or 1 degree/frame
stepAngle = Math.PI / 6 / 30;
fMinEaseAngle = stepAngle * 15;
iSpeedMode = eConstant;
rDelta = Math.cos(stepAngle);
iDelta = Math.sin(stepAngle);
rDelta2 = Math.cos(stepAngle / 30);
iDelta2 = Math.sin(stepAngle / 30);
addEventListener(Event.ENTER_FRAME, update);
stage.addEventListener(MouseEvent.MOUSE_DOWN, click);
}
// handle mouse click
private function click(e:MouseEvent):void {
init(e.stageX, e.stageY);
}
// create all the graphics objects
private function MakeThings():void {
var data:BitmapData;
txt = new TextField();
txt.width = txt.height = 465;
txt.textColor = 0xffffff;
addChild(txt);
ball = new Shape();
ball.graphics.beginFill(0xafffff);
ball.graphics.drawCircle(0, 0, fSize);
ball.graphics.endFill();
addChild(ball);
target = new Shape();
target.graphics.lineStyle(1, 0xff8000);
target.graphics.drawCircle(0, 0, fSize);
addChild(target);
data = new BitmapData(fSize * 10, fSize);
data.fillRect(data.rect, 0xffafffff);
gun = new Bitmap(data);
addChild(gun);
}
// draw curve(s)
private function DrawCoverage():void {
// draw the border of the region shots can reach.
// Note that the upper missile path always touches this
var fMidY:Number, fLeftY:Number;
fMidY = fGunY - 0.5 * fSpeed2 / fGravity;
fLeftY = fMidY + 0.5 * fGravity * fGunX * fGunX / fSpeed2;
graphics.beginFill(0x202020);
graphics.lineStyle(2, aColours[iSpeedMode]);
graphics.moveTo(0, fLeftY);
graphics.curveTo(fGunX, fMidY + fMidY - fLeftY, stage.stageWidth, fLeftY);
graphics.lineTo(stage.stageWidth, stage.stageHeight);
graphics.lineTo(0, stage.stageHeight);
graphics.lineTo(0, fLeftY);
//var iX:int, fDist:Number;
//graphics.beginFill(aColours[iSpeedMode]);
//for (iX = 0; iX < stage.stageWidth; iX++) {
// fDist = iX - fGunX;
// graphics.drawRect(iX, fGunY +
// 0.5 * (fGravity * fDist * fDist / fSpeed2 - fSpeed2 / fGravity),
// 1, 1);
//}
}
// update the gun's rotation
private function RotateGun(r2:Number, i2:Number):void {
var r:Number = -r2, i:Number = -i2;
gun.transform.matrix = new Matrix(r, i, -i, r,
fGunX + i * fSize / 2, fGunY - r * fSize / 2);
}
// set up a new target
private function init(_x:Number = fInitX, _y:Number = fInitY):void {
var fDist:Number, fHeight:Number, fGravity2:Number,
fDisc:Number, fB:Number, fTime:Number;
// clear background and redraw
graphics.clear();
graphics.beginFill(0);
graphics.drawRect(0, 0, 500, 500);
// initialise target and missile positions
target.x = tx = _x;
target.y = ty = _y;
ball.x = fGunX;
ball.y = fGunY;
// draw gun 'base'
graphics.beginFill(0x404040, 1);
graphics.drawCircle(fGunX, fGunY, fSize);
// work out the velocity
// get the range
fDist = tx - fGunX;
fHeight = ty - fGunY;
// get the discriminant of the quadratic equation
fGravity2 = fGravity * fGravity;
fB = fSpeed2 + fHeight * fGravity;
fDisc = fB * fB - fGravity2 * (fDist * fDist + fHeight * fHeight);
// if the discriminant is negative then
// cannot solve, which means target is outside of coverage area
if (fDisc < 0) {
txt.text = "Too far/too high for launch speed";
// set the time to something big, to stop updates
iTime = 1000;
// use click outside coverage to toggle varspeed
iSpeedMode = (iSpeedMode + 1) % eNumModes;
txt.appendText(aModeChangeStr[iSpeedMode]);
DrawCoverage();
return;
}
DrawCoverage();
graphics.lineStyle();
txt.text = aModeStr[iSpeedMode];
// get current aim direction
fAngle1 = Math.atan2(r1, -i1);
// finish calculation
fDisc = Math.sqrt(fDisc);
// use longer time, higher/looping shot
// (change + to - to shoot at shallow angle: see previous demo)
fTime = Math.sqrt(2 * (fB + fDisc) / fGravity2);
r2 = fDist / fTime / fSpeed;
i2 = -Math.sqrt(1 - r2 * r2);
fAngle2 = Math.atan2(r2, -i2);
txt.appendText ("\nangle from: " + String(180 / Math.PI * fAngle1));
txt.appendText ("\nangle to: " + String(180 / Math.PI * fAngle2));
bEasing = false;
AimGun();
// set times: time starts negative and counts up to zero
// while gun is rotating, then is positive while missile is
// on its way
iTime = -iTime1;
iTime2 = fTime;
}
// set up gun aiming
private function AimGun():void {
var fAngle:Number = fAngle2 - fAngle1;
if (iSpeedMode == eEaseIn &&
Math.abs(fAngle) > fMinEaseAngle &&
!bEasing) {
if (fAngle2 > fAngle1) {
rStep2 = rDelta2;
iStep2 = iDelta2;
} else {
rStep2 = rDelta2;
iStep2 = -iDelta2;
}
rStep = rStep2; iStep = iStep2;
iTime1 = 31;
bEasing = true;
} else if (iSpeedMode == eFixedTime) {
// fixed time step, varying speed
// need to recalculated the step
var fStep:Number = fAngle / 30;
rStep = Math.cos(fStep);
iStep = Math.sin(fStep);
iTime1 = 31;
bEasing = false;
} else {
// fixed speed
// it is either the same as initially calculated or
// its conjugate, based on the rotation direction
if (fAngle2 > fAngle1) {
rStep = rDelta;
iStep = iDelta;
} else {
rStep = rDelta;
iStep = -iDelta;
}
// get the time from the angles
iTime1 = Math.abs(fAngle) / stepAngle;
bEasing = false;
}
// rotate gun to current direction
RotateGun(r1, i1);
}
// update aim direction
private function UpdateAim():void {
var fR:Number, fI:Number;
if (bEasing) {
fR = rStep; fI = iStep;
rStep = fR * rStep2 - fI * iStep2;
iStep = fI * rStep2 + fR * iStep2;
}
// just multiply the complex numbers, using temporary
// variables as the answer is written back into (r1, i1)
fR = r1; fI = i1;
r1 = fR * rStep - fI * iStep;
i1 = fI * rStep + fR * iStep;
// update gun orientation
RotateGun(r1, i1);
}
// update missile location
private function UpdatePos():void {
// use equations of motion to update its position
ball.x = fGunX + fSpeed * r2 * iTime;
ball.y = fGunY + fSpeed * i2 * iTime +
0.5 * fGravity * iTime * iTime;
// draw a trail
graphics.beginFill(0xafffff, 0.1);
graphics.drawCircle(ball.x, ball.y, fSize);
}
// main update loop
private function update(e:Event):void {
iTime++;
if (iTime < 0) {
// still aiming
UpdateAim();
} else if (iTime == 0) {
if (bEasing) {
fAngle1 = Math.atan2(r1, -i1);
AimGun();
iTime = -iTime1;
UpdateAim();
} else {
// finished aiming so set gun to final position
RotateGun(r1, i1);
}
} else if (iTime <= iTime2) {
// move missile
UpdatePos();
} else {
// finished
ball.x = tx;
ball.y = ty;
}
}
}
}