Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Typed Interactions

Typed interactions let different particle types behave differently toward each other. This enables predator-prey dynamics, team-based systems, and state machines.

Defining Particle Types

Use #[derive(ParticleType)] to create a type-safe enum:

#![allow(unused)]
fn main() {
#[derive(ParticleType, Clone, Copy, PartialEq)]
enum Species {
    Prey,      // = 0
    Predator,  // = 1
}
}

The derive macro automatically:

  • Implements Into<u32> (variants get sequential IDs: 0, 1, 2...)
  • Implements From<u32> (convert back from runtime values)
  • Adds a count() method

Every particle has a particle_type: u32 field. If you don't add it, it's auto-added with value 0.

#![allow(unused)]
fn main() {
#[derive(Particle, Clone)]
struct Creature {
    position: Vec3,
    velocity: Vec3,
    particle_type: u32,
}
}

Set types in the spawner:

#![allow(unused)]
fn main() {
.with_spawner(|i, count| {
    let species = if i < 50 { Species::Predator } else { Species::Prey };
    Creature {
        position: random_pos(),
        velocity: Vec3::ZERO,
        particle_type: species.into(),
    }
})
}

Chase & Evade

For predator-prey dynamics, use the dedicated rules:

#![allow(unused)]
fn main() {
// Predators chase nearest prey
.with_rule(Rule::Chase {
    self_type: Species::Predator.into(),
    target_type: Species::Prey.into(),
    radius: 0.4,
    strength: 4.0,
})

// Prey evades nearest predator
.with_rule(Rule::Evade {
    self_type: Species::Prey.into(),
    threat_type: Species::Predator.into(),
    radius: 0.25,
    strength: 6.0,
})
}

These find the nearest target/threat and steer directly toward/away from it.

The Typed Wrapper

Rule::Typed wraps any neighbor rule with type filters:

#![allow(unused)]
fn main() {
Rule::Typed {
    self_type: u32,           // Which particles this rule affects
    other_type: Option<u32>,  // Which neighbors to consider
    rule: Box<Rule>,          // The wrapped rule
}
}

Example: Prey Flocking

#![allow(unused)]
fn main() {
// Prey flocks with other prey
.with_rule(Rule::Typed {
    self_type: Species::Prey.into(),
    other_type: Some(Species::Prey.into()),
    rule: Box::new(Rule::Cohere { radius: 0.15, strength: 1.0 }),
})
}

Interacting with All Types

Use other_type: None to interact with everyone:

#![allow(unused)]
fn main() {
// Everyone avoids collisions with everyone
.with_rule(Rule::Typed {
    self_type: Species::Prey.into(),
    other_type: None,  // All types
    rule: Box::new(Rule::Collide { radius: 0.05, response: 0.5 }),
})
}

Type Conversion

Rule::Convert changes particle types at runtime:

#![allow(unused)]
fn main() {
#[derive(ParticleType, Clone, Copy, PartialEq)]
enum Health {
    Healthy,
    Infected,
    Recovered,
}

// Healthy can become infected
.with_rule(Rule::Convert {
    from_type: Health::Healthy.into(),
    trigger_type: Health::Infected.into(),
    to_type: Health::Infected.into(),
    radius: 0.08,
    probability: 0.15,
})

// Infected eventually recover
.with_rule(Rule::Convert {
    from_type: Health::Infected.into(),
    trigger_type: Health::Infected.into(),  // Self-trigger
    to_type: Health::Recovered.into(),
    radius: 0.01,
    probability: 0.002,
})
}

Updating Visuals

When types change, you'll want colors to update. Use Rule::Custom:

#![allow(unused)]
fn main() {
.with_rule(Rule::Custom(r#"
    if p.particle_type == 0u {
        p.color = vec3<f32>(0.1, 0.9, 0.2); // Green
    } else if p.particle_type == 1u {
        p.color = vec3<f32>(1.0, 0.1, 0.1); // Red
    } else {
        p.color = vec3<f32>(0.2, 0.4, 1.0); // Blue
    }
"#.to_string()))
}

Use Cases

ScenarioTypesInteractions
Predator-PreyPredator, PreyChase/Evade rules
InfectionHealthy, Infected, RecoveredConvert rules for spread
Charged ParticlesPositive, NegativeOpposites attract, same repels
Food ChainPlant, Herbivore, CarnivoreEach level hunts the one below
TeamsTeam A, Team BSame team coheres, enemies separate
Life StagesYoung, Adult, ElderConvert based on age

Performance Note

Typed rules add conditional checks inside the neighbor loop. For best performance:

  • Use fewer distinct types when possible
  • Group related type interactions
  • Consider if untyped rules with Custom code might be simpler