Projectile trajectory with Flex 4 Keyframes and Parsley framework

UPDATE 12.05.2012
An updated version without the Parsely framework has been posted here.

One of my colleagues is developing a shooting penguins game for a Xmas promotion. He tried to keep it as simple as possible when it comes to simulating the projectile trajectory. Eventually some physics was required when requirements started to change. This reminded me of the time was in the 9th grade in high-school and I was able to do this kind of stuff in front of the blackboard with a piece of chalk in my hand (props to my physics teacher, wherever he is right now!). So to honor the “good ol’ days” and the changing requirements, I created a simple motion class and a test Flex application to demonstrate the classic problem of projectile motion trajectory.

The problem
A projectile is being fired from a certain (known) height – launchHeight – and offset – launchOffset – from the origin point (ground or above) with a known velocity – launchVelocity – and angle – launchAngle -. Determine the time that the projectile needs to hit the ground and also plot its trajectory.

Theory
There are two types of motion, vertical and horizontal. Both happen at the same time, but they are separate. In the following x or xComponent refers to the horizontal components and y or yComponent for the vertical components.

The time of flight (t) is the time it takes for the projectile to finish its trajectory, and is given by the formula:

Time of Flight

Position at t
The horizontal motion happens at constant velocity while the vertical motion happens while undergoing uniform acceleration (due to gravity). In all of the following gravity = 9.81 m/s2. Two elementary formulas are called upon relating to projectile motion:

Position at t

Solution 1.
First compute the range of the projectile (the distance that the projectile has traveled when it returns to the ground), and implicitly the landing position – on the ground x = 0. Register a listener for the Event.ENTER_FRAME, and generate the trajectory on the fly. At the same time move the projectile on the screen, at it’s new position calculated based on the time that has passed since it has launched. The position in compared against the landing position (y = 0 for the ground and x = range). When they are the equal the projectile hit the ground and the motion needs to end.

Solution 2
Calculate the time of the flight in advance and then based on that generate arbitrary intermediary positions for the trajectory of the projectile. Translate these intermediary positions into keyframes (time value pairs) using Flex 4 Keyframe class. The keyframe vectors will be pushed into the MotionPath-s of an Animate instance which will play() the trajectory animation.

Domain physics
The class doing all the math and physics is MovingObject. It accepts the initial motion parameters as constructor arguments, which will be used later on to produce the time of flight and the position at a certain time using the above formulas. The class exposes a public API with three methods:  calculateTimeOfFlight():void, calculatePositionAtTime(time:Number):Position, and generateMotionTrajectoryPositions():Vector.<Position>. A position is a flash.geom.Point point at a certain time (e.g. can store the time value). The helper MotionUtil offers some general conversion and calculation methods.

public class MovingObject
{
	private var _launchAngle:Number;
	private var _launchVelocity:Number;
	private var _launchHeight:Number;
	private var _offset:Number;

	public function MovingObject(launchAngle:Number = 60, launchVelocity:Number = 50, launchHeight:Number = 0, offset:Number = 0)
	{
		_launchAngle = launchAngle;
		_launchVelocity = launchVelocity;
		_launchHeight = launchHeight;
		_offset = offset;
	}

	public function calculatePositionAtTime(time:Number):Position
	{
		var seconds:Number = MotionUtil.seconds(time);
		var seconds2:Number = seconds * seconds;
		//dx = vx * t;
		var dx:Number = MotionUtil.xVectorComponent(_launchVelocity, _launchAngle) * seconds + _offset;
		//dy = vy - gt2/2
		var dy:Number = _launchHeight + MotionUtil.yVectorComponent(_launchVelocity, _launchAngle) * seconds - MotionUtil.GRAVITY * seconds2 * 0.5;

		return new Position(dx, dy, time);
	}

	public function calculateTimeOfFlight():Number
	{
		//v*sinO + Sqrt(v2sin2O + 2g*initialheight)/gravity
		var vsinO:Number = _launchVelocity * Math.sin(MotionUtil.radian(_launchAngle));
		var v2sinO2:Number = Math.pow(vsinO, 2);
		var g2Yo:Number = 2 * MotionUtil.GRAVITY * _launchHeight;
		var discriminant:Number = Math.sqrt(v2sinO2 + g2Yo);
		return ((vsinO + discriminant) / MotionUtil.GRAVITY) * 1000;
	}

	public function generateMotionTrajectoryPositions():Vector.
	{
		var timeOfFlight:Number = calculateTimeOfFlight();
		var trajectoryPositions:Vector. = new Vector.();
		var crtTime:Number = 0;
		while (crtTime 		{
			//position at t
			var position:Position = calculatePositionAtTime(crtTime);
			trajectoryPositions.push(position);
			crtTime += 25;
		}

		return trajectoryPositions;
	}

The function generateMotionTrajectoryPositions uses the toher 2 functions to generate the trajectory positions. Their are are publicly public so that the class can be used in solving the problem also with Solution 1. Once the trajectory positions are generated, they are sent to the view to be are converted into keyframes and played.

View code
MotionGrid is a custom container responsible for displaying a grid with 2 labeled axes. The color of the gridlines (gridLineColor),  axis (axisColor) and size of a square (cellSize) are customizable style property. The class can be used as a base component for any other physics drawing experiments. The visual workhorse is TrajectoryCanvas. It creates the projectile (random color for each new motion),  the anchors for the projectile, converts the generated positions points to keyframes, adjusts the x, and y coordinates to screen coordinates, plays the trajectory animation and also keeps track of the elapsed time.  When the motion has finished, the component dispatches the “done” event. I will only post the most interesting part the one regarding generating Animate instance and the MotionPath/Keyframes with actionscript at runtime, the rest can be inspected inside the project.

/**
 * @private
 * Instance used to perform the projectile animation based on the motion paths
 * calculated at runtime and feed to this instance
 */
private var _animator:Animate;

//...
/**
 * inits the animator instances AS3 style
 **/
private function initAnimators():void
{
	_animator = new Animate();
	_animator.addEventListener(EffectEvent.EFFECT_UPDATE, onEffectUpdate);
	_animator.addEventListener(EffectEvent.EFFECT_END, onMotionEnded);
	_animator.motionPaths = new Vector.();
	var xPath:MotionPath = new MotionPath("x");
	_animator.motionPaths.push(xPath);
	var yPath:MotionPath = new MotionPath("y");
	_animator.motionPaths.push(yPath);
}

//...
/**
 * Converts the global position points to local and generates keyframes for the
 * Animate inntance
 * @param the positions vector
 */
private function convertTrajectoryToMotionPath(positions:Vector.):void
{
	var xPath:MotionPath = _animator.motionPaths[0];
	xPath.keyframes = new Vector.();

	var yPath:MotionPath = _animator.motionPaths[1];
	yPath.keyframes = new Vector.();

	var numPositions:Number = positions.length;
	var i:uint;
	var pos:Position;
	for (i = 0; i < numPositions; i++)
	{
		pos = positions[i];
		var dx:Number = _origin.x + pos.x - _projectile.width / 2;
		var xKeyframe:Keyframe = new Keyframe(pos.time, dx);
		xPath.keyframes.push(xKeyframe);
		var dy:Number = _origin.y - pos.y - _projectile.height / 2;
		var yKeyframe:Keyframe = new Keyframe(pos.time, dy);
		yPath.keyframes.push(yKeyframe);
	}

	//...
}

Glue code
I am using the Parsley framework to handle the communication/messaging between components and inject/configure objects. The above mentioned LaunchBar it is built around the Presentation Model pattern, so every user gesture is forwarded to the presentation model (LaunchBarPM) who will broadcast custom managed events. TrajectoryCanvas will listen for the managed events directly instead of having it’s own presentation model. It is a known fact that PM is not at its best when dealing with drawing components as Parsley is not at its best when dealing with supervising presenters. So it is a trade-off I was willing to make. Also the “done” event dispatched by TrajectoryCanvas is managed by Parsley. It will be handled inside LaunchBarPM by onMotionFinished(), and it is used to enabled the launch button which stays disable during motion time. One of the things I really like about Parsley is that you it doesn’t tie the code to the framework. So if you want to switch to a different framework in the future it is really easy to do so. All the objects are configure in the Context (.mxml) external file, the only reference to the framework are in the main application file. But in the end the main application is project specific so it won’t really be reused in other different project.

<View type="{TrajectoryCanvas}">
	<MessageHandler method="launch" selector="launch"/>
	<MessageHandler method="adjustPosition" selector="heightChanged"/>
	<MessageHandler method="adjustPosition" selector="offsetChanged"/>
	<ManagedEvents names="['done']"/>
</View>
		
<Object type="{LaunchBarPM}">
	<ManagedEvents names="['generateTrajectory', 'heightChanged', 'offsetChanged']"/>
	<MessageHandler method="onMotionFinished" selector="done"/>
</Object>
		
<DynamicCommand type="{GenerateMotionTrajectoryCommand}" selector="generateTrajectory">
	<MessageDispatcher property="dispatcher"/> 
</DynamicCommand>

//======Main app - TrajectorySimulator.mxml ====
<fx:Declarations>
	<parsley:ContextBuilder config="ro.a223.sim.context.Context"/>
	<parsley:Configure target="{canvas}"/>
</fx:Declarations>

Every new trajectory (launch button click + motion input arguments attached as payload to the managed event MotionTrajectoryEvent) gets generated inside a dynamic command (GenerateMotionTrajectoryCommand) class which will also notify the TrajectoryCanvas that a new trajectory has been generated and it is ready to be animated. TrajectoryCanvas will take care of the the animation, drawing of the trajectory points on the screen and animate the elapsed time.

Live application is below, the .fxp project is also available for DOWNLOAD

3 Comments

  1. Thanks mate, I could not hope for more than finding this blog, I am working on a drawing app project using Cairngorm & Parsley.

  2. Hi Claudiu,

    can you send me the solution without using parsley ?

    thank you for your help
    Chiheb

Leave a Reply

Your email address will not be published. Required fields are marked *

*