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.
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:
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.