Coding animations in Unity

We all know that adding a few animations makes a game ten times better!
But what if you are a programmer and all you can do is to write code? You can animate from code!

Principles of Animation

One of the things that you need to understand before you begin to make animations is to start with 12 principles of animation. Those were introduced in the book: “The Illusion of Life: Disney Animation”.
This is a great place to start because it gives you some idea about how to make animations in general. At this point, there is a lot of videos that feature this subject and my favorite one is by AlanBeckerTutorials and I would highly recommend watching it first. πŸ€“

Before we start coding

So to begin with our coding part let me explain how it’s going to work.
Each animation that I’m going to create will be attached to the object as a component. Based on our use case animation can be removed after some time or it can loop infinitely. As an addition, I’m going to use Mathfx script available on Unify Wiki to have an option to change a way in which element will be animated.

Implementation

To implement our animation we are going to do it in 2 parts.
First, we will need to have a base class that will handle the animation itself.

using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// Base animation class.
/// Override methods in Animation Controls region to create new animation.
/// </summary>
public class BaseAnimation : MonoBehaviour
{
    // Types of available animation curves
    public enum AnimationCurveEnum
    {
        Hermite,
        Sinerp,
        Coserp,
        Berp,
        Bounce,
        Lerp,
        Clerp
    }

    [Header("Animation Settings")]
    // Animation duration
    public float duration = 1f;
    // Animation delay
    public float delay = 0f;
    // Is animation looping
    public bool isLooping = false;

    // Should animation start playing on enable
    public bool autoPlay = false;
    // Should animation destroy itself after end of animation
    public bool autoDestroy = true;

    // Animation curve
    public AnimationCurveEnum animationCurve = AnimationCurveEnum.Hermite;

    // On finish event
    public UnityAction OnAnimationFinished;

    // Is animation playing
    protected bool isPlaying = false;

    // Internal animation timer
    protected float timer = 0f;

    #region [Animation Controls]

    /// <summary>
    /// Method called to start animating with this component.
    /// When overriding, you can treat it as Start or Awake function.
    /// </summary>
    public virtual void StartAnimation()
    {
        isPlaying = true;
    }

    /// <summary>
    /// Internal animation loop.
    /// You can override this function but I would recomment to override UpdateAnimation(float t).
    /// </summary>
    protected virtual void UpdateAnimation()
    {
        UpdateAnimation((timer - delay) / (duration));
    }

    /// <summary>
    /// Internal animation loop.
    /// You can override it and add your animation code. Comes with parameter "t" which has value from 0.0 to 1.0.
    /// </summary>
    /// <param name="t">t receives values from 0.0 to 1.0.</param>
    protected virtual void UpdateAnimation(float t)
    {
        // Here goes code for animation with t (0.0 -> 1.0)
    }

    /// <summary>
    /// Method used to pause animation.
    /// </summary>
    public virtual void PauseAnimation()
    {
        isPlaying = false;
    }

    /// <summary>
    /// Method used to stop animation.
    /// </summary>
    public virtual void StopAnimation()
    {
        PauseAnimation();
        timer = 0;
    }

    /// <summary>
    /// Method called on animation finished.
    /// </summary>
    protected virtual void FinishAnimation()
    {
        OnAnimationFinished?.Invoke();
        StopAnimation();

        if (autoDestroy)
        {
            Destroy(this);
        }
    }

    #endregion

    #region [Utils]

    /// <summary>
    /// Wrapper function to call different functions from Mathfx class.
    /// Float version.
    /// </summary>
    /// <returns>Curve Function value.</returns>
    /// <param name="start">Start value.</param>
    /// <param name="end">End value.</param>
    /// <param name="t">t receives values from 0.0 to 1.0.</param>
    protected float CurvedValue(float start, float end, float t)
    {
        switch (animationCurve)
        {
            case AnimationCurveEnum.Hermite:
                return Mathfx.Hermite(start, end, t);
            case AnimationCurveEnum.Sinerp:
                return Mathfx.Sinerp(start, end, t);
            case AnimationCurveEnum.Coserp:
                return Mathfx.Coserp(start, end, t);
            case AnimationCurveEnum.Berp:
                return Mathfx.Berp(start, end, t);
            case AnimationCurveEnum.Bounce:
                return start + ((end - start) * Mathfx.Bounce(t));
            case AnimationCurveEnum.Lerp:
                return Mathfx.Lerp(start, end, t);
            case AnimationCurveEnum.Clerp:
                return Mathfx.Clerp(start, end, t);
            default:
                return 0;
        }
    }

    /// <summary>
    /// Wrapper function to call different functions from Mathfx class.
    /// Vector2 version.
    /// </summary>
    /// <returns>Curve Function value.</returns>
    /// <param name="start">Start value.</param>
    /// <param name="end">End value.</param>
    /// <param name="t">t receives values from 0.0 to 1.0.</param>
    protected Vector2 CurvedValue(Vector2 start, Vector2 end, float t)
    {
        return new Vector2(CurvedValue(start.x, end.x, t), CurvedValue(start.y, end.y, t));
    }

    /// <summary>
    /// Wrapper function to call different functions from Mathfx class.
    /// Vector3 version.
    /// </summary>
    /// <returns>Curve Function value.</returns>
    /// <param name="start">Start value.</param>
    /// <param name="end">End value.</param>
    /// <param name="t">t receives values from 0.0 to 1.0.</param>
    protected Vector3 CurvedValue(Vector3 start, Vector3 end, float t)
    {
        return new Vector3(CurvedValue(start.x, end.x, t), CurvedValue(start.y, end.y, t), CurvedValue(start.z, end.z, t));
    }

    #endregion

    #region [Unity]

    /// <summary>
    /// Unity method called when component is enabled.
    /// Calls StartAnimation when autoPlay is enabled.
    /// </summary>
    private void OnEnable()
    {
        if (autoPlay)
        {
            StartAnimation();
        }
    }

    /// <summary>
    /// Unity method called each frame.
    /// Used for calling animation functions from [Animation Controls] region above.
    /// </summary>
    private void Update()
    {
        // Run only if animation should play.
        if (!isPlaying)
        {
            return;
        }

        // Increase internal timer.
        timer += Time.deltaTime;

        // Wait until delay.
        if (timer > delay)
        {
            UpdateAnimation();
        }

        // When timer hit value above animation duration
        if (timer > duration + delay)
        {
            // Lower timer if animation is looping
            if (isLooping)
            {
                timer -= duration;
            }
            // Or finish animation.
            else
            {
                FinishAnimation();
            }
        }
    }

    #endregion
}

This class will only be responsible for handling animation, and it will not animate anything on its own. This will be the responsibility for our second part, which is implementing animations!

using UnityEngine;

/// <summary>
/// Move To animation.
/// Component should get where it should move.
/// </summary>
public class MoveToAnimation : BaseAnimation
{
    [Header("Move To")]
    // Where it should move.
    public Vector3 targetPosition;

    // Starting position.
    private Vector3 startPosition;

    /// <summary>
    /// Initializing variables for animation.
    /// </summary>
    public override void StartAnimation()
    {
        startPosition = transform.localPosition;
        base.StartAnimation();
    }

    /// <summary>
    /// Internal animation loop.
    /// </summary>
    /// <param name="t">t receives values from 0.0 to 1.0.</param>
    protected override void UpdateAnimation(float t)
    {
        base.UpdateAnimation(t);
        transform.localPosition = CurvedValue(startPosition, targetPosition, t);
    }

    /// <summary>
    /// On finish move object to end position.
    /// </summary>
    protected override void FinishAnimation()
    {
        transform.localPosition = targetPosition;
        base.FinishAnimation();
    }
}

Now thanks to our base class it is convenient to create new animations in code! Let’s create another example!

using UnityEngine;

/// <summary>
/// Scale By animation.
/// Component should get how much to shrink or expand.
/// </summary>
public class ScaleByAnimation : BaseAnimation
{
    [Header("Scale By")]
    // How much to scale.
    public Vector3 targetOffset;

    // Starting scale.
    private Vector3 startScale;
    // Calculated end scale.
    private Vector3 targetScale;

    /// <summary>
    /// Initializing variables for animation.
    /// </summary>
    public override void StartAnimation()
    {
        startScale = transform.localScale;
        targetScale = startScale + targetOffset;
        base.StartAnimation();
    }

    /// <summary>
    /// Internal animation loop.
    /// </summary>
    /// <param name="t">t receives values from 0.0 to 1.0.</param>
    protected override void UpdateAnimation(float t)
    {
        base.UpdateAnimation(t);
        transform.localScale = CurvedValue(startScale, targetScale, t);
    }

    /// <summary>
    /// On finish scale object to target scale.
    /// </summary>
    protected override void FinishAnimation()
    {
        transform.localScale = targetScale;
        base.FinishAnimation();
    }
}

Isn’t it awesome? πŸ€“
Now it’s time to see how you can use it! There are 2 ways to do it.
First one is just to attach component in Editor and configure it there.

And the second one is to attach and configure animation from code like here:

    private void AddRotateAnimation()
    {
        // Attaching animation component.
        var rotateAnim = gameObject.AddComponent<RotateByAnimation>();
        // Set what curve to use to animate rotation.
        rotateAnim.animationCurve = BaseAnimation.AnimationCurveEnum.Hermite;
        // Set wait duration.
        rotateAnim.duration = 1;
        // Set rotation offset.
        rotateAnim.targetOffset = Vector3.up * 90;

        // Assign what should happen after animation is finished.
        // Add wait in this case.
        rotateAnim.OnAnimationFinished += AddWaitAnimation;

        // Start wait animation.
        rotateAnim.StartAnimation();
    }

With that, we have now everything to create a moving object on the scene!

Example

As mentioned a moment ago we are going to use those animations to animate the scene. I’m going to add more animation scripts to the project so I can do more in our example. These scripts are included in public repo attached to this post somewhere at the bottom.

So let’s jump into animating!

I’m going to make three different examples of how to use these animation components.

1. Attach component in the editor and configure it there:

2. Attach animation by code or even chain animations:

using UnityEngine;

/// <summary>
/// Camera Rotator.
/// This is example how you can chain few animations together
/// </summary>
public class CameraRotator : MonoBehaviour
{
    /// <summary>
    /// Unity method called on first frame.
    /// This start animation chain.
    /// </summary>
    private void Start()
    {
        AddWaitAnimation();
    }

    /// <summary>
    /// Method that add rotate animation to camera root.
    /// </summary>
    private void AddRotateAnimation()
    {
        // Attaching animation component.
        var rotateAnim = gameObject.AddComponent<RotateByAnimation>();
        // Set what curve to use to animate rotation.
        rotateAnim.animationCurve = BaseAnimation.AnimationCurveEnum.Hermite;
        // Set wait duration.
        rotateAnim.duration = 1;
        // Set rotation offset.
        rotateAnim.targetOffset = Vector3.up * 90;

        // Assign what should happen after animation is finished.
        // Add wait in this case.
        rotateAnim.OnAnimationFinished += AddWaitAnimation;

        // Start wait animation.
        rotateAnim.StartAnimation();
    }

    /// <summary>
    /// Method that add wait animation to camera root.
    /// </summary>
    private void AddWaitAnimation()
    {
        // Attaching animation component.
        var waitAnim = gameObject.AddComponent<WaitAnimation>();
        // Set wait duration.
        waitAnim.duration = 3;

        // Assign what should happen after animation is finished.
        // Add rotation in this case.
        waitAnim.OnAnimationFinished += AddRotateAnimation;

        // Start wait animation.
        waitAnim.StartAnimation();
    }
}

With all of these things done, now we should check the end result!

Now, I’m leaving this system in your hands and your wild imagination! πŸ€“

The whole code for it with example is available in my public repository. πŸ”—

Hope you enjoy it, share it with your friend, and see you next time! πŸ”₯

avatar
  Subscribe  
Notify of