0
\$\begingroup\$

I'm trying to write a minimal, high performance uncomplicated ECS for a simple game in which I have a good idea of all of the Entity and Component types from the get-go.

I want to take advantage of compiler optimisations such as inlining and others, therefore I would like to avoid dynamic dispatch.

To do this, I am using enums and macros to generate the static dispatch code, where dynamic dispatch might overwise be used.

See below for a minimal example:

#[derive(Debug)]
struct Health(i32);

#[derive(Debug)]
struct Damage(i32);

#[derive(Debug, Clone)]
struct Position(i32, i32);

struct Ghost {
    pos: Position,
    health: Health,
    damage: Damage
}

struct Coin(Position);

enum Entity {
    Ghost(Ghost),
    Coin(Coin),
}

macro_rules! reg_component {
    ($entity:ident with $component:ident as $fn_id:ident, $fn_id_mut:ident for $($ty:pat => $field:expr,)+) => {
        impl $entity {
            #[allow(dead_code)]
            fn $fn_id_mut(&mut self) -> Option<&mut $component> {
                #[allow(unreachable_patterns)]
                match self {
                    $($ty => Some(&mut $field),)+
                    _ => None
                }
            }
            #[allow(dead_code)]
            fn $fn_id(&self) -> Option<&$component> {
                #[allow(unreachable_patterns)]
                match self {
                    $($ty => Some(&$field),)+
                    _ => None
                }
            }
        }
    }
}

reg_component!{ Entity with Health as health, health_mut for
    Entity::Ghost(g) => g.health,
}

reg_component!{ Entity with Damage as damage, damage_mut for
    Entity::Ghost(g) => g.damage,
}

reg_component!{ Entity with Position as position, position_mut for
    Entity::Ghost(g) => g.pos,
    Entity::Coin(c) => c.0,
}

fn collision_system(_entity_pos: Position, entity: &mut Entity, others: &[Entity]) {
    //test for collision and then...
    for other_entity in others {
        //If the entity has a damage component, apply the damage_system
        match other_entity.damage() {
            Some(dam) => damage_system(dam, entity),
            None => ()
        }
    }
}

fn damage_system(damage: &Damage, entity: &mut Entity) {
    //If the entity has a health component, apply the damage
    match entity.health_mut() {
        Some(health) => health.0 -= damage.0,
        None=> ()
    }
}

fn main() {
    let mut world: Vec<Entity> = Vec::new();
    world.push(Entity::Ghost(Ghost{pos: Position(0,0), health: Health(10), damage: Damage(10)}));
    world.push(Entity::Coin(Coin(Position(0,0))));
    
    //apply systems:
    for i in 1..world.len() {
        let (current, rest) = world.split_at_mut(i);
        let current: &mut Entity = current.last_mut().unwrap();
        //If the current entity has a position component, apply the collision_system
        match current.position().cloned() { //clone position to avoid borrow of current, so it can be mutably borrowed later
            Some(pos) => collision_system(pos, current, rest),
            None => (),
        }
        //other top level systems etc...
    }
}

Am I doing anything obviously wrong here? Shooting myself in the foot? Not actually getting any performance benefits that worth it? There's a more elegant /idiomatic alternative? Please let me know if this is a good idea.

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.