Fling to the Finish: Gameplay, General Systems & Architecture

Table of Contents

  1. Gameplay
  2. Systems & Architecture: Leaderboards, Achievements & DLC
  3. Localization System

Gameplay

Game mechanic: flinging

One of the major systems I worked on was the fling mechanic, so let’s look at how I implemented that.

Flinging: Implementation Logic

Desired Gameplay: Two players in a team are tethered together by a rope. One player can fling their partner towards them, up and over obstacles, or around obstacles. The design of the game requires the fling to be simple and context sensitive, rather than having a separate fling button for the different types of flinging. At the same time, an experienced player should be able to predict how the fling mechanic will – fling – their partner.

The Prototype Phase

One of the early prototypes of the fling mechanic worked using a very simple logic: If both players are roughly on the same Y level (same XZ horizontal plane), simply apply a force on the flung partner (p1) that pushes them toward the player (p2) flinging them.

\(\vec{dir} = (\vec{p2} – \vec{p1})\)

Otherwise, if the partner (p1) that is being flung is sufficiently below the player (p2) flinging them, then apply a force in the direction that is the up vector added to the vector from the flung partner (p1) to the flinger partner (p2):

\(\vec{dir} = (\vec{p2} – \vec{p1}) + \vec{up}\)

This would push the flung partner towards the player flinging them, while also pushing the flung partner up a little bit. Since the players are connected by a rope, each player acts as the center of a circle, and their respective partners then become a point on the circle. Adding a force in a direction (not directly tangent to that point, but close enough) would mean that the player would move in an arc.

The Playtesting Phase


This early prototype was not very sophisticated because players would often find themselves in annoying situations they could not easily get out of.

Let’s look at the first case. If both players are on the same elevation, that most likely means that they are on the same horizontal platform. Players often run into situations where they get tangled around a pole, and the easiest way to get out of this problem is for one player to fling their partner around the pole.

The logic I mentioned above makes this problem worse, it tangles the players more rather than untangling them, since the player that is flung (p1) just gets pushed directly towards their partner (p2).

The other case where one player is sufficiently below their partner also fails wondrously in the following case: If the flung partner (p1) is dangling by an long platform on which the flinger partner (p2) is on, then p2 would always collide with the platform and be pushed back down (even after adding the up vector) to the direction from p2 to p1.

This is bad gameplay, and during playtesting we found that more often than not, players were colliding with the edges of platforms and getting pushed back downwards.

The Iteration Phase

The problems mentioned above had to be fixed, and the simplest idea to resolve it was the fact that the rope in our game is made of 13 separate links. There is a middle link (which I’ll refer to as mid link) and instead of getting the direction from p2 to p1, the system could instead get the direction from p2 to the mid link.

\(When \hspace{5pt} \delta y \approx 0 \Rightarrow \vec{dir} = (\vec{midLink} – \vec{p1})\)

\(When \hspace{5pt} \delta y > 0 \Rightarrow \vec{dir} = (\vec{midLink} – \vec{p1}) + \vec{up}\)

This did resolve a lot of the previously mentioned issues, but couldn’t resolve extreme edge case versions of those same issues.

In this example on the left, making the first vector go from the flung player (p2) to the mid link instead of the flinger player (p1) allows the fling direction to move further away from the edge of the platform. So, problem solved, right? Well, what happens when the mid link itself moves further to the right in this example? The green vector moves closer to the blue player, and the fling direction (red) moves closer to the platform. Eventually, the system runs into the same issue that it faced earlier, the flung player starts colliding with the platform.

Also, the other case, where both players are on the same XZ plane tangled around a pole, this solution just doesn’t work.

Let’s try and make sense of why this doesn’t work. P1 and P2 are on the same XZ plane, and the pole is on the Y axis, perpendicular to the plane that P1 and P2 are on. We need to add force to P1 in a direction marked by the blue “???” vector, that is in the XZ plane, so that P1 moves around the pole in an arc marked by the green dotted line.

If we use the updated method of looking at the mid link and then adding the up vector, it doesn’t really give us the blue vector in the XZ plane, but rather a vector that is somewhere in the 3D XYZ space. This vector *may* work at times in this given context, but is not the desired vector. Instead, we need something that gives exactly the blue vector.

The Eureka Moment

As we’ve seen from the previous examples, relying on the up vector only works in the case when one player has to move upward. It fails when both players are in a horizontal plane, because instead of moving upward, the player has to move in the XZ plane. There’s also the problem of the fling direction not being “ideal” even in the situations where the up vector is ideal, because it leads to bad gameplay experience.

We needed something that moved the flung player p2 while following these conditions:

  • p1 must move towards p2
  • Final p1 position must remain planar with original position of p1, p2 and mid link
  • p1 must go around obstacles so that the players get untangled, rather than in a direction that would wrap the rope around the obstacle even more than it was before flinging

The solution to this problem is rather simple, and can be described in the following steps:

  • Get the vector from p1 to p2 (A), and the vector from p1 to mid link (B)
    • We already have these two vectors from the previous iterations.
  • Get the angle θ between these two vectors A and B
    • Use the arccosine of the dot product of the two vectors divided by the product of the magnitude of both vectors. Unity has its own API that does this for us
  • Make a third vector that starts from the (p1 -> midLink) B vector, and is rotated away from the (p1 -> p2) A vector.
    • To do this we need the axis of rotation, which is simply the normalized cross product of the two vectors we created earlier
    • Use Quaternion angle/axis representation to rotate this new vector by the angle
    • The angle of rotation φ is the same angle θ between vectors A and B. Set φ = θ

What we have done, effectively, is we have created a vector that is twice as rotated from the (p1 -> p2) vector as the (p1 -> midLink) vector. However, we cannot simply use this vector as the fling direction, because while it follows all the conditions I mentioned earlier, it still needs to satisfy a gameplay issue.

If the angle θ between the two vectors is very narrow (p1, p2 and mid link almost form a straight line), then the angle φ that the third vector is rotated by will also be very narrow. This is not helpful, because in a situation like this, we need this third vector to be rotated by a very wide angle. When the original angle θ is wide itself, the new rotation angle φ can be narrower.

What I’ve just described is an inverse relationship between the desired angle of rotation φ and the angle θ between the two vectors (p1 -> p2) and (p1 -> midLink). Instead of doing complex math for solving this inverse relationship, I used a more elegant solution: Unity’s Animation Curves – something that I try to use in almost everything since they’re so useful! This is more convenient, less intense on the processor, and much easier to adjust according to gameplay needs. We can now describe this new angle as φ = θ + c, where c is the inverse proportionality constant described by the animation curve. Another benefit of the animation curve is that the inverse proportionality doesn’t have to be linear! It can be fine tuned to fit the needs of the gameplay.

This final solution works really well for the situation where the two players are on a different elevation. The farther the mid link gets from the edge of the platform, the narrower θ becomes, which then makes φ wider. p1 always avoids collisions with the edge of the platform!

When the two players are on the same XZ plane, this solution still works. Since we’re no longer looking at the up vector, but rather a third vector that’s rotated further away, p1 is guaranteed to go around the pole.

In fact, this solution is so robust that it creates a rotation around any arbitrary plane, so regardless of what orientation the two players are in, this will always work.

Code for the fling direction calculation

Finally, before multiplying the fling direction with the fling force, we need to calculate the fling force. This is very simple, and the fling force is based on the distance between the flung player p1 and the mid link. The more the distance, the higher the fling force.

Flinging on the same XZ plane
Flinging at different Y elevations

This system even works with the following extreme example:


Systems & Architecture

While I’ve worked on all systems in the game, I’ve highlighted a few significant ones below.

Leaderboards, Achievements & DLC

DLC: Checking whether the player owns a certain DLC is a process that differs per platform. As such, this uses the same systems as above, where each platform can have its own specific implementation for checking DLC data, then informing the master DLC manager, which is what the game interacts with.

If a platform doesn’t support a specific feature above, they can simply not implement that feature without needing to edit that feature’s overarching code or affecting game functionality.

High level overview of above systems

Localization

I made a localization system for the game that parses data from a JSON file with keys, and values for each key in every language. 19 languages are supported at the time of writing this.

Since the game has an extremely limited amount of text, a single JSON file for all languages made more sense than multiple JSON files per language. The system parses the JSON file and updates the text accordingly. When a language change is made by the player, an event system notifies all applicable text fields to update their text, font, scaling and kerning accordingly.

Example of localized entry in JSON file

The JSON file is derived from a master Google Sheet that is sent to our publisher’s localization team.

Screenshot of Google Sheet used for localization