cyclone2d 0.1.2

A small 2D physics engine from 'Game Physics Engine Development'
Documentation
use crate::{
    contact_generators::{Contact, ContactGenerator},
    contact_resolver::ContactResolver,
    force_generators::ForceGenerator,
    force_registry::ForceRegistry,
    particle::Particle,
    FP,
};
use std::ptr;
use vek2d::Vec2d;

/// The `World` contains the many parts of a physics engine, such
/// as a force registry for multiple forces, a contact resolver,
/// multiple contact generators, and contacts generated.
pub struct World {
    calc_iterations:     bool,
    registry:            ForceRegistry,
    resolver:            ContactResolver,
    contact_generators:  Vec<Box<dyn ContactGenerator>>,
    contacts:            Vec<Contact>,
    max_contacts:        i32,
    particle_free_slots: Vec<usize>,
    particle_array:      Vec<Option<Particle>>,
}

impl World {
    /// Initialise a new particle physics world
    ///
    /// # Example
    ///
    /// ```
    /// use cyclone2d::world::World;
    ///
    /// let mut particle_world = World::new(1000, 1000, None);
    /// ```
    pub fn new(
        particle_capacity: usize,
        max_contacts: usize,
        iterations: Option<u16>,
    ) -> World {
        let mut contacts = Vec::with_capacity(max_contacts);
        for _ in 0..max_contacts {
            contacts.push(Contact::default());
        }
        World {
            calc_iterations: iterations.is_none(),
            registry: ForceRegistry::new(),
            resolver: ContactResolver::new(iterations.unwrap_or(0)),
            contact_generators: Vec::new(),
            contacts,
            max_contacts: max_contacts as i32,
            particle_free_slots: Vec::new(),
            particle_array: Vec::with_capacity(particle_capacity),
        }
    }

    /// Called at the start of a frame to be drawn
    #[inline]
    pub fn start_frame(&mut self) {
        for v in self.particle_array.iter_mut() {
            if let Some(p) = v {
                p.clear_forces();
            }
        }
    }

    /// Moves a created `Particle` in to the `World` particle array, returning
    /// an index number which functions as the particle ID
    #[inline]
    pub fn add_particle(&mut self, particle: Particle) -> Option<usize> {
        if self.particle_array.len() >= self.particle_array.capacity() {
            return None;
        }
        let slot;
        if let Some(free) = self.particle_free_slots.pop() {
            slot = free;
            self.particle_array[slot] = Some(particle);
        } else {
            slot = self.particle_array.len();
            self.particle_array.push(Some(particle));
        }
        Some(slot)
    }

    /// Get a reference to `Particle` located at index number (`id`).
    #[inline]
    pub fn get_particle_position(&self, id: usize) -> Option<Vec2d<FP>> {
        if let Some(p) = self.particle_array.get(id) {
            if let Some(p) = p {
                return Some(p.position());
            }
        }
        None
    }

    #[inline]
    pub fn get_particle(&self, id: usize) -> Option<&Particle> {
        if let Some(p) = self.particle_array.get(id) {
            if p.is_some() {
                return p.as_ref();
            }
        }
        None
    }

    #[inline]
    pub fn get_particle_mut(&mut self, id: usize) -> Option<&mut Particle> {
        if let Some(p) = self.particle_array.get_mut(id) {
            if p.is_some() {
                return p.as_mut();
            }
        }
        None
    }

    #[inline]
    pub fn get_particle_ptr(
        &mut self,
        id: usize,
    ) -> Option<ptr::NonNull<Particle>> {
        if let Some(p) = self.particle_array.get_mut(id) {
            if let Some(p) = p.as_mut() {
                return Some(p.as_non_null_ptr());
            }
        }
        None
    }

    /// Also removes any force or contact generators attached to this index
    pub fn rm_particle(&mut self, id: usize) {
        // TODO: get pointer from id, check generators for use
        if id < self.particle_array.len() - 1 {
            if let Some(slot) = self.particle_array.get_mut(id) {
                if let Some(particle) = slot {
                    // make the pointer a usize for comparisons
                    let ptr_uzise =
                        particle.as_non_null_ptr().as_ptr() as usize;
                    // do registry first
                    self.registry.remove_if_contains(ptr_uzise);
                    // need to loop, remove, reset since the array changes
                    'removal: loop {
                        let mut index = 0;
                        for (i, cg) in
                            self.contact_generators.iter().enumerate()
                        {
                            if cg.contains_ptr(ptr_uzise) {
                                index = i;
                                break;
                            }
                            if i >= self.contact_generators.len() - 1 {
                                break 'removal;
                            }
                        }
                        self.contact_generators.remove(index);
                    }
                }
            }
            self.particle_array[id] = None;
            self.particle_free_slots.push(id);
        }
    }

    #[inline]
    pub fn get_particle_array(&self) -> &Vec<Option<Particle>> {
        &self.particle_array
    }

    /// Add a new `ForceGenerator` to the particle physics world
    ///
    /// ```
    /// use cyclone2d::{
    ///     world::World,
    ///     force_generators::Gravity
    /// };
    /// use cyclone2d::particle::Particle;
    ///
    /// let mut particle_world = World::new(100, 1000, None);
    /// // Create a default particle
    /// let particle = Particle::default();
    /// // Add it and get the slot ID back
    /// let pid = particle_world.add_particle(particle).unwrap();
    /// // Slot ID is used to get the pointer to the particle
    /// let ptr = particle_world.get_particle_ptr(pid).unwrap();
    /// // Almost all generators require pointers to the particles to act on
    /// // Because the engine inherrently assumes that 0 is down while
    /// // positive floats are up, gravity here is a negative value
    /// particle_world.add_force_generator(Gravity::new(ptr, -9.81));
    /// ```
    #[inline]
    pub fn add_force_generator(
        &mut self,
        force: impl ForceGenerator + 'static,
    ) {
        self.registry.add(force);
    }

    /// Add a new `ForceGenerator` to the particle physics world
    ///
    /// ```
    /// use cyclone2d::{
    ///     world::World,
    ///     contact_generators::GroundContact
    /// };
    ///
    /// let mut particle_world = World::new(100, 1000, None);
    /// particle_world.add_contact_generator(GroundContact::new());
    /// ```
    #[inline]
    pub fn add_contact_generator(
        &mut self,
        force: impl ContactGenerator + 'static,
    ) {
        self.contact_generators.push(Box::new(force));
    }

    #[inline]
    fn generate_contacts(&mut self) -> usize {
        let mut limit = self.max_contacts;
        for cg in &self.contact_generators {
            let start_index = self.contacts.len() - limit as usize;
            let used = cg.add_contact(
                &mut self.contacts[start_index..],
                &mut self.particle_array,
                limit as u32,
            );
            limit -= used as i32;
            if limit <= 0 {
                break;
            }
        }
        (self.max_contacts - limit) as usize
    }

    #[inline]
    fn integrate(&mut self, dt: FP) {
        for v in self.particle_array.iter_mut() {
            if let Some(p) = v {
                p.integrate(dt);
            }
        }
    }

    #[inline]
    pub fn run_physics(&mut self, dt: FP) {
        self.registry.update_forces();
        self.integrate(dt);
        let used_contacts = self.generate_contacts();
        if used_contacts > 0 {
            if self.calc_iterations {
                self.resolver.set_iterations(used_contacts as u16 * 2);
            }
            self.resolver.resolve_contacts(
                &mut self.contacts,
                used_contacts,
                dt,
            );
        }
    }
}

impl Particle {
    #[inline]
    fn integrate(&mut self, dt: FP) {
        if self.inverse_mass() <= 0.0 {
            return;
        }
        // update linear position
        self.set_position(self.position() + self.velocity() * dt);
        // work out forces
        let mut acceleration = self.acceleration();
        acceleration += self.forces() * self.inverse_mass();
        // Update linear velocity
        self.set_velocity(self.velocity() + acceleration * dt);
        // damping required to avoid float errors
        self.set_velocity(self.velocity() * self.damping().powf(dt));
        self.clear_forces();
    }
}