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:
Unfortunately since we need the Icon and Description be fields to be serialized we had to explicitly implement the interface and wrap those fields.
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:
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 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.