Heisenbug in my simple Pong game

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
Post Reply
Cookiemonstrosity
Prole
Posts: 2
Joined: Fri Jul 26, 2013 4:59 am

Heisenbug in my simple Pong game

Post by Cookiemonstrosity »

For anyone that does not know what a Heisenbug is, it is a bug that disappears when you attempt to debug it. I finished the Pong tutorial on the HardonCollider github page and played it to realize that there is a bug with the code. What happens is the ball slides up or down the paddle for 1-2 seconds before changing it's direction. What's interesting is the bug disappears if I print out a message whenever the ball touches anything.

Steps to reproduce:
1. Start up pong.love
2. Tap W or S key once
3. Watch as the ball slides up or down the paddle for 1-2 seconds before deflecting

I reproduce the bug 50% of the time whenever I follow those steps above.

pong.love

Code: Select all

HC = require 'hardoncollider'

local text = {}

function on_collide(dt, shape_a, shape_b)
    -- determine which shape is the ball and which is not
    local other
    if shape_a == ball then
        other = shape_b
    elseif shape_b == ball then
        other = shape_a
    else -- no shape is the ball. exit
        return
    end

    -- reset on goal
    if other == goalLeft then
        ball.velocity = {x = 100, y = 0}
        ball:moveTo(400,300)
    elseif other == goalRight then
        ball.velocity = {x = -100, y = 0}
        ball:moveTo(400,300)
    elseif other == borderTop or other == borderBottom then
    -- bounce off top and bottom
        ball.velocity.y = -ball.velocity.y
    else
	 -- reflect the ball off the paddle
		local px,py = other:center()
		local bx,by = ball:center()
		ball.velocity.x = -ball.velocity.x
		ball.velocity.y = by - py

		-- keep the ball at the same speed as before
		local len = math.sqrt(ball.velocity.x^2 + ball.velocity.y^2)
		ball.velocity.x = ball.velocity.x / len * 100
		ball.velocity.y = ball.velocity.y / len * 100
		
    end
    --[[text[#text+1] = string.format("%s hit %s", 
                                    shape_a.name, shape_b.name)]]
end

function love.load()
    Collider = HC(100, on_collide)

    ball        = Collider:addCircle(400,300, 10)
	ball.name = "ball"

    leftPaddle  = Collider:addRectangle(10,250, 20,100)
	leftPaddle.name = "leftPaddle"
	
    rightPaddle = Collider:addRectangle(770,250, 20,100)
	rightPaddle.name = "rightPaddle"

    ball.velocity = {x = -100, y = 0}

    borderTop    = Collider:addRectangle(0,-100, 800,100)
	borderTop.name = "borderTop"
	
    borderBottom = Collider:addRectangle(0,600, 800,100)
	borderBottom.name = "borderBottom"
	
    goalLeft     = Collider:addRectangle(-100,0, 100,600)
	goalLeft.name = "goalLeft"
	
    goalRight    = Collider:addRectangle(800,0, 100,600)
	goalRight.name = "goalRight"
end

function love.update(dt)
    ball:move(ball.velocity.x * dt, ball.velocity.y * dt)

    -- left player movement
    if love.keyboard.isDown('w') then
        leftPaddle:move(0, -100 * dt)
    elseif love.keyboard.isDown('s') then
        leftPaddle:move(0,  100 * dt)
    end

    -- right player movement
    if love.keyboard.isDown('up') then
        rightPaddle:move(0, -100 * dt)
    elseif love.keyboard.isDown('down') then
        rightPaddle:move(0,  100 * dt)
    end

    Collider:update(dt)
end

function love.draw()
    ball:draw('fill', 16)
    leftPaddle:draw('fill')
    rightPaddle:draw('fill')
	
    --[[ print messages
    for i = 1,#text do
        love.graphics.setColor(255,255,255, 255 - (i-1) * 6)
        love.graphics.print(text[#text - (i-1)], 10, i * 15)
    end ]]
	
end
pong_debug.love

Code: Select all

HC = require 'hardoncollider'

local text = {}

function on_collide(dt, shape_a, shape_b)
    -- determine which shape is the ball and which is not
    local other
    if shape_a == ball then
        other = shape_b
    elseif shape_b == ball then
        other = shape_a
    else -- no shape is the ball. exit
        return
    end

    -- reset on goal
    if other == goalLeft then
        ball.velocity = {x = 100, y = 0}
        ball:moveTo(400,300)
    elseif other == goalRight then
        ball.velocity = {x = -100, y = 0}
        ball:moveTo(400,300)
    elseif other == borderTop or other == borderBottom then
    -- bounce off top and bottom
        ball.velocity.y = -ball.velocity.y
    else
	 -- reflect the ball off the paddle
		local px,py = other:center()
		local bx,by = ball:center()
		ball.velocity.x = -ball.velocity.x
		ball.velocity.y = by - py

		-- keep the ball at the same speed as before
		local len = math.sqrt(ball.velocity.x^2 + ball.velocity.y^2)
		ball.velocity.x = ball.velocity.x / len * 100
		ball.velocity.y = ball.velocity.y / len * 100
		
    end
    text[#text+1] = string.format("%s hit %s", 
                                    shape_a.name, shape_b.name)
end

function love.load()
    Collider = HC(100, on_collide)

    ball        = Collider:addCircle(400,300, 10)
	ball.name = "ball"

    leftPaddle  = Collider:addRectangle(10,250, 20,100)
	leftPaddle.name = "leftPaddle"
	
    rightPaddle = Collider:addRectangle(770,250, 20,100)
	rightPaddle.name = "rightPaddle"

    ball.velocity = {x = -100, y = 0}

    borderTop    = Collider:addRectangle(0,-100, 800,100)
	borderTop.name = "borderTop"
	
    borderBottom = Collider:addRectangle(0,600, 800,100)
	borderBottom.name = "borderBottom"
	
    goalLeft     = Collider:addRectangle(-100,0, 100,600)
	goalLeft.name = "goalLeft"
	
    goalRight    = Collider:addRectangle(800,0, 100,600)
	goalRight.name = "goalRight"
end

function love.update(dt)
    ball:move(ball.velocity.x * dt, ball.velocity.y * dt)

    -- left player movement
    if love.keyboard.isDown('w') then
        leftPaddle:move(0, -100 * dt)
    elseif love.keyboard.isDown('s') then
        leftPaddle:move(0,  100 * dt)
    end

    -- right player movement
    if love.keyboard.isDown('up') then
        rightPaddle:move(0, -100 * dt)
    elseif love.keyboard.isDown('down') then
        rightPaddle:move(0,  100 * dt)
    end

    Collider:update(dt)
end

function love.draw()
    ball:draw('fill', 16)
    leftPaddle:draw('fill')
    rightPaddle:draw('fill')
	
    --[ print messages
    for i = 1,#text do
        love.graphics.setColor(255,255,255, 255 - (i-1) * 6)
        love.graphics.print(text[#text - (i-1)], 10, i * 15)
    end
	
end
Can anyone confirm if they have the bug too?
Attachments
pong_debugging.love
Doesn't have bug
(19.49 KiB) Downloaded 69 times
pong.love
Has bug
(19.34 KiB) Downloaded 68 times
User avatar
Robin
The Omniscient
Posts: 6506
Joined: Fri Feb 20, 2009 4:29 pm
Location: The Netherlands
Contact:

Re: Heisenbug in my simple Pong game

Post by Robin »

What you probably have is this: say the ball is 3px away from you paddle, it then moves 10px to the left and is 7px in the paddle. Suppose the next frame, dt is a bit smaller so it moves 5px to the right. It still collides with the paddle, so it reverses direction again and goes to the left this time. It keeps doing that until it can escape.

That explains why it happens only 50% of the time. I have no idea why those conditions matter (one tap, no debug).

A solution would be to displace the ball so it no longer collides with the paddle. I think HC gives you some kind of displacement vector you can use? I forgot how it worked, you should look into the documentation for it.
Help us help you: attach a .love.
Cookiemonstrosity
Prole
Posts: 2
Joined: Fri Jul 26, 2013 4:59 am

Re: Heisenbug in my simple Pong game

Post by Cookiemonstrosity »

I fixed the problem by only allowing each paddle to touch the wall once. It seems like it was getting stuck in the paddle and constantly flipping its movement. Maybe the debugging was doing something to the deltatime...

Here is my code if anyone is curious to how I fixed it.

Code: Select all

function on_collide(dt, shape_a, shape_b)
    -- determine which shape is the ball and which is not
    local other
    if shape_a == ball then
        other = shape_b
    elseif shape_b == ball then
        other = shape_a
    else -- no shape is the ball. exit
        return
    end

    -- reset on goal
    if other == goalLeft then
        ball.velocity = {x = 100, y = 0}
        ball:moveTo(400,300)
    elseif other == goalRight then
        ball.velocity = {x = -100, y = 0}
        ball:moveTo(400,300)
    elseif other == borderTop or other == borderBottom then
    -- bounce off top and bottom
        ball.velocity.y = -ball.velocity.y
    elseif other == leftPaddle and leftPaddle.touched == false then
	 -- reflect the ball off the paddle
		local px,py = other:center()
		local bx,by = ball:center()
		ball.velocity.x = -ball.velocity.x
		ball.velocity.y = by - py
		
		-- keep the ball at the same speed as before
		local len = math.sqrt(ball.velocity.x^2 + ball.velocity.y^2)
		ball.velocity.x = ball.velocity.x / len * 100
		ball.velocity.y = ball.velocity.y / len * 100
		
		leftPaddle.touched = true
		rightPaddle.touched = false
	elseif other == rightPaddle and rightPaddle.touched == false then

	 -- reflect the ball off the paddle
		local px,py = other:center()
		local bx,by = ball:center()
		ball.velocity.x = -ball.velocity.x
		ball.velocity.y = by - py
		
		-- keep the ball at the same speed as before
		local len = math.sqrt(ball.velocity.x^2 + ball.velocity.y^2)
		ball.velocity.x = ball.velocity.x / len * 100
		ball.velocity.y = ball.velocity.y / len * 100
		
		leftPaddle.touched = false
		rightPaddle.touched = true
		
	else
		return
		
    end
    --[[text[#text+1] = string.format("%s hit %s", 
                                    shape_a.name, shape_b.name)]]
end
User avatar
micha
Inner party member
Posts: 1083
Joined: Wed Sep 26, 2012 5:13 pm

Re: Heisenbug in my simple Pong game

Post by micha »

When I implement bouncing on one of the boundaries then instead of

Code: Select all

velocityX = - velocityX
I usually do

Code: Select all

velocityX = math.abs(velocityX)
  -- or
velocityX = - math.abs(velocityX)
That way you can make sure the velocity is always point inwards.
Post Reply

Who is online

Users browsing this forum: Bing [Bot], Google [Bot] and 6 guests