What is Movement / Time

Movement over Time is a framework for accomplishing movement cleanly and efficiently in Unity. In order to use the framework you define a few fields which tell Movement over Time what to do at key points in time. Once you have the parameters of the effect defined the way you want them you can start it running by calling Movement.Run.

The Movement over Time framework has been highly polished to minimize memory allocations, maximize speed, and allow you to control every aspect of the movement effect so that you can create any custom sequence that you can imagine!

Movement over Time is designed to make it easy to add juiciness to your game or app. Juiciness is the opposite of sterile. Juiciness is when a button expands as you hover over it, when elements nudge over to the side to make room for a new button on the screen, or when random objects in your world have eyes which watch you as you pass. It’s when the environment seems to be semi-intelligent and responding to the user.

Movement over Time makes it easy and fun to add juiciness to your app. It is designed to integrate with your existing coding structure. Movement over Time will be useful whether you are just starting a project or are two weeks from release.

How It Works

If you’ve ever used Unity’s coroutines before then the following block might look familiar:

public Transform Item;
public Vector3 TargetPosition;

void Start()
{
     StartCoroutine(MoveItem(Item, TargetPosition, 5f));
}
 
IEnumerator MoveItem(Transform item, Vector3 target, float timeframe)
{
     float startTime = Time.time;
     Vector3 startPos = item.position;
 
     while(startTime + timeframe <= Time.time)
     {
          item.position = Vector3.Lerp(startPos, target, (startTime - Time.time) / timeframe);
 
          yield return null;
     }
}

 
The code above runs a coroutine that will update the position of the item every frame so that it gets closer and closer to the position of targetuntil, after 5 seconds, it finally reaches target. The key processing happens inside the while loop where start.position gets set to the result of Vector3.Lerp. It’s the line with the Vector3.Lerp command that really does all the interesting work, but in order to accomplish that one simple movement we have to surround it with approximately 15 lines of boilerplate code!

Movement over Time does the same basic thing: It also runs a loop which calculates a new position every frame to create a smooth transition between the starting position and the ending position. We’ve improved the basic loop above in several ways that you probably never considered (like making it work even when time is running backwards) and we’ve optimized each and every line.

With Movement over Time the syntax is a little different: Instead of writing out a function full of boilerplate code, you just define the key actions which will be executed at the key points in the loop. These actions aren’t variables, they are typically anonymous functions which do one simple thing and do it well.

Effect<Controller, float> goUp = new Effect<Controller, float>
{
     Duration = 5f,
     RetrieveEnd = (me) => me.TargetPosition,
     OnUpdate = (me, value) => me.Item.position = value
};
 
Sequence<Controller, float> rise = new Sequence<Controller, float>
{
     Reference = this,
     RetrieveSequenceStart = me => me.Item.position
};
 
rise.Add(goUp);
 
Movement.Run(rise);

 
The above code is an example of how you start a sequence in Movement over Time.

  1. Define an effect, which holds a few key methods and the duration.
  2. Define a sequence which holds the rest of the methods we need in order to perform this particular transition.
  3. Add the effect to the sequence.
  4. Start the sequence running.

Using lambda expressions

Many of the fields you specify on the movement effect are actually actions. For example, one of the most important actions is the RetrieveEnd field, which retrieves the location that the sequence should move towards and end up at.

Every action, including the RetrieveEnd action, receives a reference as input. This reference is usually the object or script which holds all the information necessary to do the work you need. Many times the easiest thing to do is to set Reference to the “this” pointer.

In order to define RetrieveEnd one option is to create a function in your script that accepts a reference variable and returns the value, like so:

float RetrieveEndFunction(Controller reference)
{
      return 5f;
}

 
You can then set RetrieveEnd to point to the function you just created:

var moveOver = new Effect<Controller, float>();
moveOver.RetrieveEnd = RetrieveEndFunction;

 
However, it can get quite cumbersome to define a new function for every action that you want to define. Fortunately the C# compiler accepts lambda expressions, which are a quick way to define a function without even giving that function a name. A pointer to that nameless function is then stored in the RetrieveEnd field. Movement over Time can then use that function to retrieve the end value whenever it needs it. You know it’s a lambda expression when you see the => sign:

var moveOver = new Effect<Controller, float>();
moveOver.RetrieveEnd = reference => 5f;

 
If the function accepts more than one variable then you need parenthesis, and if you want the function to run more than one instruction then you need brackets, but in this case you need neither.

Templated types

Effects and Sequences are declared like this:

Effect<ReferenceType, ValueType> myEffect = new Effect<RefrenceType, ValueType>();
Sequence<ReferenceType, ValueType> mySequence = new Sequence<RefrenceType, ValueType>();

 
This section is about the ReferenceType and the ValueType. These are both called template type arguments.

ReferenceType is the type of the Sequence.Reference variable. It can be the type of any class, struct, or variable. The type just has to match the type of the reference variable. It can even be a built in type, like int, but if you make it a built in type then you’re doing it wrong. If you’re leaving the Reference variable as null then make this type object. However, if you leave Reference as null then you’re also doing it wrong.

ValueType is the type of value that you are acting on. It can be float, double, Vector2, Vector3, Vector4, Rect, Color, or Quaternion. Whenever you see anything in these documents that says “function x passes a value” then that value is of type ValueType.

You can’t put effects in a sequence unless their template type arguments match. If you want to form a sequence that switches types you’ll have to make two separate sequences and chain them together using OnComplete.

The 6 Essential Fields

There are 6 essential fields that really define a sequence. 4 of them are attached to an effect object and the other 2 are attached to a sequence object. Once you master these 6 fields you’ll be able to define any sequence, all of the other fields are just ways to control the specifics of how the effect plays out.

The 6 fields are:

Effect.Duration
Effect.RetrieveStart
Effect.RetrieveEnd
Effect.OnUpdate
Sequence.Reference
Sequence.RetrieveSequenceStart
  1. Effect.Duration is simple enough, it’s a float. It holds the number of seconds that the sequence should take to get from the start value to the end value.
  2. Effect.RetrieveStart is an action. It receives the Reference (like all actions do) and it also receives a value that I always label “lastEndValue” (or lastEnd if I’m in a hurry.) lastEnd is the value that was returned by the last call to Effect.RetrieveEnd. Usually, if you’re running a sequence with several effects in it, you’ll want to start the current effect at the point where the last effect left off. In fact, if you leave this field as null then the start value will be set to the last effect’s end value by default.
  3. Effect.RetireveEnd receives the Reference and returns the end value (or target) of the effect.
  4. Effect.OnUpdate is the heart of the whole affair. This function receives a reference and the current computed value. This function should be as short as possible, since it will be called every frame. It’s one task is to set whichever aspect of your environment that you are performing the effect on to the value that is passed in.
  5. Sequence.Reference should be assigned to whichever object holds the data that will need to be referenced in order to do the effect. This can be any object of any type. When you define the Effect or Sequence variable you specify the type of the Reference as the first templated argument (i.e. new Effect<"Reference Type", "Variable type you are acting on">). Often the Reference can be set to type of the current script that you are using, and then you can set Sequence.Reference to “this”. When you do that your reference can access any public or private variables in the current script. If you set Reference to the this pointer a good convention is to name it “me” when it’s passed in to any of the other actions.
  6. Sequence.RetrieveSequenceStart is a little different then the Effect.RetrieveStart function. Remember that Effect.RetrieveStart receives the lastEndValue each time it is called, which begs the question “what value is supplied to the first effect in the sequence?” This field has the answer: it’s the value that is returned by Sequence.RetrieveSequenceStart.. or a zero value if you leave this field null. You don’t have to use this field, but you’ll find that it comes in very handy if you end up re-ordering your list of effects, using smoothing, or reversing time.

Closure is not a Good Thing

Before we move on we need to take a quick break to have a little talk about closures.

Take a look at the following function:

void foo()
{
     Vector3 Offset = new Vector3(0f, 1f, 2f);
     
     Effect<Controller, Vector3> moveEffect = new Effect<Controller, Vector3>();
     moveEffect.Duration = 3f;
     moveEffect.RetrieveEnd = me => me.TargetLocation;
     moveEffect.OnUpdate = 
        (me, value) => me.MovingObject.transform.position = value + Offset;

     Movement.Run(this, moveEffect);
}

 

There are actually two problems with this block. The first is that we aren’t setting the RetrieveSequenceStart or RetrieveStart for the effect, so this effect will default to starting at (0, 0, 0) regardless of where the object happens to be at the moment, but that can be easily fixed by adding one of those two actions. The second problem is far more tricky.

The second problem is that we are using the variable Offset inside the action that we are storing in OnUpdate. Offset is a local variable that is scoped to the function foo, and by the time the OnUpdate method is called foo has already finished executing and quit. See the problem? How do we reference a variable every frame for three seconds when that variable has a different scope than our function?

However, this function will compile and run. So what is actually happening here?

What happens is the compiler recognizes that you are referencing a variable here that exists in a different context, so it takes a snapshot of that variable and bundles it along side the lambda expression in memory. Making a single copy of the variable wouldn’t be so bad, but the compiler can’t actually be sure you didn’t modify the variable while accessing it, so it ends up making another copy of Offset for every single time OnUpdate is run. This will eat up your memory fast.

Closures can be created for other reasons as well. A common mistake would be to use the line MovingObject.transform.position = value rather than me.MovingObject.transform.position = value. If you don’t put the me. in front of the line you’re breaking that bubble and accessing outside variables.

This is an important point. So important, in fact, that the Movement over Time framework checks for closures in the OnUpdate function before running every sequence. If you accidentally create a closure you’ll start getting a bunch of warnings in your console. Don’t ignore them, they’re important!

Closures are ok in functions that are executed once per effect, like RetrieveEnd. They only really hurt you in Effect.OnUpdate, Effect.RunEffectUntilTime, Effect.RunEffectUntilValue, and Effect.CalculatePercent.

Conceivably, you could run into a situation where you just have to create a closure. If you understand the performance implications but want to do it anyway then you can set Sequence.SupressClosureWarnings to true so that the warnings won’t pop up in your inspector. Use that field at your own risk.