Climbing ladders

Godot Version

Godot 4.5.1`

Question

I am new to Godot but used to create simple games in C++ with an Allegro 5 library.
`I have a problem with climbing ladders. In C++ it’s simple: when I parse a “Tiled” file, a collision map is created as a 1D or 2D int array or as a vector. Then in the “player” code:
if (Input::up && map->get_cell(x >> 6, y >> 6) & LADDER) )
state = CLIMB;
The code is simple and fast. How can I get something similar in Godot if the “player” is a CharacterBody2D? Or maybe I need an Area2D? In the TileSet I created a CollisionLayer0 and marked the necessary tiles. In the scene I created a Node “Ladders” as a TileMapLayer and assigned the group “ladders”. It should be noted that the ladders are above the collision tiles, and do not allow climbing the ladders, so I have to disable “collision_enabled” on the base layer while on the ladders. What could be the solution in this case?

1 Like

First, it’s good to tell us whether you’re making a 2D and 3D game. I have inferred it from context clues, but I had to get to the bottom of your post then re-read everything again to understand your question.

So the code is compact, but I don’t know that I’d call it simple. Here’s my interpretation of what it does:

If the player presses the Up input and then uses a pointer to map to access the cell that is bitwise shifted by 6 from the current x/y coordinates of something (presumably where the player is located) and to bitwise mask that with a LADDER constant. If all that comes out true, then we change the player’s state to CLIMB.

Here’s my suggestion:

Unless you have a TON of ladders and you just have to make it so when you place a tile they are automatically connected, just place two Area2D nodes at the top and bottom. Turn off all collision layers of the Area2D, and set the collision mask to the player’s layer so enemies don’t trigger it. Then add some code.

class_name LadderEnd extends Area2D

@export var ladder_end: LadderEnd
@export var move_action: String

var player: Node2D


func _ready() -> void:
	set_process_input(false)
	body_entered.connect(_on_body_entered)
	body_exited.connect(_on_body_exited)


func _input(event: InputEvent) -> void:
	if event.is_action_pressed(move_action):
		player.position = ladder_end.position


func _on_body_entered(body: Node2D) -> void:
	set_process_input(true)
	player = body


func _on_body_exited(body: Node2D) -> void:
	set_process_input(false)
	player = null

Place one at the top and one at the bottom. Then assign the ladder_end variable to the other LadderEd, and type in the action you’ve created for up or down respectively.

Once you’ve got this working, you can then create something more advanced. Such as creating a Ladder scene that holds the two ladder ends, along with the ladder tile displayed between them on a Sprite2D. Then you can add that scene to your TileMapLayer and place it as a tile wherever you want.

Thank you, I really appreciate it. I’ll try the code tomorrow.

You can add custom data to tilesets and retrieve it by coordinate, with some sample code using a custom data layer.

Your example may look like this in GDScript

func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("Climb"):
		var cell_position := tilemap_layer.local_to_map(position)
		var cell := tilemap_layer.get_cell_tile_data(cell_position)
		if data and data.get_custom_data("Ladder"):
			state = CLIMB

Here’s where you can set up custom data layers in your tileset (Left) and paint the data on/off (Right)

1 Like

Thank you!
My code is similar and “player” can climb up the ladder, but can’t climb down because it can’t detect ladder going down when standing on the ground.
And another problem is when climbing, the movement happens automatically by “move_and_slide()”.
Another option is that I could use “move_and_slide()” only when the movement is horizontal.
But maybe it’s better to use Area2D instead of CharacterBody2D. I’ll try that.

This is how it looks like now:

func is_ladder() -> bool:
	var layer = get_node("/root/LayerTest/Ladders")
	var cell_pos = layer.local_to_map(position)
	var data = layer.get_cell_tile_data(cell_pos)
	if data and data.get_custom_data("ladder"):#.type == LADDER:
		return true
	return false
func handle_input() -> void:
    var up = Input.is_action_pressed("ui_up")
	var down = Input.is_action_pressed("ui_down")
    if up:
		if is_ladder():
			baselayer.collision_enabled = false
			change_state(CLIMB)
			velocity.y -= climb_speed
			if !on_ladder:
				on_ladder = true
		else:#is on ground
			change_state(STAY)
			baselayer.collision_enabled = true
            on_ladder = false
	elif down:
		if is_ladder():
			baselayer.collision_enabled = false
			change_state(CLIMB)
			velocity.y += climb_speed
			on_ladder = true
		else:#is on ground
			change_state(STAY)
			baselayer.collision_enabled = true
            on_ladder = false
func _physics_process(delta: float) -> void:
	handle_input()
	
	# Add the gravity.
	if not is_on_floor() and not on_ladder:
		velocity += get_gravity() * delta
	
	move_and_slide()
	update_states()#updates animation

I rewrote the code. Now it’s like this:

func handle_input() -> void:
	var up = Input.is_action_just_pressed("ui_up")
	var down = Input.is_action_just_pressed("ui_down")
		
	if up:
		if state == CLIMB:	# already climbing
			if is_ladder():
				velocity.y -= climb_speed
			else:# now we are on the ground
				change_state(STAY)
				velocity.y = 0
		elif is_ladder():# first encounter
			change_state(CLIMB)
			velocity.y -= climb_speed
	if down:
		if state == CLIMB:# already climbing
			if is_ladder():
				velocity.y += climb_speed
			else:# now we are on the ground
				change_state(STAY)
				velocity.y = 0
		elif is_ladder():# first encounter
			change_state(CLIMB)
			velocity.y += climb_speed
			

func _physics_process(delta: float) -> void:
	handle_input()
	
	# Add the gravity.
	if not is_on_floor() and state != CLIMB:
		velocity += get_gravity() * delta
	
	if state != CLIMB:
		move_and_slide()
	update_states()

But the problem remains, player can’t go down the ladders. In C++ I put the player’s “y” at the feet of the sprite where it remains all the time, and the code checks if there is a ladder under player’s feet

if (Input::down && map->get_cell((y + 1) >> 6, x >> 6) & LADDER) {
     state = CLIMB;
    y += slimb_speed;
}

I think checking below the player’s feet would prevent you from touching the bottom of the ladder. Maybe you should be checking both at the player’s position and below them.

func is_ladder_point(point: Vector2) -> bool:
	var layer = get_node("/root/LayerTest/Ladders")
	var cell_pos = layer.local_to_map(point)
	var data = layer.get_cell_tile_data(cell_pos)
	if data and data.get_custom_data("ladder"):#.type == LADDER:
		return true
	return false

func is_ladder_self() -> bool:
	return is_ladder_point(self.position) or is_ladder_point(self.position + Vector2(0, 64))

Thank you!
Now the code works! I modified it a little, and now it looks like this:

func is_ladder(dir) -> bool:
	var layer = get_node("/root/LayerTest/Ladders")
	var cell_pos = layer.local_to_map(position + Vector2(0, dir))
	var data = layer.get_cell_tile_data(cell_pos)
	if data and data.get_custom_data("ladder"):#.type == LADDER:
		return true
	return false

func handle_input() -> void:
	var up = Input.is_action_pressed("ui_up")
	var down = Input.is_action_pressed("ui_down")
	velocity.x = 0
		
	if up:
		if state == CLIMB:	# already climbing
			if is_ladder(40):
				position.y -= climb_speed
				#velocity.y = -1
			else:# now we are on the ground
				change_state(STAY)
				#velocity.y = -1
		elif is_ladder(40):# first encounter
			change_state(CLIMB)
			#velocity.y = -1
	elif down:
		if state == CLIMB:# already climbing
			if is_ladder(64):
				position.y += climb_speed
				#velocity.y += climb_speed
			else:# now we are on the ground
				change_state(STAY)
				#velocity.y = 0
		elif is_ladder(64):# first encounter
			change_state(CLIMB)

Of course, the code can be optimized a little, but for now it will be good