0
\$\begingroup\$

I'm trying to make a dynamic buff system, where each component is manipulating the base value of the skill. Those components should be executed in a certain order but the order within the same type does not matter.

All modifiers have the same basis.

public interface IBaseMod {
    float ApplyModifier(float original);
}

There exists for now three different layers that are build on this.

public interface IPlusMod: IBaseMod {}
public interface IPMultMod: IBaseMod {}
public interface IPCapMod: IBaseMod {}

Those are used for the actual implementation, namely

public class Add35: MonoBehaviour, IPlusMod {

   public float ApplyModifier(float original) {
       return original + 35;
   }

}

public class Mult2: MonoBehaviour, IMultMod {

   public float ApplyModifier(float original) {
       return original * 2;
   }

}

public class Cap50: MonoBehaviour, ICapMod {

   public float ApplyModifier(float original) {
       return original <= 50 ? original : 50;
   }

}

I have a test skill that has those three components

Skillsetup

Now within the skill I would like the access the implementation of the modifier in a defined way.

public class TestSkill: MonoBehaviour {

    public void Start() {
        Test1();
        Test2();
        Test3();
    }

    private void Test1() {
        float baseValue = 5f;
        foreach (var mod in GetComponents<IBaseMod>()) {
            baseValue = mod.ApplyModifier(baseValue);
        }
        Debug.LogFormat("Test1 returns {0}", baseValue);
    }
    
    private void Test2() {
        float baseValue = 5f;
        foreach (var mod in GetComponents<IPlusMod>()) {
            baseValue = mod.ApplyModifier(baseValue);
        }
        foreach (var mod in GetComponents<IMultMod>()) {
            baseValue = mod.ApplyModifier(baseValue);
        }
        foreach (var mod in GetComponents<ICapMod>()) {
            baseValue = mod.ApplyModifier(baseValue);
        }
        Debug.LogFormat("Test2 returns {0}", baseValue);
    }
    
    public interface[] order3 = new interface[]{IPlusMod, IMultMod, ICapMod};
    
    private void Test3() {
        float baseValue = 5f;
        foreach (var modType in order3) {
            foreach (var mod in GetComponents<modType>()) {
                baseValue = mod.ApplyModifier(baseValue);
            }    
        }
        Debug.LogFormat("Test3 returns {0}", baseValue);
    }

    enum MODTYP {CAP, MULT, ADD};
    private MODTYP[] order4 = new MODTYP[3] {MODTYP.ADD, MODTYP.MULT, MODTYP.CAP};
    private IBaseMod[] helper(MODTYP modtyp) {
        switch (modtyp) {
            case MODTYP.ADD: {
                return GetComponents<IPlusMod>();
            }
            case MODTYP.MULT: {
                return GetComponents<IMultMod>();
            }
            case MODTYP.CAP: {
                return GetComponents<ICapMod>();
            }
        }
        return null;
    }

    private void Test4() {
        float baseValue = 5;
        foreach (var modType in order4) {
            foreach (var mod in helper(modType)) {
                baseValue = mod.ApplyModifier(baseValue);
            }
        }
        Debug.LogFormat("Test4 returns {0}", baseValue);
    }

    public Type[] order5 = {typeof(IPlusMod), typeof(IMultMod), typeof(ICapMod)};        
    private void Test5() {
        float baseValue = 5f;
        foreach (var modType in order5) {
            foreach (var mod in GetComponents(modType)) {
                baseValue = ((IBaseMod) mod).ApplyModifier(baseValue);
            }    
        }
        Debug.LogFormat("Test5 returns {0}", baseValue);
    }
}

The return value for Test1() is 45 and for Test2() is 50. I would like to have the result of Test2() in a way that is not hard coded (those 3 interfaces are for now only an example, I would like to be able to change the order dynamically at runtime and not always have all of them). With Test4() the order can be changed at runtime with the help of an enum that defines the order and a helper class that has a case for each of the enum and a corresponding GetComponents. That still feels a bit off.

If possible I would like to have a logic similar to Test3() where I can store the order in an array or list, iterate over it and use that as the type for GetComponents. When possible I would like to avoid that the interface itself has the order hardcoded.

Test3() would be the solution I would like to have but it fails in placing the interfaces into an array and/ or using the stored interface from the array as an option to place as type for GetComponents<InterfaceFromArray>. Test3() does not compile as is.

The latest Test5() is using the cast with typeOf on the interface to store it and using it to get the component. The only last leftover drawback is, that the return value from the inner foreach is returning a component and it needs to be cast back to the interface.

\$\endgroup\$
3
  • \$\begingroup\$ Did you try writing a version of Test 3 with System.Type[] and the GetComponent(Type type) overload? \$\endgroup\$
    – DMGregory
    Oct 9, 2022 at 20:46
  • \$\begingroup\$ Yes, using the the cast to type solves the GetComponents part. Is there a reverse option to not need to use baseValue = ((IBaseMod) mod).ApplyModifier(baseValue); the IBaseMod but the corresponding interface? As it is this is still the best solution since the interface is only used as a helper but if there is something added in the future \$\endgroup\$
    – Zibelas
    Oct 9, 2022 at 21:24
  • \$\begingroup\$ I'd say your Test 5 version is worth posting as an answer, then. There's not much getting around that cast - you could write it differently using LINQ, but it would still be there. \$\endgroup\$
    – DMGregory
    Oct 9, 2022 at 21:55

1 Answer 1

0
\$\begingroup\$

You can't do this in C#

public interface[] order = new interface[]{IPlusMod, IMultMod, ICapMod};

private void Test3() {
    float baseValue = 5f;
    foreach (var modType in order) {
        foreach (var mod in GetComponents<modType>()) {
            baseValue = mod.ApplyModifier(baseValue);
        }    
    }
    Debug.LogFormat("Test3 returns {0}", baseValue);
}

But you should be able to do something like this:

// This defines the order of "component getters".
// It's just an array of lambdas - inline functions of the form 
// (parameter list) => function_body
//
// Func<IBaseMod[]> just means: a function that takes 
// no params, and returns an array of IBaseMod objects.
// Func<IBaseMod[]>[] is an array of such functions.
private Func<IBaseMod[]>[] modGetters = new Func<IBaseMod[]>[] {
    () => (IBaseMod[])GetComponents<IPlusMod>(), 
    () => (IBaseMod[])GetComponents<IMultMod>(), 
    () => (IBaseMod[])GetComponents<ICapMod>() 
};

// So, to summarize, you're just sticking some functions in 
// an array, so that you can iterate through it later, and
// invoke (call) those functions dynamically based on some game logic.
// For more info, look up Func and Action delegates. 

private void Test3() {
    float baseValue = 5f;

    foreach (var getMods in modGetters) {
        foreach (var mod in getMods()) {
            baseValue = mod.ApplyModifier(baseValue);
        }
    }

    Debug.LogFormat("Test3 returns {0}", baseValue);
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .