12.coding-a-rock-the-player-can-push

Coding a rock the player can push

In this lesson, we will create a rock the player can push and roll over platforms.

We’ll later use that to code doors that open when you push a rock over a remote pressure plate.

It’s a good opportunity to explore how to make the character interact physically with an object by pushing it and how to use the last type of physics body: rigid bodies.

Rigid bodies automatically move and interact with somewhat realistic physics. They roll, push one another, slide, and more based on their physics properties.

In Godot, rigid bodies come in the form of the RigidBody2D node.

Unlike the KinematicBody2D, we cannot move a RigidBody2D node by changing its position or calling the move_and_slide() function.

Instead, we tweak physical properties such as mass and friction. We can then apply impulses or forces to it.

Note: With RigidBody2D, we don’t need to manually apply gravity because it is built into the node and handled by the physics simulation.

Let’s start by creating the rock scene.

Creating the rock scene

Create a new scene with a RigidBody2D named Rock as the root.

Save the scene as Rock.tscn and add two child nodes:

Assign the image asteroid.png to the Sprite’s Texture property.

Add a CircleShape2D to the CollisionShape2D and make the shape match the inner mass of the rock.

You can also click the CircleShape2D to expand its properties and adjust its Radius directly. We set it to 80 pixels.

Be sure to center the sprite and collision shape on the scene’s origin point, as shown below.

The physics engine treats that point as the rigid body’s center of mass. If you offset the collision shape, the rock will roll back and forth like a rocking chair and be much harder to move.

We can now reopen the Level scene and drag Rock.tscn in it. Place it where you like. We placed it on a platform next to the character.

You can run the scene and run into the rock to see your character push it around like it’s a plastic ball.

By default, the physics engine treats kinematic bodies as having infinite inertia, so they can just push rigid bodies out of their way.

We have to update the robot’s call to move_and_slide() to address this issue.

We’re now ready to code the interaction between the player character and the rock.

Planning the code

We want to push the rock when the robot touches it and moves toward it. Here, we could write the code that moves the rock either on the rock or on the character. Both options can work.

However, you will generally want to change the character’s animation when pushing an object, which you want to do from the character’s script. That’s why we’ll write the push code on the character.

After calling move_and_slide() on the character, we can detect if the character touches anything. Thanks to that, we can check if the character has collided with the rock. If so, then we will apply an impulse to the rock.

Over the next sections, we will:

  1. Prevent the Robot from automatically pushing the rock out of its way like it’s nothing.
  2. Detect if we collided with something during the move_and_slide() call.
  3. Check if we collided with the rock.
  4. If so, apply an impulse to the rock to push it.

To get started, reopen the Robot.gd script.

Removing the robot’s infinite inertia

So far, we only saw the first two arguments of the move_and_slide() function, but it has four more, all of which are optional.

The third and fifth arguments control the body’s motion on slopes, and the fourth one helps to make the character’s movement feel smooth.

You can read more on each by pressing the F1 key and searching for move_and_slide in the built-in help.

We are only interested in the last argument: infinite_inertia. By default, if we omit the argument, it will be true and the kinematic body will automatically push rigid bodies out of its way.

We need to explicitly set to false. To do this in GDScript, we must provide all the arguments that come before it.

Update the line that calls the move_and_slide function like so.

    # The third, fourth, and fifth arguments control movement on slopes and
    # curved terrain.
    #
    # The last argument prevents the body from pushing RigidBody2D nodes
    # automatically.
    velocity = move_and_slide(velocity, Vector2.UP, false, 4, PI / 4, false)

Detecting move and slide collisions

In a KinematicBody2D, to detect if collisions occurred after calling move_and_slide(), we use get_slide_count().

It is not the most explicit function name! It returns the number of times the body collided in a given frame.

The move_and_slide() function can move the kinematic body multiple times per frame. In games, physics bodies move in straight lines, but when moving along a curved terrain, this can cause the body to get a bit stuck multiple times per second. If you’ve ever played a game where the characters seemed to bump into slopes, this might be why.

There’s a trick game developers can use to make the motion smooth and avoid this problem: moving the body multiple times in a row, and adjusting its motion vector each time. Godot’s KinematicBody2D does it for you so you don’t have to worry about coding it yourself.

However, it’s important to know about these multiple collisions to process them and know what the robot collided with.

At the end of the _physics_process() function, write:

func _physics_process(delta: float) -> void:
    # ...
    for index in get_slide_count():
        var collision := get_slide_collision(index)

As long as the character is on the ground, get_slide_count() will return at least 1. If we bumped into the wall while walking, it would return 2.

The robot would first collide with the floor, then the wall.

We can retrieve information about each collision with get_slide_collision(n), where n is the collision’s index, starting at 0.

Checking if we bumped into a rock

The get_slide_collision() function returns an object of type KinematicCollision2D that contains information about the collision.

If we only have one collision (the ground), then get_slide_collision(0) will return a KinematicCollision2D representing the collision with the ground.

Note: The KinematicCollision2D is not the ground. It has information about the collision with the ground. It’s an object that says, “we collided with that physics body, at these coordinates, in this direction.” From that object, we can retrieve the ground.

We will use two variables from it:

Add the following condition to check if the collider is a RigidBody2D.

func _physics_process(delta: float) -> void:
    # ...
    for index in get_slide_count():
        # ...
        if collision.collider is RigidBody2D:
            pass

Note: We only have one type of RigidBody2D in this project: the rock instances. We don’t have any other node that is a RigidBody2D. If we did, we would need to verify if the RigidBody2D we’ve collided with is a rock. For example, we could use the node groups feature we saw in the obstacle course series.

With those lines, we’ve detected if we collided with a rock. All that’s left is pushing!

Pushing the rock

To push the rock, we call the RigidBody2D.apply_central_impulse() function, which takes a vector as its argument.

We will use the KinematicCollision2D.normal property, which gives us the collision direction.

We want to push in the opposite direction to the collision, so we will invert the normal. We use the minus sign to reverse the direction of a vector.

-collision.normal

Note: A normal vector is a vector perpendicular to the surface we collided with. It represents the direction of the collision. In this case, because we check for a collision with the rock, it will be a line perpendicular to the point of collision, pointing away from the rock.

We then multiply this direction by an arbitrary value representing the impulse’s strength.

func _physics_process(delta: float) -> void:
    # ...
    for index in get_slide_count():
        # ...
        if collision.collider is RigidBody2D:
            # Replace "pass" with the following two lines.
            var impulse := -collision.normal * 5000 * delta
            collision.collider.apply_central_impulse(impulse)

Our final code looks like this:

func _physics_process(delta: float) -> void:
    # ...
    for index in get_slide_count():
        var collision := get_slide_collision(index)
        # Check if we collided with a RigidBody2D.
        if collision.collider is RigidBody2D:
            # Push the rock from the center.
            var impulse := -collision.normal * 5000 * delta
            collision.collider.apply_central_impulse(impulse)

If you try the game now, you’ll see you can push the rock!

But it feels very floaty. Let’s make it a bit better.

Tweaking the properties of rigid bodies

You can control how a RigidBody2D behaves by tweaking its properties. We will only use two here:

Open the Rock.tscn scene once again and select the Rock node.

Let’s first give the rock some good mass. Set the Mass property to 15.

Notice how the Weight changes too. These two properties are linked so changing one causes the other to update.

Note: The mass is the amount of matter in an object, while weight measures how the force of gravity acts upon that object. The default gravity is 9.8, so if the mass is 15, the weight will be 15 * 9.8 = 147.

If you try the scene now, you’ll notice the rock is harder to move. It doesn’t bounce around as soon as you touch it. However, it still falls slowly.

Note: Intuitively, we think that heavy objects fall faster than light objects, but that’s not the case. In a vacuum, all objects fall with the same acceleration: the value of gravity. What affects the falling speed of bodies is air resistance. That’s why an empty plastic ball falls as fast as a ball made of heavy metal.

The simulation does not handle air resistance, so we have to change how the node reacts to gravity.

Let’s change the Gravity Scale, so gravity gets applied more on each frame, and our object falls faster. Set it to 20.

Note: We can also change the main gravity of the whole project in Project > Project Settings > Physics > 2D, but since we probably want different values for different objects, it’s often more convenient to change it in the Gravity Scale.

Play the scene now. Congrats! You can now push the rock.

Your questions

How do I know which values I should use for rigid bodies?

There are no right or wrong values when working with rigid bodies. You must try different ones in your game and choose them based on what feels best when playing.

In this lesson, we used a value of 5000 for the impulse applied to the rock, a Mass of 15, and a Gravity Scale of 20.

We found them through trial and error. We kept changing the values until we got a result that felt good enough.

Should I use a rigid body or a kinematic body?

You can technically use either a rigid or a kinematic body for any object that moves in your game, character or otherwise.

Rigid bodies can seem simpler to start with because of the built-in gravity, but in practice, they are harder to control precisely than kinematic bodies.

Kinematic bodies give you precise control over how objects move but require a little more initial code. Now, in most 2D games, that precision matters a lot.

That’s why we recommend favoring KinematicBody2D for the player character and enemies and RigidBody2D for things that should roll and bounce around somewhat realistically, as the rock in this lesson.

Practice: Marbles

Open the practice Marbles.

In the next lesson, we will add doors that open when you place a rock on the corresponding pressure plate.

The code

Here’s the complete Robot.gd script.

extends KinematicBody2D

export var speed := 600.0
export var gravity := 4500.0

var jump_strengths := [1400.0, 1000.0]
var extra_jumps := jump_strengths.size()
var jump_number := 0
var velocity := Vector2.ZERO


func _physics_process(delta: float) -> void:
    var horizontal_direction := Input.get_axis("move_left", "move_right")
    velocity.x = horizontal_direction * speed
    velocity.y += gravity * delta

    var is_jumping := Input.is_action_just_pressed("jump")
    var is_jump_cancelled := Input.is_action_just_released("jump") and velocity.y < 0.0
    if is_jumping and jump_number < extra_jumps:
        velocity.y = -jump_strengths[jump_number]
        jump_number += 1
    elif is_jump_cancelled:
        velocity.y = 0.0
    elif is_on_floor():
        jump_number = 0
    
    # The third, fourth, and fifth arguments control movement on slopes and
    # curved terrain.
    #
    # The last argument prevents the body from pushing RigidBody2D nodes
    # automatically.
    velocity = move_and_slide(velocity, Vector2.UP, false, 4, PI / 4, false)

    # Loop over all collisions that occurred in this frame
    for index in get_slide_count():
        var collision := get_slide_collision(index)
        # Check if we collided with a RigidBody2D.
        if collision.collider is RigidBody2D:
            # Push the rock from the center.
            var impulse := -collision.normal * 5000 * delta
            collision.collider.apply_central_impulse(impulse)