Status Effect system with dynamic removal conditions

I’ve read a lot of guides to status systems but some were a bit over my head (like this one) and some didn’t meet my need for conditional removal of status effects (like this one). I’m digging into figuring out more about generics because that feels like the key piece I’m missing, but I also wanted to ask here if this was the right approach.

I want to be able to track a list of active status effects on a player so I can show them in the UI as well as having them implement different effects and removal conditions. For example, something like a poison effect would run for 10 seconds and deal damage each second and then remove itself—I feel good about handling this. But what if the removal condition was interacting with some object or using a specific item? This is where I’m getting hung up.

The most generic example I can think of is knock-ups. When something explodes, it knocks nearby units into the air disabling much of their functionality until they touch the ground again. How could I achieve something like this with the scriptable objects model? Here’s what I’m working with so far…

Knockback effect that applies the debuff:

public class AOEKnockback : MonoBehaviour
{
    public float knockbackForce, knockupForce, radius;
    public StatusEffect effectToApply;

    private void Knockback()
    {
        Collider[] colliders = Physics.OverlapSphere(transform.position, radius);
        foreach (Collider c in colliders)
        {
            if(c.GetComponent<UnitCore>())
            {
                Rigidbody rb = c.GetComponent<Rigidbody>();
                if(rb)
                {
                    rb.AddExplosionForce(knockbackForce, transform.position, radius, knockupForce);
                    //Apply the debuff to all affected units
                    c.GetComponent<UnitCore>().statusEffects.ApplyEffect(effectToApply);
                }
            }
        }
    }
}

A unit’s status effect manager who receives the debuff:

public class UnitStatusEffects : MonoBehaviour
{
    public List<StatusEffect> activeEffects = new List<StatusEffect>();

    public void ApplyEffect(StatusEffect se)
    {
        if (!activeEffects.Contains(se))
        {
            activeEffects.Add(se);
            //Activate countdown or listen for removal condition
            se.Apply();
        }
    }
}

The status effect scriptable object:

public class StatusEffect : ScriptableObject
{
    public Image icon;
    public string description;

    public void Apply()
    {
        //?????
    }
}

For timed effects, I’ve just been starting a coroutine within Apply() that removes the object upon completion, but with these more complex removal conditions, I’m at a loss for how to make it dynamic. In the knock-up example, the condition would be that the collider touches the ground again. Something like:

private void OnTriggerEnter(Collider other)
    {
        if(other.CompareTag("Ground"))
            //Remove the knock-up debuff
    }

So I could have a coroutine that WaitsUntil some bool is true, but I can’t wrap my head around how to wrap this piece of the puzzle within the scriptable object while making it dynamic enough to receive other kinds of removal conditions. Any guidance is greatly appreciated!

Working off of what you have… I’d do a couple things.

  1. I’d abstract the effect as a contract interface instead of explicitly as a ScriptableObject. You can still use the ScriptableObject, but this way you’re able to expand to other options as your project grows.

  2. I’d pass along handles to what entity is getting effected by the effect (and other information if so deemed necessary) during ‘Apply’.

  3. Then from within my effect I would attach temporary Components if I need to hook into special events.

For example… my contract interface:

public interface IStatusEffect
{
 
   void Apply(UnitStatusEffects effectContainer, GameObject entityRoot);
   //add other properties and the sort that might be needed for ALL statuseffects like icon/description
}

Our general purpose StatusEffect SO just needs to implement this interface:

public abstract class StatusEffect : ScriptableObject, IStatusEffect
{
   public Image icon;
   public string description;

   public virtual void Apply(UnitStatusEffects effectContainer, GameObject entityRoot);
}

(I’ve changed this to abstract just because I assume we usually need to override ‘Apply’ to actually do an effect for a specific kind of effect. I don’t know your actual design… if your base StatusEffect doesn’t need this… do it however)

And our UnitStatusEffects changes to something like this:

public class UnitStatusEffects : MonoBehaviour
{
 
    public List<IStatusEffect> activeEffects = new List<IStatusEffect>();
     private GameObject _entityRoot;
 
   void Start()
   {
       _entityRoot = Helper.GetEntityRoot(); // I generally have some script or something to flag the roots of entities... makes grabbing the scripts of the entity easier
   }
    public void ApplyEffect(IStatusEffect se)
    {
        if (!activeEffects.Contains(se))
        {
            activeEffects.Add(se);
            //Activate countdown or listen for removal condition
            se.Apply(this, _entityRoot);
        }
    }
 
   public void RemoveEffect(IStatusEffect se)
   {
       activeEffects.Remove(se);
       //optionall do other stuff like fire events or other things
   }
}

Now onto a status effect actually occuring. So here might be a poison effect:

public class PoisonEffect : StatusEffect
{
   public float DamagePerSecond;
   public int Duration;

   public overrides void Apply(UnitStatusEffects effectContainer, GameObject entityRoot)
   {
       var health = entityRoot.GetComponent<Health>(); //I don't know your entity layout, I generally have a 'Entity' script like I said... I'm generalizing here
       effectContainer.StartCoroutine(effectContainer, health); //I use the effectContainer as our coroutine hook. This is nice since if said UnitStatusEffects gets destroyed the coroutine haults
   }
 
   private IEnumerator DoEffect(UnitStatusEffects effectContainer, Health target)
   {
       int counter = this.Duration;
       while(counter > 0)
       {
           health.Strike(this.DamagePerSecond);
           yield return new WaitForSeconds(1f);
       }
     
       effectContainer.RemoveEffect(this);
   }
 
}

But you can also hook to the entityroot… like say here:

public class KnockbackStatusEffect : StatusEffect
{
   public overrides void Apply(UnitStatusEffects effectContainer, GameObject entityRoot)
   {
       var c = entityRoot.AddComponent<KnockbackStatusResolver>();
       c.StatusEffect = this;
       c.EffectsContainer = effectContainer;
       c.Health = entityRoot.GetComponent<Health>();
   }
 
   private class KnockbackStatusResolver : MonoBehaviour
   {
       public KnockbackStatusEffect StatusEffect;
       public UnitStatusEffects EffectsContainer;
       public Health Health;
     
       private float _startTime;
     
       void Start()
       {
           _startTime = Time.time;
       }
 
       void Update()
       {
           //you can use update to effect the entity like to do damage similar to poison, or whatever you want
         
           //we can even end the effect after some duration just in case:
           if(Time.time - _startTime > 10f)
           {
               EndEffect();
           }
       }
 
       void OnTriggerEnter(Collider other)
       {
           if(other.CompareTag("Ground");
           {
               EndEffect();
           }
       }
     
       public void EndEffect()
       {
           EffectsContainer.RemoveEffect(StatusEffect);
           Destroy(this);
       }
   }
 
}

And as for why the interface… lets say you wanted to change up your AOEKnockback so that you didn’t have to create this ScriptableObject. It always has the same exact effect and the SO is just extra nonsense we don’t really need (for whatever reason… like I said, I don’t know your game as of yet… just demonstrating the flexibility the interface gets you):

public class AOEKnockback : MonoBehaviour, IStatusEffect
{
   public float knockbackForce, knockupForce, radius;

   private void Knockback()
   {
       Collider[] colliders = Physics.OverlapSphere(transform.position, radius);
       foreach (Collider c in colliders)
       {
           var core = c.GetComponent<UnityCore>();
           if(core && c.attachedRigidbody) //Collider has the attached rb as a faster access
           {
               c.attachedRigidbody.AddExplosionForce(knockbackForce, transform.position, radius, knockupForce);
               //Apply the debuff to all affected units
               core.statusEffects.ApplyEffect(this);
           }
       }
   }
  
   public overrides void Apply(UnitStatusEffects effectContainer, GameObject entityRoot)
   {
       var c = entityRoot.AddComponent<KnockbackStatusResolver>();
       c.StatusEffect = this;
       c.EffectsContainer = effectContainer;
       c.Health = entityRoot.GetComponent<Health>();
   }
  
   private class KnockbackStatusResolver : MonoBehaviour
   {
       public KnockbackStatusEffect StatusEffect;
       public UnitStatusEffects EffectsContainer;
       public Health Health;
      
       private float _startTime;
      
       void Start()
       {
           _startTime = Time.time;
       }
  
       void Update()
       {
           //you can use update to effect the entity like to do damage similar to poison, or whatever you want
          
           //we can even end the effect after some duration just in case:
           if(Time.time - _startTime > 10f)
           {
               EndEffect();
           }
       }
  
       void OnTriggerEnter(Collider other)
       {
           if(other.CompareTag("Ground");
           {
               EndEffect();
           }
       }
      
       public void EndEffect()
       {
           EffectsContainer.RemoveEffect(StatusEffect);
           Destroy(this);
       }
   }
  
}

Wow @lordofduct , thanks for the detailed response! I hadn’t thought about using interfaces for this but I can really see a lot of benefits! I also really liked your terminology of “Resolver” for the component that holds the conditional removal. I took a blend of our approaches and now have scriptable objects for each different kind of status effect and a related resolver component that gets added and manages all the conditionals—it’s working surprisingly well!

In your last paragraph, you talk about removing scriptable objects in favor of interfaces altogether (at least that’s how I understood it, feel free to correct me!) but I couldn’t figure out how to make that happen. The SO needs to hold the data for things like duration, damage per tick, icon, etc. so rather than using IStatusEffect on the effects themselves, I’m using IStatusEffectResolver on all of the resolver components so I can easily clear them out with something like a cleanse spell or when the unit is defeated.

Thanks again!

I didn’t mean remove them all together.

I just meant that in situations where you don’t really need them configurable as an asset, you could just implement the interface directly.

If you need that icon/description/etc info, just define them on the interface and they’ll be available to your UI stuff.

As for properties specific to just an individual status effect, you define it on the individual status effect. And if they don’t need to be configurable as an asset, you just set those values when you create the non-SO status effect in code.

For example a poison cloud might look something like:

//this script expects a trigger collider attached
public class PoisonCloud : MonoBehaviour
{

   public float PoisonDuration;
   public float PoisonDamagePerSecond;

    void OnTriggerEnter(Collider other)
    {
       var core = other.GetComponent<UnitCore>();
       if(core)
       {
           core.statusEffects.ApplyEffect(new PoisonEffect() {
               Duration = PoisonDuration,
               DamagePerSecond = PoisonDamagePerSecond
           });
       }
    }
  
   private class PoisonEffect : IStatusEffect
   {
      public float DamagePerSecond;
      public int Duration;

      public void Apply(UnitStatusEffects effectContainer, GameObject entityRoot)
      {
          var health = entityRoot.GetComponent<Health>(); //I don't know your entity layout, I generally have a 'Entity' script like I said... I'm generalizing here
          effectContainer.StartCoroutine(effectContainer, health); //I use the effectContainer as our coroutine hook. This is nice since if said UnitStatusEffects gets destroyed the coroutine haults
      }

      private IEnumerator DoEffect(UnitStatusEffects effectContainer, Health target)
      {
          int counter = this.Duration;
          while(counter > 0)
          {
              health.Strike(this.DamagePerSecond);
              yield return new WaitForSeconds(1f);
          }

          effectContainer.RemoveEffect(this);
      }

   }

}

With this you could just attach the PoisonCloud to a GameObject with its trigger collider. No need to also create some ScriptableObject.

ScriptableObject’s are good for creating configurable data assets. But if it’s just adding an extra step, what’s the point?

This isn’t to say ScriptableObject’s are bad. Far from, I use them a lot.

It’s just the added bonus that the interface gives you. Anything can be a IStatusEffect regardless of if it’s a SO, Component, plain old Class, whatever.

Ohh I see what you’re saying now. So it seems like if I’m going to create a lot of variants of one type of status effect the SO would be more useful, but if it’s something straightforward like a knockback or poison cloud or whatever then it would just be adding extra overhead.

I don’t quite get this one though—when I try to add a property to an interface I get error CS0525, interfaces cannot contain instance fields.

Update 1: I responded too quickly! Didn’t realize you have to define get/set for interface properties.

Also, and this is only tangentially related but since you’re already familiar with my issues here: I also can’t figure out how to make a generic method in my interface. I’ve got a base StatusEffect scriptable object then when I find something I want to make a variant for, I’ll make a new SO that inherits from it. For example, I’ve got DamageOverTimeEffect that inherits from StatusEffect.

In the interface for IStatusResolver I want to have an initialization method that can accept any children of StatusEffect so I’m trying:

    public interface IStatusResolver
    {
        void Initilize<T>(UnitStatusEffects effectContainer, T statusEffect);
    }

and then in the resolver I’m trying to use

    public void Initilize<T>(UnitStatusEffects effectContainer, T statusEffect) where T : DamageOverTimeEffect
    {
//...
}

where T could be set as anything that inherits from StatusEffect. But I’m getting an error I don’t understand. CS0425:
The constraints for type parameter ‘T’ of method ‘DamageOverTimeResolver.Initilize(UnitStatusEffects, T)’ must match the constraints for type parameter ‘T’ of interface method ‘IStatusResolver.Initilize(UnitStatusEffects, T)’. Consider using an explicit interface implementation instead.

Update 2: I needed to declare the interface as generic rather than the method itself. I changed it up and now it’s working great! (At least, I think it is )

//UtilityHelpers.cs
public interface IStatusResolver<T>
    {
        UnitCore core { get; set; }
        UnitStatusEffects effectsContainer { get; set; }

        void Initilize(UnitStatusEffects effectContainer, T statusEffect);
        void Resolve();
    }

//DamageOverTimeResolver.cs
public class DamageOverTimeResolver : MonoBehaviour, IStatusResolver<DamageOverTimeEffect>
{
//...
 public void Initilize(UnitStatusEffects effectContainer, DamageOverTimeEffect statusEffect)
//...
}

//KnockbackResolver.cs
public class DamageOverTimeResolver : MonoBehaviour, IStatusResolver<KnockbackEffect>
{
//...
 public void Initilize(UnitStatusEffects effectContainer, KnockbackEffect statusEffect)
//...
}

WARNING - BIG OL’ WALL O’ TEXT COMING

This post is when you’ll learn I can type a lot…

You have to define it as a member, but not instance member, of the interface which the class would then implement. The same way we did the ‘Apply’ method:

public interface IStatusEffect
{
   Image Icon { get; }
    string Description { get; }
    void Apply(UnitStatusEffects effectContainer, GameObject entityRoot);
}

public class StatusEffect : ScriptableObject, IStatusEffect
{
   public Image Icon;
   public string Description;

   public void Apply(UnitStatusEffects effectContainer, GameObject entityRoot)
   {
       //?????
   }
 
   //explicit IStatusEffect imp
   Image IStatusEffect.Icon { get { return Icon; } }
   string IStatusEffect.Description { get { return Description; } }
}

Unfortunately since we need the Icon and Description be fields to be serialized we had to explicitly implement the interface and wrap those fields.

BUT

There is a way to forward to the underlying field of a property in the newer versions of C# available in newer versions of Unity and get the auto-implementation of the interface like Apply gets:

public class StatusEffect : ScriptableObject, IStatusEffect
{
   [file: SerializeField]
   public Image Icon { get; set; }
   [file: SerializeField]
   public string Description { get; set; }

   public void Apply(UnitStatusEffects effectContainer, GameObject entityRoot)
   {
       //?????
   }
}

Thing is, in Unity 2019 the name showing in the inspector would be weird like k___BackingField.

But in Unity 2020 it will show correctly now since Unity updated the way it gets the name for the inspector, and you’ll see ‘Icon’.

This thread talks about it:
https://discussions.unity.com/t/683762

This is because your signatures don’t match.

Ok, here’s our wall of text… I’m going to put this in spoiler tags just to uhhh, make this no so unbearable for other readers.

The interface is allowing ANY T, your concrete ‘DamageOverTimeResolver’ is contraining its version of the method to only DamageOverTimeEffect.

This means the signatures are not the same.

Think of it like this… an interface is a ‘contract’ that says “different objects of different types can be treated the same because they have similar properties/functions”.

See, for a second forget about interfaces as you define them with the “interface ISomething”. In the long long ago times before that ‘interface’ keyword came about. The word “interface” actually meant something (like when you say variable or field or property).

The interface is the “public facing members of a type”.

Say I define this class:

public class Blargh : MonoBehaviour
{
    private int _counter;

    public int Value { get; set; }
    public string Name { get; private set; }

    void Start()
    {
        this.Name = this.gameObject.name;
    }

    public void Foo()
    {
        _counter++;
        Debug.Log(Name + " : " + (_counter + Value));
    }
}

We would say the “interface” of this class is:
a read/write int named Value
a read-only string named Name
a method named Foo with no return

Note that ‘Start’ and ‘_counter’ are not part of the interface. The interface is the parts of our type/class that are accessible from outside the type. This interface is what controls how we can interact with the encapsulated parts of the type.

Think of it…

private members are encapsulated (encapsulated means enclosed, you can’t get to it because it’s enclosed)
public members are the interface (interface, as in they can be interacted with)

This being the fundamental way languages like C# and other C-like languages enforce the Object-Oriented principal of “encapsulation”.

So with that said. In steps the “interface” keyword such as in our IStatusEffect example.

These interfaces are used to define a possible interface for multiple different types. As long as those different types implement that interface (define the same public members as), then it can be arbitrarily treated as that generalized type.

This is called “polymorphism” in Object-Oriented talk. Poly meaning ‘many’, morph meaning ‘shape’. The “shape” refers to the “interface” of the “object” we’re “interacting” with to gain access to its “encapsulated state”.

Example, IEnumerable is implemented by List, Array, Queue, Stack, HashSet, and many more. We can effectively treat all of these types similarly because they all have a ‘GetEnumerator’ method defined on them. As a result we can use the in ‘foreach’ loops (which uses GetEnumerator), or call ‘GetEnumerator’ on them ourselves (which will return an IEnumerator). As a result we can define a function like so:

public static object Find(IEnumerable e, System.Func<object, bool> predicate)
{
    var en = e.GetEnumerator();
    while(en.MoveNext())
    {
        if (predicate(en.Current)) return en.Current;
    }
    return null; //none of the objects in e satisified predicate, return null
}

This function will accept ANYTHING that implement IEnumerable. Since all things that implement it are accepted in ‘foreach’ method. Note I explicitly wrote it using the GetEnumerator rather than using a ‘foreach’… foreach is just syntax sugar that the compiler unravels into this example above. This is important because I want you to see the access of generalized members of the interface for what’s to come later in the explanation.

NOW, why isn’t your generic thing not working?

Well… lets define 2 types that implement our original IStatusEffect interface:

public interface IStatusEffect
{
   Image Icon { get; }
    string Description { get; }
    void Apply(UnitStatusEffects effectContainer, GameObject entityRoot);
}

public class PoisonStatusEffect : ScriptableObject
{
   [file: SerializeField]
   public Image Icon { get; set; }
   [file: SerializeField]
   public string Description { get; set; }
 
   public int SomeFieldSpecificToPoison;

   public void Apply(UnitStatusEffects effectContainer, GameObject entityRoot)
   {
       //DO THE POISON
   }
}

public class KnockbackStatusEffect : ScriptableObject
{
   [file: SerializeField]
   public Image Icon { get; set; }
   [file: SerializeField]
   public string Description { get; set; }

   public float SomeFieldSpecificToKnockback;


   public void Apply(UnitStatusEffects effectContainer, GameObject entityRoot)
   {
       //DO THE KNOCKBACK
   }
}

Alright. Now lets define a generalized function that takes in an IStatusEffect:

public static void LogDescription(IStatusEffect effect)
{
    Debug.Log(effect.Description);
}

Pretty simple. This all works because the compiler knows that IStatusEffect has a Description property, that it’s readable, and that its type is ‘string’. That last part is important… it know what type it is.

But we can’t say do:

public static void LogDescription(IStatusEffect effect)
{
    Debug.Log(effect.SomeFieldSpecificToKnockback);
}

This fails… because the compiler doesn’t know if ‘SomeFieldSpecificToKnockback’ is a member of the passed in effect. It only knows that it’s a ‘IStatusEffect’.

Now lets do your generic constraint classes:

public interface IStatusResolver
{
   void Initilize<T>(UnitStatusEffects effectContainer, T statusEffect);
}

public class PoisonStatusResolver : IStatusResolver
{
   public void Initilize<T>(UnitStatusEffects effectContainer, T statusEffect) where T : PoisonStatusEffect
   {
       Debug.Log(statusEffect.SomeFieldSpecificToPoison);
   }
}

public class KnockbackStatusResolver : IStatusResolver
{
   public void Initilize<T>(UnitStatusEffects effectContainer, T statusEffect) where T : KnockbackStatusEffect
   {
       Debug.Log(statusEffect.SomeFieldSpecificToKnockback);
   }
}

So I’ve directly access members distinct to KnockbackStatusEffect and PoisonStatusEffect. I do this because why else would you want to be able to constraint it to those types? It’s so you can treat them as those types explicitly.

But here comes the problem.

public static void FindStatusResolverAndInitialize<T>(GameObject go, UnitStatusEffects effectContainer, IStatusEffect effect)
{
    IStatusResolver resolver = go.GetComponent<IStatusResolver>();
    resolver.Initialize<T>(effectContainer, effect);
}

Now yes, you might say “why would I write this method?” And you likely wouldn’t. BUT, it’s legal. Based on your definition of IStatusResolver and IStatusEffect, this is completely legitimate code.

And the problem is… the compiler has no idea that there is a constraint.

All it knows is we have:
1 GameObject
1 UnitStatusEffects
1 IStatusEffect
1 IStatusResolver

All it knows about IStatusResolver is that it has a function shaped:

void Initilize<T>(UnitStatusEffects effectContainer, T statusEffect);

That’s it. IStatusResolver is not constrained in any way… it takes in any object of any type, that typed along as well, but it takes ANY TYPE. Even if you constrained the interface to say ‘IStausEffect’ like so:

void Initilize<T>(UnitStatusEffects effectContainer, T statusEffect) where T : IStatusEffect;

It still would take ANY type of IStatusEffect.

Now of course this complication is maybe hard to see because of how generics work.

See generics really are a weird runtime compiling sugary bonus that falls out of the fact C# is a JIT compiled language.

See what a generic is really doing is at runtime any time you call the “Foo” with say “Foo” your Foo method gets a whole new method created that overloads it. And implements the method replacing T with ‘int’ (or whatever type).

So really when you say:

resolver.Initialize<PoisonStatusEffect(effectContainer, effect);
resolver.Initialize<KnockbackStatusEffect(effectContainer, effect);

2 distinct functions now exist on the resolver. One that takes in PoisonStatusEffect, and one that takes in KnockbackStatusEffect. All the constraint is doing is saying “I’m limiting what kind of T is available”, this way you can treat the T object as that specific type rather than generally as ANY type (with no contraint it’s basically constrained to ‘System.Object’).

To simplify this… lets take out the generic and you may better see what is going wrong.

public interface IStatusResolver
{
   void Initilize(UnitStatusEffects effectContainer, object statusEffect);
}

public class PoisonStatusResolver : IStatusResolver
{
   public void Initilize(UnitStatusEffects effectContainer, PoisonStatusEffect statusEffect)
   {
       Debug.Log(statusEffect.SomeFieldSpecificToPoison);
   }
}

public class KnockbackStatusResolver : IStatusResolver
{
   public void Initilize(UnitStatusEffects effectContainer, KnockbackStatusEffect statusEffect)
   {
       Debug.Log(statusEffect.SomeFieldSpecificToKnockback);
   }
}

This is basically what you’re trying to do, just the long way around.

And if we added my ‘IStatusEffect’ constraint it’d just change it to:

public interface IStatusResolver
{
   void Initilize(UnitStatusEffects effectContainer, IStatusEffect statusEffect);
}

Note how the implementations don’t match. IStatusResolver expects the second arg to by 1 type, but KnockbackStatusResolver expects it to be another type.

NOW.

There is technically a way to get what you want… but it’s super complicated, and honestly, isn’t worth the effort. If you want to see it, I can show you. Just ask… but I advise against it, and I’m not going to type it here because this post is MASSIVE enough already.

Instead I’ll just show you the easy way to do it.

public interface IStatusResolver
{
   void Initilize(UnitStatusEffects effectContainer, IStatusEffect statusEffect);
}

public class KnockbackStatusResolver : IStatusResolver
{
   public void Initilize(UnitStatusEffects effectContainer, IStatusEffect statusEffect)
   {
       if (statusEffect is KnockbackStatusEffect knockback)
       {
           Debug.Log(knockback.SomeFieldSpecificToKnockback);
       }
       else
       {
           //if we got here, that's weird. This means that the wrong kind of resolver was attached by the statuseffect. Some statuseffect must need to be fixed.
           Debug.LogWarning("A StatusEffect of type '" + statusEffect.GetType().Name + "' attempted to create a KnockbackStatusResolver and initialize it. Check that shit yo.");
           Destroy(this);
       }
   }
}

I hope that explained how interfaces/generics/etc works and how you’re misusing it.

Man, this is great! Thank you so much. I feel like any questions I ask at this point are due to my inability to process all that information right now. I kind of understood interfaces before but this really helped to solidify a lot!

I think the big piece I was missing was that I was trying to do IStatusResolver without using IStatusEffect which led to all my gnarly brittle code. After reading everything I’ve revamped it and it all works except one thing is confusing me still…

So I’ve got some ability that is responsible for applying the debuff. In this case, let’s say it’s an explosion that applies both knockback and poison. The ability tells UnitStatusEffects to apply this effect, part of the application process adds the resolver component, and the resolver is in charge of managing the status effect’s removal conditions. Here are the related bits of the updated code:

//UtilityHelpers.cs
    public interface IStatusEffect
    {
        void Apply(UnitStatusEffects effectContainer);
    }

    public interface IStatusResolver
    {
        IStatusEffect effect { get; set; }
        UnitStatusEffects container { get; set; }

        void Initilize(UnitStatusEffects effectContainer, IStatusEffect statusEffect);
        void Resolve();
    }

//UnitStatusEffects.cs
public class UnitStatusEffects : MonoBehaviour
{
    public List<IStatusEffect> activeEffects = new List<IStatusEffect>();
    //... I don't like that I can't see these in the inspector even with [SerializeReference] but I recently got OdinInspector so I'm sure I can figure something out!

    public void ApplyEffect(IStatusEffect se)
    {
        //...
        se.Apply(this);
    }
}

//DamageOverTimeEffect.cs (and essentially KnockbackEffect.cs)
public class DamageOverTimeEffect : StatusEffect, IStatusEffect
{
    //...
    //Just showing this guy is now implementing IStatusEffect and inheriting from StatusEffect so variants can be created as a scriptable object but also used as part of the interface
}

//DamageOverTimeResolver.cs
public class DamageOverTimeResolver : MonoBehaviour, IStatusResolver
{
    public IStatusEffect effect { get; set; }
    public UnitStatusEffects container { get; set; }
    //...
}

So here’s the confusing part. This all seems at least somewhat in alignment with what you’re saying about interfaces, but I wanted to double-check my implementation of the ability that actually applies the status effect. Let’s say I have a poison explosion that applies knockback and a poison DOT. I’ve got to add each one separately:

//PoisonExplosion.cs
public class PoisonExplosion : MonoBehaviour
{
// So I can explicitly define each property type...
    public DamageOverTimeEffect poisonEffect;
    public KnockbackEffect knockbackEffect;
//OR I can bring them both in as StatusEffects...
    public StatusEffect poisonEffect, knockbackEffect;

    //... but when it comes time to apply these effects with either approach I've got to cast them as IStatusEffect for UnitStatusEffects to know what to do with them:

    core.statusEffects.ApplyEffect((IStatusEffect)knockbackEffect);
    core.statusEffects.ApplyEffect((IStatusEffect)poisonEffect);
}

I don’t really understand why I can’t use IStatusEffects as the property type but when I try to do this it throws a ton of errors:

 [SerializeReference]
public IStatusEffect knockbackEffect, poisonEffect;

Instead I’ve got to use another property type and then cast it to be IStatusEffect when applying it because UnitStatusEffect is set up to only receive IStatusEffects. Something about it seems wrong like I shouldn’t have to cast the scriptable object OR I should be able to use the interface as a property type. This is what I’m using now and it works, but like I said I don’t get why it has to be this way.

So unfortunately you can’t use the interface as a property effect because Unity just doesn’t support serializing it. That’s a Unity limitation, rather than an interface/C# limitation. Just a quark of their serialization engine.

As for this though:

    //PoisonExplosion.cs
    public class PoisonExplosion : MonoBehaviour
    {
    // So I can explicitly define each property type...
        public DamageOverTimeEffect poisonEffect;
        public KnockbackEffect knockbackEffect;
    //OR I can bring them both in as StatusEffects...
        public StatusEffect poisonEffect, knockbackEffect;
    
        //... but when it comes time to apply these effects with either approach I've got to cast them as IStatusEffect for UnitStatusEffects to know what to do with them:
    
        core.statusEffects.ApplyEffect((IStatusEffect)knockbackEffect);
        core.statusEffects.ApplyEffect((IStatusEffect)poisonEffect);
    }

That’s peculiar. If Those types (StatusEffect, DamageOverTimeEffect, KnockbackEffect) all implement IStatusEffect, the compiler would know this and not force you to cast it.

What is the error you’re getting? Maybe there’s something off in the code that isn’t shown here in what you posted.

In tangentially related things to interfaces, if you want to see other usefulness for them. Go check out this thread I was also in today:

Oof, it’s because I didn’t have IStatusEffect on my base StatusEffect class—it was only on the children. That fixed it!

Also, this really got me thinking about using interfaces across more of my project (more refactoring, yay haha) particularly with targetable locations on the world map and hover tooltips which it looks like that other thread touches on!

Thanks for taking the time to really dig into this, I can’t tell you how much I appreciate it. If you ever get bored and want to rip apart my whole project sometime just let me know :smile: