A Complete Tutorial Using Rust and Macroquad
In this tutorial, you'll create a mesmerizing particle constellation effect where particles drift across the screen and connect to nearby neighbors with glowing lines. This is a great project for learning:
Open your terminal and run:
cargo new particle_constellation
cd particle_constellation
Open Cargo.toml and add macroquad:
[dependencies]
macroquad = "0.4"
Macroquad is a simple game framework that handles window creation, rendering, and input for us.
You can download the complete source code here: main.rs or follow the steps below.
Each particle needs to track its position, velocity, and size. Let's build the Particle struct:
use macroquad::prelude::*;
struct Particle {
pos: Vec2, // 2D position (x, y)
vel: Vec2, // 2D velocity (speed in x and y directions)
size: f32, // Radius of the particle
}
Vec2 is Macroquad's 2D vector type. It's perfect for positions and velocities because it bundles x and y coordinates together.
We need a function to create particles with random positions and velocities:
impl Particle {
fn new() -> Self {
Self {
// Random position anywhere on screen
pos: vec2(
rand::gen_range(0.0, screen_width()),
rand::gen_range(0.0, screen_height()),
),
// Random velocity between -1 and 1 pixels per frame
vel: vec2(
rand::gen_range(-1.0, 1.0),
rand::gen_range(-1.0, 1.0),
),
// Random size between 2 and 4 pixels
size: rand::gen_range(2.0, 4.0),
}
}
}
rand::gen_range() is provided by Macroquad's built-in random number generator. You don't need to add the rand crate separately!
Each frame, we need to move particles and handle screen wrapping:
fn update(&mut self) {
// Move particle by its velocity
self.pos += self.vel;
// Wrap around screen edges (like Pac-Man)
if self.pos.x < 0.0 {
self.pos.x = screen_width();
} else if self.pos.x > screen_width() {
self.pos.x = 0.0;
}
if self.pos.y < 0.0 {
self.pos.y = screen_height();
} else if self.pos.y > screen_height() {
self.pos.y = 0.0;
}
}
The wrapping creates seamless, continuous motion where particles that exit one side reappear on the opposite side.
Simple rendering method to draw each particle as a white circle:
fn draw(&self) {
draw_circle(self.pos.x, self.pos.y, self.size, WHITE);
}
}
Now for the core of the application. The game loop runs every frame:
#[macroquad::main("Particle Constellations")]
async fn main() {
// Configuration
let particle_count = 100;
let connection_distance = 120.0;
// Create all particles
let mut particles: Vec<Particle> = (0..particle_count)
.map(|_| Particle::new())
.collect();
loop {
// Clear screen with dark blue background
clear_background(Color::from_rgba(10, 10, 30, 255));
// Update all particles
for particle in particles.iter_mut() {
particle.update();
}
// ... (connection drawing - next step)
// Draw all particles
for particle in &particles {
particle.draw();
}
// Wait for next frame
next_frame().await
}
}
This is where the constellation effect comes alive. We check every pair of particles and draw lines between nearby ones:
// Draw connections between nearby particles
for i in 0..particles.len() {
for j in (i + 1)..particles.len() {
// Calculate distance between particles
let dx = particles[i].pos.x - particles[j].pos.x;
let dy = particles[i].pos.y - particles[j].pos.y;
let distance = (dx * dx + dy * dy).sqrt();
// Only draw if particles are close enough
if distance < connection_distance {
// Fade line based on distance (closer = more opaque)
let alpha = 1.0 - (distance / connection_distance);
let color = Color::from_rgba(
100, 150, 255,
(alpha * 100.0) as u8
);
draw_line(
particles[i].pos.x,
particles[i].pos.y,
particles[j].pos.x,
particles[j].pos.y,
1.0,
color,
);
}
}
}
sqrt(dx² + dy²) - the Pythagorean theorem!j = i + 1)Let's add FPS and particle count to the screen:
// Display info
draw_text(
&format!("FPS: {}", get_fps()),
10.0, 20.0, 20.0, WHITE,
);
draw_text(
&format!("Particles: {}", particle_count),
10.0, 40.0, 20.0, WHITE,
);
cargo run --release
The --release flag enables optimizations for smooth performance.
Next Rust tutorial: