ScriptableObjects and Coroutines

A coworker and I recently fell into the ScriptableObject rabbit hole and thought it would be awesome if you could start Coroutines on ScriptableObjects. At the moment this is not possible and I do not see the Unity team changing this in the near future.

So, why do we even need this? In our case we wanted to have an authentication ScriptableObject which sends a webrequests to authenticate a user. There are a few workarounds to this but we did not like them a lot, but I will list them anyways.

  • Have some GameObject with a script attached in the scene. Find the script instance from the Scriptable Object with FindObjectOfType() or other ways to search your scene and trigger a public function on said script.
  • Have a singleton GameObject in your scene to skip the scene searching step by accessing the static T Instance attribute and call the public function from there.
  • Don’t use ScriptableObjects at all, be forever stuck in MonoBehaviour/Singleton-land and lose your will to live after trying to merge multiple git branches with overlapping Scene/ Prefab/ Nested-Prefab changes and hard-linked MonoBehaviours

So we came up with another option. The basic idea is that a ScriptableObject which wants to run a Coroutine spawns a GameObject in the Scene which will hold the Coroutine until it is finished and destroys itself afterwards.

Inherit your custom ScriptableObject from SO_Base instead of ScriptableObject and use StartCoroutine the same way you would in a normal MonoBehaviour.

using System.Collections;
using UnityEngine;

public class SO_Custom : SO_Base
{
    public void SomeFunction(int _a, int _b)
    {
        StartCoroutine(SomeHeavyCalculations(_a, _b));
    }

    private IEnumerator SomeHeavyCalculations(int _a, int _b)
    {
        yield return new WaitForSeconds(5);

        Debug.Log(_a + _b);
    }
}
using System.Collections;
using UnityEngine;

public class SO_Base : ScriptableObject
{
    protected void StartCoroutine(IEnumerator _task)
    {
        if (!Application.isPlaying)
        {
            Debug.LogError("Can not run coroutine outside of play mode.");
            return;
        }

        CoWorker coworker = new GameObject("CoWorker_" + _task.ToString()).AddComponent<CoWorker>();
        coworker.Work(_task);
    }
}
using System.Collections;
using UnityEngine;

public class CoWorker : MonoBehaviour
{
    public void Work(IEnumerator _coroutine)
    {
        StartCoroutine(WorkCoroutine(_coroutine));
    }

    private IEnumerator WorkCoroutine(IEnumerator _coroutine)
    {
        yield return StartCoroutine(_coroutine);
        Destroy(this.gameObject);
    }
}

What do you guys think?

Honestly, you’re dramatically overblowing the downsides of all of these things compared to your own solution. It’s fine to use MonoBehaviours and even Singleton patterns if you’re responsible with them, and in this context it’s actually a little difficult to get irresponsible. This feels a bit like busting out a saw and sandpaper because you can’t fit a square peg in a round hole instead of just putting it in the square hole.

4 Likes

We did something similar for our in game tutorial

    public abstract class TutorialStep : ScriptableObject
    {
        public abstract IEnumerator Execute();
    }

    [CreateAssetMenu(menuName = "Tutorial/PlaceBombStep")]
    public class PlaceBombStep : TutorialStep
    {
        public Transform PlacementPrefab;
        public override IEnumerator Execute()
        {
            var bomb = Get<BombCase>();
            var placeMentGameObject = Instantiate(PlacementPrefab);
            var placement = placeMentGameObject.GetComponentInChildren<BombCasePlacement>();
            ShowPopup(placement.transform, "Place the bomb inside the marker.");
            while (bomb.State != BombCase.BombState.Placed)
                yield return null; 
            Destroy(placeMentGameObject.gameObject);
        }
    }

Its up to the caller to execute it. Scripable assets are just serializable data containers. I see no wrong in letting a outer worker handle the execution of the Coroutine as it deem fit

4 Likes

It is overcompliated.

You can create a global singleton object that will spawn itself on the first request, and add a static “spawnCoroutine” method to it. Instead you spawn multiple ones and enforce inheritance from a specific base which is more hassle.

3 Likes

To create a ScriptableObject you have to inherit from ScriptableObject anyways, so its not that much of an inconvenience to inherit from a custom ScriptableObject base class. I would even recommend it to be honest. A simple description string you can fill out in the editor to describe what your ScriptableObject does is pretty neat. Stole that idea from unity-atoms. So our ScriptableObject base class actually looks like this:

using UnityEngine;

public class SO_Base : ScriptableObject
{
    [TextArea(5, 25), SerializeField]
    private string description;

    protected void StartCoroutine(IEnumerator _task)
    {
        if (!Application.isPlaying)
        {
            Debug.LogError("Can not run coroutine outside of play mode.");
            return;
        }

        CoWorker coworker = new GameObject("CoWorker_" + _task.ToString()).AddComponent<CoWorker>();
        coworker.Work(_task);
    }
}

And we will probably add more stuff to it in the future. Another advantage would be, that you dont have to split up your logic into ScriptableObject and MonoBehaviour code snippets.
If you are even using ScriptableObjects for that stuff. Something I really like is that your scene is very clean in the editor because your code does not have to exist on a GameObject in the scene permanently. But that’s just me.

The actual discussion should probably be: Is it a good idea to use ScriptableObjects for stuff like this.

The only ‘official’ source from Unity I could find to not use ScriptableObjects only as data containers is this:

https://www.youtube.com/watch?v=6vmRwLYWNRo

He is doing something very similar here at [46:40]. This is where the basic idea for this came from.

1 Like

In the video they are doing exactly how I do it. They execute the method that returns the enumerator and runs the Coroutine from a scene aware place.

2 Likes

Yeah but you are not doing what they do in the video. Creating a new gameobject each time to handle coroutine execution is a bad way to approach this and creates unnecessary garbage. Empty gameobjects are not “free” performance wise btw, far from it. What if I fire off 10 of these, I now have 10 dead gameobjects floating around? And for what exactly?

You should have an existing MB handle executing the coroutine, the SO should just contain a way to call it from outside but not handle its execution directly. This makes total sense because a SO is a scriptable serializable data container, it is absolutely not a “script”. It exists at asset level, not scene level.

3 Likes

Correction, a empty gameobject with managed wrapper is not free. A empty game object in scene is pretty much free. Unity loads its managed wrappers lazy so there for the footprint of empty scene game objects is negligible

Its not entirely free, thats why entities in DOTS are so much lighter weight than GameObjects when comparing the two :slight_smile: You will definately see some (small) difference if you spawn 10000 empty gameobjects vs none in a scene

But yeah I know what you mean and are saying :slight_smile: I also dont think the perf hit of gameobjects matters that much in this context, but I do think its important to know they are not actually “empty” underneath

Although I know how deeply things are nested matters too, a flat hierarchy is super cheap vs deep nested one

1 Like

A world is built on thousands of gameobjects that are just containers for mesh renders, colliders etc. These only reside in native memory, the cost of these gameobjects are close to free. That’s my point :slight_smile:

Edit: 1000 empty gameobjects is probably not measurable.

1 Like

Yeah I missed the word “managed” in your original post :smile: I thought you were saying “they take up no memory anywhere!” and I was like wtf how? But this makes way more sense lol

1 Like

:stuck_out_tongue:

Btw, the rumored switch from Mono to CoreClr (Net 6) will shift alot of this. It will probably make il2cpp obsolete for example. In my testas Net and more so Net 6 beats il2cpp and even handwritten cpp in many cases.

I’m myself do scene aware stuff in none scene types sometimes. Here is an exexample

    public class InstanceCountPool
    {
        private static InstanceCountPool instance;
        private readonly Dictionary<string, InstanceCountPoolNamed> pools = new Dictionary<string, InstanceCountPoolNamed>();

        static InstanceCountPool()
        {
            SceneManager.sceneUnloaded += SceneUnloaded;
        }

        private static void SceneUnloaded(Scene scene)
        {
              instance = null;           
        }

        public static InstanceCountPoolNamed Get(string name, int maxCount)
        {
            if (instance == null)
                instance = new InstanceCountPool();

            if(!instance.pools.ContainsKey(name))
                instance.pools[name] = new InstanceCountPoolNamed(maxCount);

            return instance.pools[name];
        }
    }



    public class InstanceCountPoolNamed
    {
        private readonly int maxCount;
        private readonly Queue<PrefabRel> active = new Queue<PrefabRel>();
        private readonly Dictionary<Component, Queue<Component>> pool = new Dictionary<Component, Queue<Component>>();

        public InstanceCountPoolNamed(int maxCount)
        {
            this.maxCount = maxCount;
        }

        public T Get<T>(T prefab) where T : Component
        {
            if (active.Count > maxCount)
            {
                var stale = active.Dequeue();
                pool[stale.Prefab].Enqueue(stale.Instance);
                stale.Instance.gameObject.SetActive(false);
            }

            if (!pool.ContainsKey(prefab))
                pool[prefab] = new Queue<Component>();

            var queue = pool[prefab];
            T instance;
            if (queue.Count == 0)
                instance = GameObject.Instantiate(prefab);
            else
            {
                instance = (T) queue.Dequeue();
                instance.gameObject.SetActive(true);
            }

            active.Enqueue(new PrefabRel{ Prefab = prefab, Instance = instance});
            return instance;
        }

        private struct PrefabRel
        {
            public Component Prefab { get; set; }
            public Component Instance { get; set; }
        }
    }

Used like

        public void EjectCasing() {
            if (Bullet == null) return;

            var bulletPrefab = Bullet.Spent ? Bullet.Type.BulletEmptyPrefab : Bullet.Type.BulletPrefab;
            var casing = InstanceCountPool
                .Get("Casings", 360) //12 players and 30 shots per mag ca
                .Get(bulletPrefab);

            casing.transform.position = CasingSpawnPoint.position;
            casing.transform.rotation = CasingSpawnPoint.rotation;

            var body = casing.Body;

            casing.Init(IgnoredColliders);

            body.velocity = Velocity;
            body.maxAngularVelocity = body.maxAngularVelocity * 10;
            AddForceToCasing(body);

            Bullet = null;          
        }

Somehow this feels more ok :stuck_out_tongue:

1 Like

While I do not necessarily agree with your second point here, your first point got me thinking. What if someone starts a lot of Coroutines every frame. That would create a lot of CoWorker GameObjects every frame, that are floating around in your scene until the Coroutine is finished. And after that the GC would have to clean up a lot of stuff.
So we tried to enforce only one instance of our CoWorker class.

using System.Collections;
using UnityEngine;

public class CoWorker : MonoBehaviour
{
    private static CoWorker _instance;

    public static Coroutine Work(IEnumerator _task)
    {
        if (!Application.isPlaying)
        {
            Debug.LogError("Can not run coroutine outside of play mode.");
            return null;
        }

        if (!_instance) //edit old: if(_instance == null)
        {
            _instance = new GameObject("CoroutineWorker").AddComponent<CoWorker>();
            DontDestroyOnLoad(_instance.gameObject); //edit2, probably a good idea
        }

        Coroutine coroutine = _instance.StartCoroutine(_task);
        return coroutine;
    }
}

And our ScriptableObject base class now looks like this, but you really dont have to use it anymore, because all it does is call CoWorker.Work(). Which is pretty cool because the inheritance is not mandatory anymore but you can still use it if you want to use normal Unity syntax to start your Coroutines.

using System.Collections;
using UnityEngine;

public class SO_Base : ScriptableObject
{
    protected Coroutine StartCoroutine(IEnumerator _task)
    {
        return CoWorker.Work(_task);
    }
}

Another thing we did not like about our old version was that our StartCoroutine() / CoWorker.Work() did not return a Coroutine. But not it does and you can do fancy stuff like waiting for a Coroutine inside of a Coroutine. So we fixed that and found another problem.

I really wanted the CoWorker do destroy itself after it was not used anymore tho! Maybe after a second of no SO Coroutines or something…
Unfortunately I could not get it to work. You might have done something like this inside of a Coroutine before:

yield return StartCoroutine(A_Coroutine());

In our case we would have to wait for the Coroutine from the CoWorker and the ScriptableObject.
Which gets us to this cool new Unity message:
Only one coroutine can wait for another coroutine

:frowning:

I then tried to save a list of Coroutines running on the CoWorker to check them for null at certain intervals, but they never get nulled. I know you can save Coroutines in variables to check if there is already a Coroutine running, but this does not work with Coroutine lists. Really dont know why.
After that I got some horrible ideas about writing my own Coroutines and that is when I decided it was time to drop the idea of a self destructing CoWorker.

If you have some ideas on how it could be possible to destroy the CoWorker instance after all Coroutines are done please let me know. Otherwise I think my ScriptableObject / Coroutine journey ends here.

Your code will break if you unload current scene and load a new one. That will kill the GO and next time you use it its destroyed

Change if(instance == null) to if(!instance) it will fix that

2 Likes

Just like I said it earlier.
https://discussions.unity.com/t/841443/4

A coroutine is not a thread, and you can roll your own, if you really want to.

All a coroutine does is returning IEnumerator and when it calls a subroutine, it yield returns another Enumerator. Unity seems to be storing this expression internally. They’re a hack based on generator expressions.

For example, following expression, should be able to “unroll” a coroutine instantly.

    static void unrollCoroutine(IEnumerator enumerator){
        if (enumerator == null)
            return;
        while(enumerator.MoveNext()){
            var nested = enumerator.Current as IEnumerator;
            if (nested != null)
                unrollCoroutine(nested);
        }
    }
2 Likes

Thats dangerous though :smile:
Potentielly hangup render thread.

edit: well it will stack overflow in this case since its recursive.

Nope, you didn’t pay enough attention.

It goes without saying that you should use common sense when trying to unroll an infinite loop. And should only do so when necessary.