+1 vote

Hello,

I am a total noob in Godot and I am working on a simple 2D game.
In this game I have a ball which is a RigidBody2D (with the mode set to RIGID) and a platform which is a StaticBody2D.
The ball is launched in the air and bounces from the platform.
I need to reset the ball to the initial position after 2 bounces.
I do this by setting the ball's mode to STATIC and changing its position.
Then I need to relaunch the ball from the initial position.
This doesn't work for some reason, when I switch the ball back to RIGID mode it starts from the last position where it was before I reset the position.
Could you please explain what I am doing wrong here?

The code:

extends Node2D

# The goal of this script is to:
# 1. Launch the ball after waiting for 5 seconds
# 2. Reset the ball's position to what it was in the beginning after 2 collisions with the platform
# 3. Go to 1

onready var ball_rigid_body_2d = $BallRigidBody2D
onready var ball_collision_shape = $BallRigidBody2D/CollisionShape2D 

var initial_ball_position:Vector2 = Vector2.ZERO
var collision_count:int = 0
var reset:bool = false

func _ready():
    initial_ball_position = ball_rigid_body_2d.position
    create_timer()

func create_timer():
    var timer = Timer.new()
    timer.connect("timeout", self, "_on_timer_timeout") 
    add_child(timer)
    timer.start(5)

# Should relaunch the ball every 5 seconds
func _on_timer_timeout():
    ball_rigid_body_2d.mode = RigidBody2D.MODE_RIGID
    ball_collision_shape.disabled = false
    # Reset velocities
    ball_rigid_body_2d.linear_velocity *= 0
    ball_rigid_body_2d.angular_velocity *= 0

    # Reset the collision counter
    collision_count = 0
    var impulse_vector = Vector2(1, -1)
    ball_rigid_body_2d.apply_central_impulse(impulse_vector * 100)

func _physics_process(delta):
    if reset:
        reset = false
        ball_rigid_body_2d.mode = RigidBody2D.MODE_STATIC
        ball_rigid_body_2d.linear_velocity *= 0
        ball_rigid_body_2d.angular_velocity *= 0
        ball_rigid_body_2d.position = initial_ball_position
        ball_collision_shape.disabled = true

func _on_BallBody_body_entered(body):
    print("Collision!")
    collision_count += 1
    # Reset after 2 collisions
    if collision_count > 1:
        reset = true

Here is a short animation of the issue.
https://drive.google.com/file/d/10xVBC7Lo8iOQ_P_3GwedNQY8DahwLkaU/view?usp=sharing

I created a super simple test project to demonstrate the problem
https://drive.google.com/file/d/1wYeBJbLCgZm8xvrU-qM7usq_jUAw0gWB/view?usp=sharing

Thank you.

Godot version 3.5
in Engine by (16 points)

Here is the working solution (big thanks to zenbobilly) :
godot-launch-ball-test

I added the source code, in case the file is no longer available:
SceneRoot.gd

extends Node2D

# The goal of this script is to:
# 1. Launch the ball
# 2. Reset the ball's position after 2 collisions with the platform to what it was in the beginning 
# 3. Go to 1

onready var ball_rigid_body_2d:Ball = $BallRigidBody2D

var ball_initial_position: = Vector2.ZERO
var collision_count: = 0
var timer_interval: = 7

func _ready():
    ball_initial_position = ball_rigid_body_2d.position
    ball_rigid_body_2d.connect("body_entered", self, "_on_BallRigidBody2D_body_entered")
    launch_ball()
    create_timer()

func launch_ball() -> void:
    var strength: = 220
    var impulse_vector = Vector2(1, -1)
    ball_rigid_body_2d.make_rigid_and_apply_impulse(impulse_vector * strength)

func reset_ball() -> void:
    ball_rigid_body_2d.make_static_and_set_position(ball_initial_position)

func create_timer():
    var timer = Timer.new()
    timer.connect("timeout", self, "_on_timer_timeout") 
    add_child(timer)
    timer.start(timer_interval)

# Relaunch the ball on timer
func _on_timer_timeout():
    collision_count = 0
    launch_ball()

func _on_BallRigidBody2D_body_entered(body:Node):
    print("Collision!")
    collision_count += 1
    # Reset after 2 collisions
    if collision_count > 1:
        reset_ball()

BallRigidBody2D.gd

extends RigidBody2D
class_name Ball

onready var ball_collision_shape = $CollisionShape2D 

var _position: = Vector2.ZERO
var _reset_position: = false 

var _impulse: = Vector2.ZERO
var _apply_impulse: = false

func make_ridid_deferred() -> void:
    set_deferred("mode", RigidBody2D.MODE_RIGID)
    ball_collision_shape.set_deferred("disabled", false)

func make_static_deferred() -> void:
    set_deferred("mode", RigidBody2D.MODE_STATIC)
    ball_collision_shape.set_deferred("disabled", true)

func make_static_and_set_position(position:Vector2):
    _position = position
    _reset_position = true

func make_rigid_and_apply_impulse(impulse:Vector2):
    _impulse = impulse
    _apply_impulse = true
    # It is important to call the two metods below here, not in the _integrate_forces
    make_ridid_deferred()

func _integrate_forces(state):
    if _reset_position:
        _reset_position = false

        # Stop all movement
        state.linear_velocity = Vector2.ZERO
        state.angular_velocity = 0
        state.transform.origin = _position

        # Make the ball static  and disable collision after the current frame's physics step
        make_static_deferred()

    elif _apply_impulse:
        _apply_impulse = false
        apply_central_impulse(_impulse)

Project tree:

  • SceneRoot

    • BallRigidBody2D

      • BallSprite
      • CollisionShape2D
    • PlatformStaticBody2D

      • CollisionShape2D
      • ColorRect

1 Answer

0 votes
Best answer

I got this working using integrateforces () rather than physicsprocess () when resetting the RigidBody2D's state and position. The former is the correct point to change/influence the physics state of a body without causing problems with the Physics Engine.

Furthermore, use create_timer () rather than Timer, because the former doesn't require the creation of a node.

by (141 points)
selected by

Thank you very much for your suggestion, zenbobilly. I spent a bit of time on this and noticed that I cannot set mode to MODE_STATIC from integrateforces, it just has no effect, and if you set it elsewhere integrateforces is not called :( I assume it is not called on static bodies. I need to spend more time on this...

This is what I tried on the RIgidBody2D so far and it didn't work:

extends RigidBody2D
class_name Ball
onready var ball_collision_shape = $CollisionShape2D 

var _reset_position: = Vector2.ZERO
var _reset_position_requested = false 

var _impulse_to_apply = Vector2.ZERO
var _apply_impulse_requested = false

func _integrate_forces(state):
    if _reset_position_requested:
        _reset_position_requested = false
        mode=RigidBody2D.MODE_STATIC
        state.linear_velocity = Vector2.ZERO
        state.angular_velocity = 0
        state.transform.origin= _reset_position
        ball_collision_shape.disabled = true
    elif _apply_impulse_requested:
        _apply_impulse_requested = false
        mode = RigidBody2D.MODE_RIGID
        apply_central_impulse(_impulse_to_apply)
        ball_collision_shape.disabled = false

func make_static_and_set_position(position:Vector2):
    _reset_position = position
    _reset_position_requested = true

func make_rigid_and_apply_impulse(impulse:Vector2):
    _impulse_to_apply = impulse
    _apply_impulse_requested = true

makestaticandsetposition and makerigidandapplyimpulse are called from the parent script. The ball resets now and moves to the initial position, but I cannot make it static again. Maybe I could try setting that in physicsprocess? This whole thing is becoming very hacky IMO for such a simple problem :)

I tried create_timer() too, but it can only create one shot timer, not a repeatable timer.

With integrateforces(), you shouldn't have to change the type to STATIC when you reset. Simply reset all the forces & position & maybe turn gravity off. Anyway, you're right about the timer, if you want a repeatable timeout you may as well add the child Timer node to your scene rather than having to create it each time though,but that's not really your core problem, so it's not really important.

Thank you, zenbobilly. I do understand that I can just keep resetting velocity in every physics frame without setting the mode to MODESTATIC. The reason I wanted to keep it static is to avoid random collisions due to jitter when the ball is on the floor. I think I figured it out how to do without that though, if your RigidBody2D "can sleep" and you set mode to STATIC it automatically starts sleeping, but if I turn off "Can Sleep" property on RIgidBody2D and add some calls to setdeferred("mode", RigidBody2D.MODE_STATIC) I can actually set mode to STATIC! It works now! I need some time to clean up the code and I will post the solution. Your answer led me to it. Thank you very much again!

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read Frequently asked questions and How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to [email protected] with your username.