[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 |
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
@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) |
Hey, thanks for this - nice work!
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.
@ 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.
mathlib.lua is great, thanks.
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.
Nice! I've bookmarked this for my future reference too. Thank you!!
Naomi
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