Trajectory plotting

Posted by horacebury, Posted on December 30, 2011, Last updated February 15, 2012

0 votes

[EDIT: I have updated the code to provide a better user interface and make the calculated trajectory path longer if necessary]

[NOTICE: A maintainable post for the up to date mathlib.lua is available here: http://developer.anscamobile.com/code/maths-library
I have not removed it from this post as it may change beyond this sample's use.]

I've been wanting to pre-calculate the curve of an object fired Angry Birds style for some time. I am terrible with maths, trigonometry etc and this has been a real challenge. I also wanted to make this code available. I'm not going to explain the code - it is fairly self explanatory. In short, the three file listings below are all you need. To use in the simulator, simply tap to create the starting point and tap again to indicate force (as if pulled back)...

main.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
-- References:
-- http://www.boscobel.k12.wi.us/~schnrich/calculating_acceleration.htm
-- http://www.box2d.org/forum/viewtopic.php?f=8&t=793
-- http://hyperphysics.phy-astr.gsu.edu/hbase/traj.html#tra3
-- http://www.physchem.co.za/OB12-mec/projectiles.htm
-- http://math.stackexchange.com/questions/93652/finding-force-for-trajectory
-- http://www.euclideanspace.com/maths/algebra/vectors/angleBetween/index.htm
-- http://stackoverflow.com/questions/3486172/angle-between-3-points
 
 
local physics = require("physics")
local trajapi = require("trajectorylib")
local mathapi = require("mathlib")
 
physics.start()
physics.setGravity(0,10)
 
local path = nil
 
 
--[[ environment setup ]]--
 
local start, sling = nil, nil
local offsetX, offsetY = 50, 300
local iteration = 0.1 -- gap between increments of trajectory points
 
 
--[[ trajectory plotting ]]--
 
function trajectory( start, velocity, angle, iteration, untiloffscreen )
        if (path) then path:removeSelf(); end
        path = display.newGroup()
        
        local flightmultiple = 1
        if (untiloffscreen) then
                flightmultiple = 2
        end
    local points, r, h, f = trajapi.calcTrajectoryPoints( 0, 0, velocity, angle, iteration, flightmultiple )
    
    print( 'angle: ', angle )
    print( 'velocity: ', velocity )
    print( 'range: ', r )
    print( 'height: ', h )
    print( 'flight time: ', f )
    
    for i=1, #points do
            local radius = (points[i].velocityx + points[i].velocityy) * 0.1
            if (radius < 2) then
                    radius = 2
            end
            display.newCircle( path, start.x + points[i].x, start.y - points[i].y, radius )
    end
    
    return points
end
 
 
--[[ throwing the ball ]]--
function throw( points, sling, start, distance, angle )
        local radius = 30
        local ball = display.newCircle( path, start.x, start.y, radius )
        ball.alpha = .7
        ball:setFillColor( 90,90,200 )
        physics.addBody( ball, "dynamic", {friction=.1, bounce=.1, density=1, radius=radius } )
        
        local point = points[2]
        local area = trajapi.calcAreaOfCircle( radius )
        local density = 1
        local mass = density * area
        local g = 2.1
        print('area: '..area)
        print('mass: '..mass)
        print('dist: '.. distance)
        
        local ax, ay = points[1].velocityx, points[1].velocityy
        local fx = mass*ax
        local fy = mass*ay
        local ratio = 100 * 1.62 -- MAGIC VALUE
        print('velocity: '..points[2].velocityx, points[2].velocityy)
        print('force: '..fx,fy)
        print('time: '..points[2].time)
        ball:applyForce( fx/ratio, -fy/ratio )
        
        function ball:timer(event)
                timer.cancel( ball.t )
                Runtime:removeEventListener("enterFrame", ball)
                ball:removeSelf()
        end
        ball.t = timer.performWithDelay(8000, ball, 1)
        
        function ball:enterFrame(event)
                local c = display.newCircle(path, ball.x, ball.y,3)
                c:setFillColor(255,0,0)
        end
        Runtime:addEventListener("enterFrame", ball)
end
 
 
--[[ initial point capture ]]--
 
function touch(event)
        if (not start) then
                start = event
                display.newCircle( event.x, event.y, 2 )
        elseif (not sling) then
                sling = event
                display.newCircle( event.x, event.y, 2 )
                local c = display.newCircle( start.x, start.y, mathapi.lengthOf( start, sling ) )
                c:setStrokeColor( 0,0,255 )
                c:setFillColor( 0,0,0,0 )
                c.strokeWidth = 2
                
                local angle = math.abs( mathapi.angleOf( sling, start ) )
                local distance = mathapi.lengthOf( sling, start )
                local points = trajectory( start, distance, angle, iteration )
                throw( points, sling, start, distance, angle )
        end
end
 
 
Runtime:addEventListener("tap", touch)
 
local control = display.newGroup()
local sphere, line, dot, track = nil, nil, nil, nil
 
function drag( event )
        if (event.phase == "began") then
                if (dot) then dot:removeSelf(); end
                dot = display.newCircle( control, event.x, event.y, 4 )
                dot:setFillColor(255, 0, 0)
        elseif (event.phase == "moved") then
                local len = mathapi.lengthOf( dot, event )
                if (sphere) then sphere:removeSelf(); end
                if (track) then track:removeSelf(); end
                if (line) then line:removeSelf(); end
                sphere = display.newCircle( control, dot.x, dot.y, len )
                dot:removeSelf()
                dot = display.newCircle( control, sphere.x, sphere.y, 4 )
                dot:setFillColor(255, 0, 0)
                track = display.newCircle( control, event.x, event.y, 25 )
                track:setFillColor( 0, 255, 0 )
                line = display.newLine( control, sphere.x, sphere.y, event.x, event.y )
                line.width = 2
                line:setColor( 0, 0, 255 )
        else
                control.alpha = .5
                
        local angle = math.abs( mathapi.angleOf( event, dot ) )
        local distance = mathapi.lengthOf( event, dot )
        local points = trajectory( dot, distance, angle, iteration, true )
        throw( points, event, dot, distance, angle )
        end
        return true     
end
 
Runtime:addEventListener( "touch", drag )

mathlib.lua:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
module(..., package.seeall)
 
 
-- returns the distance between points a and b
function lengthOf( a, b )
    local width, height = b.x-a.x, b.y-a.y
    return math.sqrt(width*width + height*height)
end
 
-- converts degree value to radian value, useful for angle calculations
function convertDegreesToRadians( degrees )
--      return (math.pi * degrees) / 180
        return math.rad(degrees)
end
 
function convertRadiansToDegrees( radians )
        return math.deg(radians)
end
 
-- rotates a point around the (0,0) point by degrees
-- returns new point object
function rotatePoint( point, degrees )
        local x, y = point.x, point.y
        
        local theta = convertDegreesToRadians( degrees )
        
        local pt = {
                x = x * math.cos(theta) - y * math.sin(theta),
                y = x * math.sin(theta) + y * math.cos(theta)
        }
 
        return pt
end
 
-- rotates point around the centre by degrees
-- rounds the returned coordinates using math.round() if round == true
-- returns new coordinates object
function rotateAboutPoint( point, centre, degrees, round )
        local pt = { x=point.x - centre.x, y=point.y - centre.y }
        pt = rotatePoint( pt, degrees )
        pt.x, pt.y = pt.x + centre.x, pt.y + centre.y
        if (round) then
                pt.x = math.round(pt.x)
                pt.y = math.round(pt.y)
        end
        return pt
end
 
-- returns the degrees between (0,0) and pt
-- note: 0 degrees is 'east'
function angleOfPoint( pt )
        local x, y = pt.x, pt.y
        local radian = math.atan2(y,x)
        --print('radian: '..radian)
        local angle = radian*180/math.pi
        --print('angle: '..angle)
        if angle < 0 then angle = 360 + angle end
        --print('final angle: '..angle)
        return angle
end
 
-- returns the degrees between two points
-- note: 0 degrees is 'east'
function angleBetweenPoints( a, b )
        local x, y = b.x - a.x, b.y - a.y
        return angleOfPoint( { x=x, y=y } )
end
 
function angleOf( a, b )
        return math.atan2( b.y - a.y, b.x - a.x ) * 180 / math.pi
end
 
-- Takes a centre point, internal point and radius of a circle and returns the location of the extruded point on the circumference
-- In other words: Gives you the intersection between a line and a circle, if the line starts from the centre of the circle
function calcCirclePoint( centre, point, radius )
        local distance = lengthOf( centre, point )
        local fraction = distance / radius
        
        local remainder = 1 - fraction
        
        local width, height = point.x - centre.x, point.y - centre.y
        
        local x, y = centre.x + width / fraction, centre.y + height / fraction
        
        local px, py = x - point.x, y - point.y
        
        return px, py
end
 
-- returns the smallest angle between the two angles
-- ie: the difference between the two angles via the shortest distance
function smallestAngleDiff( target, source )
        local a = target - source
        
        if (a > 180) then
                a = a - 360
        elseif (a < -180) then
                a = a + 360
        end
        
        return a
end
 
-- Returns the angle in degrees between the first and second points, measured at the centre
-- Always a positive value
-- Ref: http://stackoverflow.com/questions/1211212/how-to-calculate-an-angle-from-three-points/1211243#1211243
-- Ref: http://www.mathwords.com/c/cosine_inverse.htm
function angleAt( centre, first, second )
        local a, b, c = centre, first, second
        local ab = lengthOf( a, b )
        local bc = lengthOf( b, c )
        local ac = lengthOf( a, c )
        local angle = math.deg( math.acos( (ab*ab + ac*ac - bc*bc) / (2 * ab * ac) ) )
        return angle
end

trajectorylib.lua:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
module(..., package.seeall)
 
 
--[[ references ]]--
 
-- The code in this listing was derived from this first URL:
-- http://hyperphysics.phy-astr.gsu.edu/hbase/traj.html
-- http://en.wikipedia.org/wiki/Trajectory_of_a_projectile
 
 
--[[ support functions ]]--
 
local function vxf( velocity, acceleration )
        return velocity * math.cos( acceleration * math.pi / 180 )
end
 
local function vyt( velocity, acceleration, time )
        return velocity * math.sin( acceleration * math.pi / 180 ) - 9.8 * time
end
 
local function yt( velocity, acceleration, time )
        return velocity * math.sin( acceleration * math.pi / 180 ) * time - 0.5 * 9.8 * time * time
end
 
 
--[[ worker functions ]]--
 
function totalRangeHeightFlightTime( v, ag )
        local h = vyt(v, ag, 0) * vyt(v, ag, 0) / (2 * 9.8)
        local t = 2 * vyt(v, ag, 0) / 9.8
        local r = v * v * math.sin( 2 * ag * math.pi / 180 ) / 9.8
        return r, h, t
end
 
function positionAtTime(v, ag, t)
        local vx = vxf(v,ag) -- horizontal velocity
        local x = vx * t -- horizontal distance
        local vy = vyt(v, ag, t) -- vertical velocity
        local y = yt(v, ag, t) -- height at time 't'
        
        return x, y, vx, vy
end
 
 
--[[ calculate trajectories ]]--
 
-- returns a collection of points determined as the trajectory of the object
function calcTrajectoryPoints( startX, startY, velocity, angle, iteration, flightmultiple )
        if (not iteration) then
                iteration = 0.1
        end
        
        local r, h, f = totalRangeHeightFlightTime(velocity, angle) -- total range, height and flight time
        local points = {}
        
        for t=0, f*flightmultiple, iteration do
                local x, y, vx, vy = positionAtTime(velocity, angle, t)
                points[ #points+1 ] = { x=x, y=y, time=t, velocityx=vx, velocityy=vy }
        end
        
        return points, r, h, f
end
 
-- http://answers.yahoo.com/question/index?qid=20080617223556AA8DD8M
function calcInitialVelocity( startX, startY, angle, force )
        return force * math.cos(angle), force * math.sin(angle)
end
 
 
--[[ area functions ]]--
 
function calcAreaOfCircle( radius )
        return math.pi * radius * radius
end


Replies

mroberti's picture
mroberti
User offline. Last seen 1 day 2 hours ago. Offline
Joined: 20 Nov 2010

Damn, you really nailed it!! I of course, HAD to go mess with the gravity setting and change it from 10 to other values, but the trajectory doesn't get adjusted whereas the test ball does indeed go flying off into space as expected.

Is there somewhere that you're manually calculating the trajectory with a '10' value like the gravity that I can change to match the gravity set in 'main.lua'?

I know I know!! You JUST get done kicking this math problem's ass and I jump in and start asking for more!! :)

Seriously, excellent wrestling match with the maths. Glad you won!!!

-Mario

horacebury's picture
horacebury
User offline. Last seen 1 hour 55 min ago. Offline
Joined: 17 Aug 2010

Hey, glad you like it. Again use this code for good, not evil!

But yeah, the '9.8' (sorry, magic number!) in 'trajectorylib.lua' is the trajectory code's value for gravity.

If you like my code here, have you seen the 'simple draggable fan':

https://developer.anscamobile.com/forum/2010/10/21/simple-draggable-fan-demo

Ignis Design's picture
Ignis Design
User offline. Last seen 1 hour 22 min ago. Offline
Joined: 4 Oct 2010

@horacebury, thank you very much for the code basis and formulas in your original "calculating trajectory" code posted last month. I used those formulas to write a heavily modified and simplified routine which you and others might find useful (I was going to post it under a new codeshare heading, but I noticed this posting and will simply add it here instead).

And so, here is my modified, localized, and relatively simplified code. This code takes a velocity (v) and an angle (ag), then plots the trajectory course and launches a test object of any density which should follow the plotted path within considerable accuracy.

I say "considerable accuracy" because it's not 100% mathematically or physically "perfect"... but then, Box2D is not perfect either in regards to real-world physics! There's a constant number that I use in the calculation (7.8; see comment in code); I'm annoyed to admit that I don't understand why this number works. It might be related to Corona's interpretation of "meters" (as in the Box2D gravity meters-per-second value) converted into Corona "pixel" world space. All I can say is that this number was fine-tuned over countless testing of various angles, velocities, and gravity settings. I'm pleased to report that it plots the path within about 99% accuracy, which should be sufficient for most games and Corona apps. :)

Anyway, here's the code. Run in landscape iPad view in the Simulator for best results, adjusting your config.lua file if necessary. Have fun tweaking the gravity settings too... i.e. negative horizontal gravity can be used to make the projectile fly back upon its course, as if a strong hurricane wind was pushing it back.

I hope this proves useful to some!
Brent Sorrentino
Ignis Design

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
--scenario variables (launch offsets, velocity, angle, gravity)
local offsetX, offsetY = 50, 750
local v = 120 -- launch velocity v/ms
local ag = 75 -- launch angle (degrees off "horizontal to ground")
local gravX = 0.0 ; local gravY = 9.8
 
--setup physics engine
local physics = require("physics") ; physics.start() ; physics.setScale( 60 )
physics.setGravity( gravX, gravY )
 
--set common math functions to locals for better performance
local math_rad, math_sin, math_cos, math_pi = math.rad, math.sin, math.cos, math.pi
 
 
local function newPath( thisVel, thisAng )
 
   --"deep localize" math functions for nit-picky performance :)
   local m_cos, m_sin, m_rad = math_cos, math_sin, math_rad
   local piAng = (thisAng*math_pi/180)
   --further deep localization...
   local stepX, stepY ; local grX, grY, offX, offY = gravX, gravY, offsetX, offsetY
 
   for t=1,50,0.5 do
      stepX = ( thisVel * m_cos(piAng) + (grX/2) * t ) * t
      stepY = thisVel * m_sin(piAng) * t - 0.5 * grY * t * t
      local circ = display.newCircle( stepX + offX, stepY * -1 + offY, 4 ) --(vx+vy)*.1
   end
                
   local proj = display.newCircle( offX, offY, 20 ) ; proj:setFillColor(255,255,255,255)
                physics.addBody( proj, "dynamic", { density=100.0, friction=0.0, bounce=0.0, radius=20 } )
   --convert angle and velocity into X-Y values for Corona's "setLinearVelocity" API
   local xVel = m_sin(m_rad(90-thisAng))*thisVel
   local yVel = m_cos(m_rad(90-thisAng))*thisVel
   --launch projectile! (density/mass shouldn't matter using this API)
   proj:setLinearVelocity( xVel*7.8, yVel*-7.8 )
   --I don't understand why "7.8" seems to work; if anybody can figure out why, please explain!
 
   --Runtime listener to plot actual trajectory path as projectile flies on its course
   local function redPath()
      local dot = display.newCircle( 0, 0, 8 ) ; dot:setFillColor(255,0,0,255) ; dot:toBack()
      dot.x = proj.x ; dot.y = proj.y
   end
   Runtime:addEventListener( "enterFrame", redPath )
 
end
 
--launch here!
newPath(v,ag)

horacebury's picture
horacebury
User offline. Last seen 1 hour 55 min ago. Offline
Joined: 17 Aug 2010

Hey, thanks for this - nice work!

eaguirre's picture
eaguirre
User offline. Last seen 5 weeks 2 days ago. Offline
Joined: 12 Oct 2010

Just a small improvement, you will need remove the enterFrame() routine at some point if not it will create a new circle every 30 ms. Which in the long run will end up eating your memory.

Ignis Design's picture
Ignis Design
User offline. Last seen 1 hour 22 min ago. Offline
Joined: 4 Oct 2010

@ eaguirre
Yes, absolutely, thanks for mentioning this. I wrote the code more for "sample" purposes, assuming that users can see what's going on and modify it for their own needs... but for those developers who have just recently started learning Corona, the "enterFrame()" listener *must* be removed when you're done plotting the path, and the actual "dots" should be added to a table or display group too, so they are properly indexed and can be removed from the screen later.

brad.herman
User offline. Last seen 1 hour 21 min ago. Offline
Joined: 10 Dec 2011

mathlib.lua is great, thanks.

horacebury's picture
horacebury
User offline. Last seen 1 hour 55 min ago. Offline
Joined: 17 Aug 2010

Just wanted to say that I've added a new function:

angleAt( centre, first, second )

This calculates the angle of the first and second points when measured at the centre. The distance of the two points from the centre does not matter and the value returned will always be a positive value.

Naomi's picture
Naomi
User is online Online
Joined: 6 Jun 2011

Nice! I've bookmarked this for my future reference too. Thank you!!

Naomi