Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

How to call interface method from any class that implements interface?

Discussion in 'Scripting' started by deltakai2010, Jun 15, 2022.

  1. deltakai2010

    deltakai2010

    Joined:
    Apr 9, 2022
    Posts:
    3
    For some context, I'm currently trying to create a clone of the game "Megaman Battle Network" for learning purposes. In this game, there is a battle-chip system where before the beginning of a fight, you select a number of chips that you get to use during the fight and these chips essentially act as one-time use abilities (Kind of like a card game except things are playing out in real-time rather than turn-based)
    upload_2022-6-15_16-28-19.png
    (Image of the game, the battle-chip interface is on the left)

    I'm trying to recreate this battle-chip system and what I've done so far is:
    1. Turned the chips into Scriptable Objects
    upload_2022-6-15_16-28-42.png


    2. Used the chip ID to point it to a dictionary of animations with the corresponding ID


    3. Used animation events to apply the chip effect at a certain frame with the Function "ApplyChipEffectV2" on the Object "ChipEffects" that is attached to my Megaman GameObject.

    And here is where I get stuck. In the Scriptable objects of the chips, there is a field for MonoScripts called "Effect Script". Basically I intend to create a separate script for every single chip that does the actual effect of that chip. All the scripts for the chips share an interface ChipEffectInterface with a single method called Effect().

    Code (CSharp):
    1. public interface ChipEffectInterface
    2. {
    3.  
    4.     public void Effect();
    5.  
    6. }
    7.  
    On my Megaman game object, I have a script ChipEffects attached which has a field for a chip script.
    upload_2022-6-15_16-32-30.png


    When the "ApplyChipEffectV2" method is called, it will get the Effect Script of the Active Chip which is defined int the "Player Movement" script (I know the name isn't quite accurate since it handles a lot more than just movement but I'll fix that later)

    Code (CSharp):
    1.     public void ApplyChipEffectV2()
    2.     {
    3.         chip = player.activeChip;
    4.         Debug.Log("Chip used:" + chip.GetChipName());
    5.         MonoScript CurrentChipScript = chip.GetEffectScript();
    6.         ChipScript = CurrentChipScript;
    7.  
    8.      
    9.  
    10.         ChipEffectInterface chipEffect = chip.GetEffectScript().GetClass() as ChipEffectInterface;
    11.         Debug.Log(chipEffect.ToString());
    12.         //This gives a NullReferenceException error
    13.  
    14.  
    15.         chipEffect.Effect();
    16.     }
    What I want to do is use the method "ApplyChipEffectV2" to call the Effect() method from the current chip script by casting the class as the ChipEffectInterface so that it will call the Effect() method from any given Effect Script that implements the interface. The problem is that this code doesn't work and when it gets called it just throws a NullReferenceException Error. It's probably something to do with the
    chip.GetEffectScript().GetClass() not being the correct object to cast or something not getting instantiated. I've been struggling with this for awhile and I couldn't figure out a solution.

    Example of an effect script:
    Code (CSharp):
    1. public class Cannon : MonoBehaviour, ChipEffectInterface
    2. {
    3.     public Transform firePoint;
    4.  
    5.     public void Effect()
    6.     {
    7.         Debug.Log("Attempted cannon effect");
    8.         firePoint = FindObjectOfType<ChipEffects>().firePoint;
    9.  
    10.         RaycastHit2D hitInfo = Physics2D.Raycast (firePoint.position, firePoint.right, Mathf.Infinity, LayerMask.GetMask("Enemies"));
    11.         if(hitInfo)
    12.         {
    13.             DamageFunctions script = hitInfo.transform.gameObject.GetComponent<DamageFunctions>();
    14.             script.hurtEntity(script.getHealth(), 40);
    15.             Debug.Log("Used new effect method");
    16.             Debug.Log(hitInfo.transform.name + "HP:" + script.getHealth());
    17.         }
    18.        
    19.     }
    20.  
    21. }
    tl;dr I want to call an interface method from potentially any class that implements that interface.
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,015
    To check if a class implements an interface, you can cast it, or to a cast-check (don't remember the real name:
    Code (CSharp):
    1. IChipEffect chipEffect = (ChipEffect)someClassInstance; //will throw error if cast is invalid
    2.  
    3. //OR
    4. IChipEffect chipEffect = someClassInstance as IChipEffect; //will return null if cast is invalid
    5.  
    6. //OR
    7. if (someClassInstance is IChipEffect chipEffect)
    8. {
    9.     //do stuff with chipEffect here
    10. }
    Additionally as you can see above, the naming convention for interfaces is to start with a capital 'I'.
     
  3. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,572
    You have several issues here. First and foremost: The MonoScript class is an editor only class, so you can not use it in a runtime script. It's defined inside the UnityEditor namespace.

    Given there are workarounds, I guess your "GetEffectScript()" method returns a MonoScript at the moment? The MonoScript class is just a TextAsset that represents your source code file inside your project. It is not an instance of your class. The GetClass method of MonoScript gives you the System.Type object from the .NET reflection system that describes the type that is defined inside that MonoScript file. At this point you don't have an instance of your class, just a descriptor of the class. If the class is a ScriptableObject you can use CreateInstance to actually create an instance of that class on the fly. However since you showed the Addressable screenshot at the beginning I'm assuming you actually created instances of that SO class inside the editor and set them up in the inspector.

    Unfortunately Unity can not serialize references to interfaces, only UnityEngine.Object types can be referenced. So if you want to assign a certain ScriptableObject instance and you're not interested in knowing the exact type, just use a ScriptableObject variable and drag your SO instance onto that variable. With that reference you can use your interface cast and it should work.

    Note that Unity now has the SerializeReference attribute which allows to actually serialize an interface type, however this does not work for UnityEngine.Object types. This only works for pure C# classes and those are serialized inline to the class that has the SerializeReference variable. So this won't help here.
     
    karliss_coldwild likes this.
  4. deltakai2010

    deltakai2010

    Joined:
    Apr 9, 2022
    Posts:
    3
    So I don't think this solution would work since I don't just have 1 chip scriptable object, I have many. Additionally the "Chip Script" field in the Chip Effects script will change any time the player plays a new chip and it needs to correspond to the current chip the player is using so I can't just drag in a chip scriptable object.

    In any case, after some more digging around I figured out what I needed to do was basically dynamically instantiate a class which could correspond to any Effect Script input at runtime. I found out that you couldn't use the new keyword on Monobehaviour classes and that I had to use the AddComponent method instead. Basically what I ended up doing was using the AddComponent method to temporarily attach the Effect Script to my Megaman object and from that reference I was able to cast it to my ChipEffectInterface and use the Effect() method without a hitch. I could then destroy the component after it was used.

    Code (CSharp):
    1.     public void ApplyChipEffectV2()
    2.     {
    3.  
    4.         chip = player.activeChip;
    5.         Debug.Log("Chip used:" + chip.GetChipName());
    6.         MonoScript CurrentChipScript = chip.GetEffectScript();
    7.         ChipScript = CurrentChipScript;
    8.      
    9.  
    10.         ChipEffectInterface chipEffect = gameObject.AddComponent(chip.GetEffectScript().GetClass()) as ChipEffectInterface;
    11.  
    12.         chipEffect.Effect();
    13.         Destroy(GetComponent(ChipScript.GetClass()));
    14.         ChipScript = null;
    15.     }
    I'm not sure of the performance implications of this way of doing things but I'm thinking it won't be too bad since it's not like the player will be using hundreds of chips a second.
     
    Last edited: Jun 15, 2022
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,015
    You know Monoscript is an Editor only class? You can't build your game using it.
     
    Bunny83 likes this.
  6. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,572
    Well, first of all you simply ignored what I said at the very beginning. You can not use the MonoScript class in your runtime code. Please try building your game right now to see what I mean. You will get a build error because you can not use anything from the UnityEditor namespace in your runtime code.

    Yes creating instances dynamically works if you have the System.Type object that describes a type. Of course MonoBehaviours need to be created with AddComponent, ScriptableObjects need to be created with CreateInstance and ordinary C# classes can be created with the Activator class. However this creates a new black instance of that class. In your original post you showed the screenshot of a ScriptableObject in the inspector and I thought this is one of the chips you want to use. However I can now see that you actually put a MonoScript field into that scriptable object class (which, again, doesn't work).

    If it's just a class with an interface that doesn't need any direct access to a gameobject, it would make more sense if your "effects" are scriptable objects. They are more lightweight and standalone objects. Usually you would actually create an instance of your effect ScriptableObject in the editor so you can assign that prefab of that ScriptableObject. This has several advantages. First of all, it does actually work at runtime. Second you can actually setup values / presets in that prefab instance. If the effect class does not have any internal state, you could simply grab the interface from the assigned ScriptableObject instance and call the interface method on it. However if the object does have internal state (which is very likely) you can simply Instantiate that prefab to get a serialized copy of that Scriptable object which you can use for your chip.

    If you really want to go through the System.Type route, as I also already mentioned, there are workarounds how you can create a MonoScript field in the inspector by creating a custom property drawer and just store the type name as a string. See this UA question for more details.
     
  7. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,015
    Hitting play in the editor is different to building the game via the build menu. Open up your build settings and hit Build and Run, and tell us what happens.

    Edit: I see you deleted your post when you discovered that it wouldn't work.
     
    Last edited: Jun 16, 2022
  8. deltakai2010

    deltakai2010

    Joined:
    Apr 9, 2022
    Posts:
    3
    Ok so I'm relatively new to game dev and Unity so when you said build I just assumed you meant being able to run the game within the editor - in that case the Monoscript stuff worked so I felt no need to address it. I tried actually building and running the game and yes I see now that Monoscript doesn't work.
    Anyway I took a look at the UA question in your last line and used the solution presented by MacDx and just got the type of the Effect Scripts via GetType() and a string name and then cast that into the interface. This solution worked without issues and I can also build and run the game outside of the editor.
    Code (CSharp):
    1.     public void ApplyChipEffectV2()
    2.     {
    3.  
    4.         chip = player.activeChip;
    5.         Debug.Log("Chip used:" + chip.GetChipName());
    6.         Type CurrentChipScript = Type.GetType(chip.GetEffectScript());
    7.         ChipScript = chip.GetEffectScript();
    8.      
    9.  
    10.         ChipEffectInterface chipEffect = gameObject.AddComponent(CurrentChipScript) as ChipEffectInterface;
    11.  
    12.         chipEffect.Effect();
    13.         Destroy(GetComponent(CurrentChipScript));
    14.         ChipScript = null;
    15.     }