Structs,Traits,Associated Types and Inheritance

Hello All. It has been a minute but I am back to trying to learn Rust and am making a small text rpg to help this process. I have noticed that as a Javascript guy, that I have gotten perhaps a little too comfortable with the idea of inheritance and OOP, too comfortable because this does not work with Rust. I do kind of understand that the OOP side of Rust utilizes the "has-a" vs "is-a" relationship, meaning composition. From the reading Ive done, apparently traits are kind of like interfaces for other OO languages, which I have no experience using. Im posting this looking for a bit of direction and advice on how to go about, or the correct way to build with Rust. Being that im working on a small text rpg, I will use "Weapons" as the working example. Currently I have it setup like this:

struct BaseWeapon {
    name:String,
    weapon_type:WeaponTypes,
    damage_type:DamageTypes,
    min_damage:u8,
    max_damage:u8,

}

enum CommonWeapons {
    
}

impl Weapon for CommonWeapons {
  type BaseWeapon = BaseWeapon;
    fn new(name:String,damage_type:DamageTypes,weapon_type:WeaponTypes,min_damage:u8,max_damage:u8) -> Self::BaseWeapon {
        BaseWeapon {
            name,
            damage_type,
            weapon_type,
            min_damage,
            max_damage
        }
    }
}

trait Weapon{
  type BaseWeapon;
    fn new(name:String,damage_type:DamageTypes,weapon_type:WeaponTypes,min_damage:u8,max_damage:u8) -> Self::BaseWeapon;
}

enum WeaponTypes {
    Common,
    Martial,
    Exotic
}

enum DamageTypes {
    Slashing,
    Piercing,
    Bludgeoning,
    Fire(u8),
    Ice(u8),
    Magic(u8),
    Electric(u8),
    
}

fn main(){
    
}

I guess I just want some input or constructive criticism on how to make this better. As of now the enum CommonWeapons variants are kind of unusable which leads me to believe that im probably not doing something the best it can be. Thanks in advance.

Please format your code, otherwise it's difficult to read:

Thank you for that!

1 Like

You may find the Roguelike tutorial or the author's recently released book Hands-on Rust of interest. I don't know much about gamedev, but my impression is that OOP is not that well suited for gamedev even though it may seem like it. Most Rust game engines seem to lean heavily towards ECS.

As an aside, the naming convention in Rust is to name enums like CommonWeapon or or WeaponType rather than the plural. I guess you could say that being an enum already implies that there are multiple variants. It's also a good idea to run cargo fmt on your code to make it easier to read, as it will format the code according to common style guidelines.

1 Like

You don't need a trait at all in this case; something like this should work just fine:

struct Weapon {
    name: String,
    weapon_type: WeaponType,
    damage_type: DamageType,
    min_damage: u8,
    max_damage: u8,
}

impl Weapon {
    pub fn new(
        name: String,
        damage_type: DamageType,
        weapon_type: WeaponType,
        min_damage: u8,
        max_damage: u8
    ) -> Self {
        Self { name, damage_type, weapon_type, min_damage, max_damage }
    }
}

enum WeaponType {
    Common,
    Martial,
    Exotic,
}

enum DamageType {
    Slashing,
    Piercing,
    Bludgeoning,
    Fire(u8),
    Ice(u8),
    Magic(u8),
    Electric(u8),   
}

fn main() {
    let my_weapon = Weapon::new(
        "pool noodle".to_owned(),
        DamageType::Bludgeoning,
        WeaponType::Exotic,
        1,
        5,
    );
}

The usual motivation for defining your own trait is wanting a function to be able to accept different types of data without caring about the exact type of data passed in—only that it implements a small set of behaviors. But you can get very far writing Rust without needing to define any traits yourself, and I would suggest sticking with concrete structs and enums as a beginner. These are the bread and butter of Rust programming.

Right, an empty enum is almost never what you want. enum CommonWeapon {} means "CommonWeapon is a type that has no values at all", so you can never use CommonWeapon to do actual computation. Such "uninhabited" types are sometimes useful, but probably not for a beginner application.

1 Like

Thanks for the suggestion, i got the site bookmarked and will take a look! Also thank you for the style corrections. I was actually on the playground while playing with the code so rustfmt wasnt doing its thing. However, its good to be aware of those kinds of things and to not have to rely on formatting tools so thx for pointing that out. I went plural for the exact reason you mentioned, so...

Thanks for the reply and your time. Part of the reason I made it the way i did was because i listened to a podcast about traits and associated types and wanted to try to impl this myself. I can see where and why the corrections you made make better code. For funzies tho, would you mind perhaps spinning something up where traits would be preferred and perhaps an instance where assoc types could be used properly?

I have a ready-made example for that: interacting with an HTTP service. There, every request performs the same kind of boring work: de/serializing JSON, sending and receiving bytes, setting HTTP headers, etc. So it would be nice to write this repetitive skeleton only once, and then make pure data request objects that would only provide the necessary body, parameters, headers, etc. to a generic send() method.

Then, the question arises: how should these pure data request objects be implemented? They could be an enum, but that results in a gigantic enum of maybe several dozen variants, none of which has anything to do with the others. This seem like strong coupling and a violation of the single responsibility principle.

So what I usually do instead is define a Request trait, which then unrelated types can implement independently. This in turn has a Response associated type, which tells the deserializer in send() what type it should produce. This results in a completely decoupled, non-redundant, and strongly-typed design, effectively an RPC interface, wrapping the web service.

An earlier, different exaplanation of mine can be found in this URLO thread.