This is a small review of the DOTween and UniRX plugins for Unity. Within this review I explain why UniRX and DoTween are a good combination for prototyping and even commercial products. I’ve been using these two plugins a lot in both hobby and commercial projects. For instance MASKED and Hyperplex 3D use these plugins. They allow for a faster workflow with good performance if used correctly.
To summarize the relation between the two plugins. UniRX provides timers, triggers & update manager like functionality. Along with a different methodology of programming, which is opitional. DoTween provides a quick way to easily create tweens with extention methods to ease your objects from data point A to data point B.
Within this review I will give a quick overview of the functionality of both DoTween and UniRX. At the end I will give an example of combining both and how that can speed up your process.
What is DOTween?
Dotween is a tweening framework, this framework allows you to easily create tweens for movement, fading, rotations etc. Allowing you to specify a endpoint value and duration. The reason why it is “easy” is because it adds many extention methods for a lot of classes. To give some examples:
using DG.Tweening;
using UnityEngine;
public class Example : MonoBehaviour
{
private void Awake()
{
// Moves this object to x:2, y:2, z:2 within 1 second.
this.transform.DOMove(new Vector3(2, 2, 2), 1);
// Punches the rotation of this object for 1 second. Meaning it moves back to the original rotation
this.transform.DOPunchRotation(new Vector3(1, 1, 1), 1);
// Shakes the by a strength of one for 1 second.
this.transform.DOShakeScale(1, 1);
}
}
You can also decide to further configure the tweens by creating a reference to them like so:
using DG.Tweening;
using UnityEngine;
public class Example : MonoBehaviour
{
private void Awake()
{
// Moves this object to x:2, y:2, z:2 within 1 second.
var tween = this.transform.DOMove(new Vector3(2, 2, 2), 1);
// You can set the tween to any type of ease.
tween.SetEase(Ease.InOutCubic);
tween.SetEase(Ease.InOutQuad);
tween.SetEase(Ease.InOutBounce);
// We can set it to loop 50 times
tween.SetLoops(50);
// We can also rewind the tween
tween.SmoothRewind();
// Can also flip the tween backwards
tween.Flip();
// We can also change the end value
tween.ChangeEndValue(new Vector3(1, 1, 1), false);
// Get how far the tween has elapsed (0 to 1)
tween.ElapsedPercentage();
}
}
You can also choose to create a Tween manually. Personally i don’t have to do this often due to the amount of extension methods available. Below is an example of how to create a manual tween.
How it works is you pass a getter and a setting through a method.
// Tween a float called myFloat to 10 in 1 second
DOTween.To(()=> myFloat, x=> myFloat = x, 10, 1);
It may look confusing to you if you have never used any lambda expressions before in C#. You could see them as a method you write in one line. For example “()=> myFloat” creates a new (delegate) method that returns myFloat.
“x=> myFloat = x” creates a new (delegate) method that returns a method with a parameter, in this case called x. Within this method myFloat gets set to x. Please correct me in the comments if I’m not explaining it correctly.
In order to write what you have above without lambda’s, it would look like this:
using DG.Tweening;
using UnityEngine;
public class Example : MonoBehaviour
{
float myFloat;
private void Awake()
{
DOTween.To(GetMyFloat, SetFloat, 10, 10);
}
private float GetMyFloat()
{
return myFloat;
}
private void SetFloat(float x)
{
myFloat = x;
}
}
When i’m doing Tweens I like to get a good representation of an easing function. There are plenty of sites for this. For example: https://easings.net/en
What is UniRX?
UniRX is based on ReactiveX, an API that allows for asynchronous programming.
In software programming, Reactive Extensions (also known as ReactiveX) is a set of tools allowing imperative programming languages to operate on sequences of data regardless of whether the data is synchronous or asynchronous. It provides a set of sequence operators that operate on each item in the sequence.
Source: https://en.wikipedia.org/wiki/Reactive_extensions
ReactiveX is a combination of the best ideas from
Source: http://reactivex.io/
the Observer pattern, the Iterator pattern, and functional programming
So using the UniRX plugin for Unity allows you to program in a completely new way. However, you are not required to. (in case you are interested about this new paradigm, there is a tutorial course on Youtube by Infallible Code) I personally don’t like to substitute my regular way of coding by these methods. UniRX provides some useful features that can make your life easier when making prototypes or new games. Especially combined with DoTween:
Observable Triggers
using UnityEngine;
using UniRx;
using UniRx.Triggers;
public class Example : MonoBehaviour
{
private void Awake()
{
this.gameObject.OnTriggerEnterAsObservable()
.Subscribe(_=> Debug.Log($"I have hit trigger {_.gameObject.name}!"))
.AddTo(this);
this.gameObject.OnDestroyAsObservable()
.Subscribe(_ => Debug.Log("I Have been destroyed!"))
.AddTo(this);
this.gameObject.OnDisableAsObservable()
.Subscribe(_ => Debug.Log("I Have been disabled!"))
.AddTo(this);
}
}
What I personally like about this, is that I’m able to “query” if a certain object has been disabled or destroyed without having to resort to having to create a new c# implementation that is attached to a given game object.
It’s also easy to abuse this functionality. For example, you could create a small pooling script.
Timers
using UnityEngine;
using UniRx;
using System;
public class Example : MonoBehaviour
{
private void Awake()
{
Observable.Timer(TimeSpan.FromSeconds(0.5f))
.Subscribe(_ => Debug.Log("I log after half a second!"))
.AddTo(this);
Observable.TimerFrame(5)
.Subscribe(_ => Debug.Log("I log after 5 frames!"))
.AddTo(this);
}
}
Timers are useful in case you want a specific action to happen after a set amount of time. This is one I often use alongside with DoTween. Doing a tween that lasts x amount of time, and doing an action as well after it using a Observable timer.
Update Loops & Intervals
using UnityEngine;
using UniRx;
using System;
public class Example : MonoBehaviour
{
private IDisposable updateObservable;
private IDisposable fixedUpdateObservable;
private IDisposable lateUpdateObservable;
private IDisposable intervalObservable;
private void Awake()
{
updateObservable = Observable.EveryUpdate()
.Subscribe(_ => OnUpdate());
fixedUpdateObservable = Observable.EveryFixedUpdate()
.Subscribe(_ => OnFixedUpdate());
lateUpdateObservable = Observable.EveryLateUpdate()
.Subscribe(_ => OnLateUpdate());
intervalObservable = Observable.Interval(TimeSpan.FromSeconds(1f))
.Subscribe(_ => OnInterval());
}
private void OnFixedUpdate() { Debug.Log("Executing fixed update"); }
private void OnLateUpdate() { Debug.Log("Executing late update"); }
private void OnUpdate() { Debug.Log("Executing update"); }
private void OnInterval() { Debug.Log("Gets called every second"); }
private void OnDestroy()
{
// You can also choose to dispose them before destory.
updateObservable?.Dispose();
fixedUpdateObservable?.Dispose();
lateUpdateObservable?.Dispose();
intervalObservable?.Dispose();
}
}
There are several usecases for these methods. For instance the interval observable could be used to spawn monsters at a given interval, or you could use it to create a simple game timer.
Calling Update() with UniRX comes with the added advantage of being able to turn off the update call entirely, while keeping the component enabled.
Using UniRX as an Update Manager
What is faster, having 10000 components with a Update() call? or one object that calls Update() on 10000 components? According to Unity there is a performance overhead for calling Update() from the engine internals. So the latter is faster. You can find a blog on it here. Altough, since this post is from 2015, I’ve decided to do my own tests to confirm if UniRX can be a viable update manager.
Test 1: Calling Update() from each individual component
using UnityEngine;
public class Example : MonoBehaviour
{
private void Awake()
{
for (int i = 0; i < 10000; i++)
{
new GameObject().AddComponent<Ticker>();
}
}
}
public class Ticker : MonoBehaviour
{
private void Update()
{
// Tick!
}
}
Test 2: Calling Update() from an Update Manager
using UnityEngine;
public class Example : MonoBehaviour
{
private Ticker[] tickers;
private void Awake()
{
tickers = new Ticker[10000];
for (int i = 0; i < 10000; i++)
{
tickers[i] = new GameObject().AddComponent<Ticker>();
}
}
private void Update()
{
for (int i = 0; i < 10000; i++)
{
tickers[i].OnUpdate();
}
}
}
public class Ticker : MonoBehaviour
{
// Not called by Unity internals
public void OnUpdate()
{
// Tick!
}
}
Test 3: Calling Update() from UniRX
using UniRx;
using UnityEngine;
public class Example : MonoBehaviour
{
private void Awake()
{
for (int i = 0; i < 10000; i++)
{
new GameObject().AddComponent<Ticker>();
}
}
}
public class Ticker : MonoBehaviour
{
private void Awake()
{
Observable.EveryUpdate()
.Subscribe(_ => OnUpdate())
.AddTo(this); // Ensure it stops once object is destroyed.
}
// Not called by Unity internals
private void OnUpdate()
{
// Tick!
}
}
According to these tests, UniRX does not qualify to replace an Update Manager. Do note that I have done these tests within the editor. So results may be faster in an actual build. But I doubt they will be faster for UniRX compared to the alternatives.
Combining UniRX and DoTween
The menu above is purely made with UniRX and DoTween, in Unity using C#. Making use of movement tweens and timers to delay when specific easing happens. Below is the code used to create the effect above. You could also do this purely with Coroutines + DoTween. Using WaitForSeconds(t). However, this would most likely take up more lines of code then what you see below.
using DG.Tweening;
using System;
using UniRx;
using UnityEngine;
using UnityEngine.UI;
public class ScoreMenu : MonoBehaviour
{
[SerializeField] private RectTransform menu;
[SerializeField] private RectTransform[] buttons;
[SerializeField] private Image[] stars;
[SerializeField] private Color starHighlightColor;
[SerializeField] private float buttonTargetHeight;
[SerializeField] private float pulseStarDelayTime = 1.5f;
[SerializeField] private float moveButtonsDownDelayTime = 2.5f;
[SerializeField] private float menuMoveDisplayTime = 2;
[SerializeField] private float starHighlightTime = 0.5f;
[SerializeField] private float moveButtonTime = 0.25f;
void Start()
{
menu.anchoredPosition = new Vector2(0, -Screen.height * 0.75f);
for (int i = 0; i < buttons.Length; i++)
{
buttons[i].anchoredPosition = new Vector2(buttons[i].anchoredPosition.x, 50);
}
Observable.Timer(TimeSpan.FromSeconds(pulseStarDelayTime))
.Subscribe(_ => PulseStars());
Observable.Timer(TimeSpan.FromSeconds(moveButtonsDownDelayTime))
.Subscribe(_ => MoveButtonsDown());
var moveTween = menu.DOAnchorPosY(0, menuMoveDisplayTime);
moveTween.SetEase(Ease.OutBounce);
}
private void PulseStars()
{
for (int i = 0; i < stars.Length; i++)
{
var star = stars[i];
Observable.Timer(TimeSpan.FromSeconds(0.25f * i))
.Subscribe(_ =>
{
star.rectTransform.DOPunchScale(new Vector3(0.75f, 0.75f, 0.75f), 0.5f);
star.DOColor(starHighlightColor, starHighlightTime);
Observable.Timer(TimeSpan.FromSeconds(0.55f)).Subscribe(r =>
{
var starRotationTween = star.transform.DOPunchScale(new Vector3(0.15f, 0.15f, 0.15f), 2, 1, 40);
starRotationTween.SetEase(Ease.Linear);
starRotationTween.SetLoops(int.MaxValue, LoopType.Yoyo);
});
});
}
}
private void MoveButtonsDown()
{
for (int i = 0; i < buttons.Length; i++)
{
var tween = buttons[i].DOAnchorPos(new Vector2(buttons[i].anchoredPosition.x, buttonTargetHeight), moveButtonTime);
tween.SetEase(Ease.InOutQuint);
}
}
}
But also, say you want a object to explode (scale up) and destroy after a second, you would then code something like this:
transform.DOScale(5, 1);
Observable.Timer(TimeSpan.FromSeconds(1)).Subscribe(_ => GameObject.Destroy(gameObject)).AddTo(this);
No need to yield a WaitForSeconds(t) with a newly created IEnumerator method that gets called with StartCoroutine(). Just two lines of code.
As a conclusion of this review I recommend giving the UniRx and DoTween plugins a try when doing a prototype, you will quickly notice that they can save you a lot of development time. I’ve barely had to create any custom tweens using DoTween, with some minor rare exceptions. But even creating your own tweens with DoTween is quite easy.
Try using one instance of observable.oneveryupdate instead of creating them on awake and su scribing to them for better results
Yes this is true, it will most likely be a lot more performant, but in terms of implementation it is
the same as just using one Update() method. Or do you know a good way to add additional calls to that Observable.OnEveryUpdate?