Workaround for inheritance (Rust best practice?)

Hello,

I've been stuck for two days trying to find a way to implement some C++ code that makes heavy usage of inheritance. To make things easier to understand, the piece of code I'm porting is part of a small entity-component-system of my own engine.

So far the flow is that an Entity owns a ComponentList and the ComponentList owns all the Component instances.

The real problem is that the Component struct has some common behavior functions and then, in C++, I just inherit from Component and override whatever function I need (usually it's just update() and eventually render(), but that's not 100% of the cases of course). I'm omitting the rest of the code (Scene that owns an EntityList that owns all the Entity instances) as it's pretty much the same problem, just a level higher.

The current Rust layout is pretty much the following:

pub struct Entity {
    position: Vector2<f32>,
    active: bool,
    visibile: bool,
    collidable: bool,
    scene: Option<Rc<Scene>>,
    components: ComponentList,
    tags: Vec<u32>,
    colliders: ColliderList,
    actual_depth: f32,
    depth: i32,
}

pub trait EntityTrait {
    fn added(scene: &Scene);
    fn removed(scene: &Scene);
    fn awake(scene: &Scene);
    fn scene_begin();
    fn scene_end();
    fn update();
    fn render();
    fn debug_render();
    fn add(component: &Component);
    fn remove(component: &Component);
    fn get() -> Rc<Component>;
    fn add_collider(collider: &Collider);
    fn remove_collider(collider: &Collider);
    fn tag(tag: u32);
    fn untag(tag: u32);
    fn collide_check(other: &Entity);
    fn set_depth(depth: i32);
    fn get_depth() -> i32;
}

pub struct ComponentList {
    entity: Option<Rc<Entity>>,
    components: Vec<Rc<Component>>,
    to_add: Vec<Component>,
    to_remove: Vec<Component>,
    lock_mode: LockMode,
}

pub trait Component {
    fn added(entity: Rc<Entity>);
    fn removed(entity: Rc<Entity>);
    fn entity_added();
    fn entity_removed();
    fn update();
    fn render();
    fn debug_render();
    fn remove_self();
    fn get_scene() -> Rc<Scene>;
}

I thought of implementing the Component as a BaseComponent + specializations, something like:

pub struct BaseComponent {
    entity: Option<Rc<Entity>>,
    visible: bool,
    active: bool,
}

impl BaseComponent {
    pub fn new() -> Self {
        BaseComponent {
            visible: true,
            active: true,
        }
    }
}

pub struct Position {
    base: BaseComponent,
    x: f32,
    y: f32, 
}

impl Position {
    pub fn new(x: f32, y: f32) -> Self {
        Position {
            base: BaseComponent::new(),
            x: x,
            y: y,
        }
    }
}

but then I'd have to implement all of the Component trait function for all the specialized Component and it doesn't really sound like a good option. I could split up the Component trait and keep just the update() and render() functions as they're the one I usually implement, but I'd still need to implement the other trait with all the other functions at least to call the embedded BaseComponent.

I've tried lots of different approaches to find a solution but nothing that really satisfies my needs. Eventually I might just be thinking the wrong way to be able to apply composition correctly.

I could really need a hand with this :slight_smile:

2 Likes

You could have the Component trait expose the underlying BaseComponent. That way you don't need to wrap/delegate calls to BaseComponent - you just return it and let caller use it directly.

Let's say that I want my Position struct to override the fn added(entity: Rc<Entity>); from the Component trait, I couldn't do that because if I'm using the base field directly, I would never be able to call the added function on the parent.

Yeah, my reply was based on you saying update and render are what you override (basically the text I quoted).

Can you make use of default trait function impls? You can't have state (fields) in a trait, but you can have accessors that expose the bits you need to implement the other (default) functionality. You'd still need to write the accessor boilerplate, but perhaps that's significantly less than implementing the rest of the trait API.

Another option might be to "fill out" the BaseComponent with function pointers/closures that implement the functionality. That's morally equivalent to defining your own vtable, but might be more ergonomic.

I have to read up about Default trait as I don't know much about it from the impl perspective. If I understand correctly, you mean that I can impl the Default trait for my BaseComponent to fill in automatically the default values. And add getters/setters to the Component trait to pass values that need to be used in the other functions of the impl of Component, but I don't understand how that could help. Sorry but I'm still trying to understand most of the language features.

I meant to provide a default implementation of some functions in the Component trait itself. For example, see how Iterator does that - it requires an implementer to provide next but implements the rest of the functions in terms of it: https://doc.rust-lang.org/src/core/iter/iterator.rs.html#31-2165

In your case, perhaps you can require implementations to supply some (smallish) set of functions, but then implement the common pieces in terms of those.

I think vitalyd meant define the body of trait functions in the trait, so that implementing structs that don't define their own version of the trait function use that initial definition. more info here

For example...

struct Entity();

trait EntityAdder {
    fn add_entity(&mut self, entity: Rc<Entity>) {
        // we can't access the implementor's state to store entity, so lets create an interface to get it.
        self.get_default_entity_storage().push(entity.clone());
    }

    fn get_default_entity_storage(&mut self) -> &mut Vec<Rc<Entity>>;
}

struct Position {
    entities: Vec<Rc<Entity>>,
};

impl EntityAdder for Position {
    // don't need to redefine add_entity, we'll just reuse the existing definition.

    // but we do need to provide add_entity with access to our entity storage.
    fn get_default_entity_storage(&mut self) -> &mut Vec<Rc<Entity>> {
        &mut self.entities
    }
}

playground example

1 Like

Yes, exactly - thanks for elaborating @boxofrox.

1 Like

One other thing to note. Unless you want to force all entity storage to use Vec<Rc<Entity>>, only add_entity() shall call get_default_entity_storage().

This way, you can override add_entity() to use a HashMap<usize, Entity> for some other component manager (e.g Velocity), and define Velocity.get_default_entity_storage() as not_implemented!() since it'll never be used.

Cheers :grin:

1 Like

Thanks for the heads up. I tried to follow your suggestions and I ended up with yet one more problem. It looks like I can't use a trait as a type. For example I have trait Component {} but I cannot add a field of type Vec<Rc<Component>>.

Here's a playground with my current situation

I think you ran into the problem of dynamically sized types (DST). I've usually seen this solved by using Box to wrap the DST and provide itself as a Sized type, but this costs a heap allocation. I'm looking for an alternative, but not having much luck, yet.

Hmmm... the code seems a bit off to me, but it could just be my brain is muddled today.

You have an Entity that indirectly holds a list of Components via ComponentList. All concrete components extend BaseComponent and implement Component. BaseComponent holds a list of Entity.

I don't think Rust will let you create a cycle like that. The question of who owns who (Entity owns Component or Component owns Entity) never finds an answer. I think the compiler just snagged on a DST and complained about that instead.

I believe I have both problems. The compiler is complaining about DST. I tried boxing the trait but nothing changed btw.

On the other hand, Entity is the owner of ComponentList. ComponentList owns the Component instances. Each Component holds a reference to its owning Entity. I'd like to drop that but I'm not sure there would be any way to travel the other way around tbh.

You could use an Rc<...> (or an Arc<...> if you want to be multitheaded) to solve this problem the simple way. Just remember to store a Weak into the child object to avoid cycles that would lead to memory leakage.

I introduced Box and RefCell to create a version that would compile. It may work with less.

Update: Looks like Box wasn't necessary... yet.
Rust Playground

I need to study your code, in the meanwhile thanks a lot... but! :wink: how would you add the Position component to the Entity? I tried the following but I'm obviously off...

playground example

Updated example, deref isn't necessary.

Another update to include a diff of changes for clarity.

 fn main() {
     let e = Rc::new(RefCell::new(Entity::new()));
-    let p = Rc::new(Position::new(10.0, 12.0));
-    e.deref().borrow().add(&p);
+    let p : Rc<Component> = Rc::new(Position::new(10.0, 12.0));
+    e.borrow_mut().add(&p);
}

I'm not entirely happy with this solution. Casting with as doesn't work here because of the Rc<> wrapping the thing you want to treat as a trait object.

Related reading material:

https://www.reddit.com/r/rust/comments/2m98c1/why_cant_i_treat_this_struct_as_a_trait_it/

https://doc.rust-lang.org/book/first-edition/trait-objects.html#dynamic-dispatch

Thanks for all the material. I'm about to go to bed now but I'll have a look at that tomorrow.

I wonder if I could actually turn this into something either with an enum inside a struct, just like a generic data field that is actually an enum and the different values of the enum act as additional data holders.

Or another approach might be to have different pools for different components so that they do jot actually need to impl the same trait and I can avoid using the trait as type, if things can be easier that way.

Actually just brain dumping ideas... I'll have to do lots of testing or resort to a different data structure eventually.. maybe the ECS graph isn't well suited for Rust and that's it.

Object-oriented certainly doesn't translate well to Rust. There are ECS crates [1][2]. I recommend looking at their source a bit to see how they organized their data.

It looks like specs represents entities as an ID number (they tacked on a generation ID to handle ID reuse), and a number is trivial to share. It also seems entities don't know about their components. The World object is used to register entities and components and bind them using EntityBuilders... I think.

Anway, the libraries use quite a bit of advanced Rust, but among all that, you can find simple uses of Rust that stand out and might help you move in the right direction.

[1]: https://github.com/slide-rs/specs
[2]: https://github.com/HeroesGrave/ecs-rs

Here's an article on ECS [3] I found interesting some time ago.

[3]: Roguelike game architecture in Rust 3 - The entity component system | rsaarelm blog

I'll just add that it might be worth considering not using traits at all. Perhaps if you have a finite and bounded set of object types you could just create one big enum to hold them all, and then write methods on that enum. You still have a base struct with methods on it to get the code sharing that inheritance gives you.

It might be too hairy, but there is a lot to be said for keeping things explicit, and for avoiding unneeded abstraction.

2 Likes