A week or so ago, I started toying with the idea of creating a tweening solution for Unity. Trying experimental tweening solutions are the kind of thing I find myself always spending time with: in the work I usually do, tweening is something I constantly have to deal with, and I strive to find the most efficient syntax that fits my coding style. That’s one of the reasons why I like reinventing the wheel even when mature solutions already exist, or at least the excuse I like giving myself for doing so.
Like for most visual platforms, the concept of tweening is not new to Unity. There are several different solutions available for animating UI elements in the platform, from LeanTween to iTween to the (more popular) HOTween and its successor DOTween. Still, it is something worth exploring, especially as I’m still learning the platform and the language.
Tweening values in Unity presents two interesting challenges when compared to the syntax one would use for tweening in, say, JavaScript or ActionScript.
The first challenge is the language itself. While Unity also supports for a JavaScript-based language normally called “UnityScript”, C# is a more popular choice for developers, and the design of this language make some things a little bit more tricky to write. To illustrate, consider this Tweener call:
Tweener.addTween(myMovie, {x:10, y:10, time:1, transition:"linear"});
In the above line, the second parameter is an Object
(sometimes called a map) that can contain any number of properties, and, if desired, even other objects. Tweener uses that for parameters that are optional, and, indeed, for any number of properties that one wants to tween. The name of those propertie are not known beforehand; they’re not hard-coded in the library, but instead read at runtime.
That is very easy to read and understand, but that syntax doesn’t work in C#. While the language has many advanced features over ActionScript 3 (or JavaScript), it forces a certain strictness to the way methods and functions are called. The closest equivalent of an AS3 Object in C# would be a Hashtable
, and its inline syntax would look something like this:
Tweener.addTween(myMovie, new Hashtable() { {"x":10}, {"y":10}, {"time":1}, {"transition":"linear"} });
While close in structure, the syntax is a little bit more verbose, harder to write and read, and more error-prone. While not a total disaster, it doesn’t make the act of writing the code particularly swift.
The second challenge for tweening engines in Unity the way the Unity platform’s works. In Unity, many objects are considered virtual getters and setters, and cannot be changed directly. Consider the scale of a GameObject
/Transform
object, controlled by its localScale
member, which is a Vector3
instance. Using C#, you can set the new scale like so:
transform.localScale = new Vector3(2, 2, 2);
This scales your object up to 200% of its original size, in all dimensions. Now, imagine you want to change the scale of your object in the X axis only. This may look like a possible solution:
transform.localScale.x = 2;
This code, however, is invalid in Unity’s C# implementation. The reason is that the localScale
object is created when the property is read, and changes to it wouldn’t be reapplied to the object. The correct solution would, then, look like this:
Vector3 newScale = transform.localScale;
newScale.x = 2;
transform.localScale = newScale;
Or if you want to be succinct, and can afford creating a new Vector3
instance:
transform.localScale = new Vector3(2, transform.localScale.y, transform.localScale.z);
What this means is that you cannot tween values of certain object properties directly. This is similar to the problem of tweening filters such as blur in ActionScript 3: you may change one of the filter values, but unless you re-apply it to the Sprite
that holds it, you wouldn’t see any change. That’s why tweening engines in AS3 had to work around that limitation with additional functionality. In Unity’s case, sometimes you will even run into compilation errors when trying to modify some of these properties directly.
Put together, those two challenges mean that one cannot take the syntax solutions matured in JavaScript and ActionScript and apply them to Unity. The platform requires solutions of its own.
The aforementioned iTween adopts two different syntaxes that somewhat resemble the way classic ActionScript 3 libraries deal with tweening, with all the problems it entails:
// Scale to 2x in 1 second
iTween.ScaleTo(gameObject, new Vector3(2, 2, 2), 1);
// Scale to 2x in 1 second, with more options
iTween.ScaleTo(gameObject, new Hashtable() { {"scale": new Vector3(2, 2, 2)}, {"time": 1}, {"easetype":iTween.EaseType.easeOutBack}, {"delay": 1} });
// Same as above, using a helper function
iTween.ScaleTo(gameObject, iTween.Hash("scale", new Vector3(2, 2, 2), "time", 1, "easetype", iTween.EaseType.easeOutBack, "delay", 1));
LeanTween takes a somewhat different approach:
// Scale to 2x in 1 second
LeanTween.scale(gameObject, new Vector3(2, 2, 2), 1);
// Scale to 2x in 1 second, with more options
LeanTween.scale(gameObject, new Vector3(2, 2, 2), 1).setEase(LeanTweenType.easeOutBack).setDelay(1);
This is a very interesting syntax: it allows the library to use strongly-typed methods for all kinds of actions. While it demands a number of methods to be added to the library’s interface, errors due to mistyping are minimized and developers can rely on auto completion for all their needs. In my option, it’s a much more elegant solution both for writing and reading code. Also, while it requires specialized method calls to tween some of the properties of a GameObject, this is an inevitable side effect given the way Unity works.
DOTween adopts a similar syntax with an interesting twist: it uses a C# feature called extension methods to add methods to the objects themselves, creating a syntax that reminds me of MC Tween:
// Scale to 2x in 1 second
transform.DOScale(new Vector3(2, 2, 2), 1);
// Scale to 2x in 1 second, with more options
transform.DOScale(new Vector3(2, 2, 2), 1).SetEase(Ease.OutBack).SetDelay(1);
Indeed, this “chaining” of method calls has been popular among libraries for other platforms for a while (jQuery was the library I saw using it). Some tweening solutions use this approach not just to set optional parameters for animations, but to create complex sequences in a very elegant way. Take TweenJS’s approach:
// Wait 0.5s, change alpha to 0 in 1s, then call a function
Tween.get(target).wait(500).to({alpha:0, visible:false}, 1000).call(someFunction);
Which, to me, looks like a solution that is as elegant as it gets.
When deciding on a tweening solution for Unity, I wanted something that was easy to write, read, fully type safe, and with easy auto completion. The two latter points are a given based in the language of choice, the two former ones are a little bit subjective and may require some creativity. I also wanted to explore bigger tweening chains that operated as animation sequences, something that in a solution like Tweener would require several lines of code.
The solution I decided on looks like this:
// Scales a gameObject to 200% in the Z axis in 0.2 seconds using an EaseOutExpo equation
ZTween.use(gameObject).scaleTo(new Vector3(1, 1, 2), 0.2f, Easing.expoOut);
What the code does is generate an object instance (via the use
method) that can then have commands issued to it. Each new method return the same object, so they can be chained and new methods can be added. For example:
// Scales a gameObject to 200% in the Z axis, then 200% in the Y axis, then back to 100%
ZTween.use(gameObject).scaleTo(new Vector3(1, 1, 2), 0.2f, Easing.expoOut).scaleTo(new Vector3(1, 2, 2), 0.2f, Easing.expoOut).scaleTo(new Vector3(1, 1, 1), 0.2f, Easing.expoOut);
And additional methods can exist to enforce initial values, if necessary:
// Scales a gameObject from 100% to 200% in the Z axis
ZTween.use(gameObject).scaleFrom(new Vector3(1, 1, 1).scaleTo(new Vector3(1, 1, 2), 0.2f, Easing.expoOut);
What’s interesting about all this chaining is that the concept of events is more or less ignored – you don’t have events for the beginning and ending of a particular tween, but instead you can add calls to the method chain itself. For example:
// Call functionA(), scales an object, then call functionB()
ZTween.use(gameObject).call(functionA).scaleTo(new Vector3(1, 1, 2), 0.2f, Easing.expoOut).call(functionB);
And given how C# works, parameters can be passed via lambdas, another C# feature:
// Scales an object, then writes to the console
ZTween.use(gameObject).scaleTo(new Vector3(1, 1, 2), 0.2f, Easing.expoOut).call(() => Debug.Log("Done animating");
And delays are another method, rather than a parameter:
// Wait 1s then scale an object
ZTween.use(gameObject).wait(1).scaleTo(new Vector3(1, 1, 2), 0.2f, Easing.expoOut);
Custom numeric properties get a tweening object of their own, via a reference (when a pure member):
// Transition "something" from the current value to 1
ZTween.use(ref something).valueTo(1, 0.2f, Easing.quadOut);
Or via lambdas created as a get and set parameters (for pure members, getter/setters, or get/set pairs):
// Transition "something" (a numeric property or a getter/setter) from the current value to 1
ZTween.use(() => something, val => something = val).valueTo(1, 0.2f, Easing.quadOut);
// Transition using getSomething() and setSomething() from the current value to 1
ZTween.use(getSomething, setSomething).valueTo(1, 0.2f, Easing.quadOut);
This is not a full solution by any stretch of the imagination; it currently only works for scaling, translating, and value transitions (for real world use, I’d suggest DOTween instead). However, this is a solution I’m enjoying building and what I’ll be using in future projects, including a little game test I’m building:
Again, this is just an experiment, but for the curious, source code is available on GitHub.
Hi Zeh, why do you recommend DOTween over LeanTween?
Hey Jim, if I had to recommend one, I would recommend DOTween (v2) because it seems to be in active development, and the author (Demigiant) is a really cool person, and really active in the Unity community.
However, I haven’t used LeanTween *that* much. I can say the two are very similar, despite LeanTween using static calls and DOTween using extension methods. I like them both.