Make a 2D Grappling Hook Game in Unity – Part 1

Learn how to implement your own 2D grappling hook system in this Unity tutorial. By Sean Duffy.

Leave a rating/review
Save for later
Share

Grappling hooks add a fun and interesting mechanic to your games. You can use them to traverse levels, fight in arenas, or even retrieve items. But despite looking easy, the physics of handling ropes and making them behave realistically can put you at the end of your rope!

In part one of this two-part tutorial series, you’ll implement your own 2D grappling hook system and learn the following:

  • Create an aiming system.
  • Use a line renderer and distance joint to create a rope.
  • Make the rope wrap around objects in your game.
  • Calculate an angle for swinging on a rope and add force in that direction.

Note: This tutorial is intended for an intermediate to advanced audience, and won’t cover things such as adding components, creating new GameObjects scripts or the syntax of C#. If you need to level up your Unity skills, work through our tutorials on Getting Started with Unity and Introduction to Unity Scripting first. As this tutorial is also based around the DistanceJoint2D you might want to review Physics Joints in Unity 2D as well and then return to this tutorial.

Getting Started

Download the starter project for this tutorial and open it up in the Unity editor. Make sure you’re running Unity 2017.1 or newer.

Open the Game scene from the Scenes folder and take a look at what you’ll soon begin “hooking” up:

a 2D Grappling Hooks Game

Right now, there is a basic player character (the slug) and some rocks floating about.

The notable components the Player GameObject has right now are a capsule collider and a rigidbody which allow it to interact with physical objects in the level. There’s also a simple movement script (PlayerMovement) attached that lets your slippery character slide along the ground and perform basic jumps.

Click the Play button in the editor to start the game and try out the controls to see how they feel. A or D will move you left or right, and space will perform jumps. Be careful not to slip and fall off the rocks or you’ll die!

Goodbye cruel world!

The basic controls are implemented, but the biggest concern right now is the lack of grappling hooks.

Creating the Hooks and Rope

A grappling hooks system sounds fairly simple at first, but there are a number of things you’ll need in order to make it work well. Here are some of the main requirements for a 2D grappling hook mechanic:

  • A Line Renderer which will show the rope. When the rope wraps around things, you can add more segments to the line renderer and position the vertices appropriately around the edges the rope wraps.
  • A DistanceJoint2D. This can be used to attach to the grappling hook’s current anchor point, and lets the slug swing. It’ll also allow for configuration of the distance, which can be used to rappel up and down the rope.
  • A child GameObject with a RigidBody2D that can be moved around depending on the current location of the hook’s anchor point. This will essentially be the rope hinge / anchor point.
  • A raycast for firing the hook and attaching to objects.

Select Player in the Hierarchy and add a new child GameObject to the Player named RopeHingeAnchor. This GameObject will be used to position the hinge / anchor point of the grappling hook wherever it should be during gameplay.

Add a SpriteRenderer and RigidBody2D component to RopeHingeAnchor.

On the SpriteRenderer, set the Sprite property to use UISprite and change the Order in Layer to 2. Disable the component by unchecking the box next to its name.

On the RigidBody2D component, set the Body Type property to Kinematic. This point will not move around with the physics engine but by code.

Set the layer to Rope and set the X and Y scale values to 4 on the Transform component.

Select Player again and attach a new DistanceJoint2D component.

Drag and drop the RopeHingeAnchor from the Hierarchy onto the Connected Rigid Body property on the DistanceJoint2D component and disable Auto Configure Distance.

Create a new C# script called RopeSystem in the Scripts project folder and open it with your code editor.

Remove the Update method.
At the top of the script, inside the RopeSystem class declaration, add the following variables as well as an Awake() method, and a new Update method:

// 1
public GameObject ropeHingeAnchor;
public DistanceJoint2D ropeJoint;
public Transform crosshair;
public SpriteRenderer crosshairSprite;
public PlayerMovement playerMovement;
private bool ropeAttached;
private Vector2 playerPosition;
private Rigidbody2D ropeHingeAnchorRb;
private SpriteRenderer ropeHingeAnchorSprite;

void Awake()
{
    // 2
    ropeJoint.enabled = false;
    playerPosition = transform.position;
    ropeHingeAnchorRb = ropeHingeAnchor.GetComponent<Rigidbody2D>();
    ropeHingeAnchorSprite = ropeHingeAnchor.GetComponent<SpriteRenderer>();
}

void Update()
{
    // 3
    var worldMousePosition =
        Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0f));
    var facingDirection = worldMousePosition - transform.position;
    var aimAngle = Mathf.Atan2(facingDirection.y, facingDirection.x);
    if (aimAngle < 0f)
    {
        aimAngle = Mathf.PI * 2 + aimAngle;
    }

    // 4
    var aimDirection = Quaternion.Euler(0, 0, aimAngle * Mathf.Rad2Deg) * Vector2.right;
    // 5
    playerPosition = transform.position;

    // 6
    if (!ropeAttached)
    {
    }
    else
    {
    }
}

Taking each section in turn:

  1. You’ll use these variables to keep track of the different components the RopeSystem script will interact with.
  2. The Awake method will run when the game starts and disables the ropeJoint (DistanceJoint2D component). It'll also set playerPosition to the current position of the Player.
  3. This is the most important part of your main Update() loop. First, you capture the world position of the mouse cursor using the camera's ScreenToWorldPoint method. You then calculate the facing direction by subtracting the player's position from the mouse position in the world. You then use this to create aimAngle, which is a representation of the aiming angle of the mouse cursor. The value is kept positive in the if-statement.
  4. The aimDirection is a rotation for later use. You're only interested in the Z value, as you're using a 2D camera, and this is the only relevant axis. You pass in the aimAngle * Mathf.Rad2Deg which converts the radian angle to an angle in degrees.
  5. The player position is tracked using a convenient variable to save you from referring to transform.Position all the time.
  6. Lastly, this is an if..else statement you'll soon use to determine if the rope is attached to an anchor point.

Save the script and return to the editor.

Attach a RopeSystem component to the Player and hook up the various components to the public fields you created in the RopeSystem script. Drag the Player, Crosshair and RopeHingeAnchor to the various fields like this:

  • Rope Hinge Anchor: RopeHingeAnchor
  • Rope Joint: Player
  • Crosshair: Crosshair
  • Crosshair Sprite: Crosshair
  • Player Movement: Player

Right now, you're doing all those fancy calculations for aiming, but there’s no visual candy to show off all that work. Not to worry though, you'll tackle that next.

Open the RopeSystem script and add a new method to it:

private void SetCrosshairPosition(float aimAngle)
{
    if (!crosshairSprite.enabled)
    {
        crosshairSprite.enabled = true;
    }

    var x = transform.position.x + 1f * Mathf.Cos(aimAngle);
    var y = transform.position.y + 1f * Mathf.Sin(aimAngle);

    var crossHairPosition = new Vector3(x, y, 0);
    crosshair.transform.position = crossHairPosition;
}

This method will position the crosshair based on the aimAngle that you pass in (a float value you calculated in Update()) in a way that it circles around you in a radius of 1 unit. It'll also ensure the crosshair sprite is enabled if it isn’t already.

In Update(), change the if..else statement that checks for !ropeAttached to look like this:

if (!ropeAttached)
{
	SetCrosshairPosition(aimAngle);
}
else 
{
	crosshairSprite.enabled = false;
}

Save your script, and run the game. Your slug should now have the ability to aim with a crosshair.

2d aiming

The next bit of logic you'll need to implement is a way to fire the grappling hook. You already have your aiming direction worked out, so you'll need a method to take this in as a parameter.

Add the following variables below the others in the RopeSystem script:

public LineRenderer ropeRenderer;
public LayerMask ropeLayerMask;
private float ropeMaxCastDistance = 20f;
private List<Vector2> ropePositions = new List<Vector2>();

The LineRenderer will hold a reference to the line renderer that will display the rope. The LayerMask will allow you to customize which physics layers the grappling hook's raycast will be able to interact with and potentially hit. The ropeMaxCastDistance value will set a maximum distance the raycast can fire.

Finally, the list of Vector2 positions will be used to track the rope wrapping points when you get a little further in this tutorial.

Add the following new methods:

// 1
private void HandleInput(Vector2 aimDirection)
{
    if (Input.GetMouseButton(0))
    {
        // 2
        if (ropeAttached) return;
        ropeRenderer.enabled = true;

        var hit = Physics2D.Raycast(playerPosition, aimDirection, ropeMaxCastDistance, ropeLayerMask);
        
        // 3
        if (hit.collider != null)
        {
            ropeAttached = true;
            if (!ropePositions.Contains(hit.point))
            {
            	// 4
                // Jump slightly to distance the player a little from the ground after grappling to something.
                transform.GetComponent<Rigidbody2D>().AddForce(new Vector2(0f, 2f), ForceMode2D.Impulse);
                ropePositions.Add(hit.point);
                ropeJoint.distance = Vector2.Distance(playerPosition, hit.point);
                ropeJoint.enabled = true;
                ropeHingeAnchorSprite.enabled = true;
            }
        }
        // 5
        else
        {
            ropeRenderer.enabled = false;
            ropeAttached = false;
            ropeJoint.enabled = false;
        }
    }

    if (Input.GetMouseButton(1))
    {
        ResetRope();
    }
}

// 6
private void ResetRope()
{
    ropeJoint.enabled = false;
    ropeAttached = false;
    playerMovement.isSwinging = false;
    ropeRenderer.positionCount = 2;
    ropeRenderer.SetPosition(0, transform.position);
    ropeRenderer.SetPosition(1, transform.position);
    ropePositions.Clear();
    ropeHingeAnchorSprite.enabled = false;
}

Here is an explanation of what the above code does:

  1. HandleInput is called from the Update() loop, and simply polls for input from the left and right mouse buttons.
  2. When a left mouse click is registered, the rope line renderer is enabled and a 2D raycast is fired out from the player position in the aiming direction. A maximum distance is specified so that the grappling hook can't be fired in infinite distance, and a custom mask is applied so that you can specify which physics layers the raycast is able to hit.
  3. If a valid raycast hit is found, ropeAttached is set to true, and a check is done on the list of rope vertex positions to make sure the point hit isn't in there already.
  4. Provided the above check is true, then a small impulse force is added to the slug to hop him up off the ground, and the ropeJoint (DistanceJoint2D) is enabled, and set with a distance equal to the distance between the slug and the raycast hitpoint. The anchor sprite is also enabled.
  5. If the raycast doesn't hit anything, then the rope line renderer and rope joint are disabled, and the ropeAttached flag is set to false.
  6. If the right mouse button is clicked, the ResetRope() method is called, which will disable and reset all rope/grappling hook related parameters to what they should be when the grappling hook is not being used.

At the very bottom of your existing Update method, add a call to the new HandleInput() method, and pass in the aimDirection value:

HandleInput(aimDirection);

Save your changes to RopeSystem.cs and switch back to the editor.