Ludicrous Software

Mobile and Flash Development

Corona Physics: Forced Horizontal/Vertical Bouncing

This recent article on the Ansca web site about solutions to common physics challenges is helpful, but as one commenter notes, the issue of objects “sticking” to walls isn’t easily solved by using the suggestions provided in the article. The commenter notes that the Corona Physics API doesn’t expose the collision properties required to properly handle these sticky (ha ha) situations. So here’s how you can hack around the problem when your walls are completely horizontal or vertical (situations where the collision is with non-horizontal/vertical objects are somewhat more complicated, and I’ll deal with those in a subsequent blog post).

Here’s a simple main.lua that creates four walls and a bouncing ball:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
local physics = require("physics")
physics.start()
physics.setGravity(0, 0)

-- create wall objects
local topWall = display.newRect( 0, 0, display.contentWidth, 10 )
local bottomWall = display.newRect( 0, display.contentHeight - 10, display.contentWidth, 10 )
local leftWall = display.newRect( 0, 0, 10, display.contentHeight )
local rightWall = display.newRect( display.contentWidth - 10, 0, 10, display.contentHeight )

-- make them physics bodies
physics.addBody(topWall, "static", {density = 1.0, friction = 0, bounce = 1, isSensor = false})
physics.addBody(bottomWall, "static", {density = 1.0, friction = 0, bounce = 1, isSensor = false})
physics.addBody(leftWall, "static", {density = 1.0, friction = 0, bounce = 1, isSensor = false})
physics.addBody(rightWall, "static", {density = 1.0, friction = 0, bounce = 1, isSensor = false})

-- create a ball and set it in motion
ball = display.newCircle( 0, 0, 15 )
ball.x, ball.y = display.contentWidth / 2, display.contentHeight - 80
physics.addBody(ball, "dynamic", {density = 1, friction = 0, bounce = 1, isSensor = false, radius = 15})
ball.isBullet = true
ball:applyForce(100, 10)

local function onCollision(event)
  print(event.phase, ball:getLinearVelocity())
end

ball:addEventListener("collision", onCollision)

A force is applied to the ball, and when the ball hits the bottom wall, it will ‘stick’, which is not the behaviour that we want. As you’ll see from the output of the onCollision event handler, eventually, the y velocity becomes 0, meaning that the object is not moving up or down at all, which creates the effect of sticking.

One other thing you may notice from the terminal output: the x and y velocities follow a consistent pattern from the began phase of the event to the ended phase: one of the values will flip from positive to negative depending on which way the ball was moving before and after the collision. Here’s how it breaks down:

  • Bounce to the left: the x value goes from negative to positive
  • Bounce to the right: the x value goes from positive to negative
  • Bounce downwards: the y value goes from negative to positive
  • Bounce upwards: the y value goes from positive to negative

In the example code above, the y velocity toggles between 12.73 and -12.73 (I’m rounding off for the sake of convenience). Then, at some point instead of remaining at 12.73, it becomes 0. The ball is moving downward (positive y velocity) and instead of moving upward (negative y velocity), it stops moving up/down at all.

So the immediate solution presents itself: we know what the y velocity should be, so we can override the calculated value of zero and set it to what it should be. Here’s the new resetLinearVelocity function with the modified onCollision function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local function resetLinearVelocity(event)
  local thisX, thisY = ball:getLinearVelocity()
  if thisY == 0 then
      thisY = -ball.lastY
  end
  if thisX == 0 then
      thisX = -ball.lastX
  end
  ball:setLinearVelocity(thisX, thisY)
  ball.lastX, ball.lastY = thisX, thisY
  end
end

local function onCollision(event)
  timer.performWithDelay(0, resetLinearVelocity)
end

onCollision will call resetLinearVelocity after a delay of 0 milliseconds - all this does is force Corona to call the function on the next frame. We wait a frame so that we don’t interfere with the calculations being performed by the physics engine, and so that we don’t have to wait for another collision before resetting any values that need it (try calling the resetLinearVelocity function immediately to see the difference).

If the x or y velocity is zero, we just need to set them to the signed opposite of their last non-zero value. The ball object has a couple of new properties, lastX and lastY that stores the values for the x and y velocities, respectively, from the previous collision. If either of the new velocities are zero, then we manually set the linear velocity to the proper values.

All of this is being done by the resetLinearVelocity function. Here’s what it does, in order:

  1. It gets the current linear velocity values, storing them in local variables.
  2. It checks to see if the current y velocity (stored in thisY) is 0. If it is, then it's set to the negative value ofball.lastY`.
  3. The same check is done for the x velocity.
  4. :setLinearVelocity() is called. If thisX and/or thisY have changed, then this means the ball should have bounced after the last collision, but didn’t. This will force the bounce.
  5. The x and y velocities are stored for the next time there’s a collision.

One inefficiency in the code is that it will call :setLinearVelocity() even if neither value has changed, so you may want to add some conditional logic to only make that call when necessary - I haven’t tested it, but my suspicion is that a couple of if statements will have less overhead than the :setLinearVelocity call, especially in an app with a lot of collisions.

Keep one thing in mind: the physics engine is meant to more or less accurately emulate real-world physics. My guess is that the real problem is that, in real life, the ball is supposed to stop bouncing. The y velocity is simply not great enough for an object of that (simulated) size to bounce off the wall. For example, if you comment out the collision event listener and if you play around with the values used in the :applyForce() function call (e.g. ball:applyForce(100, 30)), you’ll find that the workaround isn’t even required, and the visual effect of the bouncing ball in this case isn’t significantly different from the lesser initial y velocity of 10. However, just because Box2D is meant to model the real world, doesn’t mean you have to use it to do that, so hopefully you can use this to get you out of the ‘sticky’ situations you may sometimes find yourself in with the Corona physics engine.