Idiomatic way to implement shared state and behavior?

I am building a falling sand simulation where I want to have different types of sand that each have different properties.

Every instance of a sand particle should have its own velocity, but it's color should be constant/allocated once for a specific type of sand.

I wrote an MVP below where I solved this problem using functions but unsure if it's idiomatic or performant.

I have a feeling that to solve this properly I should be using different structs for each of the BlockKind types, then I could benefit from the Stack Overflow answer here, but I'd appreciate direction/review.

Thanks for your help. :slight_smile:

enum BlockKind {
    Empty,
    Concrete(Block),
    Water(Block),
}

struct Block {
    velocity_x: i32,
    velocity_y: i32,
}

impl Block {
    fn new() -> Self {
        Block {
            velocity_x: 0,
            velocity_y: 0,
        }
    }
}

impl BlockKind {
    fn color(&self) -> Rgb<u8> {
        match self {
            BlockKind::Concrete(_) => Rgb([90, 90, 90]),
            BlockKind::Water(_) => Rgb([0, 0, 255]),
            _ => Rgb([0, 0, 0]),
        }
    }
}

There is absolutely nothing wrong with handling the color that way, as long as you don't want to be able to change the color scheme while the program is running. (And if you do, you'd just write the color function to take a second “color table” parameter.)

However, you might want to change your structure a little bit:

enum CellState {
    Empty,
    Filled(Block),
}

struct Block {
    kind: BlockKind,
    velocity_x: i32,
    velocity_y: i32,
}

enum BlockKind {
    Concrete,
    Water,
}

The advantage of this layout is that you only have to handle Block once, rather than once for each kind that has one.

Also, you might want to use a smaller type than i32 for your velocities — i8 if you can pull it off. In a falling sand simulation or other things with huge numbers of entities, you want to make each one as cheap as it can possibly be, and using fewer bytes means they can be packed closer together in memory, which means more cache hits and more possible automatic SIMD. If you use i8 then each particle will take 3 bytes instead of 12.

5 Likes

Thanks for the help!! I saw this pattern (enum as a field) in the enums section of the book and thought it was discouraged, but I guess it was just not appropriate for their specific example. I see the advantage of reducing code-duplication by using this approach.

Also, I agree that using i8 is the way to go and I'll make that change too.

1 Like

Like @kpreid showed, any properties that are shared between all enum variants can be extracted into the containing struct.

Alternatively you could implement accessors like so:

enum X {
    A { x: i32 },
    B { x: i32, y: i32 },
}

impl X {
    fn x(&self) -> i32 {
        match *self {
            Self::A { x } => x,
            Self::B { x, .. } => x,
        }
    }

    // Note that since X does not contain y we use an Option<i32> to
    // represent the possibility of y not being available.
    fn y(&self) -> Option<i32> {
        match *self {
            Self::A { .. } => None,
            Self::B { y, .. } => Some(y),
        }
    }
}

If you need to mutate the fields, you can provide setters or mutable accessors. The above approach might benefit from some code generation through macros.

1 Like

Enum-as-struct-field is not an anti-pattern in itself.

1 Like

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.