Building an Interpolation Library in C#: Part 3 – Simple Looping

In Part 2 of the series we added support for easing functions in our tweens. We followed an extensible model that allows us to create and plug in new easing functions on the fly without requiring drastic changes to our core Interpolator logic. Today, we’re going to do the same thing with looping.

Let’s start by taking a look at this demo and outlining a couple goals for the looper.

Goals
  1. The Looper must detect when an Interpolator is complete and loop it accordingly.
  2. The Looper should be able to run for a set number of loops, or indefinitely.
  3. Interpolator / Looper consumers should be notified when an Interpolator has looped / finished.
  4. The looping behaviour should be extensible and plugged into an Interpolator, rather than part of the Interpolator itself.
  5. As of the end of part 2, Interpolators can be played backwards or forwards by updating it with positive / negative time (ever tried it?). We want to maintain that.

Now with some goals set, we can get started… and we do so by defining our Looper class.

 

The Looper Class

Our core looper class isn’t too heavy. Our goals spelled out most of it for us.

  • We need a parameter for the number of loops to iterate for (total member).
  • It is implied that we need to keep track of how many loops we’ve currently completed out of the total (count member).
  • We also add a short hand getter to check if the looper still has loops pending (hasMoreLoops property).
namespace Com.Naut.Math.Interpolation.Looping
{
	/// <summary>
	/// A looping component used to looper Interpolators. Extend this class to create new 
	/// types of looping behaviours.
	/// </summary>
	public class Looper
	{

		#region Members
		/// <summary>The total number of loops the Interpolator should be looped for, 
		/// a value below 0 indicates indefinite looping.</summary>
		public int total
		{
			get { return _total; }
			set { _total = value; }
		}
		private int _total = 0;

		/// <summary>The number of times we have looped for.</summary>
		public int count
		{
			get { return _count; }
			set { _count = value; }
		}
		private int _count = 0;

		/// <summary>Short hand check to see if the looper is complete.</summary>
		public bool hasMoreLoops
		{
			get
			{
				if (_total < 0) return true;		// infinite
				if (_count < _total) return true;	// the count is less than the total
				else return false;					// the count is greater than or equal to the total
			}
		}
		#endregion


		#region Initialization
		/// <summary>
		/// Creates a new looper that loops indefinitely.
		/// </summary>
		public Looper() : this(-1) { }

		/// <summary>
		/// Creates a new looper that loops for the specified number of iterations.
		/// </summary>
		/// <param name="total"></param>
		public Looper(int total)
		{
			_total = total;
		}
		#endregion

	}
}

This is a good start, but doesn’t actually do any looping. Next we need to set up the actual looping logic.

 

The Looper Logic

Note that in order to do a simple loop, we must update the elapsed time of the Interpolator so it counts back up to duration… We have to be careful though, we don’t want to ‘lose’ time. Let me illustrate-

Imagine a looping animation that runs for 1 second per loop. On some update the Interpolator advances from 0.9 seconds to 1.1 seconds and our Looper logic is kicked off. If we naively set Interpolator.time back to 0, we will have lost 0.1 second of integrated time. Our animation will appear to stutter for a very brief moment. To fix this, we don’t set time to 0, we instead subtract out one duration of the Interpolator (1.1 seconds – 1 second = 0.1 seconds). This prevents us from ‘losing’ the time.

We also know that we want out looper to be able to handle Interpolators ticking forwards or backwards, so lets add Looper functions to loop forward and backward.

		#region Looping
		/// <summary>
		/// Loops the specified interpolator forward one loop. Forward looping
		/// should happen when the interpolator has gone over its duration.
		/// </summary>
		/// <param name="interpolator">The interpolator to loop.</param>
		public virtual void ForwardLoop(Interpolator interpolator)
		{
			// Note that we moved a loop
			_count++;

			// Remove one duration worth of current interpolation time
			interpolator.time -= interpolator.duration;
		}

		/// <summary>
		/// Loops the specified interpolator backward one loop. Backward looping
		/// should happen when the interpolator has gone under 0 in time. It should 
		/// only happen if the interpolator is ticking backward in time.
		/// </summary>
		/// <param name="interpolator">The interpolator to loop.</param>
		public virtual void ReverseLoop(Interpolator interpolator)
		{
			// Note that we moved a loop
			_count--;

			// Add one duration worth of current interpolation time
			interpolator.time += interpolator.duration;
		}
		#endregion

Looks great. Time to put it to use in the Interpolator.

 

Adding Looper Support to Interpolator

Before adding the Looper member to the class, lets revisit one goal- “Interpolator / Looper consumers should be notified when an Interpolator has looped / finished.”

Let’s use a delegate and a few events to let the world know about loops and completions. The delegate goes above the class definition near the easing function delegate, and the events go inside the Interpolator class.  Let’s also quickly add a Looper member.

	////// OUTSIDE THE CLASS DEFINITION
	/// <summary>
	/// An event handler for catching and responding to interpolator events.
	/// </summary>
	/// <param name="interpolator">The interpolator sending the event.</param>
	public delegate void InterpolatorEventHandler(Interpolator interpolator);

	...

		////// INSIDE THE CLASS DEFINITION
		#region Events
		/// <summary>Fired when the interpolator loops because it passed its duration.</summary>
		public InterpolatorEventHandler OnLooped;

		/// <summary>Fired when the interpolator loops in reverse because it ticked under 0.</summary>
		public InterpolatorEventHandler OnReverseLooped;

		/// <summary>Fired when the interpolator reaches its duration and has no loops pending.</summary>
		public InterpolatorEventHandler OnFinished;
		#endregion
			
		
		/// <summary>The looper used to loop the interpolation. Null if no looping.</summary>
		public Looper looper
		{
			get { return _looper; }
			set { _looper = value; }
		}
		private Looper _looper;

Great, we will put these to use soon. Note that the looper member could and should be added as a parameter to the Interpolator constructor. I will leave that for an exercise for you! Download the full source below to see how I did it.

With our looper member in place and our events set up, we can dig into the triggering the looping logic. The very first thing to do is to patch the Update method so it can handle 3 cases:

  1. If time goes below 0, the interpolator needs to loop backwards or cap its time to 0. I refer to this as ‘underflow’.
  2. If time reaches duration exactly, we need to check if the interpolator is finished.
  3. If time exceeds duration, we need to loop forwards or cap its time to the duration and let the world know we’re done. I refer to this as overflow.

With these in mind, we can simplify our Update method. Note that we will create the necessary supporting methods next.

		#region Updating
		/// <summary>
		/// Advances the tween for the specified amount of time and returns the resulting value.
		/// </summary>
		/// <param name="elapsed">The amount of time to step forward or backward (in seconds).</param>
		/// <returns>The resulting value after the step.</returns>
		public float Update(float elapsed)
		{
			_time += elapsed;

			if (_time < 0) Underflow();
			else if (_time == _duration) ReachedEnding();
			else if (_time > _duration) Overflow();

			return value;
		}
		#endregion

That right there, is broken code. You won’t be able to run it because three methods don’t exist yet.

The three methods (Underflow, ReachedEnding, and Overflow) are simple, but would take quite a bit of space to step through so I am going to leave it up to the comments within the snippet below:

		#region Looping
		/// <summary>
		/// Supporting method to handle when time falls below 0 because the 
		/// interpolator was ticked backwards.
		/// </summary>
		private void Underflow()
		{
			// If time is before the interpolation, move back to the beginning or reverse loop
			while (_time < 0f)
			{
				if (_looper != null)
				{
					// Move back one loop
					_looper.ReverseLoop(this);

					// Let the world know we reverse looped
					if (OnReverseLooped != null) OnReverseLooped(this);
				}
				else
				{
					// No loop, so just set back to the begining
					_time = 0f;
				}
			}
		}

		/// <summary>
		/// Supporting method to handle when time falls exactly on duration.
		/// </summary>
		private void ReachedEnding()
		{
			// We're done if there is no looper or it is complete
			if (_looper == null || _looper.hasMoreLoops == false)
			{
				// Let the world know we finished
				if (OnFinished != null) OnFinished(this);
			}
		}

		/// <summary>
		/// Supporting method to handle when the elapsed time has exceeded the duration.
		/// </summary>
		private void Overflow()
		{
			// If time is after the interpolation, loop if we can otherwise just stop at the end
			while (_time > _duration)
			{
				if (_looper != null && _looper.hasMoreLoops)
				{
					// Advance one loop
					_looper.ForwardLoop(this);

					// Let the world know we looped
					if (OnLooped != null) OnLooped(this);
				}
				else
				{
					// Cap at the duration
					_time = _duration;

					// Let the world know we finished
					if (OnFinished != null) OnFinished(this);
				}
			}
		}
		#endregion

There you have it folks. Our Looper class is defined and supported by Interpolator. It was a considerable amount of work to create an extensible looping system, but it will be worth it. In a future post I will explain two other types of Looping that I often find useful!

Source Code

CSharp-Interpolation-Library-Part-3

I updated the unity demo to show off the new looping code.

Next Up- Advanced Looping. Stay tuned for part 4.

-S

Leave a Reply

Your email address will not be published. Required fields are marked *