Posted on by

There are many ways to move objects in Corona SDK. If you want to move an object from point A to B, the simplest approach is to use transition.to(). But what if you need to move it along a path with multiple segments, like moving a knight on a chess board in its unique “L” pattern? This tutorial outlines how to achieve sequential movement via a series of queued transitions.

Initial Setup

The basis of this process is a table of x and y coordinate pairs to move the object along, in sequence. In its most simple format, the table may look like this:

local movePath = {}
movePath[1] = { x=200, y=0 }
movePath[2] = { x=200, y=200 }
movePath[3] = { x=0, y=200 }
movePath[4] = { x=200, y=300 }
movePath[5] = { x=150, y=200 }
movePath[6] = { x=200, y=100 }
movePath[7] = { x=100, y=200 }
movePath[8] = { x=0, y=0 }

This just declares a series of movement points, starting at index 1 and progressing across as many points as you wish. By default, these points equate to specific coordinates on the screen, not points relative to the object’s starting position, but we’ll include a setting to let you choose which option is most suitable to your application.

Customizing the Path Parameters

In addition to the basic x and y coordinate setup, we’ll allow two additional parameters for each “segment” in the path: time and easingMethod. For example, we may expand our table like this:

local movePath = {}
movePath[1] = { x=200, y=0 }
movePath[2] = { x=200, y=200 }
movePath[3] = { x=0, y=200, time=500 }
movePath[4] = { x=200, y=300, time=500 }
movePath[5] = { x=150, y=200, time=250, easingMethod=easing.inOutExpo }
movePath[6] = { x=200, y=100, time=2000 }
movePath[7] = { x=100, y=200, time=500 }
movePath[8] = { x=0, y=0, time=500, easingMethod=easing.outQuad }

If a custom time parameter is specified, it will override any other time settings, and the object will move to that point over that exact duration. Similarly, if easingMethod is defined and set to one of Corona’s easing methods, the object will move to that point using that easing method, not the default linear interpolation.

Distance Function

For calculating the distance between two points, we’ll need to include a quick function. The purpose of this will be explained later, but let’s add it now:

local function distBetween( x1, y1, x2, y2 )
   local xFactor = x2 - x1
   local yFactor = y2 - y1
   local dist = math.sqrt( (xFactor*xFactor) + (yFactor*yFactor) )
   return dist
end

Create the Object(s)

Obviously, we’ll need one or more objects to move along the path we created. Let’s create two basic circles now:

local circle1 = display.newCircle( 60, 100, 15 )
circle1:setFillColor( 1, 0, 0.4 )
local circle2 = display.newCircle( 120, 100, 15 )
circle2:setFillColor( 1, 0.8, 0.4 )

The setPath() Function

Now that we’ve done the basic setup, let’s explore the function which will queue up all of the transitions for an object. Let’s call the function setPath() and provide it with three arguments: the object to move, the path to move along, and a table of params which we can use to customize the movement.

local function setPath( object, path, params )

   local delta = params.useDelta or nil
   local deltaX = 0
   local deltaY = 0
   local constant = params.constantRate or nil
   local ease = params.easingMethod or easing.linear
   local tag = params.tag or nil
   local delay = params.delay or 0
   local speedFactor = 1

In the first several lines of the function, we set some local variables, most of which are used to check if various parameters are passed in via the params table. Each of these parameters will be explained as we step through the tutorial.

Delta

This gives us the option to use delta position via a boolean value — useDelta — passed in via the params table. If set to true, the object’s path will be relative to its starting position. If set to false, or omitted, the path points will equate to explicit screen coordinates instead.

Whether to use params.useDelta = true depends on the scenario. For moving a knight on a chess board, delta would be the logical choice, since the knight may reside on any square of the board and we’d need to move it in its “L” pattern from the current square to another valid square.

If useDelta is passed in the params table, let’s set deltaX and deltaY to the object’s starting position, instead of their default of 0. When the transitions are set up, this will be used to offset each point along the path by the object’s starting position. It will also be used to refactor the constant rate of movement, discussed in a moment.

   if ( delta ) then
      deltaX = object.x
      deltaY = object.y
   end

Constant Rate of Movement

Another option we’ll add is the ability to set a “constant rate of movement.” For example, we may wish to move the object at a steady, constant rate across the entire path, even if the distance between the starting point and the 2nd point is 100 pixels but the distance between the 2nd and 3rd point is 280 pixels. To accomplish this, we just need to calculate a speed factor based on a “time” value passed in via params.constantTime.

   if ( constant ) then
      local dist = distBetween( object.x, object.y, deltaX+path[1].x, deltaY+path[1].y )
      speedFactor = constant/dist
   end

The value that should be passed to params.constantTime is simply an integer “time” value, and speedFactor is calculated by the distance between the starting point and the 2nd point. In other words, if we use a value of 1200, the object will move from the starting point to the 2nd point in 1200 milliseconds, and the speed established along that segment will be applied to all other segments in the path, no matter their distance apart.

Default Easing Method

If we wish to use a non-linear easing method for all segments in the path, we can tell the module to use any of the easing methods. For example, to use a quadratic-in-out method, we can pass easingMethod = easing.inOutQuad in the params table. This easing method will be applied to all transitions along the path except those with a specific easingMethod setting in the path table.

Transition Tagging

One of the features in the Transition 2.0 library is the ability to tag (name) any number of transitions and then cancel, pause, or resume all transitions sharing the same tag. Since we’ll be building paths that consist of multiple related transitions, tagging is essential if you want to pause or resume the path movement as it progresses from point to point. To tag all of the transitions that will collectively make up the path, just pass the tag parameter (string value) in the params table.

Looping Through the Points

Now, let’s loop through our points and set up the queue of transitions. For simplicity, we’ll declare all of them initially and apply a delay parameter on each, so each transition starts when the previous one is finished.

First, we’ll start our loop and immediately set a default segmentTime of 500 milliseconds. Then, we’ll check if params.constantTime has been supplied and, if so, we’ll overwrite segmentTime with a refactored time — specifically, the distance between the points multiplied by the speedFactor that we calculated above:

   for i = 1,#path do

      local segmentTime = 500

      --if "constant" is defined, refactor transition time based on distance between points
      if ( constant ) then
         local dist
         if ( i == 1 ) then
            dist = distBetween( object.x, object.y, deltaX+path[i].x, deltaY+path[i].y )
         else
            dist = distBetween( path[i-1].x, path[i-1].y, path[i].x, path[i].y )
         end
         segmentTime = dist*speedFactor

As mentioned above, we can optionally set a custom time on any specific segment in the path. If supplied, this value will override both the default of 500 milliseconds and the “constant rate” value, if it’s being used. Let’s check if a custom time has been specified for this segment:

      else
         --if this path segment has a custom time, use it
         if ( path[i].time ) then segmentTime = path[i].time end
      end

In addition, let’s check if a custom easingMethod parameter has been set on this specific path segment. If declared, this will override the optional default easing method applied to the path as a whole.

      --if this segment has custom easing, override the default method (if any)
      if ( path[i].easingMethod ) then ease = path[i].easingMethod end

And finally, the heart of the entire process — creating each transition for the path:

      transition.to( object, { tag=tag, time=segmentTime, x=deltaX+path[i].x, y=deltaY+path[i].y, delay=delay, transition=ease } )
      delay = delay + segmentTime
   end
end

Notice that the parameters table for each actual transition is populated based on the values we gathered or calculated in the lines above. Also, note line #66 — here, we add to the total delay value for each new transition in sequence, thus making each transition begin when the previous one has completed.

Setting an Object in Motion

Let’s start our circles in motion across the path! At the most basic level, the setPath() function may be called like this:

setPath( circle1, movePath, { tag="moveObject" } )
setPath( circle2, movePath, { tag="moveObject" } )

However, this doesn’t include any options like useDelta or constantTime, so let’s expand on it:

setPath( circle1, movePath, { useDelta=true, constantTime=1200, easingMethod=easing.inOutQuad, delay=200, tag="moveObject" } )
setPath( circle2, movePath, { useDelta=true, constantTime=1200, easingMethod=easing.inOutQuad, tag="moveObject" } )

And that’s it — the circles will both follow the same path, movePath, and the path will be offset by each object’s starting position because we used the delta setting. The rate of movement across each segment will be constant, and an in-out quadratic easing will be applied to all segments.

Pausing, Resuming, Canceling

Because we tagged every transition in the sequence with the "moveObject" tag, pausing, resuming, or canceling is simple — just pass the tag name to one of the transition control APIs:

--pause the sequence
transition.pause( "moveObject" )
--some time later, resume it
transition.resume( "moveObject" )
--all finished... cancel it!
transition.cancel( "moveObject" )

In Summary

Hopefully this tutorial gets you started on path-based movement in Corona. Note that the functional format allows you to create and re-use several unique path “patterns” and apply them to various objects by simply passing the appropriate path table to the setPath() function, along with the object and the optional parameters. In this manner, you could, for example, create several different movement paths for pieces in a board game and re-use those paths as needed depending on the player’s move.

To experiment with linear path-based movement, please download the project. If you need curved-based path methods, please refer to the Working With Curved Paths tutorial.


Posted by . Thanks for reading...

21 Responses to “Tutorial: Moving Objects Along a Path”

  1. Erich Grüttner Díaz

    Excellent tutorial, as usual!
    Perhaps could be interesting to add a “loop” param (true or false). What do you think?

    Thanks Brent!!!

    Reply
  2. Sid

    For Bezier curves check out Level Director (3rd party tools), I’ve been using this in my projects and it allows you to plot paths and beziers and allows you to make objects follow them automatically, pretty cool.

    Reply
  3. Erik

    This tutorial is amazing! Thank you very much for this guide, it is very useful. Only thing this amazing program is missing is an option to reset it back to first point and forever looping through all points with a true or false param. Would greatly appreciate a param like this :)

    Reply
  4. Joe

    Great post. One question, though….

    Under “INITIAL SETUP” towards the top of the post, it says just under the first set of code –

    “By default, these points equate to specific coordinates on the screen, not points relative to the object’s starting position, but we’ll include a setting to let you choose which option is most suitable to your application.”

    Now, I read through the post several times and did not find where this setting is shown. Did I miss it, or is it not there??? What is the “setting to let me choose which option is most suitable for my app”?

    Please advise. Many thanks.

    Reply
    • Brent

      Hi Joe,
      The setting you’re speaking of is the “delta” setting. Read over that sub-section, try both options, and observe the difference.

      Brent

      Reply
      • Joe

        Ah. I see now. *puts on glasses*

        Okay. Well, I do have one more question. under the “SetPath” section, the code starts out like this –

        local function setPath( object, path, params )

        For the “params” part – does it mean I have to put in every parameter I am going to use in my transition statement? It also talks about a params table. Where is it written in the tutorial here? I’m not understanding where Params comes from…..May just be the the word “params” thats confusing me….Anyway, I understand “object” and “path” – however, I dont understand why “params” is there…

        It seems like “params” is being called first, and defined second…..Which cant be right, right? Or am I wrong? Can you kindly explain what “params” in that snippet of code is for and why it is there? I’m just not getting it.

        Thanks in advance, sir.
        -Joe

        Reply
        • Brent

          Hi Joe,
          “params” is simply the table that is passed in as the third argument to the “setPath” function. It’s not actually named “params” in the code, so just consider that I’m referring to that table as “params”.

          Reply

Leave a Reply

  • (Will Not Be Published)