In this tutorial, we will see how to make in Godot a slingshot to throw objects around. If you thought of Angry Birds, you are spot on! That’s precisely what we’re going to reproduce. The player will drag with the mouse the object to be thrown, and once the mouse button is released, the slingshot will apply a force to the object to put it in motion.
To start, go to this Github repository and download the project’s Zip file, so you will have a starting point to follow the tutorial.
Extract the contents of the Zip file. Then, open Godot and press Scan to import the project. Browse to the path where you extracted the Zip file, select the Slingshot-Starting-Point folder, and press Select this Folder to add the project to the Godot Project Manager.
Now, you can open the project.
The starting point
Once the project is open, take a look at the node tree:
The Root node has four children:
- Player: it’s our player, the unfortunate character who will be thrown from the slingshot. We will talk more about this node soon.
- Rubber Band: it’s a Line2D node, which we will use to draw the rubber band of the slingshot. Attached to the node, you will find a script that we will modify during the tutorial.
- GUI: it’s the graphical interface of the game. It’s just a reset button to restart the game (the reset code is already in Player‘s script)
- Environment: it contains the graphic elements of the game, little more than a few colored rectangles. You can safely ignore the contents of this node.
Now let’s focus on the Player node.
Player is a RigidBody2D node. This type of node implements simulated 2D physics. You do not control it directly. Instead, you apply forces to it (gravity, impulses, etc.), and Godot’s physics engine calculates the resulting movement, including collisions with other bodies and collision responses, such as bouncing, rotating, etc.
A RigidBody2D node can operate in various modes; in this tutorial, we will use two:
- Static: the body behaves like a StaticBody2D node. In this mode, we will be able to drag the player with the mouse without being subject to physics.
- Rigid: the body behaves as a physical object. It collides with other bodies and responds to forces applied to it. It’s the mode that we will use after the launch from the slingshot.
The Player node properties are all set to the default values except for Pickable, which is enabled as we want the node to be draggable with the mouse.
Player has some child nodes:
- a Sprite node to display the player on the screen
- a CollisionShape2D node with capsule shape (required for collisions)
- a Camera2D node to follow its movement once launched. It’s initially set to follow the character only if it’s dragged off the screen (all drag margins are activated and set to 1). These properties will be changed after launch to keep the player centered on the screen.
Attached to Player, you will find a script that we will modify during the tutorial.
Dragging the character
First, we want to be able to drag the character as if we were pulling on the rubber band of the slingshot.
Open the Player.gd script. Let’s start by declaring a variable that we will use to store whether we are dragging the character or not:
var drag_enabled = false
We will change the value of this variable when we detect an input event related to the left mouse button. To do so, we need to use the _input_event() method:
func _input_event(viewport, event, shape_idx): if mode == MODE_STATIC: if event is InputEventMouseButton: if event.button_index == BUTTON_LEFT: drag_enabled = event.pressed
As you can see, we handle the input only when Player is operating in Static mode. Later we will see that, when the slingshot is released, Player will switch to Rigid mode and will only move based on the forces that are impressed on him and no longer in response to the player’s input.
Now we just have to update, when drag_enabled is true, the position of Player, setting it to the location of the mouse cursor:
func _physics_process(delta): if drag_enabled: position = get_global_mouse_position()
Now, you can move the character around the screen by dragging it with the mouse:
However, the rubber band still does not follow the character’s movement.
When we move the character, we want the rubber band to update to follow him. First, we need to insert a variable in the Slingshot.gd script that contains a reference to the Player node. And while we’re at it, let’s add a reference to the camera too, we’ll need it later.
onready var player = $"../Player" onready var camera = $"../Player/Camera2D"
Since we want the rubber band to follow the character only when it hasn’t been launched yet, we need a variable to know if the player is still on it. So, let’s add this variable to the script:
var on_slingshot = true
Obviously, the on_slingshot variable is initially set to true because the character starts on the slingshot.
In the _process() function, we will update the rubber band line when on_slingshot is true:
func _process(delta): if on_slingshot: var player_relative_position = player.position - position points = player_relative_position
The point of the line with index 0 is fixed in the starting position of Player; to update the line we only need to update point 1, setting it to the relative position of the player.
If you try to drag the player now, you will see that the line now follows it.
Releasing the rubber band
The next step is to handle the release of the rubber band.
When the player releases it, the rubber band must begin to apply a force to the character. So, we need Player‘s script to notify the rubber band when the left mouse button is released. To do this, we will use a custom signal, which we must add to the Player.gd script:
Now, we need to modify the _input_event() function to emit the signal when the mouse button is released:
func _input_event(viewport, event, shape_idx): if mode == MODE_STATIC: if event is InputEventMouseButton: if event.button_index == BUTTON_LEFT: drag_enabled = event.pressed if(not drag_enabled): emit_signal("released")
Connect this signal to Rubber Band. The _on_Player_released() function will automatically be created inside Slingshot.gd. Change the function like this:
func _on_Player_released(): player.mode = RigidBody2D.MODE_RIGID impulse_direction = (position - player.position).normalized()
When the rubber band is released, Player is set in Rigid mode so that physics is active on it.
The impulse_direction variable will store the direction along which we will apply the force to the character. Let’s declare it at the beginning of the script:
var impulse_direction = Vector2.ZERO
Applying impulse to the character
To apply the slingshot force to the player, modify the _process() function as follows:
func _process(delta): if on_slingshot: var player_relative_position = player.position - position points = player_relative_position if(impulse_direction.length() > 0): if player_relative_position.dot(impulse_direction) <= 0: player.apply_central_impulse(impulse_direction * 30 * player_relative_position.length() * delta) else: on_slingshot = false points = Vector2.ZERO camera.drag_margin_bottom = 0.2 camera.drag_margin_top = 0.2 camera.drag_margin_left = 0.2 camera.drag_margin_right = 0.2
How does the new code work?
First, the function checks that impulse_direction length is greater than zero, to figure out if the band has already been released or not.
If so, two cases can occur:
- the character is still on the rubber band, so we have to apply a force to it
- the character has left the rubber band, so from that moment, it is subjected only to the effects of gravity.
To check which of these conditions is true, the function calculates the dot product between the player’s relative position and impulse_direction. If this product is negative or equal to zero, the character is still on the rubber band. Otherwise, it has left it.
If the character is on the rubber band, we apply an impulse along the direction of impulse_direction, proportional to how long the band is (the elastic force can be considered, in first approximation, directly proportional to the deformation). If you want, you can change the constant we used (30) to increase or decrease the force applied to the character.
Given that the impulse by definition is the integral of the force over time (see here for more details), we must also multiply by delta.
In the second case, when the player leaves the rubber band, we set on_slingshot to false so that the code of this function is no longer executed, and the character is left free to move only subject to physics.
After that, we reset the rubber band to its initial position. Finally, the camera’s drag margins are set to 0.2, so that it remains centered on the character.
If you want to get a more natural movement of the character while it’s in the air, you can apply a small torque to rotate it, adding this line immediately after the one that applies the impulse:
player.apply_torque_impulse(25 * delta)
Run the game, drag the character, and release it: it will finally be thrown away by the slingshot!
You can try it by clicking the button below:
And that’s it! With a little extra effort, you could try creating your Angry Birds clone in Godot!