Make Grappling Hook in Godot

grappling hook godot tutorial preview

Grappling hook is a mechanics in 2D platformer games in which player grabs a swinging rope or chain. it is used if there is no surface to walk and player is forced to go to the other side via a swinging rope.

Another possibility is that you throw a grappling hook towards a ledge or a wall, and once it is securely hooked there, you can use the attached rope to pull yourself up, effectively scaling the wall and reaching the higher ground.

Breakdown

  1. Our first target is to make a pendulum. As the physics used for pendulum will be used for the swinging motion of the grappling hook.
    • We start with a pivot, a static attachment point for one end of the rope. It is usually a StaticBody2D somewhere in the level world.
    • A pin joint. Pin joint is used to attach 2 physics nodes. We implement this as PinJoint2D in Godot.
    • A rope. It is usually a Line2D node. Rope is purely cosmetic and servers no purpose in simulation.
    • A RigidBody2D node on the free end of the pin joint. This oscillates to-and-fro.
  2. Once the pendulum is created, the grappling hook will be created by:
    • Making the whole system invisible, until the player is nearby and player presses jump action. Once player triggers the system, everything becomes visible & the player is attached to the free end of the pendulum (thus player moves with the rigid-body node).
    • When player un-triggers the grappling hook system by pressing the jump action again, the player node is detached from the rigid-body, and player inherits the linear_velocity of the free end so player continues to have momentum.
    • The kind of grappling hook system in which we can throw the hook to the ledge or cliff can be made by throwing the static pivot using the ray-cast, to the nearest ledge or wall. And if it hit it, it stays there and the system is triggered.

Starting with the Pendulum System

Making Fixed Side of the Pendulum

Make a scene and call it ‘GrapplingHookSystem’. Add a StaticBody2D node as its child.

This static body will be the fixed point for the pendulum. It can be any static body though.

Add a collision shape (can be any), and some Sprite2D or Polygon2D node to visualize this fixed point of pendulum. Overall the scene looks like this:

making pendulum in godot

Making Moving/Free Side of the Pendulum

We have created an anchor point for the fixed side of the pendulum. Now we need to make the moveable part of the pendulum as well. It will be a RigidBody2D node, as the moveable size must respond to the gravity, and forces.

Add a RigidBody2D node and name it PlayerAnchor or PlayerAttachmentPoint (whatever you want). In future, we will attach our player node to this rigid-body to move the player. But for now, we are making a pendulum only. This is how the scene looks with the rigid body attached:

making simple pendulum in godot

Joining the two Points via a Pin Joint

We now attach the static body fixed node to the movable free object (the rigid body) via a PinJoint2D node. Create a Pinjoin2D node as child of the scene root node (not under any static body or rigid body).

In inspector, assign node_a to the static-body and node_b to the rigid-body; effectively attaching the two nodes. This is how it look like:

pin joint godot, PinJoint2D tutorial

The core setup is basically done, but we will not be able to visualize it since we have not added any Sprite2D or other visual nodes. To quickly test it, add a sprite node to the rigid-body & move the rigid-body away from its mean position to slightly on one side. And you will see that is a valid pendulum.

godot pendulum simulation, pin joint tutorial in godot

Visualizing the Rope

We have a valid pendulum, but we have no way to see the rope that is holding the rigid-body to the static-body. For this, create a Line2D node in the scene. We will make one end of line to attach with the static body and other end to attach with the rigid body (via the script code).

Just add a Line2D node (I named it Rope):

line2d godot. attach both ends of line to some points

Sync Rope Ends to the two Anchor Points

As shown in above image, attach a script to he root node (call it GrapplingHookSystem.gd). Our first target is to sync both ends of the line2d to the two nodes, so both ends of the line will follow the position of the static-body and the rigid-body respectively.

In GrapplingHookSystem.gd:

extends Node2D


# Assign these values in inspector, else they will be null
@export var grapple_anchor: StaticBody2D
@export var player_anchor: RigidBody2D
@export var rope: Line2D


func _ready():
	add_to_group("grappling-hook-system")


# Process to handle player input and swinging
func _process(delta):
	rope.points[0] = $GrappleAnchor.global_position * rope.global_transform
	rope.points[1] = player_anchor.global_position * rope.global_transform
	

Now the rope will can be seen attached to the static body on one end and to the rigid body on the other end.

Swing the Grappling Hook with User Input

We also want to push the grappling hook to left or right direction as user presses left or right keys. For this, add the following code to the _process(delta) function:

In GrapplingHookSystem.gd:

func _process(delta):

  # ... PREVIOUS CODE ...
  
	if Input.is_action_pressed("ui_right"):
		# if already moving fasdt, don't move faster
		if player_anchor.linear_velocity.length() < 400:
			player_anchor.apply_central_impulse(
				player_anchor.global_transform.x * 64
			)  # Push right
	elif Input.is_action_pressed("ui_left"):
		if player_anchor.linear_velocity.length() < 400:
			player_anchor.apply_central_impulse(
				player_anchor.global_transform.x * -64
			)  # Push left

Notice how I am applying force to the player-anchor’s local x-direction (player_anchor.global_transform.x), rather than world x-direction (Vector2(1, 0)). This is because, as the grappling hook swings, player-anchor’s orientation changes and so the local x-axis doesn’t always points to the horizontal direction.

grappling hook tutorial local axis preview
Local x-direction of rigid-body (player anchor) is shown as red arrow in the image.

Making Player Attach/Detach Logic

We need to make functions, that once called, will attach the player node to the grappling hook system.

For this, we will keep the grappling hook invisible initially, but when player calls attach function, we will make everything visible. And when player calls detach, we will again make everything invisible and release the player.

In GrapplingHookSystem.gd:


# ... PREVIOUS CODE ...

@export var max_radius: float = 512 # Only attach to things within this radius


func _ready():

	# ... PREVIOUS CODE ...
	
	rope.visible = false
	player_anchor.visible = false




# API - use these functions to interact with grappling hook system


# Function to make the player a child of player_anchor and make
# all the things visible and working.

var player_original_parent: Node = null

func attach_player(player: Node2D) -> int:
	if self.global_position.distance_to(player.global_position) > max_radius or player_original_parent != null:
		return -1
	player_original_parent = player.get_parent()
	player_anchor.position = player.global_position # Set the player_anchor to the player's position
	player.reparent(player_anchor) # Make the player a child of player_anchor
	player.position = Vector2.ZERO

	rope.points[0] = $GrappleAnchor.global_position * rope.global_transform
	rope.points[1] = player_anchor.global_position * rope.global_transform

	rope.visible = true
	player_anchor.visible = true
	return 0



# Function to make the player no longer a child of player_anchor
# and make all the things invisible and not working.

func detach_player(player: Node2D) -> int:
	if player_original_parent == null:
		return -1
	player.reparent(player_original_parent)
	player.velocity = player_anchor.linear_velocity # So it continues its momentum
	player_original_parent = null
	rope.visible = false
	player_anchor.visible = false
	return 0

Some Important Settings

  1. I removed all collision layers and masks for the rigid body (player anchor) node, so it does not interfere with the world collisions, and thus does not disrupts anything. (By default, collision layer & mask 1 is enabled, disable all of them).

Problems with the System

  • When player is made the child of the grappling hook, it still continues to recieve its normal inputs to walk, run or jump. So you must design your player in such a way that it no longer responds to the user inputs when swinging on the grappling hook system. For this, a state-machine can be made that defines in which states a player cannot listen to user inputs.
    • However, the inputs to move grappling hook left and right are implemented in the grappling hook system itself.
    • As a hacky solution, I overridden the player position to be Vector2(0, 0) every frame (when attached to the grappling hook).
    	# In GrapplingHookSystem.gd, _process(delta):
    	
    	for child in player_anchor.get_children():
    		if "position" in child:
    			child.position = Vector2(0,0)
    • The system needs some tweaking of gravity_scale, forces and other factors to make it look right; that is up to the kind of game it is to be implemented in, and the developer preferences.

      More Things

      I am posting the player logic code just for a reference, it is totally up to you, how you use the system, but the player script is just for reference:

      Player.gd:

      extends CharacterBody2D
      
      
      var is_grappling: bool = false
      
      func _process(delta):
      	# Just move the player left and right or top bottom (simple player motion)
      
      	# Add damping to the player's velocity
      	velocity = velocity.lerp(Vector2.ZERO, 0.02)
      
      	if not is_grappling: # Only move player if grappling is not active
      		if Input.is_action_pressed("ui_right"):
      			velocity.x += 100 * delta  # Move right
      		elif Input.is_action_pressed("ui_left"):
      			velocity.x -= 100 * delta  # Move left
      		if Input.is_action_pressed("ui_down"):
      			velocity.y += 100 * delta  # Move down
      		elif Input.is_action_pressed("ui_up"):
      			velocity.y -= 100 * delta  # Move up
      		
      		# Add gravity to the player's velocity
      		velocity.y += 1000 * delta
      	
      	# Move the player
      	move_and_slide()
      
      	# if player jumps, call attach_player function on all grappling hook systems
      	# so whichever will be in range will attach the player
      	if Input.is_action_just_pressed("ui_home"):
      		for grappling_hook_system in get_tree().get_nodes_in_group("grappling-hook-system"):
      			var success = grappling_hook_system.attach_player(self)
      			if success == 0:
      				is_grappling = true
      				break	
      	elif Input.is_action_just_released("ui_home"):
      		for grappling_hook_system in get_tree().get_nodes_in_group("grappling-hook-system"):
      			var success = grappling_hook_system.detach_player(self)
      			if success == 0:
      				is_grappling = false
      				break
      	
      	global_rotation = 0 # dont rotate as grappling hook swings
      

      Link to GitHub repo.

      Thank you for reading <3

      Leave a Reply

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