A non-redundant way to extend multiple structs in the same way

I have a bunch of structs that represent certain objects in a game world. E.g.:

pub struct Player {
    pub pos: Vec2,
    pub vel: Vec2,
}

Instances of these structs should be synchronized between network clients, for which they have to have a unique identifier, i.e. an additional field id: u32.

In other languages I might do this via inheritance or interfaces, but I can't think of any "clean" way to do this in Rust. The two options I can think of are:

1) Trait

trait NetId {
    fn id(&self) -> u32;
}

pub struct Player {
    //[...]
    net_id: u32,
}

impl NetId for Player {
    fn id (&self) -> u32 { self.net_id }
}

2) Wrapper

struct NetObj<T> {
    id : u32,
    obj : T,
}

The first option requires me to again and again write the same code for every struct that should implement the NetId trait, which isn't nice, while the second option requires an additional level in indirection to access all game objects throughout the whole project, which is highly inconvenient.

Is there a way to do this cleanly? I.e. in a way that attaching the networking functionality to a struct doesn't interfere with other places in the code, while also not requiring significant code duplication.

Not sure how much it helps, but for option 2 you would likely implement Deref and DerefMut to help with churn elsewhere in your codebase. (still have the indirection, but easier to manage)

2 Likes

It's hard to give a concrete recommendation because a lot will depend on the rest of your code and how you use/create/store entities in your game.

If you are concerned about having to manually rewrite the exact same code you can use a macro_rules! macro.

trait NetId {
    fn id(&self) -> u32;
}

macro_rules! impl_net_id {
  ( $(type:ty),* $(,)? ) => {
    $(
        impl NetId for $type {
          fn id(&self) -> u32 { self.net_id }
        }
      )*
  }
}

impl_net_id!(Player, Monster, Boss, Item);

Alternatively, you could store the ID separately from the entity's data, similar to what an ECS does.

struct Player {
  health: u32,
}

struct World {
  next_id: u32,
  players: HashMap<u32, Player>,
}

impl World {
  pub fn add_player(&mut self, player: Player) -> u32 {
    let id = self.next_id;
    self.next_id += 1;
    self.players.insert(id, player);
    id
  }
}

This also gives you the benefit that you have a single ID which can be used to refer to this Player from anywhere without needing something like Rc<RefCell<Player>>.

2 Likes

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.