This chapter introduces some things to keep in mind from a performance perspective when implementing third-party libraries that are often used when developing games in Unity.
DOTween *1 is a library that allows scripts to create smooth animations.For example, an animation that zooms in and out can be easily written as the following code
List 12.1: Example of DOTween usage
1: public class Example : MonoBehaviour {
2: public void Play() {
3: DOTween.Sequence()
4: .Append(transform.DOScale(Vector3.one * 1.5f, 0.25f))
5: .Append(transform.DOScale(Vector3.one, 0.125f));
6: }
7: }
Since the process of creating a tween, such as DOTween.Sequence() or transform.DOScale(...), basically involves memory allocation,consider reusing instances for animations that are frequently replayed.
By default, the tween is automatically discarded when the animation completes, so SetAutoKill(false) suppresses this.The first use case can be replaced with the following code
List 12.2: Reusing Tween Instances
1: private Tween _tween;
2:
3: private void Awake() {
4: _tween = DOTween.Sequence()
5: .Append(transform.DOScale(Vector3.one * 1.5f, 0.25f))
6: .Append(transform.DOScale(Vector3.one, 0.125f))
7: .SetAutoKill(false)
8: .Pause();
9: }
10:
11: public void Play() {
12: _tween.Restart();
13: }
Note that a tween that calls SetAutoKill(false) will leak if it is not explicitly destroyed.Call Kill() when it is no longer needed, or use the SetLink described below.
List 12.3: Explicitly destroying a tween
1: private void OnDestroy() {
2: _tween.Kill();
3: }
Tweens that call SetAutoKill(false) or that are made to repeat indefinitely with SetLoops(-1) will not be automatically destroyed, so you will need to manage their lifetime on your own.It is recommended that such a tween be associated with an associated GameObject at SetLink(gameObject) so that when the GameObject is Destroyed, the tween is also destroyed.
List 12.4: Tethering a Tween to the Lifetime of a GameObject
1: private void Awake() {
2: _tween = DOTween.Sequence()
3: .Append(transform.DOScale(Vector3.one * 1.5f, 0.25f))
4: .Append(transform.DOScale(Vector3.one, 0.125f))
5: .SetAutoKill(false)
6: .SetLink(gameObject)
7: .Pause();
8: }
During playback in the Unity Editor, a GameObject named [DOTween] You can check the state and settings of the DOTween from the Inspector by selecting the GameObject named
Figure 12.1: [DOTween] GameObject
Figure 12.2: DOTween Inspector
It is also useful to check for tween objects that continue to move even though their associated GameObjects have been discardedand for tween objects that are in a Pause state and leaking without being discarded.
UniRx *2 is a library implementing Reactive Extensions optimized for Unity.With a rich set of operators and helpers for Unity, event handling of complex conditions can be written in a concise manner.
UniRx allows you to subscribe ( Subscribe) to the stream publisher IObservable to receive notifications of its messages.
When subscribing, instances of objects to receive notifications, callbacks to process messages, etc. are created.To avoid these instances remaining in memory beyond the lifetime of the Subscribe party, it is basically the Subscribe party's responsibility to unsubscribe when it no longer needs to receivenotifications.
There are several ways to unsubscribe, but for performance considerations, it is better to explicitly Dispose retain the IDisposable return value of Subscribe.
List 12.5:
1: public class Example : MonoBehaviour {
2: private IDisposable _disposable;
3:
4: private void Awake() {
5: _disposable = Observable.EveryUpdate()
6: .Subscribe(_ => {
7: // Processes to be executed every frame
8: });
9: }
10:
11: private void OnDestroy() {
12: _disposable.Dispose();
13: }
14: }
If your class inherits from MonoBehaviour, you can also call AddTo(this) to automatically unsubscribe at the timing of your own Destroy.Although there is an overhead of calling AddComponent internally to monitor the Destroy, it is a good idea to use this method, which is simpler to write.
List 12.6:
1: private void Awake() {
2: Observable.EveryUpdate()
3: .Subscribe(_ => {
4: // Processing to be executed every frame
5: })
6: .AddTo(this);
7: }
UniTask is a powerful library for high-performance asynchronous processing in Unity, featuring zero-allocation asynchronous processing with thevalue-based UniTask type.It can also control the execution timing according to Unity's PlayerLoop, thus completely replacing conventional coroutines.
UniTask v2, a major upgrade of UniTask, was released in June 2020.UniTask v2 features significant performance improvements, such as zero-allocation of the entire async method, and added features such asasynchronous LINQ support and await support for external assets. *3
On the other hand, be careful when updating from UniTask v1, as it includesdestructive changes, such as UniTask.Delay(...) and other tasks returned by Factory being invoked at invocation time,prohibiting multiple await to normal UniTask instances, *4 and so on.However, aggressive optimizations have further improved performance, so basically UniTask v2 is the way to go.
[*4] UniTask.Preserve UniTask v2 can be converted to a UniTask that can be awaited multiple times by using
UniTask Tracker can be used to visualize waiting UniTasks and the stack trace of their creation.
Figure 12.3: UniTask Tracker
For example, suppose you have a MonoBehaviour whose _hp is decremented by 1 when it collides with something.
List 12.7:
1: public class Example : MonoBehaviour {
2: private int _hp = 10;
3:
4: public UniTask WaitForDeadAsync() {
5: return UniTask.WaitUntil(() => _hp <= 0);
6: }
7:
8: private void OnCollisionEnter(Collision collision) {
9: _hp -= 1;
10: }
11: }
If _hp of this MonoBehaviour is Destroyed beforeis fully depleted, _hp will not be depleted any further, so UniTask, the return value of WaitForDeadAsync, will lose the opportunity to complete, andwill continue to wait.
It is recommended that you use this tool to check for UniTask leaking due to a misconfiguration of termination conditions.
The reason why the example code leaks a task is that it does not take into account the case where the task itself is destroyed before the termination condition is met.
To do this, simply check to see if the task itself has been destroyed.Or, the CancellationToken obtained by this.GetCancellationTokenOnDestroy() to itself can be passed to WaitForDeadAsync so that the task is canceled whenis Destroyed.
List 12.8:
1: // Pattern for checking whether the user is Destroyed or not
2: public UniTask WaitForDeadAsync() {
3: return UniTask.WaitUntil(() => this == null || _hp <= 0);
4: }
5:
6: // Pattern for passing a CancellationToken
7: public UniTask WaitForDeadAsync(CancellationToken token) {
8: return UniTask.WaitUntil(
9: () => _hp <= 0,
10: cancellationToken: token);
11: }
List 12.9: Example of WaitForDeadAsync(CancellationToken) call
1: Example example = ... 2: var token = example.GetCancellationTokenOnDestroy(); 3: await example.WaitForDeadAsync(token);
At Destroy time, the former UniTask completes without incident, while the latter OperationCanceledException is thrown.Which behavior is preferable depends on the situation, and the appropriate implementation should be chosen.