Renaud van Strydonck

Renaud van Strydonck

Portrait of a Game [ Designer / Developer ]

Working with iTween Paths

Cities & Desire 5

From there, after six days and seven nights, you arrive at Zobeide, the white city, well exposed to the moon, with streets wound about themselves as in a skein.

They tell this tale of its foundation : men of various nations had an identical dream. They saw a woman running at night through an unknown city; she was seen from behind, with long hair, and she was naked. They dreamed of pursuing her. As they twisted and turned, each of them lost her.

After the dream, they set out in search of that city; they never found it, but they found one another; they decided to build a city like the one in the dream. In laying out the streets, each followed the course of his pursuit; at the spot where they had lost the fugitive’s trail, they arranged spaces and walls differently from the dream, so she would be unable to escape again.

This was the city of Zobeide, where they settled, waiting for that scene to be repeated one night. None of them, asleep or awake, ever saw the woman again. The city’s streets were streets where they went to work every day, with no link any more to the dreamed chase. Which, for that matter, had long been forgotten.

New men arrived from other lands, having had a dream like theirs, and in the city of Zobeide, they recognized something from the streets of the dream, and they changed the positions of arcades and stairways to resemble more closely the path of the pursued woman and so, at the spot where she had vanished, there would remain no avenue of escape.

The first to arrive could not understand what drew these people to Zobeide, this ugly city, this trap.

Italo Calvino – Invisible Cities

When we first conceived of Take My Hand, we envisioned a 2D Parkour Platformer in which players could take different routes through a city : a sort of open-world 2D parkour platformer.

You would be running and jumping and catching and launching each other along the buildings of a street until you came to an alley, at which point you could hold hands and have the camera turn to let you run down the alley instead OR leap across the alley gap and pursue your journey along the same streets and rooftops.

We quickly realised that, given the time and the resources at our disposal, the idea would most likely turn into a level design nightmare (not to mention the technical complications it implied) but we had already fallen in love with those sweeping camera turns, and with the idea that our characters could reach the corner of a block and have their field of view pivot to reveal an entire section of the city for them to explore and overcome.

After some research into developing our own system, we decided to use PixelPlacement’s iTweenPath to lay out the splines for our levels – inspired in no small part by their Path-constrained Characters example and by Roots, a previous VFS Game Design Final Project which we knew had implemented iTweenPath with some good results.  Roots and the Path-constrained Character Example had something in common, however, their movement requirements were much more limited than ours.

Before I go on, I’d like to give a shout-out to Ben Kanbour, the programmer for Roots, whose help in laying out the foundations of our movement code cannot be overstated

For those of you unfamiliar with iTween, it presents a few interesting problems :

First Problem : Consistent Speed

  1. The position of an object on the path is represented as a percentage float value –
    0% being the beginning of the path and 100% being its end.
  2. That percentage is distributed based on the number of nodes forming the path
    So that, in a path made up of 5 nodes :  A, B, C, D and E –
    – A’s percentage value is 0%
    – B’s is 25%,
    – C’s is 50%,
    – D’s is 75%, and
    – E’s is a 100%
    This means an object placed at % 37.5 will be halfway between nodes B and C irrespective of the length of the path or the relative distance between nodes.
    The position of percentages on an iTweenPath is unrelated to the objective position of its nodes
  3.  More importantly, an object “moving” at a consistent percentage per second will move from A-to-B in the same amount of time as it will from B-to-C, etc. meaning that objects move faster between distant nodes than nearby ones.
  4. This was a particular issue since the paradigm of a parkour platformer required reliable accelerations and speeds to sell the experience.  We couldn’t allow character metrics to vary depending on the structure of the spline, and it quickly became apparent that manually placing the nodes at equidistant positions wouldn’t be precise enough (not to mention that it would be a nightmare for Maria, our Level Designer).

Show Solution

We implemented a combination of the solutions presented to the “iTween even velocity” question, including a hack of Felipetnh’s iTweenPathConstantSpeed all tied to a function call so that we could control the order of operation and ensure the path was smoothed before the position of checkpoints, ledge grabs etc. was calculated.

Maria could then place nodes approximately equidistant, and the smoothing pass would take care of minor imperfections – we had initially hoped that it would remove the need for manual adjustments altogether, but large distance discrepancies still resulted in noticeable changes in character movement speeds and jump distances.

Second Problem : Objective Object Position

  1. While iTween’s .PointOnPath() function returns the Vector3 position of a percentage on the path, there is no built-in method to return the percentage position of an object relative to the path.
  2. Since all horizontal character movement had to occur as a delta in their percentage position on the path, however, this posed us some real issues regarding the placement of ledge-grabs and the mechanics permitting one character to catch or throw the other : we needed to know the percentage position of grab points in order to place the character in the right position.

Show Solution

I eventually built a static recursive binary search function, stored in the Game Manager, which returned the percentage position of a Transform relative to an iTweenPath array.  It essentially finds the Vector3 of each binary search point using .PointOnPath() and compares the sqrMagnitude to the Transform’s position to find the nearest point.

In order to control its cost, this recursive function is clamped to run a number of times set by a global value n, and returns the nearest percentage float after n iterations.

public static float RecursiveBinarySearch(Vector3 target, Vector3[] path, float min, float max, int clamp)
{
clamp--;

float _mid = min + (max-min)/2;
float _product = 0f;

if(clamp > 0)
{
Vector3 vMin = iTween.PointOnPath(path, min);
vMin.y = target.y;

Vector3 vMax = iTween.PointOnPath(path, max);
vMax.y = target.y;

if((target-vMin).sqrMagnitude > (target-vMax).sqrMagnitude)
{
return RecursiveBinarySearch(target, path, _mid, max, clamp);
}
else
{
return RecursiveBinarySearch(target, path, min, _mid, clamp);
}

else
{
return _mid;
}
}

Third Problem : Rounding Errors

  1. Owing to the rounding errors inherent in floats, calculated positions are unreliably precise, particularly on long paths with many nodes (this was a particular problem for the Recursive Binary Search method we implemented to solve the Second Problem).

Show Solution

We realised that this was one of the underlying causes of a bug fairly late on in the project.  Characters would occasionally ledge-grab in the wrong horizontal position, but in the right vertical position, and we assumed it was a flaw with the recursive binary search (in a sense, it was), but it turned out to be a combination of the collision logic and floating point errors.

Once we had figured that out, it became clear that the length of our paths was the chief culprit – long paths meant that percentage positions on that path were very long floating points, often “accurate” to seven or eight decimal places, which exacerbated the floating point error to an extent that it became visible.

This led us to devise a path-switching mechanism by which characters would be gated through a volume which moved them to the exact same world position on a new path.  This method required Maria to break up our level path into shorter path stages, each of which would overlap with the other by a space of roughly one screen width.  This allowed us to ensure that both characters were in the volume at the same time to make a smooth transition.

Fourth Problem : Placing the Spine

  1. Last but not least, even with PixelPlacement’s Visual iTweenPath Editor, laying out the splines for our levels was clumsy and unintuitive :
  2. Nodes are added and removed from the end of the path by manipulating the number of nodes, meaning that lengthening the path at the beginning would require Maria to lay out the entire spline anew, and there was similarly no way to remove nodes from the beginning of the path.
  3. Furthermore, any new nodes created appeared at the world’s zero position, rather than the last node on the path, which wasted precious time (particularly since the Editor is sluggish whenever it has to redraw a selected iTweenPath) in an already intricate Level Design process.

Show Solution

The solution to this problem was time-consuming but relatively uncomplicated :

We devised a new iTweenPath Editor, based on the original, which allowed Maria to add and remove nodes either at the beginning OR the end of the path one at a time at the click of an “Add Node” button.  Each new node would appear at the position of its predecessor (so that nodes added at the beginning appeared in the position of the first node, and those added at the end would appear at the position of the last).

We also implemented a node-stitching button which allowed us to attach one path to the end or the beginning of the other, meaning that we could build smaller segments of path and stitch them together once we were satisfied with them.

I’m still in the process of implementing a small “+” and “-” button to appear at each node on the path, allowing the user to add a node between nodes on a path, or remove a node within the path, without having to manually re-place a path segment.

I plan on cleaning up and posting these on my website as time allows, at least those pieces of code which might be useful to others.  In the meantime, please don’t hesitate ton contact me if you have any questions or comments.  I have already discovered cleaner solutions and implementations, and I am sure that there will be many, many more.

14 Comments

  1. Rob

    I’m currently experiencing the same sort of problems in using iTween. I really like the functionality it provides, but as you say, certain aspects could stand some improvement.

    I need smooth motion along the path as well, and I was disappointed to see virtually no difference between the “speed” and “time” hash parameters. I hope that spacing the nodes about the same distance from each other will suffice for my purposes.

    With respect to the VisualPathEditor, I was also surprised to see such a simple implementation of node addition, so I made a couple of simple changes: (1) new paths start with 0 points, since no sensible points can be obtained in the context of a public variable definition, and (2) newly added points either replicate the last point in the path or use the position of the gameObject to which the path is attached. Those two changes alleviated a lot of my headaches.

    Anyway, the point is that I’m hoping you post the solution code for the constant path movement soon! Oh, and I like that the design of your post uses expandable solutions.

    Reply
  2. M W Musker

    Hi Renaud, While researching how best to address iTween path’s… ehrm… issues i happenned upon your blog and found it very informative. See, we’re also using the pathing as a sort of guide for an obstacle avoidance game, and as our player is flying I had a lot of camera and bounce code to iron out. Essentially we just place a gameobject with a moveto on an itween path, and our player programmatically follows that, veering to mouse click points on the screen in order to avoid obstacles.

    So, there I was, happy with the prototype level, and then I gave it to a level designer along with a quick hack to the path editor to support N number of pathnodes.

    Obviously, what came next was as you experienced. Headaches abounded as our tidy little bezier based obstacle avoidance game showed anomaly after anomaly, and the level designer complained at length about the editor.

    I’d love to take a look at what you’ve done, as it seems we’ve experienced a lot of the same issues with iTween’s paths.

    Reply
      1. Gulmira

        The problem you have here is a relsut of the way that shadow pass is created.You are using the floor that don’t cover the whole image.Take a look at the shadow pass alone:You’ll notice that wherever you have alpha of 0 the shadow pass is black.In may opinion (I’m not the only one) this is a bug. In those areas shadow pass should be white.If it were as it should be, i.e. white your image wouldn’t change in those areas (multiplying by white doesn’t change image as you remember).Here we have antialiased edges of your object and you get dark fringe around it.The way to solve it is to take the alpha, pass it through Invert node, add it to shadow pass and use the relsut as the shadow pass.

        Reply
  3. Ryan

    Renaud,

    I wanted to let you know how much I enjoy your writing style.

    I am currently going through the same problems that you wrote about in this article. I am really looking forward to seeing your solutions when you post them.

    Congratulations on a great game! Its a lot of fun and looks like it was quite an undertaking.

    Reply
  4. Sam

    Hey thanks a lot for this code. I found it very helpful. I’m using it to keep a sprite constrained to a path while the user drags their finger around the screen. I have experienced some problems with your recursive binary search though.
    I set the min to 0 and the max to 1 (because the percent is normalized) and I noticed it seems to stick around 0.5. I tried with a max of 0.9 and it sticks less. The closer i get to 1 the more it sticks in the middle.

    Is this the rounding error you were talking about or a problem with the function? I’ve gotten around the problem by animating to the target percent instead of using it directly.

    Reply
    1. Renaud

      Hi Sam,

      I haven”t looked at this code since I wrote it, to be honest. I intended to clean it up and comment it out for people, but life and it’s myriad projects got in the way.

      On face value, it makes a certain amount of sense for the binary search to stick around 0.5, if it is going to stick somewhere. I didn’t have any such problem, but I intended the function to be used in specific instances rather than on update, which is what it sounds like you are doing.
      The rounding error should only really be a problem if your path is very long (at which point, the percentage value of your position on the path becomes too precise a decimal and the rounding error becomes an issue).

      Using 0 and 1 as the min and max value makes a lot of sense if you don’t know where on the path the sprite is when you run the function, what number are you using as a clamp? You could try increasing the clamp value and seeing if that resolves the problem (that will, of course, add to the recursions and therefore the cost).

      In any case, you’ll probably get a much smoother motion by animating to the target rather than setting the sprite’s position.

      If your path is long, you’ll need a larger clamp value, I would normally advise against running the recursive search on update (it’s potentially an awful lot of calculations to run every frame), but I have to admit that I can’t think of a more efficient solution for your setup off the top of my head.

      One thing you might consider is setting up the search (using a coroutine for example) to run every e.g. 30 frames rather than every frame. If you’re animating to your target, you might not notice any difference in performance, and you’ll be saving yourself a lot of expensive distance checks etc.

      Let me know if that helps, or if you have any other questions!

      All the best,
      R.

      Reply
  5. Dan Zwell

    I also noticed it’s a huge pain to move a spline. Before asking a coworker to tune paths, I modified the iTween visual path editor so that holding Control or Option selects all the nodes, and they will move together when dragged. Clicking individual nodes will deselect them and exclude them from the group dragging. The code is online here:

    http://wiki.unity3d.com/index.php/ITweenPathEditorPlus

    Reply
    1. Renaud

      Hi Dan,

      That’s an excellent idea!

      As it happens I’m working with iTween again at the moment to help our artists prototype a map concept, if you don’t mind I’d love to integrate your code into the Editor extensions while I’m at it.

      All the best,

      R.

      Reply
  6. Sumit

    Hi RENAUD ,
    I am working on a rail track based game, and i am using iTweenPath as a track path. Now I am unable to follow carriage train to their engine.So please Help me i am totally stuck now since 4 day.Please Help me..How to move carriage with engine.Any kind of help will be appreciated.
    Thanks.

    Reply
    1. Renaud

      Hi Sumit,

      It’s hard to be very constructive without taking a look at your code, but I’m going to assume that you’re moving your engine using .PutOnPath() and that your carriages are a predictable distance away from the next carriage/engine ahead of them.

      In which case, what I would do to start with is give carriages a constant offset % from the previous carriage and put them all in a list. That way, whenever you move the engine, you can iterate through the list and move all the carriages.

      So in essence, you iterate through your list and add your offset constant to the percentage position of carriage i-1 to get the desired position for carriage i. This will almost certainly not work perfectly, but it’ll give you starting point.

      This offset % will probably be very small. You could probably figure it out programmatically if you wanted to but, since it most likely won’t change from carraige to carriage unless your carriages vary in size, I’d just make it a constant.

      Let me know if that helps,

      R.

      Reply
      1. Sumit

        I think your tips will work for me,Hey I have one more problem ,the problem is How to rotate cube(gameobject) face according to path curve movement on iTweenPath,I have used orientToPath but it’s not giving smooth rotation of cube according to path curve. Please help me
        Thanks a lot

        Reply

DROP A COMMENT

Your email address will not be published. Required fields are marked *