A step-by-step guide using Rust and Macroquad
In this tutorial, we will build an interactive physics simulation of a Newton's Cradle. We will cover rigid body constraints (the strings) and elastic collisions (the energy transfer).
Screen recording of the program:

First, ensure you have Rust installed. Create a new project and add the dependencies.
# Terminal
cargo new newtons_cradle
cd newtons_cradle
Open Cargo.toml and add Macroquad, a simple and fast game framework for Rust.
[dependencies]
macroquad = "0.4"
Download the full code here: main.rs or follow the steps below.
We need a struct to represent each ball. A pendulum consists of an anchor (where the string is tied), a position (where the ball is), and velocity.
use macroquad::prelude::*;
const BALL_RADIUS: f32 = 30.0;
const STRING_LENGTH: f32 = 300.0;
struct Pendulum {
anchor: Vec2,
pos: Vec2,
vel: Vec2,
is_dragged: bool, // Is the user holding this ball?
}
impl Pendulum {
fn new(x: f32, y: f32) -> Self {
Self {
anchor: vec2(x, y),
pos: vec2(x, y + STRING_LENGTH),
vel: Vec2::ZERO,
is_dragged: false,
}
}
}
Normally, pendulums require trigonometry. However, in game physics, it's often easier to use Vector constraints.
impl Pendulum {
fn update(&mut self, dt: f32) {
if self.is_dragged {
self.vel = Vec2::ZERO;
return;
}
// 1. Apply Gravity
self.vel.y += 900.0 * dt;
self.vel *= 0.995; // Air resistance (Damping)
self.pos += self.vel * dt;
// 2. Enforce String Length
let to_ball = self.pos - self.anchor;
let dist = to_ball.length();
if dist > 0.0 {
// Reset position to exactly string_length away
let dir = to_ball / dist;
self.pos = self.anchor + dir * STRING_LENGTH;
// Remove velocity that pulls away from the string (Tension)
let vel_parallel = self.vel.dot(dir);
if vel_parallel > 0.0 {
self.vel -= dir * vel_parallel;
}
}
}
}
A Newton's Cradle works because the collisions are nearly perfectly elastic and the masses are identical. In physics, when two objects of equal mass collide elastically, they simply exchange velocity along the normal vector of the collision.
fn resolve_collisions(balls: &mut [Pendulum]) {
for i in 0..balls.len() {
if i + 1 >= balls.len() { break; }
// Get mutable references to two adjacent balls
let (left, right) = balls.split_at_mut(i + 1);
let b1 = &mut left[i];
let b2 = &mut right[0];
let delta = b2.pos - b1.pos;
let dist = delta.length();
let min_dist = BALL_RADIUS * 2.0;
if dist < min_dist {
let n = delta / dist; // Collision Normal
// 1. Push them apart so they don't overlap
let push = n * (min_dist - dist) * 0.5;
if !b1.is_dragged { b1.pos -= push; }
if !b2.is_dragged { b2.pos += push; }
// 2. Velocity Exchange (Elastic Collision Logic)
let v1_n = b1.vel.dot(n);
let v2_n = b2.vel.dot(n);
// Only swap if moving towards each other
if v1_n - v2_n > 0.0 {
let v1_t = b1.vel - n * v1_n; // Tangent component
let v2_t = b2.vel - n * v2_n;
// Swap normal components!
b1.vel = v1_t + n * v2_n;
b2.vel = v2_t + n * v1_n;
}
}
}
}
Finally, we wire everything together in the main function.
#[macroquad::main("Newton's Cradle")]
async fn main() {
// Create 5 balls
let mut balls: Vec<Pendulum> = (0..5)
.map(|i| Pendulum::new(300.0 + i as f32 * 60.0, 100.0))
.collect();
loop {
clear_background(LIGHTGRAY);
// Handle Mouse Input (Dragging)
let mouse_pos = Vec2::from(mouse_position());
if is_mouse_button_pressed(MouseButton::Left) {
for b in &mut balls {
if b.pos.distance(mouse_pos) < BALL_RADIUS {
b.is_dragged = true;
break;
}
}
}
if is_mouse_button_released(MouseButton::Left) {
for b in &mut balls { b.is_dragged = false; }
}
// Drag logic
for b in &mut balls {
if b.is_dragged {
let dir = (mouse_pos - b.anchor).normalize();
b.pos = b.anchor + dir * STRING_LENGTH;
b.vel = Vec2::ZERO;
}
}
// Physics Update
let dt = get_frame_time();
for b in &mut balls { b.update(dt); }
// Sub-step collisions for stability
for _ in 0..4 { resolve_collisions(&mut balls); }
// Draw
for b in &balls {
draw_line(b.anchor.x, b.anchor.y, b.pos.x, b.pos.y, 2.0, BLACK);
draw_circle(b.pos.x, b.pos.y, BALL_RADIUS, BLUE);
}
next_frame().await
}
}