Introduction
Hi!
I am Alejandro Santiago, co-founder of Tochas Studios. Main, and only programmer, in the team.
It took us three years to make SodaCity, our latest and biggest project so far, during that time we missed a few things, broke some others, overall learned a few tricks here and there. In this blog I will try to elaborate on those findings so anyone that stumbles upon this posts can learn a few things from us too and avoid the downsides.
As I am the 'tech' guy in the team most of my posts will cover technical topics.
In this first post I will address a small trick I came to find regarding the use of coroutines in Unity3d.
Optimize Memory Usage and Performance when using Coroutines in Unity3d
The
Coroutines in Unity offer great advantages for building complex behaviors and it use can span from a couple of objects to thousands as they can be used for AI agents, fade effects, motion controllers, bullets, particles, UI components, etc.
Guides on how to use coroutines and even nested coroutines are common, easy to find and digest.
But in all references I came across the same principle was applied.
IEnumerator MyCoroutine (Transform target)
{
while(Vector3.Distance(transform.position, target.position) > 0.05f)
{
transform.position = Vector3.Lerp(transform.position, target.position, smoothing * Time.deltaTime);
yield return null;
}
print("Reached the target.");
yield return new WaitForSeconds(3f);
print("MyCoroutine is now finished.");
}
Yielding a
new YieldInstruction every time a delay or pause in the coroutine execution is needed.
As the coroutine can be executed every frame or multiple times per second and the behavior can be attached to multiple objects (ex. Bullets or Enemies) this can potentially cause several even thousand of YieldInstructions to be created each frame.
To avoid the GC problem it is recommended to
pool objects. with almost every optimization guide I came across they referred to it as
GameObject pooling. But I never came across a YieldInstruction pooling.
As
SodaCity code grew in size and complexity each build was using more and more coroutines within more and more behaviors and objects. That had me worried as this could cause problems in the long run, So I decided to try to cache
YieldInstruction objects and see what happened.
The results are as follows
Yes! You can and should pool or cache your YieldInstruction objects.
As all of the yielders provided by Unity do not expose members to allow changing values of already created objects, simply caching the references and reusing them at the instance level wouldn't do the trick (for most cases).
WaitForSeconds shortWait = new WaitForSeconds(0.1f);
WaitForSeconds longWait = new WaitForSeconds(5.0f);
IEnumerator myEvenAwesomerCoroutine()
{
while (true)
{
if (iNeedToDoStuffFast)
{
doAwesomeStuffReallyFast();
yield return shortWait;
}
else{
dontDoMuch();
yield return longWait;
}
}
}
The solution I am proposing is to use a generic
Dictionary within a
Yielders static class to allow
cache and
reuse of
yielder objects at the application/game level.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public static class Yielders {
static Dictionary<float, WaitForSeconds> _timeInterval = new Dictionary<float, WaitForSeconds>(100);
static WaitForEndOfFrame _endOfFrame = new WaitForEndOfFrame();
public static WaitForEndOfFrame EndOfFrame {
get{ return _endOfFrame;}
}
static WaitForFixedUpdate _fixedUpdate = new WaitForFixedUpdate();
public static WaitForFixedUpdate FixedUpdate{
get{ return _fixedUpdate; }
}
public static WaitForSeconds Get(float seconds){
if(!_timeInterval.ContainsKey(seconds))
_timeInterval.Add(seconds, new WaitForSeconds(seconds));
return _timeInterval[seconds];
}
}
This can be done because it seems Coroutine objects only use YieldInstructions as an exit condition or objective, and each Coroutine object handle its current state independently of the current YieldInstruction yielding the execution or the GameObject the behavior is attached to.
In the case of
WaitForSeconds we need to use the Dictionary using the float 'waitForSeconds' value as a key. This will allow to reuse a WaitForSeconds for a specific time interval.
To validate this theory and measure the real gains or losses for each method I made a small test project. It can be
downloaded from
here.
To see details on the tests performed, continue reading...