1

I have a Companion trait that encompasses a base trait for Components such as Health. I store a list of Companion using trait objects because all companions must at least implement the Companion trait. However not all companions will use the subtype Health trait.

Now the heal command only accepts a list of Health traits, so I need to filter out, remap and downcast all the base Companion traits so that it supports the Health traits.

I understand this is bad design. How can I implement the same behavior without having to downcast the trait objects to a specific component? Note: I cannot have a large struct which includes all the subtypes into one type.

Here's the code I have so far:

type CompanionId = Uuid;

fn main() {
    let mut companion_storage: HashMap<CompanionId, Box<dyn Companion>> = HashMap::new();
    let companion_id: CompanionId = Uuid::new_v4();
    companion_storage.insert(
        companion_id,
        Box::new(Cheetah {
            ...
        }),
    );
    let mut player = Player {
        ...,
        companions: Vec::new(),
    };
    player.companions.push(companion_id);

    'GameLoop: loop {
        let input = poll_input().trim().to_lowercase();

        match input.as_str() {
            // TODO: Extract healing component here.
            "heal" => heal_command(companion_id, companion_storage.into_iter().filter(|(companion_id, companion)| {
                // QUESTION: How do I filter out Companions without the Health trait here so they can automatically be downcasted and mapped?
            }).collect()),
            "q" => {
                break 'GameLoop;
            }
            "s" => {
                status_command(&player, &companion_storage); // SAME PROBLEM HERE
            }
            _ => println!("Unknown command"),
        }
    }
}

struct Player {
    id: u8,
    name: String,
    companions: Vec<CompanionId>,
}

trait Companion {
    ...
}

trait Health: Companion {
   ...
}

trait Status: Health {}

struct Cheetah {
    id: CompanionId,
    name: String,
    current_health: f32,
    max_health: f32,
}

impl Companion for Cheetah {
    ...
}

impl Health for Cheetah {
    ...
}

fn heal_command(
    companion_id: CompanionId,
    companion_storage: &mut HashMap<CompanionId, Box<dyn Health>>,
) {
    let companion = companion_storage.get_mut(&companion_id).unwrap();

    companion.heal_max();

    println!("Healed to max.");
}

fn status_command(player: &Player, companion_storage: &mut HashMap<CompanionId, Box<dyn Status>>) {
    println!("Status for {}: ", player.name);
    println!("===============================");
    print!("Companions: ");
    for companion_id in &player.companions {
        let companion = companion_storage.get(companion_id).unwrap();

        print!(
            "{} [{}/{}], ",
            companion.name(),
            companion.health(),
            companion.max_health()
        );
    }
    println!();
    println!("===============================");
}

Is this code a better alternative?

type CompanionId = Uuid;

fn main() {
    let mut companion_storage: HashMap<CompanionId, Companion> = HashMap::new();
    let companion_id: CompanionId = Uuid::new_v4();
    companion_storage.insert(
        companion_id,
        Companion {
            id: companion_id,
            name: "Cheetah".to_string(),
            health: Some(Box::new(RegularHealth {
                current_health: 50.0,
                max_health: 50.0,
            })),
        },
    );
    let mut player = Player {
        id: 0,
        name: "FyiaR".to_string(),
        companions: Vec::new(),
    };
    player.companions.push(companion_id);

    'GameLoop: loop {
        let input = poll_input().trim().to_lowercase();

         match input.as_str() {
            // TODO: Extract healing component here.
            "heal" => {
                let companion = companion_storage.get_mut(&companion_id).unwrap();

                match companion.health_mut() {
                    None => {
                        println!("The selected companion doesn't have health associated with it.");
                    }
                    Some(health) => {
                        heal_command(health);

                        println!("{} was healed to max.", companion.name);
                    }
                }
            }
            "q" => {
                break 'GameLoop;
            }
            "s" => {
                status_command(&player, &companion_storage); // SAME PROBLEM HERE
            }
            _ => println!("Unknown command"),
        }
    }
}

struct Player {
    id: u8,
    name: String,
    companions: Vec<CompanionId>,
}

struct Companion {
    id: CompanionId,
    name: String,
    health: Option<Box<dyn Health>>,
}

struct RegularHealth {
    current_health: f32,
    max_health: f32,
}

trait Health {
    ...
}

impl Companion {
    fn health_mut(&mut self) -> Option<&mut dyn Health> {
        match self.health.as_mut() {
            None => None,
            Some(health) => Some(health.as_mut()),
        }
    }

    fn health(&self) -> Option<&dyn Health> {
        match self.health.as_ref() {
            None => None,
            Some(health) => Some(health.as_ref()),
        }
    }
}

impl Health for RegularHealth {
    ...
}

fn heal_command(health: &mut dyn Health) {
    health.heal_max();
}

fn status_command(player: &Player, companion_storage: &HashMap<CompanionId, Companion>) {
    println!("Status for {}: ", player.name);
    println!("===============================");
    print!("Companions: ");
    for companion_id in &player.companions {
        let companion = companion_storage.get(companion_id).unwrap();

        match companion.health.as_ref() {
            None => {}
            Some(health) => {
                print!(
                    "{} [{}/{}], ",
                    companion.name,
                    health.health(),
                    health.max_health()
                );
            }
        }
    }
    println!();
    println!("===============================");
}
TopazC
  • 11
  • 2
  • Why do you think it is a bad design? This question, as currently worded, is opinion-based and inappropriate for SO. If you have _concrete_ points you want to improve, it may suit here. But please describe exactly what is verbose/inefficient/etc. with the current design. – Chayim Friedman May 12 '22 at 13:27
  • In order to downcast, I would need to change the type for the trait object to be Any and for every function that accepts a certain downcasted trait, I would need to call .downcast_ref on the "Any" trait object inside a map and also do error handling to ensure that the subtype matches the parameter. I am looking for a better approach on binding a trait type to a function that doesn't involve downcasting. – TopazC May 12 '22 at 13:33
  • 1
    would be nice if you had added a mre instead of all your code – Netwave May 12 '22 at 13:39
  • Could you have a health type method for Companion that returns a value indicating whether the companion is eligible for healing under the current circumstances? Then you could also account for temporary modifiers like buffs/debuffs etc – MeetTitan May 12 '22 at 14:00
  • @MeetTitan I updated the main post. Is that what you were referring to? – TopazC May 12 '22 at 14:44
  • Maybe I would add to `Companion` a member `fn health(&mut self) -> Option<&mut dyn Health> { None }` and override in types that do need it to just return `Some(self)`. – rodrigo May 12 '22 at 14:48
  • you can try taking a look at https://stackoverflow.com/questions/30274091/is-it-possible-to-check-if-an-object-implements-a-trait-at-runtime. It has no accepted answer but I believe the proposed approaches are still the best the language can offer – Paolo Falabella May 12 '22 at 14:51
  • 3
    You might want to look into the Entity-Component-System pattern and associated [crates](https://crates.io/search?page=1&per_page=10&q=entity component system). – Jmb May 12 '22 at 14:55
  • @rodrigo that's a good suggestion. It makes the interface cleaner as I don't have to constantly unbox and box. – TopazC May 12 '22 at 14:59
  • @PaoloFalabella that was a good read. Rust really pushes you to write good code and I love it especially coming from C++. There are a lot of patterns and idioms I am learning that is helping me get rid of the strict OOP mindset. – TopazC May 12 '22 at 15:00
  • @Jmb I realized that I am accidentally writing a code that is very similar to entity component system. But since I am writing this project as means to learn Rust, I am probably going to skip adding ECS. But in actual projects, I will definitely use it. – TopazC May 12 '22 at 15:02

0 Answers0