Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get all components with a specific interface in Unity

Tags:

c#

unity3d

some GameObjects in my scene implement the interace ISaveable. In my script, I want to find all these interfaces and store them. Later on I can loop through them and call their implemented method SaveData().

My current workaround to find these interfaces:

    List<ISaveable> saveables = new List<ISaveable>();
    MonoBehaviour[] sceneObjects = FindObjectsOfType<MonoBehaviour>();
    
    for (int i = 0; i < sceneObjects.Length; i++)
    {
        MonoBehaviour currentObj = sceneObjects[i];
        ISaveable currentComponent = currentObj.GetComponent<ISaveable>();

        if (currentComponent != null)
        {
            saveables.Add(currentComponent);
        }
    }

The code works fine but is there a better way? I don't want to search for each Monobehaviour in the scene and then try to get its interface component.

like image 448
Question3r Avatar asked Mar 16 '18 21:03

Question3r


2 Answers

Simplified filter using Linq

You can use Linq OfType

ISaveable[] saveables = FindObjectsOfType<MonoBehaviour>().OfType<ISaveable>().ToArray();

of course it still requires to find all objects first.

Note though that it underlies the general limitations of FindObjectsOfType regarding inactive GameObjects or disabled behaviors.


Iterate through the Hierachy

You could extend it to go through all root level objects of the scene. This works since afaik GetComponentsInChildren indeed does work also with interfaces!

var saveables = new List<ISaveable>();
var rootObjs = SceneManager.GetActiveScene().GetRootGameObjects();
foreach(var root in rootObjs)
{
    // Pass in "true" to include inactive and disabled children
    saveables.AddRange(root.GetComponentsInChildren<ISaveable>(true));
}

If it's more efficient - yes, no, maybe, I don't know - but it includes also inactive and disabled objects.

And yes one could extend that to iterate through multiple loaded scenes using

var saveables = new List<ISaveable>();
for(var i = 0; i < SceneManager.sceneCount; i++)
{
    var rootObjs = SceneManager.GetSceneAt(i).GetRootGameObjects();
    foreach(var root in rootObjs)
    {
        saveables.AddRange(root.GetComponentsInChildren<ISaveable>(true));
    }
}

Not use an interface in the first place

This alternative is a bit similar to this answer but it has a huge flaw: You would need a specific implementation for the interface in order to make it work which invalidates the whole idea of an interface.

So the big question also from the comments there is: Why have an interface at all?

If it is anyway only going to be used for MonoBehaviour you should rather have an abstract class like

public abstract class SaveableBehaviour : MonoBehaviour
{
    // Inheritors have to implement this (just like with an interface)
    public abstract void SaveData();
}

This already solves the entire issue with using FindObjectsOfType anyway since now you could simply use

SaveableBehaviour[] saveables = FindObjectsOfType<SaveableBehaviour>();

but you can still go further: For even easier and more efficient access you can make them register themselves completely without the need of a manager or Singleton pattern! Why should a the type not simply handle its instances itself?

public abstract class SaveableBehaviour : MonoBehaviour
{
    // Inheritors have to implement this (just like with an interface)
    public abstract void SaveData();

    private static readonly HashSet<SaveableBehaviour> instances = new HashSet<SaveableBehaviour>();

    // public read-only access to the instances by only providing a clone
    // of the HashSet so nobody can remove items from the outside
    public static HashSet<SaveableBehaviour> Instances => new HashSet<SaveableBehaviour>(instances);

    protected virtual void Awake()
    {
        // simply register yourself to the existing instances
        instances.Add(this);
    }

    protected virtual void OnDestroy()
    {
        // don't forget to also remove yourself at the end of your lifetime
        instances.Remove(this);
    }
}

so you can then simply inherit

public class Example : SaveableBehaviour
{
    public override void SaveData()
    {
       // ...
    }

    protected override void Awake()
    {
        base.Awake(); // Make sure to always keep that line

        // do additional stuff
    }
}

and you could access all instances of that type via

HashSet<SaveableBehaviour> saveables = SaveableBehaviour.Instances;
foreach(var saveable in saveables)
{
    saveable.SaveData();
}
like image 67
derHugo Avatar answered Sep 21 '22 13:09

derHugo


You could have a manager class holding a collection/list of ISaveable.

You can then make that manager class a singleton by setting the Singleton value in its Awake method.

class SaveManager(){
public static SaveManager Instance;
Awake(){
if (Instance == null)
   {
    Instance = this;
   }
}

List<ISaveable> SaveAbles;
public void AddSaveAble(ISaveAble saveAble){
//add it to the list.
}
}

Then in the Start method of class implementing the ISaveable interface you can use the Singleton to add it to the total list.

class Example : MonoBehaviour, ISaveable
{
void Start(){
SaveManager.Instance.AddSaveAble(this);
}
}

That way, each SaveAble adds itself to the manager via the managers method when it is created.

Note that it is important that the Singleton is set in Awake so it can be used in Start, as Awake comes first in the lifecycle.

like image 39
Doh09 Avatar answered Sep 17 '22 13:09

Doh09