1

I am trying to build a entity component system as part of my journey to learn Rust. I had an idea where each component would have a static id, and objects would have a HashMap of the components it contains (with the limit of one component per type).

Here is the object itself:

pub struct Object {
    // TODO : components !
    components: HashMap<i32, Box<dyn Component>>
}

impl Object {
    pub fn add_component<C: Component>(&self) {
        self.components.insert(C::id(), Box::new(C::new()));
    }

    pub fn get_component<C: Component>(&self) -> Option<&C> {
        return self.components.get(C::id())
    }
}

And here is my Component trait:

pub trait Component {
    fn id() -> i32 {
        // one must ensure this returns different id for every component
        return 0;
    }

    fn new<C: Component>() -> C;

    fn require_rendering(&self) -> bool {
        return false;
    }

    fn some_function_on_component(&mut self) {
        // do something on the component 
    }
}

Unfortunately, I get this error : "this trait cannot be made into an object... ...because associated function id has no self parameter"

Could anyone explain why does this not work, and how to work around it?

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
  • 2
    How is hardcoding 0, instead of returning a unique value you get from inspecting `self`, compatible with ensuring that each component has a different id? – Charles Duffy Apr 23 '22 at 23:47
  • It will still now work in this case because `new()` is generic. `where Self: Sized` may work. – Chayim Friedman Apr 24 '22 at 00:04
  • 1
    The `new` function should probably be `fn new() -> Self;`, unless you intend for every possible `Component` implementation to serve as a potential factory for every other possible implementing type. – cdhowie Apr 24 '22 at 00:07
  • And this is not object safe either. – Chayim Friedman Apr 24 '22 at 00:10
  • @CharlesDuffy Reading the rest of the code, I think the idea is that each _type_ has a unique ID. – cdhowie Apr 24 '22 at 00:10
  • 1
    @ChayimFriedman I did not say that it was, only pointing out the conceptual problem with the generic argument. – cdhowie Apr 24 '22 at 00:11
  • This is not completely related to your question, but rather to your general purpose: IIRC, ECS tend not to store components within entities (if that's what your `Object` is supposed to represent), but rather have a single big table that maps entities to their components. In this model, an entity is just an identifier to an entry in that table. – jthulhu Apr 24 '22 at 05:52
  • @BlackBeans Thanks for the info ! but does that implies that entities have all the data ? for exemple, if I want a 'Mesh' component, containing vertices and triangles, I'd rather keep that data in the component struct instead of the entity itself. – LucioleMaléfique Apr 24 '22 at 06:00
  • @CharlesDuffy I'm not sure to understand what you meant ? the idea was that each type of component imlementing 'Component' would have a hard-coded different id when implementing that function 'id' – LucioleMaléfique Apr 24 '22 at 06:01
  • @LucioleMaléfique I think you misunderstand. He is talking about your map, if I understand correctly. He means that instead of storing the components in a map of your 'object' class, the map in your object class should only store references to the components. The components itself should be in a big table, one table per component type. I don't think it's relevant for you now at the beginning. But it enables looping over, say all "position/velocity" components easily and quickly for physics updates. – Finomnis Apr 24 '22 at 13:50

1 Answers1

2

Most of those are fairly easily fixable by following the compiler messages.

The biggest hurdle I came across while trying to get your code to compile is to downcast the Box<dyn> back to its original type.

That's my attempt, I have no idea if it actually does something in the end, but at least it compiles :)

use std::any::Any;
use std::collections::HashMap;

pub struct Object {
    // TODO : components !
    components: HashMap<i32, Box<dyn Component>>,
}

impl Object {
    pub fn new() -> Self {
        Self {
            components: HashMap::new(),
        }
    }

    pub fn add_component<C: 'static + Component>(&mut self) {
        self.components.insert(C::id(), Box::new(C::new()));
    }

    pub fn get_component<C: 'static + Component>(&self) -> Option<&C> {
        self.components
            .get(&C::id())
            .map(|boxed_component| boxed_component.as_any().downcast_ref::<C>().unwrap())
    }
}

pub trait Component {
    fn id() -> i32
    where
        Self: Sized;

    fn new() -> Self
    where
        Self: Sized;

    fn require_rendering(&self) -> bool {
        return false;
    }

    fn some_function_on_component(&mut self) {
        // do something on the component
    }

    fn as_any(&self) -> &dyn Any;
}

Things I've done:

  • Add Self: Sized to id() and new(). Both of those are trait functions and therefore have to be resolved at runtime. The step of resolving the type requires something called a "vtable", which only exists on types that actually have a size. (that's at least my understanding of the problem)
  • Replace the generic parameter on new with Self, as this is what you probably actually want.
  • Add mut to self in add_component
  • Remove default implementation of id() to actually force implementing structs to overwrite it
  • Downcast the &dyn Component to the actual component type in get_component, based on this post. Of course there are probably different solutions, I just picked that one because I didn't feel like doing any further research :)

I didn't solve the problem that you currently only get non-mutable objects out of get_component. This will probably be the next thing you will tackle.

All in all, here is a minimal working example of your code with a "Name" and an "Age" component, just to demonstrate that your approach is feasible:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=5d22b64ab924394606a07a043594f608

Finomnis
  • 18,094
  • 1
  • 20
  • 27
  • 1
    Dang. I just spent part of my day to make the system works, and most of the things I have done is what you said ! Thanks a lot ! Slight modification : I've added a 'on_added' function in the component trait, if any components need to be initialized, other than in the 'new' function, and the last thing I can't do yet : I would like the 'add_component' method to return a reference to the component, but I'm not good enough with lifetimes to think of a smart way to do this. Anyways, you solved it ! Thanks again ! – LucioleMaléfique Apr 24 '22 at 17:52
  • For returning a reference from the add_component method, you should take a look at the `HashMap::entry` function. Here is [my attempt](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=916013f2d327d8fe52681750162624ac). Of course, I recommend trying it yourself first before looking at what I did. – Finomnis Apr 25 '22 at 10:45