Screen recording of the program:
Introduction
Physics engines are usually built to handle soft bodies or standard rigid collisions. A Newton's Cradle is a unique challenge because it requires perfectly elastic collisions and simultaneous momentum transfer across multiple objects.
In this tutorial, we will use LÖVE's Box2D wrapper (love.physics) to trick the engine into creating a flawless Newton's Cradle.
Download the complete code here: main.lua or follow the steps below.
Step 1: The Physics Setup
First, we need to initialize our physics world. Box2D uses the MKS (meters, kilograms, and seconds) system, so we must define a meter scale to map pixels to physics units.
local world
local balls = {}
local joints = {}
local anchor
local mouseJoint = nil
-- Configuration
local numBalls = 5
local ballRadius = 25
local stringLength = 250
local screenWidth = 800
local startY = 150
function love.load()
love.window.setTitle("Interactive Newton's Cradle")
love.window.setMode(screenWidth, 600)
-- 64 pixels equals 1 meter in our world
love.physics.setMeter(64)
world = love.physics.newWorld(0, 9.81 * 64, true)
-- Static ceiling anchor
anchor = love.physics.newBody(world, screenWidth / 2, startY, "static")
setupBalls()
end
Step 2: Creating the Pendulums
We generate the balls dynamically in a loop. To make them act like a Newton's cradle, we need three crucial physics properties:
- Distance Joints: Forces the balls to remain an exact distance from the ceiling, acting like unbendable string.
- Restitution (1.0): Makes the balls perfectly bouncy so no energy is lost on impact.
- Bullet Mode: Enables Continuous Collision Detection (CCD) to stop fast-moving balls from clipping into each other.
function setupBalls()
local totalWidth = numBalls * (ballRadius * 2)
local startX = (screenWidth / 2) - (totalWidth / 2) + ballRadius
for i = 1, numBalls do
local bx = startX + (i - 1) * (ballRadius * 2)
local by = startY + stringLength
local body = love.physics.newBody(world, bx, by, "dynamic")
body:setBullet(true) -- Prevents tunneling
local shape = love.physics.newCircleShape(ballRadius)
local fixture = love.physics.newFixture(body, shape, 1)
-- Perfect elasticity and zero friction
fixture:setRestitution(1.0)
fixture:setFriction(0)
local joint = love.physics.newDistanceJoint(anchor, body, bx, startY, bx, by)
table.insert(balls, {body = body, shape = shape, fixture = fixture, startX = bx})
table.insert(joints, joint)
end
end
Step 3: Adding Interaction
We want to be able to pick up the balls with our mouse. We use a MouseJoint, which pulls the physical body toward the cursor using applied force rather than teleporting it, keeping the physics stable.
function love.update(dt)
world:update(dt)
if mouseJoint then
mouseJoint:setTarget(love.mouse.getPosition())
end
end
function love.mousepressed(x, y, button)
if button == 1 then
for _, ball in ipairs(balls) do
if ball.fixture:testPoint(x, y) then
mouseJoint = love.physics.newMouseJoint(ball.body, x, y)
mouseJoint:setMaxForce(5000 * ball.body:getMass())
break
end
end
end
end
function love.mousereleased(x, y, button)
if button == 1 and mouseJoint then
mouseJoint:destroy()
mouseJoint = nil
end
end
Step 4: Drawing to the Screen
Finally, we pull the physics data (positions) and render our visual graphics on top of them.
function love.draw()
love.graphics.setBackgroundColor(0.1, 0.1, 0.15)
-- Draw Ceiling
love.graphics.setLineWidth(4)
love.graphics.setColor(0.8, 0.8, 0.8)
local frameStartX = balls[1].startX - ballRadius
local frameEndX = balls[numBalls].startX + ballRadius
love.graphics.line(frameStartX, startY, frameEndX, startY)
-- Draw Strings and Balls
for i = 1, numBalls do
local ball = balls[i]
local bx, by = ball.body:getPosition()
local ax = ball.startX
-- String
love.graphics.setLineWidth(2)
love.graphics.setColor(0.5, 0.5, 0.5)
love.graphics.line(ax, startY, bx, by)
-- Ball
love.graphics.setColor(0.2, 0.6, 1)
love.graphics.circle("fill", bx, by, ballRadius)
love.graphics.setLineWidth(2)
love.graphics.setColor(0.9, 0.9, 0.9)
love.graphics.circle("line", bx, by, ballRadius)
end
love.graphics.setColor(1, 1, 1)
love.graphics.print("Click and drag a ball to start the simulation.", 10, 10)
end