Tutorial: Radial Gravity and Predicting Trajectory

Tutorial: Radial Gravity and Predicting Trajectory

Today’s tutorial features two more physics methods for your bag of tricks. In this tutorial, we’ll discuss “radial gravity” and predicting launch trajectory.

Both of these methods are included as downloadable projects — please reference the link at the end of this tutorial.


Radial Gravity

Radial gravity was recently made famous in Angry Birds: Space™, in which the wild crew of avian aeronauts had to contend no longer with Earth-based gravity, but rather with gravitational pull. While Corona’s physics engine doesn’t yet feature a native “radialGravity” setting, it’s relatively simple to mimic using sensors and touch joints.

This functionality could also be used for such things as magnet behavior — in fact, you might have seen this exhibited in the Corona-powered game Major Magnet.

Creating the Radial Field

The first step is to create the “radial field” that will exert a pull force on objects that enter it. Consider this the atmosphere of a planet, or the sensory range around a magnet. In Corona, we can create this field as a simple radial body set as a sensor, as follows:

local field = display.newCircle( 0, 0, 320 ) ; field.alpha = 0.3
field.name = "field"
field.x = display.contentCenterX ; field.y = display.contentCenterY
physics.addBody( field, "static", { isSensor=true, radius=320 } )

Object Collision (with the Field)

How you create the object(s) that will enter this field is largely up to you, so I won’t go into detail about that. What I will discuss is the common method behind the collision with this field. Essentially, when an object enters the field — collision phase of “began” — we’ll create a physics touch joint between the object and the center of the field. Note that in the case of a touch joint, you’re not actually joining the object to another object as you would with most physical joints. In the case of a touch joint, the “target” is simply a point in world coordinates. In this tutorial, that point will simply be the center of the radial field.

function objectCollide( self, event )
local otherName = event.other.name
local function onDelay( event )
local action = ""
if ( action == "makeJoint" ) then
self.hasJoint = true
self.touchJoint = physics.newJoint( "touch", self, self.x, self.y )
self.touchJoint.frequency = 0.25
self.touchJoint.dampingRatio = 0.0
self.touchJoint:setTarget( event.other.x, event.other.y )
. . .
if ( event.phase == "began" and otherName == "field" and self.hasJoint == false ) then
local tr = timer.performWithDelay( 10, onDelay ) ; tr.action = "makeJoint"

Note that we’ve created an onDelay function within the collision function to handle actions that can’t be executed in the same game cycle as the collision itself. In this example, we also assign a property named action to the timer itself, so when it executes, we know the action that should be taken (more timers will be needed as we progress through this tutorial, so it’s useful to identify the action associated with them).

Another important note is that you should assign a flag to the object itself — I’m using hasJoint here — as a further safety check that the object entering the field doesn’t already have a touch joint applied to it.

Post-Collision Behavior

So what happens here, in actual practice? Well, the object enters the radial field and the touch joint is applied after 10 milliseconds. The frequency of the touch joint determines the “pull” of the joint itself, and in effect, how much power or pull your planet/magnet will appear to have. Be careful to test this value repeatedly, as a minor adjustment can affect the behavior significantly. Finally, the touch joint :setTarget call sets the destination in world coordinates that the object will attempt to reach — and I must emphasize that it will only attempt to reach that point. Objects under the influence of touch joints can be interrupted by collision with other objects, or the joint itself can be explicitly broken/removed (we’ll actually do this in the next step).

Exiting the Field

In most cases, you’ll want the objects to have the capability of exiting the radial field and continuing on their expected path, especially fast-moving objects that might “skirt” the outside of the field. When this occurs, we’ll need to remove the touch joint so that the object no longer attempts to reach the center of the radial field. This is easily handled with the collision ended phase as follows:

function objectCollide( self, event )
local otherName = event.other.name
local function onDelay( event )
local action = ""
. . .
elseif ( action == "leftField" ) then
self.hasJoint = false ; self.touchJoint:removeSelf() ; self.touchJoint = nil
. . .
elseif ( event.phase == "ended" and otherName == "field" and self.hasJoint == true ) then
local tr = timer.performWithDelay( 10, onDelay ) ; tr.action = "leftField"

And that’s basically it. If the object exits the field, the touch joint is removed and the object simply goes on its way. Of course, the full sample project involves a bit more than we’ve discussed up to this point, but this is the essential method behind radial gravity in Corona. Quite simple!


Predicting Trajectory

This part of the tutorial comes to you courtesy of Matt Webster, a.k.a. HoraceBury. Matt is a Corona Ambassador with a keen interest in physics, and he volunteers his skills and advice in the Corona Code Exchange and the forums.

Predicting trajectory has traditionally been a confusing endeavor, and some developers simply guess and hope for the best. Today we’ll discuss the real solution!

The core task of predictive trajectory is gathering some basic data like the starting position and starting velocity of the projectile you want to launch. In regards to touch events in Corona, it might look like this:

local startingVelocity = { x=event.x-event.xStart, y=event.y-event.yStart }
for i = 1,180 do
local s = { x=event.xStart, y=event.yStart }
local trajectoryPosition = getTrajectoryPoint( s, startingVelocity, i )
local circ = display.newCircle( trajectoryPosition.x, trajectoryPosition.y, 5 )

This loop draws the predicted trajectory path in 180 steps — 3 seconds of launch time at 60 fps — using vector circles. Essentially, we calculate the difference between the event x/y starting and ending positions as the “velocity.” Then, the position of each circle, and thus the trajectory plotted by the path, is gathered from the getTrajectoryPoint function as illustrated in the next step.

The core calculation is done in the following function, which uses the frames per second in “time steps” along with the assumed gravity and the values we gathered above.

function getTrajectoryPoint( startingPosition, startingVelocity, n )
--velocity and gravity are given per second but we want time step values here
local t = 1/display.fps  --seconds per time step at 60fps
local stepVelocity = { x=t*startingVelocity.x, y=t*startingVelocity.y }
local stepGravity = { x=t*0, y=t*9.8 }
return {
x = startingPosition.x + n * stepVelocity.x + 0.25 * (n*n+n) * stepGravity.x,
y = startingPosition.y + n * stepVelocity.y + 0.25 * (n*n+n) * stepGravity.y

Firing the Projectile

On touch release, firing the projectile is simple. We generate the projectile (assuming it doesn’t already exist), assign the velocity values that we gathered, and send it flying across the trajectory path in a perfect match.

local function fireProj( event )
local proj = display.newImageRect( "object.png", 64, 64 )
physics.addBody( proj, { bounce=0.2, density=1.0, radius=14 } )
proj.x, proj.y = event.xStart, event.yStart
local vx, vy = event.x-event.xStart, event.y-event.yStart
proj:setLinearVelocity( vx,vy )

And that’s it! Based on a scenario set at 60 frames per second, we can accurately predict the trajectory of a flying object. Note that if you wish to use 30 frames per second, the only required adjustment is to change both instances of 0.25 in the getTrajectoryPoint function to 0.5, and everything should mesh up perfectly.

In Summary

That’s it for today’s tutorial. To see both of these examples in action, please access the PhysicsDemo folder on Dropbox. And, as always, please provide your feedback in the comments section below.

Brent Sorrentino

Brent Sorrentino is a full-time Developer Evangelist and technical writer who assists others with game development, code, and in overcoming other challenges to help them bring their app dreams to life.

  • Chris
    Posted at 14:54h, 09 April

    Wow great tutorials.
    Haven’t thought about using Joints for the radial gravity. Have been using other methods and I feel I’ve overcomplicated things now 😉

    Great read – thanks again!

  • Mark
    Posted at 10:46h, 10 April

    Brent – this is RIGHT ON TIME!


  • Star Crunch
    Posted at 16:01h, 10 April


    In case it’s useful to anybody, this complements the trajectory code:

    If you have a launch speed and a target, you can use this to get the (0, 1, or 2) viable launch angles.

    (Apologies if it’s incomplete… it was camped in a C# file waiting (and waiting and waiting…) to be converted and I don’t quite remember which project the original is from.)

    — Helper for special case: vertical trajectories
    — Solve for t: y = v0 * t – g * t^2 / 2
    local function VerticalLaunch (y, gravity, v0, get_times)
    local disc = v0 * v0 – 2 * gravity * y
    local a1, a2, t1, t2 = pi / 2, pi / 2

    if disc >= 0 then
    local root = sqrt(disc)

    — If the target is below or coincident with the the launch point, it will always
    — be a hit, given by the positive of the two times: when fired straight up, this
    — is the time coming back down; when fired straight down, this is the expected
    — drop time. In both cases the negative time is a virtual hit going up.
    if y
    — v0^2, sqrt(discriminant) > abs(v0). Accounting for this, determine the
    — positive roots.
    if get_times then
    t1 = (v0 + root) / gravity
    t2 = (root – v0) / gravity

    — Otherwise, get the times going up and coming down when the point above is hit.
    elseif get_times then
    t1 = (v0 – root) / gravity
    t2 = (v0 + root) / gravity

    return a1, a2, t1, t2

    — Computes the launch angles needed to hit a target along a parabolic trajectory.
    — Cf. “Mechanics”, J.P. Den Hartog, pp. 187-9
    — @param point Launch point.
    — @param target Target point.
    — @param gravity Gravity constant.
    — @param v0 Launch speed.
    — @param get_times If true, return times after the angles.
    — @return If @e target can be hit, an angle that will yield a hit; otherwise, @b nil.
    — @return If @e target can be hit, another angle that will yield a hit; may be the same angle as the first.
    — @return Time lapse to hit with angle #1.
    — @return Time lapse to hit with angle #2; may differ even with same angles, i.e. when firing straight up.
    function GetLaunchAngles (point, target, gravity, v0, get_times)
    assert(gravity > 0)
    assert(v0 > 0)

    local dx, dy, y = target[1] – point[1], target[2] – point[2], target[3] – point[3]
    local x2 = dx * dx + dy * dy
    local a1, a2, t1, t2

    — If the target is above or below, do special vertical case.
    if x2 1, the sine argument was invalid: the shot will miss.

    — Wikipedia offers the transformation:
    — alpha = atan((v^2 +- sqrt(v^4 – g * (g * x^2 + 2 * y * v^2))) / (g * x))
    local sin_numer = y + gravity * x2 / (v0 * v0)
    local dist = sqrt(x2 + y * y)

    if sin_numer <= dist then
    local phi = asin(y / dist)

    — 2 * alpha – phi = asin(RHS)
    — 2 * alpha = asin(RHS) + phi
    — alpha = (asin(RHS) + phi) / 2
    local angle = asin(sin_numer / dist)

    a1 = (phi + angle) / 2

    — sin(X) = sin(pi – X)
    — sin(pi – (2 * alpha – phi)) = RHS
    — pi – 2 * alpha + phi = asin(RHS)
    — -2 * alpha = asin(RHS) – pi – phi
    — 2 * alpha = phi + pi – asin(RHS)
    — alpha = (phi + pi – asin(RHS)) / 2
    a2 = (phi – angle + pi) / 2

    — Get times by solving for t: x(t) = v0 * cos(alpha) * t.
    if get_times then
    local x = sqrt(x2)

    t1 = x / (v0 * cos(a1))
    t2 = x / (v0 * cos(a2))

    — Return desired set of results.
    if t1 then
    return a1, a2, t1, t2
    elseif a1 then
    return a1, a2
    return nil

    • Patrick Waldner
      Posted at 15:37h, 28 November

      Is there any way to calculate the angle when I don’t want to shoot from a Point, but rather from the surface of a Sphere with radius r ? Imagine a spherical turret that rotates around its centre and fires out of a barrel that comes straight out of it.

      So with the angle and radius of the turret you actually define the starting point of the projectile.

  • Philipp Lenssen
    Posted at 12:09h, 11 April

    Great, more tutorials like these please!

    Two small notes. In the game Spikeball and in other apps like Color Sound Machine, I did a simpler version of magnets by just having them screen-globally look for sprites. This also works in reverse with fans. I’ve a little sprite class for which I call self:magneticTowards(goal.x, goal.y), and then it does something like

    function self:magneticTowards(x, y)
    local strength = 1000
    local distance = misc.getDistance( {x = x, y = y}, self )
    local power = app.magneticMaxValue / distance / strength
    local pushX = (x – self.x) * power
    local pushY = (y – self.y) * power
    self:applyForce(pushX, pushY, self.x, self.y)

    For the path prediction, I’m currently doing something in which I need a full path including bounces off of walls and such. So I’m sending an invisible bullet item which uses a phase timing to draw its path in decreasing opacity and with a bit of a glow. (Such path could also be precached for quicker loading if the options and placeable positions are limited.) This bullet has a special collision setting to not touch enemies*, and only walls, and it doesn’t make sounds when it bumps and such, but otherwise it reuses the same routines for handling this attackObject.

    local categoryAttackObject = ‘ X ‘
    local categoryEnemy = ‘ X ‘
    local maskIgnoresAttackObjectsAndEnemies = ‘ XXXX X’
    (I’m using a string to binary conversion to visually lay out bit collision maps.)

    • Philipp Lenssen
      Posted at 12:10h, 11 April

      The strings got eaten up by the comment functionality here, but basically it’s



  • Star Crunch
    Posted at 14:34h, 11 April

    Oh wow, the formatting got put through the wringer. 🙂

    The – glyphs should be Lua comments. Where they aren’t minus signs, anyway. :/

    Looking over it a bit… If anybody does end up using what I posted, that code was written with 3D in mind, over the XY plane. For basic 2D with display objects for point and target, you can just substitute

    local dx, y = target.x – point.x, target.y – point.y
    local x2 = x * x

    in the obvious places and everything else is the same. (Non-straight down gravity remains an exercise for the reader.)

    Following what Matt wrote, once you’ve got your angle, you can put it together with your launch speed to get the linear velocity as

    local vx, vy = speed * math.cos(angle), speed * math.sin(angle)
    proj:setLinearVelocity(vx, vy)

    (I think this came from the AI in an old demo for a basketball-like game (Tlachtli): trying to aim for the “baskets”, given an average ball knock-away speed. Another application is aiming at falling objects, though you have to make sure to aim at a FUTURE position.)

  • Ryan King
    Posted at 11:47h, 15 April

    I was wondering how you would touch and drag both the magnet and the field together, would love some help, thanks Ryan

    • Brent Sorrentino
      Posted at 18:50h, 17 April

      Hi Ryan,
      You could accomplish this in three ways, depending on your preference:

      1) Make the field a secondary body of the same physics object, and build your collision filters so that the joints are only attached when objects hit the field part (not the magnet part)

      2) Attach the field to the magnet using a weld joint.

      3) Make the magnet draggable, and then constantly update the field to the same position while the user is dragging the magnet around.

      Have fun!

  • Byron
    Posted at 12:22h, 02 May

    Is there a way to incorporate LinearDamping into the projected trajectory calculation?

    This would solve a ton of issues I am having with my app…

    Thanks in advance.

  • johannes_lalala
    Posted at 04:11h, 13 January

    Hi Brent,

    The documentation is a bit unclear about how the force of a touch joint is modelled:

    The documentation says it creates a “temporary elastic force”, which I interpret as “spring” force (i.e. x” = -d* x, when the center of the force is at 0). In other words: the force is proportional to the distance between field center and object, like there was a spring between them.

    Whereas an actual “gravitational” force is indirecly proportional to the square distance of object and center, (i.e. x” = -G/ |x|^2). The force is weaker the bigger the distance of center and object.

    Could you verify which one (if any) applies here?

    Thanks! Johannes

    • Brent Sorrentino
      Posted at 10:27h, 13 January

      Hi Johannes,
      This demo isn’t “scientifically perfect” if that’s what you’re seeking… it merely uses the Box2D mouse joint (in Corona, “touch joint”) to exert force on the object toward a specific point (in this case, toward the center of the “planet”). You can read more about the mouse joint is the Box2D manual here:

      Best regards,

  • johannes_lalala
    Posted at 08:28h, 15 January

    Thanks Brent!

    seems to me like the touch/mouse thing acts in fact like a spring (but the original docs aren’t very accurate too :).

    just in case somebody also wants more control over the nature of the force, here’s how I did the radial force affecting a body:

    physics.addBody( body, ‘dynamic’ )

    — then set up a frame listener which just does this:
    runtime:addEventListener( ‘enterFrame’, function()

    — this gives us an undampened spring movement, the body will be pulled stronger to f_center the greater the distance
    local fx =( f_centerx – body.x ) * c
    local fy =( f_centery – body.y ) * c
    — with this modification to fx and fy we get a constant force to the center, independent of the distance
    fx = fx / sqrt( fx^2 + fy^2 )
    fy = fy / sqrt( fy^2 + fy^2 )
    — ..and this gives a gravitational field
    fx = fx / ( fx^2 + fy^2 )
    fy = fy / ( fy^2 + fy^2 )

    body:applyForce( fx, fy, body.x, body.y )

    if you use the latter, depending on circumstances, perhaps you don’t need the field to be activated by a certain distance because it’s very weak at greater distances.

    regards, Johannes

    • Chris
      Posted at 12:11h, 29 May


      Great input, as this is what I was looking for! However, I just wanted to double check that your equations are accurate, specifically:
      fx = fx / ( fx^2 + fy^2 )
      fy = fy / ( fy^2 + fy^2 )

      fx = fx / sqrt( fx^2 + fy^2 )
      fy = fy / sqrt( fy^2 + fy^2 )

      You can see two of these equations does not use fx at all. I was not sure if this was a typo, and needed fx instead of fy. Where can I find more information about exactly what these equations are doing? Also, I am guessing that the variable ‘c’ in the first equation is just a constant value?

      Finally, not sure if you would know how to do this, but would it be possible to increase/decrease the force depending on the size of the object? For instance if I have a ball and two different size magnets – how would I apply a stronger “gravitational pull” toward the larger magnet? This would be similar to what you are doing in your last two equations, but most likely with another variable.

      Hopefully you see this reply…as it has been a couple of months!

      Thank you!


  • Nick
    Posted at 16:31h, 12 March

    On a connected topic it would be great to get som esuport for enabling Box2D getMass function for even more control over objects. see http://feedback.coronalabs.com/forums/188732-corona-sdk-feature-requests-feedback/suggestions/5625479-box2d-getmass

  • Mark
    Posted at 22:11h, 03 July

    Hi Brent,

    I have managed to get this working but I have an issue when transitioning from the timer to the OnDelay function. For some reason the timerit does not pass the event.other into the function so you get a nil index value (the event.other is nil within the OnDelay function). If I change the event.other.x to field.x it will work but this make it hard when creating multiple fields. Any ideas?