Coding Animations with ECS in Unity

Some time ago, I made a post about coding animations in Unity. However, as we are currently on ECS topic, I thought that it might be a cool idea to make a little update to that!

If you haven’t seen the previous post about animations, I highly recommend doing so, before diving here!

Ready?

Preparing

Before we can start building animations or tweening systems, we need to convert Mathfx class to the ECS!

using Unity.Mathematics;

// Variation of Mathfx class for ECS.
// Original Source: http://wiki.unity3d.com/index.php/Mathfx
public sealed class MathfxECS
{
    //Ease in out
    public static float Hermite(float start, float end, float value)
    {
        return math.lerp(start, end, value * value * (3.0f - 2.0f * value));
    }

    public static float2 Hermite(float2 start, float2 end, float value)
    {
        return new float2(Hermite(start.x, end.x, value), Hermite(start.y, end.y, value));
    }

    public static float3 Hermite(float3 start, float3 end, float value)
    {
        return new float3(Hermite(start.x, end.x, value), Hermite(start.y, end.y, value), Hermite(start.z, end.z, value));
    }

    //Ease out
    public static float Sinerp(float start, float end, float value)
    {
        return math.lerp(start, end, math.sin(value * math.PI * 0.5f));
    }

    public static float2 Sinerp(float2 start, float2 end, float value)
    {
        return new float2(math.lerp(start.x, end.x, math.sin(value * math.PI * 0.5f)), math.lerp(start.y, end.y, math.sin(value * math.PI * 0.5f)));
    }

    public static float3 Sinerp(float3 start, float3 end, float value)
    {
        return new float3(math.lerp(start.x, end.x, math.sin(value * math.PI * 0.5f)), math.lerp(start.y, end.y, math.sin(value * math.PI * 0.5f)), math.lerp(start.z, end.z, math.sin(value * math.PI * 0.5f)));
    }
    //Ease in
    public static float Coserp(float start, float end, float value)
    {
        return math.lerp(start, end, 1.0f - math.cos(value * math.PI * 0.5f));
    }

    public static float2 Coserp(float2 start, float2 end, float value)
    {
        return new float2(Coserp(start.x, end.x, value), Coserp(start.y, end.y, value));
    }

    public static float3 Coserp(float3 start, float3 end, float value)
    {
        return new float3(Coserp(start.x, end.x, value), Coserp(start.y, end.y, value), Coserp(start.z, end.z, value));
    }

    //Boing
    public static float Berp(float start, float end, float value)
    {
        value = math.clamp(value, 0, 1);
        value = (math.sin(value * math.PI * (0.2f + 2.5f * value * value * value)) * math.pow(1f - value, 2.2f) + value) * (1f + (1.2f * (1f - value)));
        return start + (end - start) * value;
    }

    public static float2 Berp(float2 start, float2 end, float value)
    {
        return new float2(Berp(start.x, end.x, value), Berp(start.y, end.y, value));
    }

    public static float3 Berp(float3 start, float3 end, float value)
    {
        return new float3(Berp(start.x, end.x, value), Berp(start.y, end.y, value), Berp(start.z, end.z, value));
    }

    //Like lerp with ease in ease out
    public static float SmoothStep(float x, float min, float max)
    {
        x = math.clamp(x, min, max);
        float v1 = (x - min) / (max - min);
        float v2 = (x - min) / (max - min);
        return -2 * v1 * v1 * v1 + 3 * v2 * v2;
    }

    public static float2 SmoothStep(float2 vec, float min, float max)
    {
        return new float2(SmoothStep(vec.x, min, max), SmoothStep(vec.y, min, max));
    }

    public static float3 SmoothStep(float3 vec, float min, float max)
    {
        return new float3(SmoothStep(vec.x, min, max), SmoothStep(vec.y, min, max), SmoothStep(vec.z, min, max));
    }

    public static float Lerp(float start, float end, float value)
    {
        return ((1.0f - value) * start) + (value * end);
    }

    public static float2 Lerp(float2 start, float2 end, float value)
    {
        return new float2(Lerp(start.x, end.x, value), Lerp(start.y, end.y, value));
    }

    public static float3 Lerp(float3 start, float3 end, float value)
    {
        return new float3(Lerp(start.x, end.x, value), Lerp(start.y, end.y, value), Lerp(start.z, end.z, value));
    }

    public static float3 NearestPoint(float3 lineStart, float3 lineEnd, float3 point)
    {
        float3 lineDirection = math.normalize(lineEnd - lineStart);
        float closestPoint = math.dot((point - lineStart), lineDirection);
        return lineStart + (closestPoint * lineDirection);
    }

    public static float3 NearestPointStrict(float3 lineStart, float3 lineEnd, float3 point)
    {
        float3 fullDirection = lineEnd - lineStart;
        float3 lineDirection = math.normalize(fullDirection);
        float closestPoint = math.dot((point - lineStart), lineDirection);
        return lineStart + (math.clamp(closestPoint, 0.0f, math.length(fullDirection)) * lineDirection);
    }

    //Bounce
    public static float Bounce(float x)
    {
        return math.abs(math.sin(6.28f * (x + 1f) * (x + 1f)) * (1f - x));
    }

    public static float2 Bounce(float2 vec)
    {
        return new float2(Bounce(vec.x), Bounce(vec.y));
    }

    public static float3 Bounce(float3 vec)
    {
        return new float3(Bounce(vec.x), Bounce(vec.y), Bounce(vec.z));
    }

    // test for value that is near specified float (due to floating point inprecision)
    // all thanks to Opless for this!
    public static bool Approx(float val, float about, float range)
    {
        return ((math.abs(val - about) < range));
    }

    // test if a float3 is close to another float3 (due to floating point inprecision)
    // compares the square of the distance to the square of the range as this 
    // avoids calculating a square root which is much slower than squaring the range
    public static bool Approx(float3 val, float3 about, float range)
    {
        return (math.lengthsq(val - about) < range * range);
    }

    /*
      * CLerp - Circular Lerp - is like lerp but handles the wraparound from 0 to 360.
      * This is useful when interpolating eulerAngles and the object
      * crosses the 0/360 boundary.  The standard Lerp function causes the object
      * to rotate in the wrong direction and looks stupid. Clerp fixes that.
      */
    public static float Clerp(float start, float end, float value)
    {
        float min = 0.0f;
        float max = 360.0f;
        float half = math.abs((max - min) / 2.0f);//half the distance between min and max
        float retval = 0.0f;
        float diff = 0.0f;

        if ((end - start) < -half)
        {
            diff = ((max - start) + end) * value;
            retval = start + diff;
        }
        else if ((end - start) > half)
        {
            diff = -((max - end) + start) * value;
            retval = start + diff;
        }
        else retval = start + (end - start) * value;

        // Debug.Log("Start: "  + start + "   End: " + end + "  Value: " + value + "  Half: " + half + "  Diff: " + diff + "  Retval: " + retval);
        return retval;
    }
}

Great! We will also need a little helper for ease of use!

using UnityEngine;
using Unity.Mathematics;

/// <summary>
/// A little helper for mathfx class.
/// </summary>
public static class MathfxHelper
{
    #region [Mathfx]

    /// <summary>
    /// Return value based on curve from Mathfx class.
    /// </summary>
    /// <returns>The value.</returns>
    /// <param name="animationCurve">Animation curve.</param>
    /// <param name="start">Start.</param>
    /// <param name="end">End.</param>
    /// <param name="t">T.</param>
    public static float CurvedValue(AnimationCurveEnum animationCurve, 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>
    /// Return value based on curve from Mathfx class.
    /// </summary>
    /// <returns>The value.</returns>
    /// <param name="animationCurve">Animation curve.</param>
    /// <param name="start">Start.</param>
    /// <param name="end">End.</param>
    /// <param name="t">T.</param>
    public static Vector2 CurvedValue(AnimationCurveEnum animationCurve, Vector2 start, Vector2 end, float t)
    {
        return new Vector2(CurvedValue(animationCurve, start.x, end.x, t), CurvedValue(animationCurve, start.y, end.y, t));
    }

    /// <summary>
    /// Return value based on curve from Mathfx class.
    /// </summary>
    /// <returns>The value.</returns>
    /// <param name="animationCurve">Animation curve.</param>
    /// <param name="start">Start.</param>
    /// <param name="end">End.</param>
    /// <param name="t">T.</param>
    public static Vector3 CurvedValue(AnimationCurveEnum animationCurve, Vector3 start, Vector3 end, float t)
    {
        return new Vector3(CurvedValue(animationCurve, start.x, end.x, t), CurvedValue(animationCurve, start.y, end.y, t), CurvedValue(animationCurve, start.z, end.z, t));
    }

    #endregion

    #region [MathfxECS]

    /// <summary>
    /// Return value based on curve from MathfxECS class.
    /// </summary>
    /// <returns>The value.</returns>
    /// <param name="animationCurve">Animation curve.</param>
    /// <param name="start">Start.</param>
    /// <param name="end">End.</param>
    /// <param name="t">T.</param>
    public static float CurvedValueECS(AnimationCurveEnum animationCurve, float start, float end, float t)
    {
        switch (animationCurve)
        {
            case AnimationCurveEnum.Hermite:
                return MathfxECS.Hermite(start, end, t);
            case AnimationCurveEnum.Sinerp:
                return MathfxECS.Sinerp(start, end, t);
            case AnimationCurveEnum.Coserp:
                return MathfxECS.Coserp(start, end, t);
            case AnimationCurveEnum.Berp:
                return MathfxECS.Berp(start, end, t);
            case AnimationCurveEnum.Bounce:
                return start + ((end - start) * MathfxECS.Bounce(t));
            case AnimationCurveEnum.Lerp:
                return MathfxECS.Lerp(start, end, t);
            case AnimationCurveEnum.Clerp:
                return MathfxECS.Clerp(start, end, t);
            default:
                return 0;
        }
    }

    /// <summary>
    /// Return value based on curve from MathfxECS class.
    /// </summary>
    /// <returns>The value.</returns>
    /// <param name="animationCurve">Animation curve.</param>
    /// <param name="start">Start.</param>
    /// <param name="end">End.</param>
    /// <param name="t">T.</param>
    public static float2 CurvedValueECS(AnimationCurveEnum animationCurve, float2 start, float2 end, float t)
    {
        return new float2(CurvedValueECS(animationCurve, start.x, end.x, t), CurvedValueECS(animationCurve, start.y, end.y, t));
    }

    /// <summary>
    /// Return value based on curve from MathfxECS class.
    /// </summary>
    /// <returns>The value.</returns>
    /// <param name="animationCurve">Animation curve.</param>
    /// <param name="start">Start.</param>
    /// <param name="end">End.</param>
    /// <param name="t">T.</param>
    public static float3 CurvedValueECS(AnimationCurveEnum animationCurve, float3 start, float3 end, float t)
    {
        return new float3(CurvedValueECS(animationCurve, start.x, end.x, t), CurvedValueECS(animationCurve, start.y, end.y, t), CurvedValueECS(animationCurve, start.z, end.z, t));
    }

    #endregion
}

Awesome! Preparation is done!

Implementation

Now we can start building our animation systems! As you remember, ECS is made of 2 parts: component and system. The first one stores data and the second one is responsible for logic.

Base line

In the previous implementation, we had variables like the duration, delay, looping, autoPlay, autoDestroy, and of course, animationCurve. With the new implementation, I’m going to get rid of autoPlay and autoDestroy as there is no sense of having such a component if it’s not doing anything besides, occupying memory.

There is just one downside of ECS here. Components are structs so we can’t use inheritance to clean our code a little bit more, and we will have to paste animation variables to each, new component.

MoveTo

With that said, let’s start with something simple! – MoveTo.

using Unity.Entities;
using Unity.Mathematics;
using Unity.Collections;

/// <summary>
/// ECS component for MoveTo Tween.
/// </summary>
[System.Serializable]
public struct MoveTo : IComponentData
{
    // Animation Baseline
    [ReadOnly] public float Delay; // Animation Delay.
    [ReadOnly] public float Duration; // Animation Duration.
    [ReadOnly] public bool LoopAnimation; // Is animation looping?
    [ReadOnly] public AnimationCurveEnum AnimationCurve; // Animation curve used for this tween.

    public float ElapsedTime; // Animation Elapsed Time.

    // Move To
    [ReadOnly] public float3 StartValue;
    [ReadOnly] public float3 EndValue;
}
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

/// <summary>
/// ECS system for MoveTo Tween
/// </summary>
public class MoveToSystem : ComponentSystem
{
    /// <summary>
    /// Unity ECS Update called each frame.
    /// </summary>
    protected override void OnUpdate()
    {
        Entities.ForEach((Entity entity, ref MoveTo moveTo, ref Translation translation) =>
        {
            moveTo.ElapsedTime += Time.deltaTime;

            translation.Value = MathfxHelper.CurvedValueECS(moveTo.AnimationCurve, moveTo.StartValue, moveTo.EndValue, math.clamp((moveTo.ElapsedTime - moveTo.Delay) / moveTo.Duration, 0.0f, 1.0f));

            if (moveTo.ElapsedTime >= moveTo.Delay + moveTo.Duration)
            {
                if (moveTo.LoopAnimation)
                {
                    moveTo.ElapsedTime -= moveTo.Duration;
                }
                else
                {
                    translation.Value = moveTo.EndValue;
                    PostUpdateCommands.RemoveComponent<MoveTo>(entity);
                }
            }
        });
    }
}

Great! Making other components like RotateTo or ScaleTo will be as easy as changing variables, and that’s it!

ScaleBy

Another example would be changing the value by some different amount.

using Unity.Entities;
using Unity.Mathematics;
using Unity.Collections;

/// <summary>
/// ECS component for ScaleBy Tween.
/// </summary>
[System.Serializable]
public struct ScaleBy : IComponentData
{
    // Animation Baseline
    [ReadOnly] public float Delay; // Animation Delay.
    [ReadOnly] public float Duration; // Animation Duration.
    [ReadOnly] public bool LoopAnimation; // Is animation looping?
    [ReadOnly] public AnimationCurveEnum AnimationCurve; // Animation curve used for this tween.

    public float ElapsedTime; // Animation Elapsed Time.

    // Scale By
    [ReadOnly] public float3 TargetOffset;
}
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

/// <summary>
/// ECS system for ScaleBy Tween
/// </summary>
public class ScaleBySystem : ComponentSystem
{
    /// <summary>
    /// Unity ECS Update called each frame.
    /// </summary>
    protected override void OnUpdate()
    {
        Entities.ForEach((Entity entity, ref ScaleBy scaleBy, ref NonUniformScale scale) =>
        {
            var start = MathfxHelper.CurvedValueECS(scaleBy.AnimationCurve, float3.zero, scaleBy.TargetOffset, math.clamp((scaleBy.ElapsedTime - scaleBy.Delay) / scaleBy.Duration, 0.0f, 1.0f));
            var end = MathfxHelper.CurvedValueECS(scaleBy.AnimationCurve, float3.zero, scaleBy.TargetOffset, math.clamp((scaleBy.ElapsedTime - scaleBy.Delay + Time.deltaTime) / scaleBy.Duration, 0.0f, 1.0f));

            scale.Value += end - start;

            scaleBy.ElapsedTime += Time.deltaTime;

            if (scaleBy.ElapsedTime >= scaleBy.Delay + scaleBy.Duration)
            {
                if (scaleBy.LoopAnimation)
                {
                    scaleBy.ElapsedTime -= scaleBy.Duration;
                }
                else
                {
                    PostUpdateCommands.RemoveComponent<ScaleBy>(entity);
                }
            }
        });
    }
}

And again, doing the same thing to the other types will be just a simple type change.

Example

So now as we made some animations in the ECS, let’s use them!

using UnityEngine;
using Unity.Entities;
using Unity.Transforms;
using Unity.Rendering;
using Unity.Mathematics;

/// <summary>
/// This class generates the ECS animation example.
/// </summary>
public class ExampleGenerator : MonoBehaviour
{
    // Reference to mesh for sprite.
    [SerializeField]
    private Mesh spriteMesh;

    // References to materials for sprites.
    [SerializeField]
    private Material[] spriteMaterials;

    /// <summary>
    /// Unity method called on the first frame.
    /// </summary>
    void Start()
    {
        GenerateExample();
    }

    /// <summary>
    /// Method that generate example ECS.
    /// </summary>
    private void GenerateExample()
    {
        // Reference to Entity Manager.
        var entityManager = World.Active.EntityManager;

        // Generating Entity with scaling tween.
        var scaleEntity = entityManager.CreateEntity(
                typeof(RenderMesh),
                typeof(LocalToWorld),
                typeof(Translation),
                typeof(NonUniformScale),
                typeof(ScaleTo)
            );

        // Assigning values to the entity with scale tween.
        entityManager.SetSharedComponentData(scaleEntity, new RenderMesh { mesh = spriteMesh, material = spriteMaterials[0] });
        entityManager.SetComponentData(scaleEntity, new Translation { Value = new float3(-2.5f, 0, 0) });
        entityManager.SetComponentData(scaleEntity, new ScaleTo
        {
            AnimationCurve = AnimationCurveEnum.Bounce,
            Duration = 3,
            StartValue = new float3(1),
            EndValue = new float3(3),
            LoopAnimation = true
        });

        // Generating Entity with hover tween.
        var hoverEntity = entityManager.CreateEntity(
                typeof(RenderMesh),
                typeof(LocalToWorld),
                typeof(Translation),
                typeof(Hover)
            );

        // Assigning values to the entity with hover tween.
        entityManager.SetSharedComponentData(hoverEntity, new RenderMesh { mesh = spriteMesh, material = spriteMaterials[1] });
        entityManager.SetComponentData(hoverEntity, new Translation { Value = new float3(0, 0, 0) });
        entityManager.SetComponentData(hoverEntity, new Hover
        {
            AnimationCurve = AnimationCurveEnum.Hermite,
            Duration = 4,
            BottomValue = new float3(0, -1, 0),
            TopValue = new float3(0, 1, 0),
            LoopAnimation = true
        });

        // Generating Entity with rotate tween.
        var rotateEntity = entityManager.CreateEntity(
            typeof(RenderMesh),
            typeof(LocalToWorld),
            typeof(Translation),
            typeof(Rotation),
            typeof(RotateTo)
        );

        // Assigning values to the entity with rotate tween.
        entityManager.SetSharedComponentData(rotateEntity, new RenderMesh { mesh = spriteMesh, material = spriteMaterials[2] });
        entityManager.SetComponentData(rotateEntity, new Translation { Value = new float3(2, 0, 0) });
        entityManager.SetComponentData(rotateEntity, new RotateTo
        {
            AnimationCurve = AnimationCurveEnum.Hermite,
            Duration = 5,
            StartValue = new float3(0, 0, 0),
            EndValue = new float3(0, 0, 2 * math.PI),
            LoopAnimation = true
        });
    }
}

And let’s run this example!

3 small animations build with ECS.

Yay!

What do you think about it? What other tweens you are using? Let me know in the comment section below!

If you want to get notified on future content, sign up for the newsletter!

As always, the whole project is available at my public repository. ๐Ÿ”—

And I hope to see you back soon! ๐Ÿค“

0 0 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments