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.

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.

[call_to_action]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[/call_to_action]

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

[section_title icon=”fa-forward” text=”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).

[toggle first_toggle=”yes” title=”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.

[/toggle]

[section_title icon=”fa-crosshairs” text=”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.

[toggle title=”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;
}
}

[/toggle]

[section_title icon=”fa-code” text=”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).

[toggle title=”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.

[/toggle]
[section_title icon=”fa-location-arrow” text=”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.

[toggle last_toggle=”yes” title=”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.

[/toggle]

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.