FREE TRIAL

|

☀
☾

Custom Animations

This article contains useful tips and tricks for creating custom animations.

See the Grid Inventory, Mobile Settings Menu, and XR Hand Menu samples for examples of of custom animations in action.

See Animation Overview for details on Nova's animation system.

Serializing Animations

By adding the Serializable attribute to your animation structs, you can configure your animations in the editor. Not only does this help simplify the code responsible for running the animation, now just requiring a call to Run without needing to assign the fields explicitly, but it also enables a cleaner separation between game-logic code and visual aesthetics.

For example, the Inventory Sample (link needed) uses the following BodyColorAnimation for hover/unhover animations. By marking it as Serializable, and serializing an instance of the BodyColorAnimation in a hoverAnimation field of the InventoryItemVisuals class, the TargetColor and Target UIBlock can be configured in editor, at design time.

When an actual OnHover gesture is received, running the animation simply becomes:

hoverAnimation.Run(animationDuration);
[Serializable]
public struct BodyColorAnimation : IAnimation
{
    [Tooltip("The end body color of the animation.")]
    public Color TargetColor;
    [Tooltip("The UIBlock whose body color will be animated.")]
    public UIBlock Target;

    // not serialized
    private Color startColor;

    public void Update(float percentDone)
    {
        if (percentDone == 0f)
        {
            startColor = Target.Color;
        }

        Target.Color = Color.Lerp(startColor, TargetColor, percentDone);
    }
}

Modular Animations

Rather than creating custom animation types for every unique animation scenario, basic animations can be combined and sequenced to create more complex animations using the Include and Chain methods, respectively. Compounding simple animations increases code-reusability, avoiding the need to write new animation structs for every blend of animatable properties.

For example, say you wanted to create an animation which animated a UIBlock2D's body Color, Border Width, and then at the end disabled the target UIBlock2D GameObject. Instead of creating a single, bespoke animation which accomplishes all three of these things, the same outcome can be achieved by combining the above BodyColorAnimation with the following BorderWidthAnimation and GameObjectActiveAnimation like so:

bodyColorAnimation.Run(animationDuration).Include(borderWidthAnimation)
                  .Chain(disableGameObjectAnimation, duration: 0f);
[Serializable]
public struct BorderWidthAnimation : IAnimation
{
    [Tooltip("The UIBlock whose border will be animated.")]
    public UIBlock2D Target;
    [Tooltip("The end border width of the animation.")]
    public float TargetWidth;

    private float startWidth;

    public void Update(float percentDone)
    {
        if (percentDone == 0f)
        {
            startWidth = Target.Border.Width.Value;
        }

        Target.Border.Width.Value = Mathf.Lerp(startWidth, TargetWidth, percentDone);
    }
}

[Serializable]
public struct GameObjectActiveAnimation : IAnimation
{
    [Tooltip("The GameObject to enable or disable.")]
    public GameObject Target;
    [Tooltip("Should the Target be enabled (true) or disabled (false)?")]
    public bool TargetActive;

    public void Update(float percentDone)
    {
        // Don't care about lerping anything here, just set the active state
        Target.SetActive(TargetActive);
    }
}

IAnimationWithEvents

As discussed in the Animation Overview article, the IAnimationWithEvents interface has additional hooks which provide more information about an animation's running state. This may be useful in a variety of situations, such as if an animation needs to ensure cleanup of resources when it is canceled, a looped animation knowing when an iteration ends or begins, etc.

The following CountdownAnimation example will, given two TextBlocks, display an animated countdown timer capable of handling paused/resume/canceled states.

To run the animation for 10 seconds, simply:

    countdownAnimation.CountdownSeconds = 10;

    // Loop the animation 10 times, 1 second per iteration.
    AnimationHandle timerHandle = countdownAnimation.Loop(1, 10);

    // Will trigger CountdownAnimation.OnPaused()
    timerHandle.Pause();

    // Will trigger CountdownAnimation.OnResumed()
    timerHandle.Resume();

    // Will trigger CountdownAnimation.OnCanceled()
    timerHandle.Cancel();
using Nova;
using System;
using UnityEngine;

[Serializable]
public struct CountdownAnimation : IAnimationWithEvents
{
    public TextBlock SecondsText;
    public TextBlock TenthsOfASecondText;

    public int CountdownSeconds;
    private int iteration;

    public void Begin(int currentIteration)
    {
        // ensure these are both enabled
        SecondsText.gameObject.SetActive(true);
        TenthsOfASecondText.gameObject.SetActive(true);
        
        iteration = currentIteration;

        int secondsRemaining = CountdownSeconds - iteration - 1;
        SecondsText.Text = $"{secondsRemaining}";
    }

    public void Update(float percentDone)
    {
        // Display 0 for the first 20th of a second
        float percentRemaining = percentDone <= 0.05f ? 0 : 1 - percentDone;

        int tenthsRemaining = Mathf.RoundToInt(percentRemaining * 10);
        TenthsOfASecondText.Text = $".{tenthsRemaining}";
    }

    public void Complete()
    {
        SecondsText.Text = "Time's Up!";
        TenthsOfASecondText.gameObject.SetActive(false);
    }

    public void OnPaused()
    {
        SecondsText.Text = "Paused";
        TenthsOfASecondText.gameObject.SetActive(false);
    }

    public void OnResumed()
    {
        // Resume to the cached iteration
        Begin(iteration);
    }

    public void OnCanceled()
    {
        // disable both since the timer didn't
        // complete but is no longer running
        SecondsText.gameObject.SetActive(false);
        TenthsOfASecondText.gameObject.SetActive(false);
    }

    public void End() 
    {
        // called at the end of each iteration,
        // but we don't need to do anything here
        // in this example.
    }
}
☀
☾
In This Article
Legal EmailContact Github
Copyright © 2022 Supernova Technologies, LLC