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
- 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.
- We start with a pivot, a static attachment point for one end of the rope. It is usually a
- 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.
- Making the whole system invisible, until the player is nearby and player presses
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 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:
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:
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.
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
):
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.
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
- 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