CharacterBody2D with 2 collision shapes, each with it's own collision layer would be perfect

Hello,

I’m working on a 2d game and I’m trying to make my character be able to hang from cliffs and from ropes or pipes, similar to the game Sunblaze. So I figured, 2 collision shapes, one for the character, and one grab collision above it’s head for the cliffs/pipes. Unfortunately the fact that these collision shapes can’t be assigned different collision_layers/masks, but share the same layer from the Characterbody2D, creates issues. Grabbing on cliffs works fine, then I detect with an Area2d whether there’s a pipe right below the grab collision, works somewhat, but then I also have the ability to jump through platforms, where I also get stuck on the grab collision shape.

Anyone experience with this, or any tips how to tackle the problem? Enabling/disabling collisions? Ton of raycasts?

Thanks in advance for help.

You can have an Area2D as a child of your CharacterBody2D. Underneath the Area2D put your second CollisionShape2D for your grab collision.
Then your CharacterBody2D and Area2D can have different collision layers and masks.

2 Likes

Well, Area2d isn’t a body, so it’s gonna fall through stuff, I can check whether bodies enter, disable gravity then, calculate slope… Characterbody2d does all this for me and only touches other bodies, not enters, so I wanted the 2nd collisionshape under it.

I think for now I’m gonna disable/enable the grab collision depending on whether I detect a pipe, and for ledge I’m gonna use is_on_wall() and then find out with raycast where the edge of the ledge is. Don’t know if that’s gonna work yet…

So I’d still like to know what the best approach would be.

You must’ve misunderstood me, I’m not saying you should switch from CharacterBody2D to Area2D, but you should use both. You can put the Area2D as a child of your CharacterBody2D, so you can have the best of 2 worlds - you keep all your CharacterBody2D goodies and you can detect your grab area with the Area2D.

3 Likes

You must’ve misunderstood me. I tried Area2D already. You have to recreate the functionality of CharacterBody2D for Area2D then, the physics shenanigans. Thx for help, but I don’t think Area2D is it.

You don’t need to recreate anything, you can still use CharacterBody2D physics when you place your Area2D as a child of it.
What I’m talking about is this kind of structure:

This solves the problem you described in the original post:

You can set the CharacterBody2D to have one set of collision layers and masks, while the Area2D can have a different set of collision layers and masks.
CharacterBody2D is responsible for your regular character controller and collisions, while the Area2D would be responsible for detecting cliff grabs etc.

Is this not what you wanted?
If not, then please elaborate, because I’m not sure how to read and understand your message in any other way.

2 Likes

Also, based on your use case @Tomek you don’t want your Area2D to be on any layers. It should only have a mask to detect the layer for grabbing.

2 Likes

And what if that pipe has a slope with changing inclination? Imagine a hanging rope or something (which I already have), that the character is supposed to grab with it’s hands Tomb Raider style (or Sunblaze, but Sunblaze has just pipes). If the ground has a slope, the CharacterBody2D does all the physics for you, but Area2D isn’t a physics body (unless I missed something). You gotta manually find out the inclination, move the character in the right directions, and maybe fall off when there’s too much inclination. That’s why I was thinking about a 2nd CollisionShape2D instead. When you want the character to hold onto cliffs, this is possible with 2nd CollisionShape2Ds, which would be a little cube to the side of the characters head. But onto pipes and ropes it’d only work well with the CollisionShape2Ds themselves having different collision layers.

Right now I have an Area2D that detects when there’s a rope/pipe nearby and then activates a 2nd CollisionShape2D above my characters head. I don’t know if that’s the optimal solution, maybe Area2D which you’re proposing is indeed better. Or I did indeed miss something that would enable the collision to be on another layer without removing it being a physics body. Sorry if I’m making things difficult and I still appreciate the help, unhealthy dose of perfectionism here maybe. I’d like to hear what other solutions there are, what the perfect solution is, even if I might be forced in the future to revert to just an Area2D.

But you still do have the CharacterBody2D to handle all the physics you mentioned, you’re not replacing it with the Area2D, you keep both of them.

See here my very crude implementation of a “ledge grabber” using CharacterBody2D handling all physics and Area2D as a ledge grabber - responsible only for detecting the ledge.
The CharacterBody2D / Player has orange color.
The Area2D / LedgeGrabber has red color.
The Area2D / Ledge has green color.
The StaticBody2D / Floor and Walls have blue color.
You can see I can jump over the first wall with the help of the ledge, but not over the second one without the ledge.

This is the scene tree:

This is the code on the CharacterBody2D node:

extends CharacterBody2D


const MAX_SPEED: float = 500.0
const ACCELERATION: float = 1000.0
const JUMP_SPEED: float = 500.0

@onready var ledge_grabber: Area2D = $"Area2D-LedgeGrabber"
var ledge_grabbed: bool


func _ready() -> void:
	ledge_grabber.area_entered.connect(_on_ledge_grabbed)


func _physics_process(delta: float) -> void:
	handle_gravity(delta)
	handle_jump()
	handle_movement(delta)
	
	move_and_slide()


func handle_gravity(delta: float) -> void:
	if not is_on_floor() and not ledge_grabbed:
		velocity.y += get_gravity().y * delta
	else:
		velocity.y = 0.0


func handle_jump() -> void:
	if Input.is_action_just_pressed("jump") and (is_on_floor() or ledge_grabbed):
		velocity.y -= JUMP_SPEED
		ledge_grabbed = false


func handle_movement(delta: float) -> void:
	if not ledge_grabbed:
		var direction: float = Input.get_axis("move_left", "move_right")
		velocity.x = move_toward(velocity.x, direction * MAX_SPEED, delta * ACCELERATION)


func _on_ledge_grabbed(_area: Area2D) -> void:
	ledge_grabbed = true

As @dragonforge-dev rightfully pointed, the LedgeGrabber area has no collision layers, only collision mask 2.

While the Ledge area the opposite - no collision mask and collision layer 2

Please let me know if this is what you need in your game, or am I still missing the point?

2 Likes

I appreciate the effort you put into the response. But you do miss the point a little bit. Imagine what you have as a ledge now is installed on the ceiling instead and isn’t horizontal, but has an inclination. Or even better, it is a rope hanging from cliff to cliff and sagging a bit.

If it were a hanging bridge, you’d walk over it and the CharacterBody2D would handle all the physics while going down and then up again on said bridge. But when it’s a rope instead (and your character isn’t a circus freak) he’d have to climb across with his hands (maybe hands+feet). Like Tomb Raider. (Here I can also point out that in English there seems to be no word for the German word “hangeln”, which means something like climbing along a rope hand over hand. Don’t you do this exercise in the military?). How would you do that with just Area2D? It looks doable but very cumbersome to me.

So, since I want the CharacterBody2D to handle it, my best solution for now is this:


The ledgegrab_collision is above the Character’s head and is disabled by default. The Area2D_hangable_detect detects when ropes or pipes enter the area around the character and then activates the ledgegrab_collision, then deactivates it again when the ropes are out of reach. Thankfully CollisionShape2D nodes can individually have one-way-collision. So either the ledgegrab_collision or the ropes are one-way, so that the character can pass through the rope object. I’d make another screenshot, but I have a .. not so small bug with the camera.

If I just wanted to grab ledges/cliffs, 2 CollisionShape2Ds would be enough without the Area2D.

So that’s my best solution for now. There isn’t anything better, right?

I figured out the camera bug. And here’s how it looks like:


It’s a dynamic split screen, that one gave me trouble as well. On the left the character is hanging with it’s ledgegrab_collision node on something that could be a pipe. The big square is the Area2D detecting that pipe.

There’s still a problem when both players interact with this pipe like thing, it deactivates too early. I guess I’ll figure it out, but that’s a reason the ledgegrab_collision simply having a different layer/mask would be optimal.

Ok, I see what you mean now.
Indeed the solution you implemented with enabling and disabling the CollisionShape2D are your best bet. You could also dynamically change collision layers and masks of your CharacterBody2D, if you need it to.