Welcome to the second and final part of this two-part series on how to make a 2D grappling hook game in Unity!
In Part 1 of this series, you learned how to hook up a fairly nifty grappling hook with a rope wrapping mechanic. However, you were left wanting for more. The rope could wrap around objects in the level, but didn’t unravel when you swung back past them again.
By the end of this tutorial, you’ll be unwrapping that rope like a professional!
Getting Started
In Unity, open your completed project from Part 1 in this tutorial series, or download the starter project for this part of the series and open 2DGrapplingHook-Part2-Starter. As with Part 1, you should be using Unity 2017.1 or newer.
Open the Game scene under the Scenes project folder in the editor.
Run the Game scene and try your grappling hook on the rocks above you, then swing around so that the rope wraps over a couple of edges on the rocks.
When swinging back again, you’ll notice that the points on the rocks where the rope had previously wrapped around don’t unwrap again.
Think about the point at which the rope should unwrap. To make this easier, it might be best to think about the case where you have the rope wrapping over edges.
If the slug swings to the right while grappled to a rock above, the rope will wrap at the threshold where it passes the 180 degree point with the edge the slug is currently grappled to, as indicated by the circled green point in the image below.
When the slug swings back around again in the other direction, the rope should unwrap at that same point again (the point highlighted in red below):
Unwrapping Logic
To calculate when to unwrap the rope from points it has wrapped around, you’ll need to employ the use of some geometry mathematics. Specifically, you’ll need to use angle comparison to work out when the rope should unwrap.
Thinking about this problem can be a little daunting. Math can invoke feelings of terror and despair even in those with the strongest of fortitude.
Luckily, Unity has some excellent math helper functions that should make your life a little bit easier.
Open RopeSystem in your IDE, and create a new method named HandleRopeUnwrap()
.
private void HandleRopeUnwrap()
{
}
Locate Update()
and add a call to your shiny new method at the very end.
HandleRopeUnwrap();
Right now, HandleRopeUnwrap()
doesn’t do anything, but you now have a handle on the logic that deals with this whole unwrapping business.
You may recall from part 1 of this series that you stored rope wrap positions in a collection named ropePositions
, which is a List
collection. Every time the rope wraps around an edge, you store the position of that wrap point in this collection.
In order to keep things more efficient, you won’t worry about running any of the logic in HandleRopeUnwrap()
if this collection’s count of stored positions is 1 or less.
In other words, when the slug is grappled to a starting point, and its rope has not wrapped around any edges yet, the ropePositions
count will be 1, and you won’t worry about handling unwrapping logic.
Add this simple return
statement at the top of HandleRopeUnwrap()
to save precious CPU cycles for these cases, as this method is being called from Update()
many times a second.
if (ropePositions.Count <= 1)
{
return;
}
Adding Extra Variables
Below this newly added check, you'll want some measurements and references to the various angles required to do the bulk of the unwrap logic. Add the following code to HandleRopeUnwrap()
:
// Hinge = next point up from the player position
// Anchor = next point up from the Hinge
// Hinge Angle = Angle between anchor and hinge
// Player Angle = Angle between anchor and player
// 1
var anchorIndex = ropePositions.Count - 2;
// 2
var hingeIndex = ropePositions.Count - 1;
// 3
var anchorPosition = ropePositions[anchorIndex];
// 4
var hingePosition = ropePositions[hingeIndex];
// 5
var hingeDir = hingePosition - anchorPosition;
// 6
var hingeAngle = Vector2.Angle(anchorPosition, hingeDir);
// 7
var playerDir = playerPosition - anchorPosition;
// 8
var playerAngle = Vector2.Angle(anchorPosition, playerDir);
That's a lot of variables, so here is some explanation around each one, along with a handy illustration that will help you match up each one to its purpose.
anchorIndex
is the index in theropePositions
collection two positions from the end of the collection. You can look at this as two positions in the rope back from the slug's position. In the image below, this happens to be the grappling hook's first hook point into the terrain. As theropePositions
collection fills with more wrap points, this point will always be the wrap point two positions away from the slug.hingeIndex
is the index in the collection where the current hinge point is stored; in other words, the position where the rope is currently wrapping around a point closest to the 'slug' end of the rope. It’s always one position away from the slug, which is why you useropePositions.Count - 1
.anchorPosition
is calculated by referencing theanchorIndex
location in theropePositions
collection, and is simply a Vector2 value of that position.hingePosition
is calculated by referencing thehingeIndex
location in theropePositions
collection, and is simply a Vector2 value of that position.hingeDir
a vector that points from theanchorPosition
to thehingePosition
. It is used in the next variable to work out an angle.hingeAngle
is where the ever usefulVector2.Angle()
helper function is used to calculate the angle betweenanchorPosition
and the hinge point.playerDir
is the vector that points fromanchorPosition
to the current position of the slug (playerPosition)playerAngle
is then calculated by getting the angle between the anchor point and the player (slug).
These variables are all being calculated by looking at positions stored as Vector2 values in the ropePositions
collection, and comparing these positions to other positions, or the current position of the player (slug).
The two important variables you now have stored for comparison are hingeAngle
and playerAngle
.
The value stored in hingeAngle
should stay static, as it is always a fixed angle between the point two 'rope bends' away from the slug, and the current 'rope bend' closest to the slug which doesn't move until it unwraps or a new wrap point is added after this.
The playerAngle
value is what changes while the slug is swinging. By comparing this angle to the hingeAngle
, as well as whether the slug was last left or right of this angle, you can determine if the current wrap point closest to the slug should unwrap or not.
In part 1 of this tutorial, you stored wrap positions in a Dictionary collection named wrapPointsLookup
. Each time you stored a wrap point, you added it to the dictionary with the position as the key, and 0 as the value. That 0 value was pretty mysterious though right?
This value is what you'll use to store the slug's position, relative to its angle to the hinge point (the current closest wrap point to the slug).
You'll set this to a value of -1 when the slug's angle (playerAngle
) is less than the hinge's angle (hingeAngle
), and a value of 1, when playerAngle
is greater than hingeAngle
.
By storing this in the dictionary, every time you check playerAngle
against hingeAngle
, you'll be able to tell if the slug has just passed the threshold at which the rope should unwrap.
Another way to put this is if the slug's angle has just been checked, and is less than the hinge's angle, but the last time it was stored in the wrap point dictionary it was marked with a value indicating it was on the other side of this angle, then the point should be immediately unwrapped!
Unwrapping
Take a look at this annotated screen capture where our friendly slug has anchored to a rock, then swung upward, wrapping the grappling hook rope around a rock edge on its way up.
You'll notice that at the apex of its swing, where the slug is a solid color, its current closest wrap point (where the white dot is) would be saved in the wrapPointsLookup
dictionary with a value of 1.
On its way down, as playerAngle
becomes less than hingeAngle
(those two dotted green lines) as illustrated by the blue arrow, a check will be made, and if the wrap point's last (current) value was 1, then the point should be unwrapped.
You'll now code that logic in. But before you do that, create a placeholder for the method that will do the unwrapping first. Then the logic you're about to add won’t cause an error after you create it.
Add a new method UnwrapRopePosition(anchorIndex, hingeIndex)
by adding the following lines:
private void UnwrapRopePosition(int anchorIndex, int hingeIndex)
{
}
After you've done that, return to HandleRopeUnwrap()
. Just below the newly added variables, add the following logic which will handle the two cases, where playerAngle
is less than hingeAngle
, or playerAngle
is greater than hingeAngle
:
if (playerAngle < hingeAngle)
{
// 1
if (wrapPointsLookup[hingePosition] == 1)
{
UnwrapRopePosition(anchorIndex, hingeIndex);
return;
}
// 2
wrapPointsLookup[hingePosition] = -1;
}
else
{
// 3
if (wrapPointsLookup[hingePosition] == -1)
{
UnwrapRopePosition(anchorIndex, hingeIndex);
return;
}
// 4
wrapPointsLookup[hingePosition] = 1;
}
This code should align with the explanation of the logic above for the first case (where playerAngle
< hingeAngle
), but also handles the other case (where playerAngle
> hingeAngle
).
- If the current closest wrap point to the slug has a value of 1 at the point where
playerAngle
<hingeAngle
then unwrap that point, and return so that the rest of the method is not handled. - Otherwise, if the wrap point was not last marked with a value of 1, but
playerAngle
is less than thehingeAngle
, the value is set to -1 instead. - If the current closest wrap point to the slug has a value of -1 at the point where
playerAngle
>hingeAngle
, unwrap the point and return. - Otherwise, set the wrap point dictionary entry value at the hinge position to 1.
This code will now ensure that the wrapPointsLookup
dictionary is always updated to ensure the current wrap point (closest to the slug) is always up to date with the slug's current angle relative to the wrap point.
Remember that -1 is when the slug's angle is less than the hinge angle (relative to the anchor position), and that 1 is when the slug's angle is greater than the hinge angle.
Now complete UnwrapRopePosition()
in the RopeSystem script with the code that will actually do the unwrap by moving the anchored position and resetting the rope's DistanceJoint2D distance value to the new distance. Add the following lines to the placeholder you created earlier:
// 1
var newAnchorPosition = ropePositions[anchorIndex];
wrapPointsLookup.Remove(ropePositions[hingeIndex]);
ropePositions.RemoveAt(hingeIndex);
// 2
ropeHingeAnchorRb.transform.position = newAnchorPosition;
distanceSet = false;
// Set new rope distance joint distance for anchor position if not yet set.
if (distanceSet)
{
return;
}
ropeJoint.distance = Vector2.Distance(transform.position, newAnchorPosition);
distanceSet = true;
- The current anchor index (the second rope position away from the slug) becomes the new hinge position and the old hinge position is removed (the one that was previously closest to the slug that we are now 'unwrapping'). The
newAnchorPosition
variable is set to theanchorIndex
value in the rope positions list. This will be used to position the updated anchor position next. - The rope hinge RigidBody2D (which is what the rope's DistanceJoint2D is attached to) has its position changed here to the new anchor position. This allows the seamless continued movement of the slug on his rope as he is connected to the DistanceJoint2D, and this joint should allow him to continue swinging based off the new position he is anchored to — in other words, the next point down the rope from his position.
- Next, the distance joint's distance value needs to be updated to account for the sudden change in distance of the slug to the new anchor point. A quick check against the
distanceSet
flag ensures that this is done, if not already done, and the distance is set based on calculated the distance between the slug and the new anchor position.
Save your script and return to the editor. Run the game again, and marvel at the rope unwrapping from edges as the slug passes each wrap point threshold!
Although the logic is complete, add one small bit of housekeeping code to HandleRopeUnwrap()
just before the check of playerAngle
against hingeAngle
(if (playerAngle < hingeAngle)
).
if (!wrapPointsLookup.ContainsKey(hingePosition))
{
Debug.LogError("We were not tracking hingePosition (" + hingePosition + ") in the look up dictionary.");
return;
}
This shouldn't really ever happen, as you're already resetting and detaching the grappling hook if it wraps around an edge twice, but it doesn't hurt to bail out of this method if this does happen with a simple return
statement and an error message to the console.
Plus it makes you feel rather dapper when you handle edge cases like this; and furthermore, you get a custom error message indicating you've done something you shouldn't have.
Where to Go From Here?
Here's a link to the completed project for this second and final part of the tutorial.
Congratulations on completing this tutorial series! Things got pretty complex with all the angle and position comparisons, but you persevered and now have great grappling hook and rope system that can wrap and unwrap objects in your game like nobody's business.
Did you know the Unity team has created a book? If not, check out Unity Games By Tutorials. The book will teach you to create four complete games from scratch:
- A twin-stick shooter
- A first-person shooter
- A tower defense game (with VR support!)
- A 2D platformer
By the end of this book, you’ll be ready to make your own games for Windows, macOS, iOS, and more!
This book is for complete beginners to Unity, as well as for those who’d like to bring their Unity skills to a professional level. The book assumes you have some prior programming experience (in any language).
If you have any questions or comments on this tutorial or tutorial series as a whole, please join the discussion below!
The post Make a 2D Grappling Hook Game in Unity – Part 2 appeared first on Ray Wenderlich.