Budowanie Kołyski Newtona

Przewodnik krok po kroku z użyciem Rust i Macroquad

W tym samouczku zbudujemy interaktywną symulację fizyczną Kołyski Newtona. Omówimy więzy ciała sztywnego (sznurki) oraz zderzenia sprężyste (transfer energii).

Nagranie ekranu programu:

1. Konfiguracja Projektu

Najpierw upewnij się, że masz zainstalowany Rust. Utwórz nowy projekt i dodaj zależności.

# Terminal
cargo new newtons_cradle
cd newtons_cradle

Otwórz Cargo.toml i dodaj Macroquad, prosty i szybki framework do gier dla Rusta.

[dependencies]
macroquad = "0.4"

Pobierz pełny kod tutaj: main.rs lub postępuj zgodnie z poniższymi krokami.

2. Definiowanie Obiektu Fizycznego

Potrzebujemy struktury reprezentującej każdą kulę. Wahadło składa się z kotwicy (gdzie sznurek jest przywiązany), pozycji (gdzie znajduje się kula) oraz prędkości.

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, // Czy użytkownik trzyma tę kulę?
}

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,
        }
    }
}

3. Implementacja Fizyki: Więzy Sznura

Zazwyczaj wahadła wymagają trygonometrii. Jednak w fizyce gier często łatwiej jest używać więzów wektorowych.

  1. Zastosuj grawitację do prędkości.
  2. Przesuń pozycję na podstawie prędkości.
  3. Więzy: Jeśli kula oddali się od kotwicy bardziej niż pozwala na to sznur, natychmiast ją cofnij.
impl Pendulum {
    fn update(&mut self, dt: f32) {
        if self.is_dragged { 
            self.vel = Vec2::ZERO; 
            return; 
        }

        // 1. Zastosuj grawitację
        self.vel.y += 900.0 * dt;
        self.vel *= 0.995; // Opór powietrza (Tłumienie)
        self.pos += self.vel * dt;

        // 2. Wymuś długość sznura
        let to_ball = self.pos - self.anchor;
        let dist = to_ball.length();

        if dist > 0.0 {
            // Zresetuj pozycję dokładnie na odległość długości sznura
            let dir = to_ball / dist;
            self.pos = self.anchor + dir * STRING_LENGTH;

            // Usuń prędkość, która odciąga od sznura (Napięcie)
            let vel_parallel = self.vel.dot(dir);
            if vel_parallel > 0.0 {
                self.vel -= dir * vel_parallel;
            }
        }
    }
}

4. Sekretny Składnik: Zderzenia Sprężyste

Kołyska Newtona działa, ponieważ zderzenia są niemal idealnie sprężyste, a masy są identyczne. W fizyce, gdy dwa obiekty o równej masie zderzają się sprężyście, po prostu wymieniają się prędkością wzdłuż wektora normalnego zderzenia.

Dlaczego pętla wielokrotna? Ponieważ kule się stykają, zderzenie na początku łańcucha musi rozprzestrzenić się do końca łańcucha w jednej klatce. Uruchamiamy rozwiązanie kolizji 4 razy na klatkę, aby pozwolić tej fali uderzeniowej podróżować.
fn resolve_collisions(balls: &mut [Pendulum]) {
    for i in 0..balls.len() {
        if i + 1 >= balls.len() { break; }

        // Pobierz mutowalne referencje do dwóch sąsiednich kul
        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; // Normalna kolizji

            // 1. Rozsuń je, aby na siebie nie nachodziły
            let push = n * (min_dist - dist) * 0.5;
            if !b1.is_dragged { b1.pos -= push; }
            if !b2.is_dragged { b2.pos += push; }

            // 2. Wymiana prędkości (Logika zderzenia sprężystego)
            let v1_n = b1.vel.dot(n);
            let v2_n = b2.vel.dot(n);

            // Zamień tylko, jeśli poruszają się w swoją stronę
            if v1_n - v2_n > 0.0 {
                let v1_t = b1.vel - n * v1_n; // Składowa styczna
                let v2_t = b2.vel - n * v2_n;

                // Zamień składowe normalne!
                b1.vel = v1_t + n * v2_n;
                b2.vel = v2_t + n * v1_n;
            }
        }
    }
}

5. Główna Pętla

Wreszcie łączymy wszystko w funkcji main.

#[macroquad::main("Newton's Cradle")]
async fn main() {
    // Utwórz 5 kul
    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);

        // Obsługa myszy (Przeciąganie)
        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; }
        }

        // Logika przeciągania
        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;
            }
        }

        // Aktualizacja fizyki
        let dt = get_frame_time();
        for b in &mut balls { b.update(dt); }
        
        // Podkroki kolizji dla stabilności
        for _ in 0..4 { resolve_collisions(&mut balls); }

        // Rysowanie
        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
    }
}

Następny artykuł:

Bezpieczeństwo pamięci: Rust a C++