Grabbing and Carrying an Object in 2D

Godot Version

Godot 4.3

Question

I’m building a 2D platformer, where a character can grab, carry, and release an object (similar to Chip 'n Dale: Rescue Rangers on the Nintendo Entertainment System). I’m encountering strange behavior though.

In this GIF, you can see the player character has a short red line projecting outwards. The line turns green when an object that can be grabbed is within reach. The object in this case has its CollisionShape2D visible: a 16 x 16 RectangleShape2D.

The player character is able to grab and carry the object, then release it. But oddly, even though the CollisionShape2D appears to move, I don’t think it actually does. The object stays put when released, and then when the player character walks back to where the object originally was, a collision occurs. Notice how the line turns green again, and the player character can stand on seemingly nothing. The player character can then pick the object back up from that original position, and the object will reposition back to being above him (not pictured).

Grab and Carry Collision Bug

My scene hierarchy is this:

TestScene (Node2D)
↳ Player (CharacterBody2D)
|↳ CollisionShape2D (CollisionShape2D)
|↳ AnimatedSprite2D (AnimatedSprite2D)
|↳ GrabRaycast (Raycast2D)
| ↳ Line2D (Line2D)
↳ GrabbableObject (Rigidbody2D)
 ↳ Sprite2D (Sprite2D)
 ↳ CollisionShape2D (CollisionShape2D)

And here’s the code on the Player:

using Godot;

public partial class CharacterMovement : CharacterBody2D {

	private RigidBody2D grabbedObject;
	[Export] private RayCast2D grabRaycast; // Raycast object attached to the player
	[Export] private int grabbableLayer = 2;
	[Export] private Vector2 grabOffset = new Vector2(0, -16);

    // Truncated to remove irrelevant properties and methods

#region Grab Objects
	// Detect and grab an object — called manually by the player
	private void Grab() {
		// If nothing is within reach, cancel the grab function
		if (!grabRaycast.IsColliding()) {
			GD.Print("Nothing within reach.");
			return;
		}

		Node potentialGrab = grabRaycast.GetCollider() as Node;

		// Traverse up the node tree to find the RigidBody2D parent
		while (potentialGrab != null && !(potentialGrab is RigidBody2D)) {
			potentialGrab = potentialGrab.GetParent();
		}		

		if (potentialGrab is not RigidBody2D rigidBody || rigidBody.GetCollisionLayerValue(grabbableLayer) == false) {
			GD.Print("Not a grabbable object.");
			return; // Not a grabbable RigidBody2D
		}

		grabbedObject = rigidBody;
		GD.Print($"Grabbed {grabbedObject.Name}.");
	}

	// Release the object — called manually by the player
	private void Release() {
		grabbedObject = null;
	}

	// Carry the grabbed object — called during _PhysicsProcess
	private void CarryGrabbedObject() {
		if (grabbedObject is null) return;

		grabbedObject.Position = Position + grabOffset;
	}
#endregion Grab Objects
}

The GD.Print lines confirm that the GrabbableObject is being grabbed. However, in attempting to debug this, I’m encountering even weirder behavior. I stuck this code into _Process so I could confirm the positions of the object and its collider.

		if (grabbedObject is not null) {
			GD.Print($"{grabbedObject.Name} position: {grabbedObject.Position}.");
			GD.Print($"Collison shape position: {collisionShape.Position}.");
		}

The behaviour remained the same, and the collisionShape had its local Position read as 0, 0, which is expected. But then I altered it to check for its GlobalPosition.

		if (grabbedObject is not null) {
			GD.Print($"{grabbedObject.Name} position: {grabbedObject.Position}.");
			GD.Print($"Collison shape position: {collisionShape.GlobalPosition}.");
		}

The number of the collisionShape.GlobalPosition matched the grabbedObject.Position. But that’s not the weird part. Just checking its values somehow changed the behavior of the object. Look:

Global Position Bug

The object is moving differently, and the collision is occurring in different places. Switching from reading the local to global position and back will alter its behavior.

Is this a Godot bug? I’m really confused now. I would appreciate insight on why this is happening, and how to get this behavior working as intended.

A quick jump to the docs relating to RigidBody2D says:

Note: Changing the 2D transform or linear_velocity of a RigidBody2D very often may lead to some unpredictable behaviors. If you need to directly affect the body, prefer _integrate_forces as it allows you to directly access the physics state.

If you’re so dead set on debugging this, you might wanna try changing the RigidBody2D to something else like KinematicBody or Whatever to find out if it’s because of RigidBody2D physics.

I’ve added this line to the Grab method:

		grabbedObject.Freeze = true;

And its opposite the Release method:

		grabbedObject.Freeze = false;

And I’ve set the FreezeMode to Kinematic. It changed the behavior a bit. Now when released, the sprite of the grabbable object will snap to the position of the CollisionShape2D box.

Snap Back On Release

As for the _IntegrateForces method, there’s no definition for that found in CharacterBody2D, so I haven’t been able to make use of that in this script.

First of all, you should not be directly changing the position of a rigid body, it messes with the physics. You should only apply forces and impulses. It is stated clearly in the docs.

But there is also a way to safely change the position of a rigid body without applying forces and impulses, and that is using the integrate_forces function. This function allows you to safely alter the position of a rigid body without messing up the physiccs.

NOTE: Integrate forces method is found in rigid body not character body.

In GdScript, changing the position of a rigid body with integrate forces looks like this:

func _integrate_forces(state):
    state.transform.origin.x += (20.0 * state.get_step() #Step is time step (physics delta)

This moves your rigid body by 20.0 every tick.

Another way is to use a character body which may be easier to deal with and gives you more control, but you will need to figure out things like gravity just like you would for a character controller using a character body 2d.

You can try adding a grabable item to the player and remove phisics while carrying