Search Unity

Question Problems while working with polymorphism and inheritance

Discussion in 'Scripting' started by Kirinomizumi_Ouyang, Apr 11, 2023.

  1. Kirinomizumi_Ouyang

    Kirinomizumi_Ouyang

    Joined:
    Nov 19, 2020
    Posts:
    12
    Basically the problem is that I have a SkillManager Script, which would be activating skills by executing ExecSkills(). I have all sorts of unique script for different skills like Punch.cs, Throw.cs that inherits Skills.cs. Skills defines virtual methods like Perform() and would be overwritten by inherited class.
    Something like:
    Code (CSharp):
    1. SkillManager : MonoBehaviour{
    2.   var actualMethod;
    3.  
    4.   void Start(){
    5.      actualMethod = this.gameobject.GetComponent<XXX>();
    6.   }
    7.  
    8.   void Update(){
    9.     actualMethod.Perform();
    10.     // Can't Do This ↑
    11. }
    12. }
    obviously, you can't force the code editor to assume that actualMethod is 100% a script that inherits Skills.cs and conatins Perform(), and so there would be an error if you try to write actualMethod.Perform();

    Is there a possible solution or I should change the whole way of thinking?
     
  2. DevDunk

    DevDunk

    Joined:
    Feb 13, 2020
    Posts:
    5,063
    There is a way to force a variable to inherit from Skills.
    Just use 'Skills _cachedSkill' and make sure the base class Skills has a Perform() method which every class inheriting has to implement (maybe use abstract)
     
    Kirinomizumi_Ouyang likes this.
  3. Madgvox

    Madgvox

    Joined:
    Apr 13, 2014
    Posts:
    1,317
    To make it visual:

    Code (CSharp):
    1. public abstract class Skills : MonoBehaviour {
    2.   public abstract void Perform ();
    3. }
    4.  
    5. public class Throw : Skills {
    6.   public override void Perform () {
    7.     // do throw logic
    8.   }
    9. }
     
    Kirinomizumi_Ouyang and DevDunk like this.
  4. Kirinomizumi_Ouyang

    Kirinomizumi_Ouyang

    Joined:
    Nov 19, 2020
    Posts:
    12
    Thanks guys! True it's actually better to use abstract class, but I'm still unable to solve the problem, and I encountered new problems while optimizing.
    I changed:
    Code (CSharp):
    1. actualMethod = this.gameobject.GetComponent<XXX>();
    into:
    Code (CSharp):
    1. actualMethod = this.gameobject.AddComponent<Type.GetType(skillClassName)>();
    And found that Type.GetType(skillClassName) returns null. (But it functions properly while getting classes that directly derives from MonoBehaviour). I assume that if i'm able to fix this new problem, I can get pass the old problem.

    As for forcing a variable to inherit from Skills, I don't know how to write it correctly.
    I tried:
    Code (CSharp):
    1. actualMethod = this.gameobject.GetComponent<Throw>();
    adding:
    Code (CSharp):
    1. Skills actualMethod.Perform()
    But editor reported an error.
     
  5. DevDunk

    DevDunk

    Joined:
    Feb 13, 2020
    Posts:
    5,063
    If a skill is already on the object you can just do GetComponent<Skill>() and it should work. Not sure about AddComponent. You probably just need to AddComponent<Throw>() for the throw skill for example
     
  6. Kirinomizumi_Ouyang

    Kirinomizumi_Ouyang

    Joined:
    Nov 19, 2020
    Posts:
    12
    I'm passing information to the manager from scriptable objects at Start(), so i'm using a for loop + addcomponent + (string)classNameToBeAdded. Therefore i'm expecting to use addcomponent so that I won't have to drag a lot of copies of scripts.
     
  7. DevDunk

    DevDunk

    Joined:
    Feb 13, 2020
    Posts:
    5,063
    Why use strings? It is easier and less error prone to just use the classes like .AddComponent<Component>();
     
  8. Kirinomizumi_Ouyang

    Kirinomizumi_Ouyang

    Joined:
    Nov 19, 2020
    Posts:
    12
    I completely forgot that I can make scripts a variable in Scriptable Objects, my bad :confused:
     
    DevDunk likes this.
  9. Olipool

    Olipool

    Joined:
    Feb 8, 2015
    Posts:
    322
    In your first script, can you just change
    var actualMethod

    to
    Skill actualMethod

    ?
    Then actualMehtod.Perform() shouldn't be a problem for the code editor.
     
    Owen-Reynolds likes this.
  10. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,998
    And about that use of
    var
    , I think where-ever you learned polymorphism or whomever said to use
    var
    gave you bad advice, at least for C#. In some languages you get free polymorphism -- pass in any variable type you want and if it happens to have
    x.Perform()
    , things are cool. That's basically what
    var actualMethod;
    is saying, in a language like that.

    But C# is the opposite. Your class needs
    Perform()
    , but also has to "register" it by inheriting from Skill, and has to mark Perform() as virtual so it knows you want polymorphism for it. And then, the whole point of having the base class
    Skill
    is so you can use
    Skill actualType;
    to say "this variable can be any type that inherits from Skill, which we know for a fact means it has the correct Perform() function, and it can't be any other type (not even one with another Perform())."
     
    Olipool likes this.
  11. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,914
    Actually
    var
    is illegal in C# field declarations in the first place, so the var example wouldn't even compile.
     
    Bunny83 likes this.
  12. Kirinomizumi_Ouyang

    Kirinomizumi_Ouyang

    Joined:
    Nov 19, 2020
    Posts:
    12
    Thanks everyone, it's a great help. Following Owen's reply, I was able to use GetComponent to get my Throw.cs (Which derives from Skills) with:
    Code (CSharp):
    1. Skills actualMethod = this.gameobject.GetComponent<Throw>();
    But there is still a problem that I'm not able to assign Throw.cs to
    Code (CSharp):
    1. [SerializeField] Skills actualType;
    by dragging it in editor.
    My current solution is instead of trying to assgin script components by AddComponent<> , I made copies of empty gameobject prefabs. So everytime the game starts, prefabs are Instantiated (Instructed by certain Scriptable Objects) as a child, and I'm able to use gameObject.GetComponentsInChildren<Skills> and store every acutalMethod classes in an array.
    I'm curious if there is a better way.
     
  13. DevDunk

    DevDunk

    Joined:
    Feb 13, 2020
    Posts:
    5,063
    Do the skills need to inherit from MonoBehavior?
    Otherwise you can just make a new Skill() to fill all variables.


    Otherwise getcomponent should work yeah. Haven't tried scriptable objects too much
     
  14. Kirinomizumi_Ouyang

    Kirinomizumi_Ouyang

    Joined:
    Nov 19, 2020
    Posts:
    12
    I think Skills does have to inherit from MonoBehavior because I'm now using the prefab method, I'm not sure if I can make Skills (and derived classes) a component on a gameobject if Skills isn't deriving from MonoBehavior.
    The main reason I'm using Scriptable Object is to store fundamental variables with different numbers for different skills, eg: skillCooldownTime, skillID, skillActivatekey. It's easier to create and add properities in case if I'm adding more things for Skills.
    The whole concept is to have a SkillManager as a factory, SkillManager creates a bunch of acutualSkills depending on a Scriptable Object array (array acts kinda like a TODO list). While in the game, SkillManager determine which skill to use and executes Perform() method in acutualSkills.
     
  15. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,014
    What exactly are you dragging? The actual script file? Or a gameobject in the scene that has this component on it? You can only drag **instances** of that script to a Skill field. So you have to drag a gameobject or the actual component header on a gameobject onto that field. Dragging the script itself does not work.
     
  16. DevDunk

    DevDunk

    Joined:
    Feb 13, 2020
    Posts:
    5,063
    You could drag on the prefabs if those already got the components needed maybe?
     
  17. Kirinomizumi_Ouyang

    Kirinomizumi_Ouyang

    Joined:
    Nov 19, 2020
    Posts:
    12
    Yep, that pretty much what i'm doing, it's working properly for now
     
  18. Kirinomizumi_Ouyang

    Kirinomizumi_Ouyang

    Joined:
    Nov 19, 2020
    Posts:
    12
    I have a variable set as:
    Code (CSharp):
    1. [SerializeField] Skills actualSkill;
    and I was trying to drag a script called Throw.cs (Non abstract class and drives from Skills class) in the inspector. I'm doing this because I wan't to create a Throw Script Component using AddComponent method while running, But this strategy seems bad so I discarded this method and went using the empty gameobject prefab method.
     
  19. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,014
    That's what I thought. This doesn't work this way since a script asset is actually a MonoScript instance which is actually a TextAsset. However the MonoScript class is an editor only class as this is how script assets are represented in the editor.

    I made a SerializableMonoScript class which you can use to essentially serialize the actual type information inside the inspector by dragging a script asset onto the field. It actually stores the assembly qualified type name of the type that the MonoScript implements. At runtime you can read the "Type" property which gives you the System.Type which you can use in AddComponent, or when you use the generic version, it has a neat helper method "AddToGameObject" which allows to add this type as a component to the given gameobject and return the casted instance.

    The generic version also allows you to restrict the types to a certain base class or interface. Note since I use an ordinary ObjectField you can actually drop any MonoScript onto that field, however incompatible types will be rejected with a log message. This is not the best user experience, however this way we get all the ObjectField stuff for free (selecting / pinging the script in the project, object picker).

    So with this file in your project you can simply do

    Code (CSharp):
    1.  
    2. using B83;
    3.  
    4. // [ ... ]
    5.  
    6. public SerializableMonoScript<Skills> skillType;
    and when you want to actually add this skill type as a component to a gameobject, you can simply do

    Code (CSharp):
    1. Skills skillInst = skillType.AddToGameObject(gameObject);
    or the manual way like this:

    Code (CSharp):
    1. Skills skillInst = (Skills)gameObject.AddComponent(skillType.Type)
    which does the same thing.

    Note that this class also works for ScriptableObjects equally. The generic version also has a dedicated "CreateSOInstance()" method.

    Just a short warning: Since a "SerializableMonoScript" actually just stores the assembly qualified type name, "references" stored that way would break when you refactor the type name or move it to a different assembly. So keep that in mind.

    ps: I also made a SerializableInterface some time ago which allows you to store and filter a UnityEngine.Object instance reference based on an interface or base class type. So this also works for MonoBehaviours and ScriptableObjects. Since it's generic you also get easy access to the properly casted instance. When you drag a gameobject onto that field and multiple components match the filter, a drop down pops up that lets you select the instance you want.
     
  20. Kirinomizumi_Ouyang

    Kirinomizumi_Ouyang

    Joined:
    Nov 19, 2020
    Posts:
    12
    That directly solves my problem in an elegant way, Unity really should consider making this method built-in.;)
     
  21. Madgvox

    Madgvox

    Joined:
    Apr 13, 2014
    Posts:
    1,317
    There may be a way to make this more robust if you serialize the guid of the file instead (or in addition). The type name would be used in builds.
     
    Bunny83 likes this.
  22. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,014
    Yes, I actually thought about that for a second. However it makes the whole thing a lot more complex. Also at the moment I also have a "setter" for the Type property. That would need to be removed as you can't determine the guid at runtime.

    The hybrid approach would work, but would also require to re-serialize the field when a refactoring has taken place. When you forget this you would still have the same issue again. The main problem would be some fields you have set half a year ago on some prefab you have completely forgotten exists :)
     
  23. Madgvox

    Madgvox

    Joined:
    Apr 13, 2014
    Posts:
    1,317
    Not at all, it would use the guid in the editor, and revert to type semantics at runtime.

    What I'm imagining is that guid is king in editor and used for both get/set. Then, no reserialization needed! The lynch pin is that it would need some special logic to serialize itself differently for builds. I'm not sure how difficult/janky that would be to implement though, I haven't done much build scripting.

    No debate there!

    It's an interesting thought experiment, at least.
     
  24. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    268
    This should work:

    Code (CSharp):
    1. SkillManager : MonoBehaviour
    2. {
    3.     ISkill skill;
    4.     void Start()
    5.     {
    6.          skill = this.gameobject.GetComponent<FireSkill>();
    7.     }
    8.  
    9.      void Update()
    10.     {
    11.         skill.Perform();
    12.      }
    13. }
    14.  
    15. public interface ISkill
    16. {
    17.     public void Perform();
    18. }
    Just make sure FireSkill implements ISkill