Fang
Fang

Reputation: 1

Unity ScriptableObject Factory

My factory creates instances based off ScriptableObject's ("SO") properties.

I'd like to not need to append to the Factory each new SO that I create. So ideally something like each SO registering itself in the factory on it's awake would be great.

I've explored two approaches, one is having the factory not be a MonoBehaviour:


public class ThingFactory
{
    private static ThingFactory instance = null;

    public static ThingFactory Instance
    {
        get
        {
            if (instance != null)
            {
                return instance;
            }
            else
            {
                instance = new ThingFactory();

                return instance;
            }
        }

        private set
        {
            instance = value;
        }
    }

    public ThingFactory()
    {
    }

    public void RegisterThing(ThingType thingType)
    {
        GameObject newThing = MonoBehaviour.Instantiate(thingType.prefab, Camera.main.transform);

        newThing.SetActive(true);

        newThing.GetComponentInChildren<Text>().text = thingType.name;

        newThing.GetComponent<Image>().sprite = thingType.sprite;
    }
}

public abstract class ThingType: ScriptableObject
{
    public GameObject prefab;

    public Sprite sprite;


#if UNITY_EDITOR
#else
    public void OnEnable()
    {
        Debug.Log("Enabling ThingType of type: " + this.name);

        ThingFactory.Instance.RegisterThing(this);
    }
#endif
}


[CreateAssetMenu(menuName = "Things/ThingA")]
public class ThingAType : ThingType
{       
}

Three problems with this approach.

  1. The first is that OnEnable is called from the editor, I've gotten around that with the if UNITY_EDITOR.

  2. The second problem is that OnEnable is called only when the scene has a MonoBehaviour referencing that ScriptableObject asset which isn't always the case. Even now when I'm reproducing the behaviour in a small project I can't put my finger on the exact requirements for OnEnable to be called.

  3. The third problem is that I'm receiving this exception:

Some objects were not cleaned up when closing the scene. (Did you spawn new GameObjects from OnDestroy?) Image prefabOfThing(clone)

I don't understand this exception, it makes sense to me that if OnEnable is called before the scene loads, and then the SO is destroyed at scene load.


The second approach is to have the factory be a MonoBehaviour, and do reflection magic.

In it's awake have something like:

var thingTypes = Assembly.GetAssembly(typeof(ThingType)).GetTypes().Where(thingType => thingType.IsClass && !thingType.IsAbstract && thingType.IsSubclassOf(typeof(ThingType)));

But now I'm stuck - I have the classes for ThingTypes, like, ThingAType but not the individual SOs.

And an additional problem, is that OnEnable is called before my factory's Awake, so it is not initialized yet. Script execution order does not improve the situation.

I don't know of a good way of instantiating the factory pre-awake, I thought about using something like the following. But the issue with that approach is that the instance of the MonoBehaviourFactory would not have the references dropped in from the inspector such as someInspectorText.

public static MonoBehaviourFactory Instance
    {
        get
        {
            if (instance != null)
            {
                return instance;
            }
            else
            {
                instance = /*SomeRandomGameObject*/.AddComponent<MonoBehaviourFactory>();

                return instance;
            }
        }

        private set
        {
            instance = value;
        }
    }

    private void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(this.gameObject);
        }
        else
        {
            instance = this;
        }
    }

    public Text someInspectorText = null;

How can I use OnEnable on ScriptableObjects to populate my factory with the types it will dispense?

Upvotes: 0

Views: 1567

Answers (1)

Adrian Babilinski
Adrian Babilinski

Reputation: 31

Instead of having the scriptable objects add themselfs, consider having the factory callResources.LoadAll<T> this will load all the objects of type T in your project.

If you really want to call a method on a ScriptableObject when the game starts, consider using the [RuntimeInitializeOnLoadMethod]attribute [RuntimeInitializeOnLoadMethod]- Allow a runtime class method to be initialized when a game is loaded at runtime without action from the user. (The script does not need to be in the scene) The code for that would look something like this:

// The name of the method does not matter. The magic is in the attribue.
//Add this method to any class to see the log print when the game starts.
[RuntimeInitializeOnLoadMethod]
private static void Init()
{

 Debug.Log("The game has started and this class is Initialized");
}

As pointed out by derHugo: The attribute initializes the class itself so it would not work as a way to add instances to a list

Another possible other solution: If you want to avoid Resource.LoadAll would be to do something like:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DemoScriptable : ScriptableObject
{
    //Hot Cold Split. This can also be a dictionary, but this way you have list and can run a for or foreach loop.
    public static List<DemoScriptable> DemoScriptables = new List<DemoScriptable>();
    public static List<Guid> DemoScriptableHashCodes = new List<Guid>();

    private Guid guid;
    private void Awake()
    {
        //can be HashCode, but this is not guaranteed to be unique.
        // the other option is using InstanceID
        guid = Guid.NewGuid();

        //check guid to avoid complex comparison
        if (!DemoScriptableHashCodes.Contains(guid))
        {
            DemoScriptableHashCodes.Add(guid);
            DemoScriptables.Add(this);
        }

    }

    private void Destroy()
    {
        // check guid to avoid complex comparison
        if (DemoScriptableHashCodes.Contains(guid))
        {
            DemoScriptables.Remove(this);
            DemoScriptableHashCodes.Remove(guid);
        }
    }

}

To access the list call DemoScriptable.DemoScriptables

Upvotes: 1

Related Questions