Search Unity

Question Accessing a list of assorted (different) scripts

Discussion in 'Scripting' started by TiggyFairy, Jan 24, 2023.

  1. TiggyFairy

    TiggyFairy

    Joined:
    Dec 22, 2019
    Posts:
    505
    So, I've found out you can actually create a variable that can hold literally any kind of MonoBehaviour script like so:
    Code (CSharp):
    1. public MonoBehaviour script;
    What I want to do is use this to trigger off totally different scripts by calling methods that all have the same name. Like, say,
    Activate()
    or
    Toggle()
    . Or a customised
    Trigger()
    in a trap script fired off by a button or pressure plate. This would be huge for me as it would make my pressure plates and buttons totally universal, and also make my game a lot more moddable. Unfortunately, triggering the method itself doesn't seem to work - and I get the following error:

    Is there a way to store a generic script like this (and trigger a method inside it) without hard-coding the name of the script?
     
  2. TzuriTeshuba

    TzuriTeshuba

    Joined:
    Aug 6, 2019
    Posts:
    185
    If you know that all these scripts (MonoBehaviours) will have functions Foo1(...), Foo2(...), etc. Why not define an interface that contains declarations of these functions? then you can have all your relevent scripts implement the interface. your field would become
    Code (CSharp):
    1. public iMyInterface script;
    The issue you are having is that c# is typed, and not every monobehaviour has the function "Toggle()"

    something like:
    Code (CSharp):
    1. public interface IMyInterface{
    2. void Activate();
    3. void Toggle();
    4. void Trigger();
    5. }
     
    TiggyFairy likes this.
  3. Daviiid

    Daviiid

    Joined:
    Oct 9, 2015
    Posts:
    50
    You should use Interfaces for this.
    check out a tutorial on it
     
    TiggyFairy likes this.
  4. TiggyFairy

    TiggyFairy

    Joined:
    Dec 22, 2019
    Posts:
    505
    That sounds like a great idea, thanks a lot. I do have an issue though - the field no longer shows up in the editor, despite being public. Any ideas on how to fix that? I tried
    [SerializeField, SerializeReference] public IInterface script;
    and all I got was the name.
     
    Last edited: Jan 24, 2023
  5. Daviiid

    Daviiid

    Joined:
    Oct 9, 2015
    Posts:
    50
    I think your misunderstanding something.
    (Edit: gets more interesting from 9min onward)
    Watch this:

    Till the end
     
    TiggyFairy likes this.
  6. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,859
    It's not about serialising via an interface (Unity can't serialise Unity objects via interfaces), it's about checking if the component implements the interface, like this:
    Code (CSharp):
    1. if (myScript is ISomething something)
    2. {
    3.     something.DoSomething();
    4. }
     
  7. TzuriTeshuba

    TzuriTeshuba

    Joined:
    Aug 6, 2019
    Posts:
    185

    you may have to tag your class implementations of your interface with [System.Serializable]. Another option, similar to interfaces, is using abstract classes.
    Code (CSharp):
    1. public abstract class MyAbstractClass : MonoBehaviour{
    2. abstract void Toggle();
    3. ...
    4. }
    there are pros and cons depending on your situation to using interfaces or abstract classes.
    Ill check into it when i get a chance. I could swear that i serialize interface instances all the time, but may be wrong.
     
  8. TiggyFairy

    TiggyFairy

    Joined:
    Dec 22, 2019
    Posts:
    505
    Thanks, that would be very helpful.
     
  9. TiggyFairy

    TiggyFairy

    Joined:
    Dec 22, 2019
    Posts:
    505
    Well, I mean, what I want to do is have a slot I can drag and drop scripts to in the editor that will store the interface without needing a
    GetComponent<IInterface>()
    . It's not a dealbreaker if I can't, but it's helpful if I can.

    That was very helpful, thanks. Though it didn't show a way to serialize a slot to add one to.
     
  10. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,859
    You can't.
     
  11. TiggyFairy

    TiggyFairy

    Joined:
    Dec 22, 2019
    Posts:
    505
    That's a shame. Though I've found a workaround. I'm storing it using
    public MonoBehaviour script;
    and accessing it using
    (v.script as IActivator).Activate();
     
  12. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,859
    I'd personally recommend doing the cast-check in my other post. Using
    as
    for a cast, if the type can't be cast, gives you null. Doing the older style of cast
    ((IActivator)v.script).Activate();
    will give you an Invalid Cast Exception.

    So in both cases you'll run headlong into an error. Whereas you could at least have it gracefully fail and inform you of it rather than stopping playmode entirely.
     
  13. TzuriTeshuba

    TzuriTeshuba

    Joined:
    Aug 6, 2019
    Posts:
    185
    so you can't serialize interfaces, but you absolutely can serialize abstract classes (and lists of them). look into abstract classes as you'll get more info than I can give here, but it basically allows you to declare functions of a class and delay the implementation for child classes. its similar to normal inheritence, which could also be a solution for your issue.

    I want to note that you cant just serialize any abstract class, its has to be serializeable in its own merit, but marking it abstract will not disqualify it. monobehaviour is reference serializeable to the inspector, and extending Monobehaviour should maintain that serializeability.
     
  14. TiggyFairy

    TiggyFairy

    Joined:
    Dec 22, 2019
    Posts:
    505
    Isn't that what I did?

    Thanks. I had a look and I think I prefer using the Interfaces with the script stored in the monobehaviour due to the ability to add multiple types to one script. It seems to work really well.
     
  15. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,859
    No I mean what I wrote here:
     
  16. TiggyFairy

    TiggyFairy

    Joined:
    Dec 22, 2019
    Posts:
    505
    Oh, okay, thank you. Any idea how I do this with variables btw? I've added
    public interface IStats { double weight { get; set; } }
    to my list as a test, but even though I have the variable already it says:

    This is odd because I was following a tutorial.
     
  17. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    Do you have the variable? Or the required properties?

    public double weight;
    is not the same as
    public double weight {get; set;}

    Try removing the variable and re-adding it through Intellisense.
     
  18. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    I usually prefer
    (v.script as IActivator)?.Activate()
    but I guess that's just preference.
     
  19. Daviiid

    Daviiid

    Joined:
    Oct 9, 2015
    Posts:
    50
    I can't see your problem.
    First you make your Interface:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public interface yourInterfaceName
    6. {
    7.     void Activate();
    8.     void Toggle();
    9.     void  Trigger();
    10. }
    Then if you declare [SerialeField] yourInterfaceName _yourInterfaceName; in your script where you activate, toggle, trigger from. You can drag and drop scripts there.
    Then you go and execute it _yourInterfaceName.Activate(); somewhere in your code


    and in each of your scripts:

    Code (CSharp):
    1. public class Script_11111 : MonoBehaviour, yourInterfaceName
    2. {
    3.    public void Activate()
    4.    {
    5.         //Do Something...
    6.    }
    7.    
    8.    public void Toggle()
    9.    {
    10.         //Do Something...
    11.    }
    12.  
    13. }

    Code (CSharp):
    1. public class Script_2222 : MonoBehaviour, yourInterfaceName
    2. {
    3.    public void Activate()
    4. {
    5.   //Do Something else........
    6. }
    7. }

    You can change the contents of your Activate Toggle Trigger methods.
     
  20. TiggyFairy

    TiggyFairy

    Joined:
    Dec 22, 2019
    Posts:
    505
    Well my Interface is
    public interface IStats { double weight { get; set; } }
    and for that Intelisense is suggesting I add this to the classes I want to use the Interface with:

    Code (CSharp):
    1. double IStats.weight { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }[
    Which is a bit much, honestly. No idea what I'm meant to do with that. I just want to access the variable remotely.
     
  21. TiggyFairy

    TiggyFairy

    Joined:
    Dec 22, 2019
    Posts:
    505
    Also, for some reason, all the buttons I'm testing this on are coming up with Null errors even though I'm checking if the things are null and these are different scripts that do different things. For example:

    Code (CSharp):
    1. public MonoBehaviour activateThis;
    2.  
    3. public void ToggleButton()
    4.     {
    5.         (activateThis as IActivate).Toggle();
    6.     }
    I've checked the activateThis. It's not null.
     
    Last edited: Jan 27, 2023
  22. DTECTOR

    DTECTOR

    Joined:
    Aug 23, 2020
    Posts:
    132
    You want an abstract class that inherits from MonoBehavior. You get the serialization and Unity feature support from MonoBehavior and then you create your "interface" contracts aka the universal methods for your executing in the abstract class. I have used this for the exact purpose you are trying to implement. Universal api for all components. I also use this as a powerful alternative to conditional statements because each object is self contained so when I add to the array of conditions, I am just making a new class with desired effect in method so they're condition is essentially encapsulated in the method so I can scale without having to implement new logic in several places. Just loop and call method on objects rather than if this else if this or switch this and that. I never have to refactor anything. Polymorphism is very powerful. Of course you can't use the abstract class directly so then your scripts should inherit from the abstract class. When you store them in a structure you can call those universal methods that are declared/defined in the abstract class and they will execute the logic exclusive to their class method's implementation, and if you decide do a virtual base method for the abstract class you can always use that in your inheriting classes via

    base.NameOfMethod()


    A great example for this would be the key monobehavior methods such as Awake, Start, Update, etc. I often have a:

    base.Awake();


    In the child classes of the abstract classes calling some universal logic that should be shared among the types.

    And as for your preference of monobehavior with interfaces because you can keep adding, um just keep adding interfaces to your abstract class that inherits from monobehavior:p, you now have both your problems solved (keep in mind interfaces are super slow compared to abstract classes).

    Unity uses this same approach for all their apis. For example, an editor window is just a scriptableobject. Same on the rendering side. For postprocessing how do you create a volume?


    m_Vignette = ScriptableObject.CreateInstance<Vignette>();


    You can also call this method from just about any class that deals with the editor or postprocessing.

    Another example, creating an editor window:

    EditorWindow.CreateInstance<ChickenNuggetWindow>();


    That method is from?

    ScriptableObject because it's just an extended ScriptableObject instance.

    If we go to the EditorWindow.CreateInstance method definition, we will find that's not defined in the editor window class and is directly called from it's parent class ScriptableObject:

    public static ScriptableObject CreateInstance(System.Type type) => ScriptableObject.CreateScriptableObjectInstanceFromType(type, true);

    public static T CreateInstance<T>() where T : ScriptableObject => (T) ScriptableObject.CreateInstance(typeof (T));
     
    Last edited: Jan 27, 2023
  23. TiggyFairy

    TiggyFairy

    Joined:
    Dec 22, 2019
    Posts:
    505
    Thanks for your help. I may try abstracts, but are you sure they're slower? I've heard other people say there's barely any difference, but that abstract classes are *theoretically* faster.

    To be honest, part of what you say seems to be wrong as abstract classes demand all their methods be overridden in the derived class.
     
    Last edited: Jan 29, 2023
  24. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    Interfaces declare methods/functions, not variables. That's why it's requiring you to add a getter/setter instead of a variable. If you want to have the actual variable in there, you should be using an (abstract) base class.

    Abstract classes demand all their ABSTRACT methods are overridden. They can have their own (virtual) implementations, and if these are not marked abstract they do not need to be implemented in child classes (like with any other class).

    As far as your null-references go: Make sure you've dragged in the correct script. When you're adding a "MonoBehaviour" or "Component", it will pull the first script from any GameObject/Prefab you drag in. This might not be the script you're trying to add. If that's the case, open 2 inspectors (by locking one of them) and drag the actual script from the prefab(-inspector) onto your field.
     
  25. TiggyFairy

    TiggyFairy

    Joined:
    Dec 22, 2019
    Posts:
    505
    Thank you. Though I have checked the nulls and they were all dragged in.
     
  26. SF_FrankvHoof

    SF_FrankvHoof

    Joined:
    Apr 1, 2022
    Posts:
    780
    Are you sure they're the correct scripts, and not other MonoBehaviours from those GameObjects?
     
  27. DTECTOR

    DTECTOR

    Joined:
    Aug 23, 2020
    Posts:
    132
    I think you misread the post above by SF_FrankvHoof, only methods with the keyword abstract must be redefined. Abstract prevents the structure from being used directly, so declaring a class abstract requires you to declare another class to inherit it, thus using abstract on a method will then require you to define in child class. But you can do a base definition of anything you want in an abstract class. Do what you want, I've been where you are a long time ago. The details of C# performance are of great importance to me. I am very familiar with the behaviors of the language. And in your case the abstract class is the best option. If you already have a working solution with an interface I'd say just keep going. It probably won't be a big deal, the performance difference is marginal, but implement it everywhere and those marginal differences add up. You'll have a better idea of whether to refactor later when you've built out your features.

    Interface is just a contract, it tells what to look for. Inheritance provides them so no need to search. This performance difference is more prominent in programming languages that do not support multiple inheritance. C# is such a case. In fact that is what interfaces address.

    Anything you declare in an interface will be required to be found each time, anything in an abstract class will be provided immediately.
     
    Last edited: Jan 31, 2023