Making a block sink when stepping on it.

Hi,

I’ve been struggling with a simple game mechanic for a few days now, but I don’t have any good ideas on how to implement it.

Concern 1:

In my 2D platformer, I have a block (StaticBody with Sprite and Collision2D) that I want to use as a switch. The block is available as a separate scene (for reuse in the level scene).

When stepped on, the block should sink down a defined number of pixels (softly). A kind of pressure plate in the ground. I don’t know how to reconcile this with Area2D and how to make the block sink down gently in the end. I’m stuck for ideas at the moment.

Concern 2:

If three of these blocks are pressed, the level is complete. I could use a Boolean here. But where do I store it all the time? I’m a bit confused right now.

How could I solve this?

Thanks for any tips.

The simplest way to create a smooth transition for anything is by using Tweens, you can make it so that when the player node enters the Area2D it triggers a function which would change the state of the block, and use the Tween to modify it’s position variable.
Another way is to use an AnimationPlayer, and creating an animation which will change the node’s position by using keyframes.

If the block only needs to go down once, and not back up, then that’s all about as much as you need to do, play the animation/tween a single time.

As for keeping track of pressed blocks, there are also many ways to do this, I think the easiest would be to have a separate node to keep track of the pressed block count. The node could have a pressed_block_countvariable of some sort (i.e. an int) , and each time a block is pressed you could emit a Signal from the block node connected to the node responsible for checking so that it knows that a block has been pressed, and increment the value, then check if it equals the target amount.

This is just a brief overview of one way to do things, but hopefully it is helpful.

2 Likes

For Concern 1:

What marcythejinx said about moving the block is a really good way to do it

For Concern 2:

A method I like to use is having a level manager that handles all the values that would be needed for that specific level (don’t try to follow my exact code. Make it around your own codebase, I am just trying to give a general idea)

So the general idea would be like this:

This script will be attached to a Node2D in the level called level_1_manager (or anything you want)

#a bool is initialized as false
var button_1: bool
var button_2: bool
var button_3: bool

#I would attach the required buttons through the inspector
@export var button_1_ref: Node2D
@export var button_2_ref: Node2D
@export var button_3_ref: Node2D

func _ready() -> void:
	#some error checking
	if button_1_ref == null || button_2_ref == null || button_3_ref == null: 
		printerr("button not assigned to level in inspector")
	else:
		button_1_ref.connect("pressed", _button_pressed)
		button_2_ref.connect("pressed", _button_pressed)
		button_3_ref.connect("pressed", _button_pressed)

#read this AFTER reading script below
func _button_pressed(button_id: int) -> void 
	match button_id:
		0: assert(button_id < 1, "need to add button id")
			#this is something personal I do in case I miss the first warning
		1: 
			button_1 = true
			open_door_tutorial()
		2:
			button_2 = true
			open_door_tutorial()
		3:
			button_3 = true
			open_door_tutorial()

open_door_tutorial() -> void:
	if button_1 == true && button_2 == true && button_3 == true:
		print("signal to open a door (maybe)")

This script will be attached to your button:

#in the inspector you would be able to set which number button this is
@export_range(0,10,1) var button_id: int

signal pressed(id:int)

#ideally you area2D for the button will only be masked 
#(example shown below this script block)
_on_area2d_body_entered(body) -> void:
	if button_id != 0:
		pressed.emit(button_id) #this connects back to the first script

	#some more error checking is always useful for if you miss something
	else: printerr("no button id set on " + self.name) 

This is what a collision layer on my specific game looks like like, as you can see the button is on its own layer (layer 6) and only looking at the player collision layer (layer 2):

The code I provided isn’t very flexible but it is a good place to start

Don’t be afraid to refactor if you need to make it more flexible

2 Likes

Thanks to everyone for the tips so far.
I have now found a Tween tutorial and have set up my block (saved as a separate scene) as follows:

The top node is a Sprite2D (my block image), followed by the other nodes, as shown above. I then added the following script to the root node:

extends Sprite2D

#----------------------------------------------------------
# GLOBALS
#----------------------------------------------------------
var has_fallen: bool = false
var distance = 70 # move block by x pixels
var time = 2      # moving takes x seconds


#----------------------------------------------------------
# As soon area 2d zone is entered...
#----------------------------------------------------------
func _on_area_switch_step_body_entered(body: Node2D) -> void:
	# Check the body type and see if the block has moved.
	if body is CharacterBody2D and !has_fallen:
		
		# Create that weird Tweeny thing
		var pos_tween: Tween = create_tween().set_trans((Tween.TRANS_SINE))
		
		# Move block with that Tweeny variable
		pos_tween.tween_property(self, "global_position", global_position+Vector2(0,distance), time)
		
		# Lock this function to prevent further movement
		has_fallen = true

I will have to see what I can do about my second issue.

1 Like

So, a tween or an AnimationPlayer are both valid options. I would recommend an AnimatableBody2D. Specifically because it deals with the physics issue of the player standing on the block and it moving. I did a little test project (you can get the whole thing here), and this is what I came up with to solve both your problems. For my demo I used the Kenney Platformer Pack in which the blocks are 70x70 pixels.

PlatformerSwitch2D

  1. I created an AnimatableBody2D as the root node. It is on Collision Layer 1 only, no mask layers.
  2. Added CollisionShape2D with a 70x70px rectangle.
  3. Added a Sprite2D to hold the block.
  4. Added an Area2D which is on NO collision layers. It is on Collision Mask 2 only.
  5. Added a CollisionShape2D to the Area2D. Moved it up 35 pixels so it’s right at the top of the block. Made it 70px wide, and only 2px tall. Just enough for the player to land on. (Colored it red in the screenshot below.)

Then I added a script to the node. It’s a bit fancier than it needs to be, but I wanted it to be immediately useable by you. You can set the block_size to whatever yours is, the move_direction (y = 1.0 is straight down one whole block), and the overall speed measured in blocks per second.

class_name PlatformerSwitch2D extends AnimatableBody2D

signal changed

## Speed in blocks the platform travels per second. Default is one block pers second.
@export var speed: float = 1.0:
	set(value):
		speed = value
		pixel_speed = value * block_size
@export var block_size: float = 64.0
## The direction the switch moves when stepped on, measured in blocks.
## Negative is left on x-axis and up on y-axis.
## Positive is right on x-axis, and down on y-axis.
## Anything other than 1.0 or -1.0 will alter the platform distance traveled.
@export var move_direction: Vector2:
	set(value):
		move_direction = value
		pixel_move_direction = value * block_size

var on: bool = false

@onready var pixel_speed: float = speed * block_size
@onready var pixel_move_direction: Vector2 = move_direction * block_size
@onready var start_position = global_position
@onready var target_position = start_position + pixel_move_direction
@onready var area_2d: Area2D = $Area2D


func _ready() -> void:
	set_physics_process(false)
	area_2d.body_entered.connect(_on_body_entered)


func _physics_process(delta: float) -> void:
	global_position = global_position.move_toward(target_position, pixel_speed * delta)
	
	if global_position == target_position:
		set_process(true)
		on = true
		changed.emit()


func _on_body_entered(body: Node2D) -> void:
	set_physics_process(true)

Then I configured the platform like so:

Door

For the door, I wanted to make sure that it could operate on any number of switches. (Note you’ll get an error if you assign no switches to it and run the game.)

It’s just a StaticBody2D with a CollisionShape2D and two Sprite2D nodes for the door itself.

image

Then I attached a script to it that stores an array of all the switches. When the game starts, it connects to every switch’s changed signal. (More on this later.) Then, whenever that signal fires, it queries all the doors. If they are all open, it open, if not nothing changes.

When the open_door() function runs, it changes the textures and disables the CollisionShape2D so the Player can pass through. Your door would probably do more. (More below.)

extends StaticBody2D

const DOOR_OPEN_TOP = preload("uid://dvqiytukctnky")
const DOOR_OPEN_MID = preload("uid://i6y47hb81xts")

@export var switches: Array[PlatformerSwitch2D]

@onready var top_sprite_2d: Sprite2D = $TopSprite2D
@onready var bottom_sprite_2d: Sprite2D = $BottomSprite2D
@onready var collision_shape_2d: CollisionShape2D = $CollisionShape2D


func _ready() -> void:
	for switch in switches:
		switch.changed.connect(_switch_changed)


func open_door() -> void:
	top_sprite_2d.texture = DOOR_OPEN_TOP
	bottom_sprite_2d.texture = DOOR_OPEN_MID
	collision_shape_2d.disabled = true


func _switch_changed() -> void:
	var open = false
	for switch in switches:
		if switch.on:
			open = true
		else:
			open = false
		if not open:
			return
	open_door()

Player

I used the default script template for CharacterBody2D Added a Sprite2D for the texture, CollisionShape2D, and a Camera2D. The player is only on Collision Layer 2 so it will trigger the switches, and has Collision Mask 1 on so it will interact with the environment and no fall through the block/pass through the door.

Final Product

Game Start

Switch One

Switch Two

Next Steps

  1. You can create an Area2D on Collision Mask 2 to interact with the player and transport them to a new level. If you make its CollisionShape2D slightly smaller than the door’s CollisionShape2D, the player will not be able to trigger it until the door is open.
  2. The switches stay down when pressed. You could alter them so that they only go all the way down if the player stands on them, otherwise they bounce back up. Then you could add another block or item the player can push onto the switch to keep them down. Put it on Collision Layer 3 and add Collision Mask 3 to the switch base script.
  3. Make switches that move in other directions like in towards walls.
  4. You could make the PlatofmerSwitch2D node into a single node by combining all subnodes into nodes created by the script. This is a much more advanced exercise, but then you can add them directly from the Add Node dialog. For an example of how to do this, see my Curved Terrain 2D plugin.
3 Likes

Thank you very much for your efforts and the great example. I am still learning a lot and need to study the mechanics in the example in detail. I have a rough understanding of what is happening, but I am still a little overwhelmed.

Thank you!

1 Like

I get that. For me it was a fun little problem to solve on a lazy Saturday. It was a logic problem for me. Like the way some people like to do the Sunday crossword puzzle in a newspaper.

Some of what I did was more complicated than it needed to be. I re-purposed code for moving platforms. That’s also why I gave you a little working project. So you could play around with it working.

I also wanted to show you some features of Godot. Like the ability to turn _physics_process() off and on. Or how delta can be used to do your frame animation for you, and then you can just pick the interval.

There are lots of ways to do things. Some are easier than others depending on what you know. Knowing multiple ways gives you more options when something isn’t working the way you want it to.

Good luck!

1 Like

Thank you very much for taking the time to provide me with a demo. I think I will be able to learn a lot from it!

EDIT:

I was now able to adapt the sinking mechanism to my project and it works great! Thank you so much!

1 Like

Awesome. If my solution worked for you, please mark it as the solution for people coming after you who find this through a google search.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.