0 votes

I've been trying to create a small volleyball android game. What I want to do is stop the ball when it hits the ground and let players move through it (no collision detection at this point). After three seconds, I want to respawn the ball and let it collide with players again.

Current implementation

Ball.gd:

extends RigidBody2D

export var sounds = []

const TIME_TO_RESET = 3.0
var reset_timer = TIME_TO_RESET
var pos_to_reset = null

# Queue ball respawn in _process function - any change to ball's position
# needs to be done in _integrate_forces (beacuse it's RigidBody2D)
var should_reset_position = false

# Used to disable contact_monitor when ball needs to be respawned
# and enable it again after _integrate_forces is called - this way
# ball is not affected by any collisions before respawned
var enable_contact_monitor = true

[...]

func _integrate_forces(state):
    print("_integrate_forces(state)")
    if should_reset_position:
        state.transform.origin = pos_to_reset
        should_reset_position = false
        enable_contact_monitor = true

func _process(delta):
   [...]

    if reset_timer < TIME_TO_RESET:
        reset_timer += delta
        if reset_timer > TIME_TO_RESET:
            _queue_ball_reset()
    contact_monitor = enable_contact_monitor

func _queue_ball_reset():
    set_mode(RigidBody2D.MODE_RIGID)
    # Set sleeping to false so that _integrate_forces can be called again
    sleeping = false
    stop_playback = false
    should_reset_position = true

func reset(pos_to_reset):
    self.pos_to_reset = pos_to_reset
    set_mode(RigidBody2D.MODE_STATIC)
    reset_timer = 0.0
    stop_playback = true
    enable_contact_monitor = false

Ball is a RigidBody2D. When it hits ground, its reset method is called. Its movement is stopped (set_mode(RigidBody2D.MODE_STATIC)) as well as collision detection (enable_contact_monitor = false). After three seconds, I switch it back to RigidBody2D and reset its position in _integrate_forces. stop_playback is for handling audio so it's not important here.

Problem

After many tries, it works almost perfectly except one thing. Generally, queuing the change of contact_monitor to true in _integrate_forces ensures that when I switch the ball back to RigidBody2D it's not affected by any force that could be applied during that switch. Namely, if a player stands near the disabled ball this collision may still be detected and it'll change ball's velocity - instead of falling down after the respawn the ball will fly in some direction.

I managed to eliminate this problem except there are some random moments when this happens again. I'm not sure how Godot handles _process and _integrate_forces but it feels to me like these run on separate threads and it could be that contact_monitor is set to true before _integrate_forces is done affecting ball's initial velocity after respawn.

I tried to add another if statement to _integrate_forces which sets ball's velocity to zero vector in the next run of this method. It solves the problem completely but the ball still moves slightly when it respawns which looks like a tiny visual glitch. Also, it makes the code look dirtier.

Question

Is there any better way to implement this? Is RigidBody2D appropriate for that?

asked Jun 14 in Engine by CosmicKid (16 points)

1 Answer

+1 vote

Yeah I'd say Rigid Body is the way to go. Just want to say a few things about your implementation that might help.

  1. It's most likely best to use _physics_process instead of _process. After all, it is a physics body.

  2. Use a Timer node. In this case, there's no need for a custom timer and it might even make your code cleaner.

  3. You can disable a physics body's collision shape so that it can't detect collisions. The collision shape's property disabled is what you'd be looking for.

  4. When a rigid body's mode is static, you wouldn't need to worry about changing position outside of the _integrate_forces function.

So if you were to go based off of what I said, your code would look like this.

extends RigidBody2D

export var sounds = []
#maybe you want this exportable?
export var pos_to_reset = Vector2()

const TIME_TO_RESET = 3.0
#assuming your timer node is a direct child of the rigid body...
onready var timer = $Timer
#...and that this is your collision shape.
onready var collision_shape = $CollisionShape2D

func _ready():
    connect("body_entered", self, "collision")
    timer.connect("timeout", self, "respawn")

    timer.wait_time = TIME_TO_RESET
    timer.one_shot = true

    contact_monitor = true
    contacts_reported = 2

#replaces your reset
func collision(body):
    set_mode(RigidBody.MODE_STATIC)
    collision_shape.disabled = true
    timer.start()

    #Just In Case
    linear_velocity *= 0
    angular_velocity *= 0

#replaces your _queue_ball_reset
func respawn():
    position = pos_to_reset
    set_mode(RigidBody.MODE_RIGID)
    collision_shape.disabled = false

I might have not put in variables that you have, but you can put them in yourself.

answered Jun 15 by SIsilicon (1,934 points)

Thanks for your response, a Timer node has really come in handy. However, it didn't solve my problem. Everytime I set the ball's position right before switching the ball back to rigid mode, it doesn't change anything. The ball appears at its correct position for a frame or so but as soon as it moves, it comes back to its previous position. For me, changing the ball's position outside of _integrate_forces function just doesn't work - of course it works for static mode but as soon as I change it back to rigid mode, any changes are reverted.

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 How to use this Q&A? before posting your first questions.