Monday 19 December 2011

Catmull Splines: Object follows path

Following on from my introductory blog on catmull splines I have applied and worked on an example simplifying the excellent Flight Path sample template which can be found here for the Corona SDK framework.

What I was trying to do is understand the equation I put forward an started making it a little more useful to mine (and hopefully your needs)

Right lets look at some code. Only two files here and have tried commenting as much as possible

main.lua
-- This line makes the physics engine features available under the “physics” namespace
require( "physics" )
-- A further file to create a smooth curve when drawing
require( "smoothCurve")

-- ither instantiates or resumes the world. IMPORTANT: it must be called before any of the other functions in this section.
physics.start()
-- ets the x,y components of the global gravity vector, in units of m/s2. The default is ( 0, 9.8 ) to simulate standard
-- Earth gravity, pointing downwards on the y-axis.
physics.setGravity(0,0) 

-- Creates a group in which you can add and remove child display objects. Initially, there are no children in a group. 
-- The local origin is at the parent’s origin; the reference point is initialized to this local origin
local mainGroup = display.newGroup()
local lineGroup = display.newGroup()  mainGroup:insert(lineGroup)
local objectGroup = display.newGroup()  mainGroup:insert(objectGroup)

-- Calculation interval of drawing between the dotted lines
local lineDrawInterval = 50

-- Method called when our object (blue box) is touched
function onObjectTouched(self, event)
--[[
  Touch events are a special kind of hit event. When a user's finger touches the screen, they are starting a sequence of touch events, each with different phases.

  event.name is the string "touch".
  event.x is the x-position in screen coordinates of the touch.
  event.y is the y-position in screen coordinates of the touch.
  event.xStart is the x-position of the touch from the "began" phase of the touch sequence.
  event.yStart is the y-position of the touch from the "began" phase of the touch sequence.
  event.phase is a string identifying where in the touch sequence the event occurred:
   "began" a finger touched the screen.
   "moved" a finger moved on the screen.
   "ended" a finger was lifted from the screen.
   "cancelled" the system cancelled tracking of the touch.

]]
 if(event.phase == "began") then
  -- Determine whether we are to draw a line
  self.allowLineDraw = true
  -- Determine the stype of line output
  self.lineType = "dotted"
  -- Instantiate our table to hold the points
  self.points = {}
  -- Insert our initial touch
  table.insert(self.points, {x = event.x, y = event.y})
  -- Returns a reference to the current stage object, which is the root group for all display objects and groups. 
  -- Currently, Corona has a single stage instance, so this function always returns a reference to the same object
  display.getCurrentStage():setFocus( self )
  -- Set the focus to our object (box)
  self.isFocus = true
 elseif (event.phase == "moved" and self.isFocus == true) then
  -- Make a check to see we have at least one point
  if(#self.points == 0) then
   -- Instantiate our table to hold the points
   self.points = {}
   -- Insert our current position
   table.insert(self.points, {x = self.x, y = self.y})
   table.insert(self.points, {x = event.x, y = event.y})
  end
  -- Determine the distance between our points
  -- math.sqrt: Returns the square root of a value.
  -- math.pow: Returns the result of raising a number to the power of another number.
  local distance = math.sqrt(math.pow(self.points[#self.points].x - event.x, 2) + math.pow(self.points[#self.points].y - event.y, 2))
  -- Insert our current position
  table.insert(self.points, {x = event.x, y = event.y})
  -- If we have set our object to draw a line first check
  if(self.allowLineDraw == true) then 
   -- Draw the line
   drawLine(self) 
   -- Now get our object to follow it 
   followPoints(self)   
   -- Since we have followed it and moved over our control point, remove the lone
   self.allowLineDraw = false
   -- Dependent on our line draw interval recall our method 
   timer.performWithDelay(lineDrawInterval, function(new) self.allowLineDraw = true end)
  end
 elseif(event.phase == "ended") then
  -- Now that we have lifted our touch determined the distance
  local distance = math.sqrt(math.pow(self.points[#self.points].x - event.x, 2) + math.pow(self.points[#self.points].y - event.y, 2))
  -- Returns a reference to the current stage object, which is the root group for all display objects and groups. 
  -- Currently, Corona has a single stage instance, so this function always returns a reference to the same object
  display.getCurrentStage():setFocus(nil)
  -- Remove the focus from our object now
  self.isFocus = false
  -- Follow to the end point
  followPoints(self)  
  -- Show on the stage the line to be solid
  self.lineType = "solid"
  -- Draw it solid rather than dotted
  drawLine(self)
 end
end

function drawLine(box)
 -- Do we have a line already
 if(box.lineGroup ~= nil) then
  -- Make sure we destory anything in memory 
  box.lineGroup:removeSelf()
  box.lineGroup = nil 
 end
 -- Retrieve our point (see other description) i.e. our catmull spline
 local smoothPoints = smoothCurve.getSmoothCurvePoints(box.points)
 -- If we don't have any points then exit now 
 if(smoothPoints == nil) then return end
 
 -- Lets create a new linegroup display object for our box 
 box.lineGroup = display.newGroup()
 --insert this into our linegroup display object on our stage 
 lineGroup:insert(box.lineGroup)

 -- Return modulus dependent on line type
 local modNumber = box.lineType == "dotted" and 2 or 1
 
 -- Iterate through our points
 for i = 0 ,#smoothPoints do
  if(i % modNumber == 0 and smoothPoints[i] ~= nil and smoothPoints[i + 1] ~= nil) then 
   local line = display.newLine(smoothPoints[i].x, smoothPoints[i].y, smoothPoints[i + 1].x, smoothPoints[i + 1].y)
   
   -- Determine line width dependent on modulus
   line.width = modNumber == 1 and 1 or 3
   -- Insert the line into our display object
   box.lineGroup:insert(line)
  end
 end
 -- Return the points now we have them all
 return smoothPoints
 
end 

function followPoints(box)
 -- Check to see if we have any points if not exit
 if(box.points == nil or #box.points == 0) then 
  return 
 end
 -- Get our point to follow
 point = box.points[1]
 -- Get the angle for our box to move
 local angle = math.atan2((box.y - point.y) , (box.x - point.x) ) * (180 / math.pi)
 -- Detemine the x and y velocity to move our object
 local velocityX = math.cos(math.rad(angle)) * 50 * -1
  local velocityY = math.sin(math.rad(angle)) * 50 * -1
 -- Now set the object (box) velocity
 box:setLinearVelocity( velocityX, velocityY)
 -- Initialise our method
 local enterFrameFunction = nil
 local checkForNextPoint
 checkForNextPoint = function(box)
  -- Do we have an object (box)
  if(box ~= nil) then
   -- Get our destination
   local dest = box.dest
   -- Return the velocity we have just set
   local velX, velY = box:getLinearVelocity()
   -- Cbeck a load of stuff
   if( (velX < 0 and box.x < dest.x and velY < 0 and box.y < dest.y) or  (velX > 0 and box.x > dest.x and velY < 0 and box.y < dest.y) or
    (velX > 0 and box.x > dest.x and velY > 0 and box.y > dest.y) or  (velX < 0 and box.x < dest.x and velY > 0 and box.y > dest.y) or #box.points == 0) then
    -- Remove our event listener
    Runtime:removeEventListener("enterFrame", enterFrameFunction)
    -- Remove the point as we have just got to it so not to keep drawing the same one over and over
    table.remove(box.points, 1)
    -- Now draw it
    drawLine(box)
    -- If we have at least another point left then repeat all this again
    if(#box.points > 0) then
     followPoints(box)
    else
     -- If we don't then stop the object (box) waiting for another touch
     box:setLinearVelocity( 0, 0)
    end

   end  
  else
   -- Nothing to do so remove our listener
   Runtime:removeEventListener("enterFrame", enterFrameFunction)
  end
 end
 
 -- Set the box destination to the current point
 box.dest = point
 -- Lets do it all again
 enterFrameFunction = function(event) checkForNextPoint(box) end
 -- Add another listener
 Runtime:addEventListener("enterFrame", enterFrameFunction)

end

-- Lets create our object which is a blue box
local box = display.newImage( "bluesquare.jpeg")
-- Position it roughly top left corner
box.x = 50
box.y = 50
-- Sets its calling method for when the object (box) is touched
box.touch = onObjectTouched
-- Add a listener so it will do something
box:addEventListener("touch", box)
-- Apply physics so that velocity etc will have some effect
physics.addBody(box, "dynamic",  {density = 100, isSensor = true, radius = 40})
-- Now add our local box to the main object display group
objectGroup:insert(box)

and the all important catmull splines magic

smoothCurve.lua
--[[
Creates a module. If there is a table in package.loaded[name], this table is the module. 
Otherwise, if there is a global table t with the given name, this table is the module. 
Otherwise creates a new table t and sets it as the value of the global name and the value of package.loaded[name]. 
This function also initializes t._NAME with the given name, t._M with the module (t itself), 
and t._PACKAGE with the package name (the full module name minus last component; see below). 
Finally, module sets t as the new environment of the current function and the new value of package.loaded[name], so that require returns t.

This function may receive optional options after the module name, where each option is a function to be applied over the module.
]]
module(..., package.seeall)

function getSmoothCurvePoints(points)

 -- Number of points must be at least 3 to calculate the catmull spline
 if(#points < 3) then return nil end
 -- Create our table to hold our points
 local smoothPoints = {}
    -- Steps Per Segment - The Higher The Number - The Smoother The Curve - The Longer It Takes To Calculate
    local curveSteps = 30
    -- First Segment (Remember the complication control points in the introduction blog)
    local firstSegement = drawCatmullSpline( points[1] , points[1] , points[2] , points[3] , curveSteps )
    -- Increment through control points and add it to our table
    for i = 1 , #firstSegement , 1 do
   table.insert(smoothPoints, {x = firstSegement[i].x, y = firstSegement[i].y})
    end
    -- Now increment through our segments inbetween
    for i = 2 , #points - 2 , 1 do
            local middleSegment = drawCatmullSpline( points[i-1] , points[i] , points[i+1] , points[i+2] , curveSteps )
            for i = 2 , #middleSegment , 1 do
               --Add our smoothpoints between our control points
      table.insert(smoothPoints, {x = middleSegment[i].x, y = middleSegment[i].y})
            end
    end
    
    -- Just to finish it all off apply our last segment
    local lastSegment = drawCatmullSpline( points[#points-2] , points[#points-1] , points[#points] , points[#points] , curveSteps )
    for i = 2 , #lastSegment , 1 do
   table.insert(smoothPoints, {x = lastSegment[i].x, y = lastSegment[i].y})
    end
 -- Now return our nice smooty curve
 return smoothPoints
end

function drawCatmullSpline( p0 , p1 , p2 , p3 , steps )
  -- Lets create a table for our points
    local points = {}
 
    for t = 0 , 1 , 1 / steps do
   -- Remember that complicated catmull spline equation in my blog.  Here we use it to work with our control points supplied
            local xPoint = 0.5 * ( ( 2 * p1.x ) + ( p2.x - p0.x ) * t + ( 2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x ) * t * t + ( 3 * p1.x - p0.x - 3 * p2.x + p3.x ) * t * t * t )
            local yPoint = 0.5 * ( ( 2 * p1.y ) + ( p2.y - p0.y ) * t + ( 2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y ) * t * t + ( 3 * p1.y - p0.y - 3 * p2.y + p3.y ) * t * t * t )
            
   -- Now insert these into the our table
            table.insert( points , { x = xPoint , y = yPoint } )
    end
    -- Finished with them all then return our points table
    return points
 
end
Right now we are cooking on gas what is this going to give us. Well here are a series of screenshots to give you a taster but why not give it a go to see what happens.

Now I have an object following a path what I want to implement is a chase and evade AI element to it. I'll post just as soon as I have worked it out.

Stay tuned ...

0 comments:

Post a Comment

 
;