Modifying items inside a collection while simultaneously iterating over it

I have a game board that contains up to n allieds, which are stored in an array. Each of these units has an ability that can target itself or allies (or enemies, but I left those out for now).

#[derive(Debug)]
pub struct Board {
    /// Allied units on the field. None means no unit is at that given position
    allies: [Option<BoardUnit>; 6],
   
    // snip...
}

The BoardUnit struct just stores the current state of each unit like health, stamina, etc. It also stores what GameplayEffects are applied to the unit. These are lightweight structs that contain changes in a unit's state which are applied to the unit at certain points in time (start, immediate end of turn).

#[derive(Debug, Default, Clone)]
pub struct BoardUnit {
    pub id: UnitId,
    pub health: UnitStat,
    // snip...
    pub current_effects: Vec<GameplayEffect>,
}

Now here's my problem: Every turn I iterate over each unit (allies only for now) and each of those uses an ability that can apply one or more effects to itself or other units.
Naturally Rust's borrowing rules prevent me from simply using a range based for loop, so I went doing it like this:

for i in 0..self.allies.len() {
    let unit = self.allies[i].as_mut();
    let Some(unit) = unit else {
        continue;
    };

    // Do some ability related stuff here and create the effect that is applied to the target
    let effect = GameplayEffect::default();
    match ability.target {
        Caster => {
            unit.current_effects.push(effect);
        }
        Ally(multi) => {
            match multi {
                Single =>todo!(), // Single random ally, might also be the caster
                Multi { min, max } => todo!(),  // Between min and max random allies, might also be the caster
                All => todo!(),  // All allies on the board, including the caster
        }
    }
}

The problem is the application of the effect. The case where the ability target is the caster itself or the target is a single ally and the random roll chose the caster can be handled by using the existing mutable reference (I did this by randomly generating an index and compare it to i, and if it's the same I used the existing reference). However, when other units are affected I run into problems. To change them I'd need to get another mutable reference to the alliesvector, which I isn't allowed by the borrowing rules.

Now, I read about interior mutability -- would that the the appropriate solution here? Or do I need to rethink my architecture?

Copy out of unit what you need might be quickest and easiest (Depends on data.) Once your done with unit you then have full access.

Interior mutability can add overhead, just depends how much you want performance and simplicity.

I don't really need anything from the unit itself. Changes to them are happening when an effect is resolved, i.e. after what I want to do already happened.

Basically it's: iterate over all units, check which ability they are using in the current turn and then apply the effects to all relevant units. Do some housekeeping and then resolve the effects that need to be resolved.

For the code you show rust will force you to have unit dropped, it does it automatically for some reference types that don't implement Drop once not in use. Placing a second copy inside the variant is enough.

Caster => {
    let unit = self.allies[i].as_mut();
    unit......

p.s. Likely gets optimised out.

You could use this function (slice - Rust) to split the slice before i and after i. This would give you three slices with length i, 1, n-i-1 respectively. Using these you should be able to get a mutable reference to any random element while also having a mutable reference to element i (reusing that reference of course).

Finishing touches could include putting this logic into an iterator that takes a slice and produces items of type (&mut [T], &mut T, &mut[T]).

I don't think you want to go down the path of interior mutability. For me, adding interior mutability to a type means my architecture has changed so now we've got shared ownership - i.e. there's been a fundamental change in my application's ownership story. Adding it just to work around some local coding problem tends to be a code smell.

One technique I've seen is to split the "what needs to change" and "let's apply that change" work into separate stages. For example, if only a couple things need to change in every pass, you could create a queue of "operations" that get constructed during the initial loop then applied in a subsequent loop.

Another trick is to make your code more functional and do current_board + effect -> new_board. Depending on the scale and size of data we're talking about, creating a new state of the world after every tick might be feasible.

Mutation during loops often results in an addition/removal messing up your indices, so many Entity-Component-System architectures deal with by abstracting out an ID for each entity and storing data in a very clever HashMap<EntityId, BoardUnit>. You don't necessarily need to use an ECS, but maybe looking at examples from an ECS like legion could give you inspiration?

1 Like

The board is designed in such a way that the arrays containing of allies and enemies is fixed to six each and the only dynamic thing there is whether or not a unit is present, which I express through Option<BoardUnit>

That was an approach I looked into as well and it would work since the datasize is very small, i.e. I would copy the unit arrays, work on that and then replace the original array with the changed one.

Interesting, I need to look into this a bit more.

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.