0

I am trying to make an animation system that affects different types so I thought I would use generics.

I have an animation controller that gets all animation objects into a collection (or at least that was the plan) then then run the animation on all the animation objects.

The problem is I can't do it this way in C# as it does not allow polymorphic collections for different types on generics.

Here is the code structure involved for different animations:

public abstract class Animation<T> {
    protected T TargetValue {protected set; get;}  // could be int/float/vector2 etc etc
    protected T StartValue {protected set; get;} 
    public T CurrentValue {protected set; get;} 
    public Action OnUpdate;

    public virtual T Animate(float deltaTime) => OnUpdate?.Invoke();
}

public class Fade : Animation<float> { // lerp between two floats

   public override float Animate(float deltaTime) {
      CurrentValue = float.Lerp(StartValue,TargetValue,deltaTime);
      base.Animate(deltaTime);
   }

}

public class Translation : Animation<Vector2> { // move to target Vector2
   public override Vector2 Animate(float deltaTime) {
       CurrentValue = Vector2.Lerp(StartValue,TargetValue,deltaTime);  
       base.Animate(deltaTime); 
   }
}

Now my animation controller class is supposed to get all animations an run animations on them:

public class AnimationController {
   
    private List<Animation<???>> _animations = new List<Animation<???>>();    
    void Init() => _animations = GetAllComponents<Animation>(); // get all animations as collection


    public void Animation(float deltaTime)
       for(int i = 0; i < _animations.Count; i++) 
          _animations[i].Animate(deltaTime);
    }
}

However, this is not an option for me as I cannot have collections of generics with different types for the generic T.

So what would be a better design for this system because I do not know a good solution ???

2
  • Your Animate() methods have different return types. It doesn't look like you're actually using the return values though ... can you remove it or make the return type consistent and have Animate<T> implement an interface?
    – svidgen
    Dec 18, 2020 at 3:35
  • Well the type is defined by the derived class's T type. So they only return what ever <T> will be. Which is why i chose generics.
    – WDUK
    Dec 18, 2020 at 4:18

1 Answer 1

3

Approach One

Each concrete implementation of Animation appears to be used by other classes in different ways:

  • At the minimum, Animation items need to be stored in a collection.
  • There is a need to call the Animation.Animate(float) method on each item in the collection. This caller (which processes the collection) does not need to be aware of the type of state parameters (time-varying variables).
  • Each Animation item may also be used by some other classes (which are not illustrated in the code example above). Each of these other classes make use of the state parameters (time-varying variables) of some specific concrete Animation items; therefore, they must be aware of the types.

Suggestion (C# specific. The suggestion may need to be tailored or modified for other languages.)

Firstly, define an Animation interface or abstract class that is non-generic.

public interface Animation
{
    void Animate(float deltaTime);
}

Then, define a generic Animation interface where the types of time-varying variables are specified.

public interface Animation<T> : Animation
{
    T CurrentValue { get; }
}

Finally, have your abstract classes implement Animation<T>. By doing so, they also indirectly implement Animation (the non-generic interface), which allows the AnimationController to call its void Animate(float) method even if AnimationController doesn't know the types of each item's time-varying variables.

This is a very common usage pattern in C# generics.


Summary of Approach One

In C#, it is widely recognized that generic classes may require some kind of uniform handling in which the generic parameters (the <T>) don't matter and shouldn't matter. To do so, the widely-accepted practice is to create a parent interface or parent abstract class that is non-generic, that provides limited access to the concrete classes, without the knowledge of the <T>.


Approach Two

Provide an abstraction for the concrete implementations of time-varying variables (float, Vector2, etc). Let's call it Lerpable.

The Lerpable interface will have a method called Lerp.

1
  • I use approach #1 all the time - as it turns out I was just mucking about in some code today related to that very pattern. In my case I had something like a Job<T> that depended on an expression tree of T, but then I wanted to compile the tree and invoke an IJob generically from a list.
    – J Trana
    Dec 19, 2020 at 4:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.