Simple CharacterBody and RigidBody Interaction

When it comes to Godot, there seems to be two prevailing ‘solutions’ that are given in order to address the common issue of wanting to have a CharacterBody interact with a RigidBody.

These are the ‘infinite inertia’ method, whereby the CharacterBody simply ignores collisions with RigidBodies thus pushing them around with impunity. And the impulse approach, where you check for collisions and apply force impulses as needed to simulate the physics. A decent article on these approaches can be found here: Character to Rigid Body Interaction :: Godot 4 Recipes

Every single post I’ve seen online that relates to CharacterBody and RigidBody interactions recommends one of these two approaches. However, there are MAJOR flaws with both. If you go infinite inertia you cannot stand on top of the RigidBodies and you can push them into walls easily. If you go for impulses, the behavior seems inconsistent. The RigidBody either bounces up and down on the top of the CharacterBody or simply freezes the CharacterBody in place, preventing movement.

Now as to why these things happen that way I have only vague guesses. But it’s not really important to this post, so I will move on to the main question of what can you do about it?

The answer I have come up with, is to combine both methods. I use impulse interaction by default, except for when the RigidBody lands on top of the CharacterBody, in that instance I switch the RigidBody to a different collision layer and then swap it back after it is no longer on top.

The way I achieve this is with a predefined Area2D above the CharacterBody that detects via signals any objects entering and exiting. I have set that Area2D to mask layer’s 1 and 2. And the root node of the CharacterBody masks only layer 1. All RigidBodies exist on layer 1 by default, and only move to layer 2 when they land on top as stated earlier.

I can only hope that this helps someone like me, who ends up searching forums and reddit posts for days trying to solve this. Here is the code for my implementation:

extends CharacterBody2D

@onready var head_area: Area2D = %HeadArea

const SPEED = 300.0
const JUMP_VELOCITY = -400.0


func _physics_process(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta

	# Handle jump.
	if Input.is_action_just_pressed("ui_accept") and is_on_floor():
		velocity.y = JUMP_VELOCITY

	var direction := Input.get_axis("ui_left", "ui_right")
	if direction:
		velocity.x = direction * SPEED
		if direction > 0 :
			backpack.position = Vector2(-40, 0)
		else:
			backpack.position = Vector2(40, 0)
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)
	
	move_and_slide()
	
	for i in get_slide_collision_count():
		var collision = get_slide_collision(i)
		var collider = collision.get_collider()
		
		if collider is RigidBody2D:
			collider.apply_central_impulse(-collision.get_normal() * 30)


func _on_head_area_body_exited(body: Node2D) -> void:
	if body.is_in_group("on_top"):
		body.set_collision_layer_value(1, true)
		body.set_collision_layer_value(2, false)
		body.remove_from_group("on_top")


func _on_head_area_body_entered(body: Node2D) -> void:
	if body is RigidBody2D and not body.is_in_group("on_top"):
		body.set_collision_layer_value(2, true)
		body.set_collision_layer_value(1, false)
		body.add_to_group("on_top")