Am I overengineering my physics?

Godot Version

4.6

Question

Hello, I’ve been fighting against the 2D physics engine for a while and I’m afraid I’m just over engineering my solution, so I wanted feedback from the community.

I’m developing a 2D platformer with a low-res pixel art. I tried to add vertically moving platforms and I noticed that there is a delay between the movement of the platform and the movement of the object that is on top of it. After a lot of debugging and research, I found out that the reason is that the physics queries don’t return an updated state until the end of the frame.

That means that if a platform moves down by one pixel, the object on top can’t move down by one pixel in that same frame because the physics engine thinks that the platform is still in its previous position. I’ve tried many workarounds and none seemed to work properly, so I ended up creating my own physics system.

This system took me around 3 weeks to build and it overrides completely the default engine, so I don’t have any of the default functions like move_and_side or move_and_collide available. It works for my project but if I wanted to do anything slightly different I would have to rewrite a big part of it.

My question is: am I over-engineering the solution? Is there any way to avoid the behaviour I mentioned at the beginning of the post? Although I’ve learnt a lot by doing this, I can’t believe the solution to such a common need is to work for so much time in basic movement mechanics.

The engine has built in facilities to handle moving platforms (e.g. there’s literally “moving platform” section in character body’s properties). So try to maximally utilize what’s provided. It’ll make your system simpler and better functioning.

Yes, you are. Like waaaaay over-engineering. You didn’t show us your scene tree, but I’m guessing you are not using an AnimateableBody2D.

First, like @normalized said, take a look at your CharacterBody2D and change the platform_on_leave property to Do Nothing. If you are not using a CharacterBody2D, that is part of your problem. (You’ll also notice the snap length of 10 pixels - this is to make running down inclines smooth.)

Then create an AnimateableBody2D, and add a CollisionShape2D and Sprite2D to it like this:

image

  1. Add the script below to it.
  2. Change the BLOCK_SIZE to whatever size your Tilemap tiles are.
  3. Change the Move Direction to whatever direction you want the platform to move, and how far.

    image
    (Keeping in mind that the block size will change to whatever you change it to.)
  4. Change the Speed if you want.
    image
class_name MovingPlatform extends AnimatableBody2D

const BLOCK_SIZE = 128.0

## Speed in blocks the platform travels per second. Default is one block (128 pixels).
@export var speed: float = 1.0:
	set(value):
		speed = value
		pixel_speed = value * BLOCK_SIZE
## The direction the platform travels, measured in 128 pixel 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 before returning.
@export var move_direction: Vector2:
	set(value):
		move_direction = value
		pixel_move_direction = value * BLOCK_SIZE

@onready var pixel_speed: float = speed * BLOCK_SIZE
@onready var pixel_move_direction: Vector2 = move_direction * BLOCK_SIZE

@onready var start_position: Vector2 = global_position
@onready var target_position: Vector2 = start_position + pixel_move_direction


func _physics_process(delta: float) -> void:
	global_position = global_position.move_toward(target_position, pixel_speed * delta)
	
	if global_position == start_position:
		target_position = start_position + pixel_move_direction
	elif global_position == start_position + pixel_move_direction:
		target_position = start_position

Your physics problems will go away.

That is more or less the way I was doing it before. And it causes the gap I mentioned in my post.

Judging by the BLOCK_SIZE constant set to 128, I guess you also have that gap but you just don’t notice it. My tiles are 16x16, which makes that gap big enough to notice.

Ok, well that’s my bad for posting that much without knowing what you tried.

But more or less isn’t exactly.

So if you want help we are going to need to know, in detail, what you are doing. Code (properly formatted), screen shot of the scene tree, and CharacterBody2D configuration.

No, this is how I fixed that problem. But again, I don’t think this is worth discussing further until we have specifics.

Sure, sorry.

Platform

This is the platform. It is an AnimatableBody2D with the CollisionShape2D and the Sprite, just as you said.

The code it was not exactly the same as yours but I just made a new version using your code. It is a component inside the platform, so the global position is modified on the owner instead of the node itself.

class_name ReceiverWeighterComponent2 extends Node

var parent_receiver : Receiver

const BLOCK_SIZE = 16.0

## Speed in blocks the platform travels per second. Default is one block (128 pixels).
@export var speed: float = 1.0:
	set(value):
		speed = value
		pixel_speed = value * BLOCK_SIZE
## The direction the platform travels, measured in 128 pixel 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 before returning.
@export var move_direction: Vector2:
	set(value):
		move_direction = value
		pixel_move_direction = value * BLOCK_SIZE

@onready var pixel_speed: float = speed * BLOCK_SIZE
@onready var pixel_move_direction: Vector2 = move_direction * BLOCK_SIZE

@onready var start_position: Vector2
@onready var target_position: Vector2


func _ready() -> void:
	parent_receiver = owner as Receiver
	
	start_position = parent_receiver.global_position
	target_position = start_position + pixel_move_direction


func _physics_process(delta: float) -> void:
	parent_receiver.global_position = parent_receiver.global_position.move_toward(target_position, pixel_speed * delta)
	
	if parent_receiver.global_position == start_position:
		target_position = start_position + pixel_move_direction
	elif parent_receiver.global_position == start_position + pixel_move_direction:
		target_position = start_position

Box

This is a box. It is a CharacterBody2D, also with a CollisionShape2D and a Sprite2D. Everything’s set as you said.

The code looks like this:

func _physics_process(delta: float) -> void:

	if parent.current_state == InteractiveElement.state.IDLE:
		
		parent.velocity.x = move_toward(parent.velocity.x, 0, 50)
	
		if !parent.is_on_floor():
			state_updated.emit(InteractiveElement.state.FALLING)
			
	elif parent.current_state == InteractiveElement.state.FALLING:
		
		if parent.is_on_floor():	
			state_updated.emit(InteractiveElement.state.IDLE)
			
	parent.velocity.y += gravity * delta

	parent.move_and_slide()

Gap when moving

Now, if we put a couple boxes on top of the platform, the first one has a little gap, and the one on top has an even bigger gap. Once the platform starts moving up, instead of a gap they sink.

For the record, this is how they look when they are on stable floor, so it is not an issue with the collider sizes or something like that.

Ok, so the first thing I noticed in your screenshot is this:

image

This indicates that this is a Node2D, not an AnimatableBody2D.

image

This indicates that your Platform node is not an AnimatableBody2D. The blue color on the script indicates this is a custom node. The fact that it has the AnimatableBody2D icon indicates that you did something really weird in extending an AnimatableBody2D node. From the Inspector, I can tell that it’s a Node2D.

My recommendation is to start by changing the class Receiver extends from Node2D to AnimatableBody2D. If you cannot do that, then we probably need to talk about your architecture.

If you can do that, and that doesn’t solve the problem then we can dig into your other code.

I would also recommend you copy my code to a new script, change the block size, and create a temporary platform just to see if that solves your problem. Without putting it through your component system. Simplify to debug. Whether it fixes it or not - it gives us more information.

The node is an AnimatableBody2D, the script attached to it extends Node2D. As far as I know that is not problematic since it is explicitly supported by Godot. But anyway, I tried to make everything as simple and straightforward as possible, and the problem persists.

I’ve put the code of the platform in the node itself, instead of a component. The platform node is now 100% an AnimatableBody2D (both the node and the script). The code of the box is also in the node itself.

Still the same behaviour.

If I put a breakpoint right where the move_and_slide is called, you can see the following:

  1. At the beginning of the frame, the top of Box A is at position 0.
  2. Box A moves down by 1 pixel. So the top should be at position 1.
  3. Box B should be able to move all the way down to position 1, but it collides with an invisible block at position 0. That is the previous position of Box A, because the engine thinks that Box A is still there.

It is possible, but it is undefined behavior. Godot does not support multiple inheritance and you are trying to shoehorn that in anyway. I recommend you don’t do that. It has caused me problems in the past. I would not say it is explicitly supported. But, you do you.

So I took another look at your Box:


And again, you’ve got a lot going on. So I recommend doing this.

  1. Create a RigidBody2D
  2. Add a Sprite2D to it.
  3. Add a CollisionShape2D to it.
  4. Configure them to be the same as your Box.
  5. Stack them on the platform and see what happens.

Then we can go from there.

Yeah, no worries. That’s a whole different issue.

It… causes the opposite problem? The platform is going down and the box on top is sinking. It is very hard to see but I drew a line to show it.

The box is configured like this (I just changed the freeze rotation property):

The project settings are these (nothing special to it, all is set to the default value):

Ok let me play around with this a bit.

So I couldn’t recreate your problem, but it was a little jerky.

So I set Texture Filter to Nearest

And turned Pixel Snap on for Transforms and Vertices.

That smoothed things out for me. See if that helps.

Hello!

The texture has been always set to “nearest” since it’s a must for pixel art. The snapping is an option that I tried before, although in these last tests it wasn’t activated. I tried it now and it doesn’t solve the problem either.

But I noticed something: the sprite of the box was 16x16 but the actual drawing of the box occupied only 12x12 pixels. That means that the box scene was 16x16, but since the collision shape was set to fit the occupied pixels, it was only 12x12.

I used a texture that actually occupies the whole 16x16 area and the sinking problem disappeared for the RigidBody2D. But it still persists in the case of CharacterBody2D.

The thing is that I don’t really want to use a RigidBody2D for the boxes. My game is based on controlling the boxes with some sort of “telekinetic power” and they have to follow exactly the same movement as the player, and I’m not sure I can achieve that with RigidBody2D since it is supposed to work on its own physics simulation, not via code.


Can we try something? Can you try to replicate the scene using CharacterBody2D for the boxes? If you advance frame by frame and zoom in, do you see the gap?

The code for the CharacterBody2D boxes can be as simple as this.

class_name Box extends CharacterBody2D

var gravity : float = ProjectSettings.get_setting("physics/2d/default_gravity")

	
func _physics_process(delta: float) -> void:
	
	if is_on_floor():
		velocity.x = move_toward(velocity.x, 0, 50)
		
	velocity.y += gravity * delta
	
	move_and_slide()

Also, thank you for spending so much time with this. Just wanted to say that if you’re going out of you way just to help me, it’s okay if we don’t find a solution. As I said in my original message, I already created my own system and it works.

Right, but you didn’t mention you did that, and I don’t know what you know.

Yeah, that’s definitely a problem with something so small.

So I replaced one of the RigiBody2D’s with a CharacterBody2D. Without a script (and lacking move_and_slide()) it doesn’t collide with the platform, but it does with the other box, which is trippy.

Your code is from Godot 3.x. I don’t know where you got it, but that’s not how you get gravity anymore. For reference, here’s the current CharacterBody2D template script:

CharacterBody2D
extends CharacterBody2D


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

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	var direction := Input.get_axis("ui_left", "ui_right")
	if direction:
		velocity.x = direction * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	move_and_slide()

The minimum script for a test would be this (which I tried):

extends CharacterBody2D


func _physics_process(delta: float) -> void:
	if not is_on_floor():
		velocity += get_gravity() * delta

	move_and_slide()

Both of these scripts work without your gap issue, even at 5x zoom:

Incidentally, I used my Camera2D Plugin for the zooming.

I then tried your code, which slowly moves to the left until it throws itself violently off the platform, and takes the RigidBody2D with it.

I was able to recreate your issue if I changed the Stretch Scale Mode in Project Settings to integer.

But only with that setting on, and only at some zoom levels. So at zoom level 1.0 and 5.0 I do not see it.

I also don’t see it if I change the code to this:

class_name Box extends CharacterBody2D

var gravity : float = ProjectSettings.get_setting("physics/2d/default_gravity")

	
func _physics_process(delta: float) -> void:
	
	if is_on_floor():
		velocity.x = -1
		
	velocity.y += gravity * delta
	
	move_and_slide()

I think the problem with your code is that you are moving things at such a small fractional number of an Integer, that it’s causing display problems because your objects are only 16 pixels in size and you are zoomed in.

I’d recommend you use larger numbers, or consider using larger textures. You’re currently creating some really weird issues for yourself.

You’re welcome.

Yeah, but you’ve over-engineered it, and I suspect this will not be the last problem you run into. I recommend taking a step back and fixing this at a more fundamental level or you’re going to run into more problems in the future.