Looking for a better way to structure this

Hi, I'm working on a game and I was wondering whether there was a better way to structure what I have so far:

struct Actor {
    pos: Vec2,
    vel: Vec2,
    color: Color,
}

impl Actor {
    fn update(&mut self, dt: f32) { ... }

    fn draw(&self, window_info: &WindowInfo) { ... }
}

struct Player {
    actor: Actor,
}

impl Player {
    fn update(&mut self, dt: f32) {
        self.actor.update(dt);
    }

    fn draw(&self, window_info: &WindowInfo) {
        self.actor.draw(window_info);
    }

    fn get_input(&mut self, dt: f32) { ... }
}

struct Enemy {
    actor: Actor
}

impl Enemy { 
    fn update(&mut self, dt: f32) {
        self.actor.update(dt);
    }

    fn draw(&self, window_info: &WindowInfo) {
        self.actor.draw(window_info);
    }
}

This definitely doesn't seem like a very rusty solution; it feels like a workaround to get inheritance-like behavior. But, so far, this is the only way I could find to share code between similar objects.

Thanks

You could nest the other way up:

struct Actor<T> {
    pos: Vec2,
    vel: Vec2,
    color: Color,
    extra : T,
}

impl<T> Actor<T> {
    fn update(&mut self, dt: f32) {
        //...
    }

    fn draw(&self, window_info: &WindowInfo) { 
        //...
    }
}

struct PlayerData;
type Player = Actor<PlayerData>;
impl Player {
    fn get_input(&mut self, dt: f32) { 
        //...
    }
}

struct EnemyData;
type Enemy = Actor<EnemyData>;

Of course, this won't work if you need to mix Enemys and Players together in a single collection of Actors, but neither will your original code. General code for working with Actors can be made generic on T in the approach above, but if you want to be able to check whether you have a Player at runtime, you can have an enum instead of T and match on it at runtime:

struct PlayerData;
struct EnemyData;

enum ActorType {
    Player(PlayerData),
    Enemy(EnemyData),
}

struct Actor {
    pos: Vec2,
    vel: Vec2,
    color: Color,
    actorType : ActorType,
}

impl Actor {
    fn update(&mut self, dt: f32) {
        //...
    }

    fn draw(&self, window_info: &WindowInfo) { 
        //...
    }
    
    fn get_input(&mut self, dt: f32) { 
        if let ActorType::Player(ref mut data) = self.actorType {
            //...
        }
    }
}
2 Likes

you can sort of simulate ~inheritance~

use core::any::TypeId;

use core::any::Any;

struct WindowInfo;

#[derive(Default)]
struct Actor {
    pos: [f32; 2],
    vel: [f32; 2],
    color: [u8; 3],
}

impl Actor {
    fn update(&mut self, dt: f32) {}

    fn draw(&self, window_info: &WindowInfo) {}
}

trait Entity: Any {
    fn actor(&self) -> &Actor;
    fn actor_mut(&mut self) -> &mut Actor; 

    fn update(&mut self, dt: f32) {
        self.actor_mut().update(dt)
    }

    fn draw(&self, window_info: &WindowInfo) {
        self.actor().draw(window_info)
    }
}

// Need a bit of unsafe but it's copied straight from `core::any::Any`
impl dyn Entity {
    pub fn is<T: Any>(&self) -> bool {
        // Get `TypeId` of the type this function is instantiated with.
        let t = TypeId::of::<T>();

        // Get `TypeId` of the type in the trait object (`self`).
        let concrete = self.type_id();

        // Compare both `TypeId`s on equality.
        t == concrete
    }
    
    pub fn downcast_ref<T: Entity>(&self) -> Option<&T> {
        if self.is::<T>() {
            // SAFETY: just checked whether we are pointing to the correct type, and we can rely on
            // that check for memory safety because we have implemented Any for all types; no other
            // impls can exist as they would conflict with our impl.
            unsafe { Some(&*(self as *const dyn Entity as *const T)) }
        } else {
            None
        }
    }
    
    pub fn downcast_mut<T: Entity>(&mut self) -> Option<&mut T> {
        if self.is::<T>() {
            // SAFETY: just checked whether we are pointing to the correct type, and we can rely on
            // that check for memory safety because we have implemented Any for all types; no other
            // impls can exist as they would conflict with our impl.
            unsafe { Some(&mut *(self as *mut dyn Entity as *mut T)) }
        } else {
            None
        }
    }
}

#[derive(Default)]
struct Player {
    actor: Actor,
}

impl Entity for Player {
    fn actor(&self) -> &Actor {
        &self.actor
    }
    fn actor_mut(&mut self) -> &mut Actor {
        &mut self.actor
    }
}

impl Player {
    fn get_input(&mut self, dt: f32) {}
}

#[derive(Default)]
struct Enemy {
    actor: Actor,
}

impl Entity for Enemy {
    fn actor(&self) -> &Actor {
        &self.actor
    }
    fn actor_mut(&mut self) -> &mut Actor {
        &mut self.actor
    }
}

fn main() {
    let window = WindowInfo;
    
    // list of all the entities
    let mut entities = Vec::<Box<dyn Entity>>::new();
    
    entities.push(Box::new(Enemy::default()));
    entities.push(Box::new(Player::default()));
    entities.push(Box::new(Enemy::default()));
    entities.push(Box::new(Enemy::default()));
    
    for tick in 0..100 {
        for entity in &mut entities {
            /// if the entity happens to be a player
            if let Some(player) = entity.downcast_mut::<Player>() {
                player.get_input(1.0 / 60.0);
            }
            
            entity.update(1.0 / 60.0);
            
            entity.draw(&window);
        }
    }
}

If this suits your needs, go ahead and use it as-is. There isn't really an issue with this IMO, at least not in terms of architecture. As you've discovered for yourself, composition is quite capable of modeling at least some forms of inheritance.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.