Predicting Trajectory

Predicting trajectory has traditionally been a confusing endeavor, and some developers simply "guess" and hope for the best. In this tutorial, we'll discuss the real solution!

Test Project Setup

For purposes of this tutorial, let's quickly create a test project:

  1. Open the Corona Simulator.

  2. Click New Project from the welcome window or select New Project... from the File menu.

  3. For the project/application name, type TestLaunch and ensure that the Blank template option is selected. Leave the screen size settings at default but set Default Orientation to Sideways, then click OK (Windows) or Next (Mac). This will create the basic files for the test project in the location (folder) that you specified.

  4. Locate the project folder, open the main.lua file in your chosen text editor, and insert these lines of code:

local physics = require( "physics" )
physics.start()  -- Start the physics engine
physics.setGravity( 0, 9.8 )  -- Set typical Earth gravity

-- Create display group for predicted trajectory
local predictedPath = display.newGroup()

-- Create function forward references
local getTrajectoryPoint
local launchProjectile

Touch Handling

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. This can be accomplished with a "touch" event listener, so add the following code to your test project after the previous commands:

local function screenTouch( event )

    if ( event.phase == "moved" ) then

        -- Remove trajectory path group (to clear previous path), then re-create it
        display.remove( predictedPath )
        predictedPath = display.newGroup()

        local startingVelocity = { x=event.x-event.xStart, y=event.y-event.yStart }

        for i = 1,240,2 do
            local s = { x=event.xStart, y=event.yStart }
            local trajectoryPosition = getTrajectoryPoint( s, startingVelocity, i )
            local dot = display.newCircle( predictedPath, trajectoryPosition.x, trajectoryPosition.y, 4 )
        end

    elseif ( event.phase == "ended" ) then

        launchProjectile( event )
    end
    return true
end
Runtime:addEventListener( "touch", screenTouch )

Let's inspect this code in more detail:

  1. The core of this function works on the "moved" touch phase. This allows us to start touching at a certain point on the screen (this will be the origin of the projectile) and then drag around to render a dotted path that represents the predicted trajectory.

  2. Inside the conditional "moved" phase block, we first remove the predictedPath display group created earlier, then we re-create it. It may seem odd to remove the group then immediately re-create it, but this is a simple way to remove all of the previously-drawn dots before drawing an entirely new set which represents the adjusted trajectory.

  3. Following that, we calculate the starting x and y "velocity" values by subtracting the starting touch location (event.xStart/event.yStart) from the current touch location (event.x/event.y).

  4. Finally, with this basic information, we run a for loop which draws the predicted trajectory path in 240 steps — 4 seconds of launch time at 60 frames-per-second — using vector circles (you could use images if desired). Note that we iterate by 2 in the loop since, for test purposes, 120 path points (dots) is enough to clearly represent the predicted trajectory.

Calculating Trajectory

Within the for loop in the above function, the core calculation is done via the following function which uses the frames-per-second in "time steps" along with the starting position and velocity. Add this code block to your project following the previous lines:

getTrajectoryPoint = function( startingPosition, startingVelocity, n )

    -- Velocity and gravity are given per second but we want time step values
    local t = 1/display.fps  -- Seconds per time step at 60 frames-per-second (default)
    local stepVelocity = { x=t*startingVelocity.x, y=t*startingVelocity.y }
    local gx, gy = physics.getGravity()
    local stepGravity = { x=t*gx, y=t*gy }

    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
    }
end
Note

This trajectory calculation is based on an app running at 60 frames-per-second (fps value within config.lua). If you wish to use 30 frames per second, change each instance of 0.25 in the returned x and y values to 0.5 and everything should mesh up perfectly.

Launching a Projectile

On touch release, firing the projectile is simple. We simply generate the projectile, add a physical body, and assign the velocity values that we gathered to send it flying across the trajectory path in a perfect match. To test, add this code block to your project following the previous lines:

launchProjectile = function( event )
    
    -- Create projectile object
    local proj = display.newCircle( event.xStart, event.yStart, 24 )
    proj:setFillColor( 1,0.2,0.2 )

    -- Add physical body to object
    physics.addBody( proj, { bounce=0.2, density=1.0, radius=24 } )

    -- Apply velocity values to object
    local vx, vy = event.x-event.xStart, event.y-event.yStart
    proj:setLinearVelocity( vx, vy )
end

Now save the project, refresh/reload it, and touch/drag around the screen to draw a predicted trajectory path. Upon release, a projectile (red circle) should generate at the starting point and it should perfectly follow the predicted path!

Conclusion

Hopefully this tutorial illustrates that predictive trajectory is not an insurmountable task, nor one that requires a "best guess" sort of operation. With these calculations, accurate trajectory paths are easily accomplished in Corona!