Enumeration problem

I have 3 classes:

Monster:

[Serializable]
public class Monster : ICloneable
{
    public string name;
  
    public int Level;
}

Shelf (only a collection of monsters and actions that will be applied to each monster before iterate):

[Serializable]
public class Shelf : IEnumerable<Monster>
{
    public List<Monster> monsters = new List<Monster>();

    // Things that will be applied to monster before iterate
    public List<Action<Monster>> modifiers = new List<Action<Monster>>();

    public IEnumerator<Monster> GetEnumerator()
    {
        foreach (var monster in monsters)
        {
            foreach (var modifier in modifiers)
            {
                modifier.Invoke(monster);
            }

            yield return monster;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Example (This is what will call things in runtime):

[ExecuteAlways]
public class Example : MonoBehaviour
{
    public Shelf shelf;

    public void Show()
    {
        foreach (var monster in shelf)
        {
            Debug.Log($"Monster: {monster.name}\tLvl: {monster.Level}");
        }
    }
}

My problem is that I want more control about how enumerations happen, for example, let’s say that I want to increase the level field of the monster if their name contains the letter A, so I could add this to the Example script:

private void OnEnable()
{
    shelf.modifiers.Add(monster =>
    {
        if (monster.name.Contains("A"))
            monster.Level += 1;
    });
}

And it works just fine, but I need to be able to add modifiers that will be based on the result of modifiers, for example, “I want to give more 2 levels to each monster if the sum of levels is greater than 10”:

private void OnEnable()
{
    shelf.modifiers.Add(monster =>
    {
        var levelSum = shelf.Sum(m => m.Level);

        if (levelSum > 10)
            monster.Level += 2;
    });
}

This will gimme a stack error because it will try to enumerate infinitely, as this kind of behavior isn’t an optional feature I came looking for any ideas or workarounds to be able to achieve this.

Thanks in advance

N

Your problem here is applying modifiers - which are changing the source data here - every time you enumerate the shelf. Generally speaking, iterators should not on their own modify the source data - if I’m someone using your classes, I would sort of expect to be able to iterate over the list 20 times and the data within the list will still be the same, unless I’ve actually done something to it while iterating.

An additional thing I would tend to assume is that modifiers are not permanent. If I remove that level bonus, I expect the level to drop accordingly.

So you need to support modifiers without applying them to your source objects every time you iterate over them. I would suggest one of 3 things:

  1. You can create a clone of the monster while iterating, and apply the modifiers to that clone, and that clone is what you “yield return”. This has the advantage that your modifier code remains clean and simple (just alter values on the monster however you like in your delegate as you are here), but cloning monsters may have bad side effects (can’t compare references, if the monsters are MonoBehaviours you’re just creating more monsters, GC allocations, etc).

  2. Return the monster along with its modifiers, and make the changes for the modifier when retrieving the value, never. Modifiers will probably have to become an abstract class with derivations, and virtual properties to retrieve every type of attribute capable of modification. This is obviously more complicated than your current setup, but it might be what is required.

  3. Tweak your modifiers to only apply once. You apply them at the moment the modifier is added and never again. If the modifiers are removable, you’ll need to add an “on remove” delegate as well to reverse the effect.

2 Likes

Tbh what I showed isn’t my actual setup, this is only a minor version that can show the problem

This is what happens in the original setup, I clone the data before operating to don’t repeat the modifiers, but this will not solve the problem, will only make sure that we don’t touch the original data with modifiers, which is good

Can you elaborate a little more you what you mean with an abstract class? I think that I don’t get in which way this kind of approach can help

This is a good option, I’ve already tried it, but the problem is that some of my modifiers have another field to decide if the modifier will or not apply, something like private List<Func<bool>, Action<Monster>> modifiers; (tbh its a dictionary with a list of actions as value and a predicate as key, but the previous example will show the idea), so I can’t apply this once as the application will depend on the user predicate.