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

Subclass Referencing Abstract Main Class?

Discussion in 'Scripting' started by funnylittlecritter, Aug 16, 2021.

  1. funnylittlecritter

    funnylittlecritter

    Joined:
    Mar 17, 2021
    Posts:
    7
    Hello! I'm currently building a weapon system for a game I'm developing, and was having some trouble referencing properties from my subclass scripts. I'm using an abstract MonoBehaviour script as the base script for all weapons (aptly named "PlayerWeaponBase"), and I'm currently developing a weapon derived from this class called "NinjaStarWeapon".

    I thought it would be clever to have common functions like checking for ammunition and processing player input in a main script, but it seems to be generating more hassle than help. While I don't get any compiler or run-time errors, I keep running into this frustrating problem: while the NinjaStarWeapon script seems to be aware of how much ammunition it holds, no other script seems to know that. Furthermore, when I try to call the functions of the weapon derived from PlayerWeaponBase, the functions keep thinking the weapon is out of ammunition. This is the case for all of its properties & variables.

    My only guess is that the script isn't actually generating an instance of PlayerWeaponBase... is this true? In my mind, it's either that, or Unity is high and it's referencing the properties of an abstract class.

    I've attached some of my code below for reference. Also, if there's any way I can/should improve this post, let me know! I'm all ears.
    - goob

    Code (CSharp):
    1. // NOTE: This is a highly-trimmed down version of my work, so a lot of code has been stripped out for the sake of brevity. If you would like more context, I'd be happy to oblige.
    2.  
    3. // script 1
    4. public abstract class PlayerWeaponBase : MonoBehaviour
    5. {
    6.    internal abstract int WeaponCharges {get;}
    7.    internal abstract int CurrentCharges {get; set;}
    8.    
    9.    private void Awake()
    10.    {
    11.       CurrentCharges = WeaponCharges; // fill up ammo
    12.    }
    13. }
    14.  
    15. // script 2
    16. public class NinjaStarWeapon : PlayerWeaponBase
    17. {
    18.    internal override int WeaponCharges => 3;
    19.    internal override int CurrentCharges {get; set;}
    20. }
    21.  
    22. // script 3
    23. public class Test1 : MonoBehaviour
    24. {
    25.  
    26.    [SerializeField] PlayerWeaponBase weapon;
    27.    void Awake()
    28.    {
    29.       Debug.Log(weapon.CurrentCharges);
    30.       // line always yields 0, regardless of the value of CurrentCharges written in the weapon's dedicated script.
    31.    }
    32.  
    33. }
     
  2. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,748
    The example you have here looks like it ought to work, but I suspect you've trimmed out something that you might assume was not an issue, so let me ask this: Does NinjaStarWeapon have an Awake() function? If so, then the base class's Awake() is never called. That's where CurrentCharges is being set to WeaponCharges, so if it's never called, it'll just be at the default (which is 0).

    You can make Awake() virtual and override it in NinjaStarWeapon while calling base.Awake().
     
  3. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,128
    First, welcome to the unity forum and thank you for using code tags :)

    Second:

    Edit: a) and b) are just none-sense by me. As StarManta pointed out you are probably dragging in the NinjaStar in the inspector, my bad (there is no strike text format in the forum?, Edit2: found it!)

    a) I am not sure if it's a side effect of you trimming down the code but your Test1 class has not connection at all to NinjaStarWeapon. Therefore I wouldn't expect it to have any value but 0.

    b) Awake is a method called by unity on every object in the scene graph. Your base weapon is just a serialized property within Test1. Therefore Awake() of PlayerWeaponBase will never be called.


    c) You are using a lot of syntactic sugar. I'd recommend to type it out and litter it with Debug.Log() or use the debugger to figure out what's happening.

    d) You'd better use composition instead of inheritance imho (opinions may differ in this).

    Happy coding !
     
    Last edited: Aug 16, 2021
  4. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,748
    Er, no? PlayerWeaponBase is a MonoBehaviour, so it'll be attached to an object that OP would drag into the slot on the inspector. And it would get the Awake() message (though as mentioned in the first reply, a derived class's Awake() would hide it)
     
    _geo__ likes this.
  5. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,748
    One other possibility: Is your NinjaStarWeapon object active and in the scene? If not, Awake() isn't called on that either.
     
  6. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,128
    Yes, true. My assumption that it's not part of the scene graph was wrong. I did not think of the OP dragging it in there. Thanks. I'll edit it so the OP will not follow my wrong lead.
     
  7. funnylittlecritter

    funnylittlecritter

    Joined:
    Mar 17, 2021
    Posts:
    7
    NinjaStarWeapon doesn't have an Awake() function, no. Thanks for the tip, though!

    Perhaps I can provide further clarity by showing more of my code. This is derived from the PlayerWeaponBase script; TryAttack is the only time (outside of Awake()) when CurrentCharges is modified:

    Code (CSharp):
    1. virtual internal bool MayShoot()
    2. {
    3.     return (
    4.         (Time.time - this.m_LastTimeAttacked) >= this.DelayBetweenAttacks
    5.         &
    6.         this.CurrentCharges >= 1
    7.     );
    8. }
    9.  
    10. bool TryAttack()
    11. {
    12.    Debug.Log(this +  "currently has " + CurrentCharges + " charges.");
    13.    if (MayShoot()) {
    14.       CurrentCharges -= 1;
    15.       HandleAttack(); // attack code
    16.       m_LastTimeAttacked = Time.time;
    17.       return true;
    18.    }
    19.    else {
    20.       TryAttackFailure();
    21.       return false;
    22.    }
    23. }
    Despite the fact that the NinjaStarWeapon object has 3 charges in it by default,
    ("charges" are what I'm calling "ammo" as not all weapons have ammunition, but rather a limited number of uses)
    when this function is called, Debug.Log prints out this:

    Code (CSharp):
    1. NinjaStarWeapon currently has 0 charges.
     
  8. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,128
    Since I made a fool of myself with my first answer ( :oops: ) I booted up Unity and tried your inital code sample. It works just fine (prints "3"). So there has to be something else going on here. Have you tried putting a Debug.Log() into your Awake() and TryAttack()? Maybe you are calling it before Awake()?
    You can use Debug.Break() to stop Unity and then proceed frame by frame.

    Update I don't think the extra code you posted is the issue. Here is the test class I've written to add a button. The only way I can get "0" as a result is if I call it before first activation in the scene.

    Code (CSharp):
    1. #if UNITY_EDITOR
    2. using UnityEditor;
    3. #endif
    4. using UnityEngine;
    5.  
    6. public class NinjaStarWeapon : PlayerWeaponBase
    7. {
    8.     internal override int WeaponCharges => 3;
    9.     internal override int CurrentCharges { get; set; }
    10.  
    11.     private float m_LastTimeAttacked;
    12.     public float DelayBetweenAttacks = 1f;
    13.  
    14.     virtual internal bool MayShoot()
    15.     {
    16.         return (
    17.             (Time.time - this.m_LastTimeAttacked) >= this.DelayBetweenAttacks
    18.             &
    19.             this.CurrentCharges >= 1
    20.         );
    21.     }
    22.  
    23.     public bool TryAttack()
    24.     {
    25.         Debug.Log(this + "currently has " + CurrentCharges + " charges.");
    26.         if (MayShoot())
    27.         {
    28.             CurrentCharges -= 1;
    29.             //HandleAttack(); // attack code
    30.             m_LastTimeAttacked = Time.time;
    31.             return true;
    32.         }
    33.         else
    34.         {
    35.             //TryAttackFailure();
    36.             return false;
    37.         }
    38.     }
    39.  
    40.     // Adds a TryAttack button to your weapon inspector.
    41. #if UNITY_EDITOR
    42.     [CustomEditor(typeof(NinjaStarWeapon))]
    43.     public class NinjaStarWeaponEditor : Editor
    44.     {
    45.         public override void OnInspectorGUI()
    46.         {
    47.             NinjaStarWeapon ninjaStar = this.target as NinjaStarWeapon;
    48.  
    49.             if (GUILayout.Button("TryAttack()"))
    50.             {
    51.                 ninjaStar.TryAttack();
    52.             }
    53.  
    54.             base.OnInspectorGUI();
    55.         }
    56.     }
    57. #endif
    58. }
     
    Last edited: Aug 16, 2021
  9. funnylittlecritter

    funnylittlecritter

    Joined:
    Mar 17, 2021
    Posts:
    7
    I have tried using Debug.Log() during Awake() and TryAttack(); the number of charges printed out in the console is accurate during Awake(), but not TryAttack()--unless, of course, if I call it directly through the Custom Editor button, then it works like a charm. :mad:

    The strange thing about this is that, while NinjaStarWeapon has a hold on its properties when referenced from within its own script, all other scripts seem to think it has 0 Charges. I mean, I know I've said this already a handful of times, but look at my console output:
    Code (CSharp):
    1. NinjaStarWeapon: I have 3 of 3 charges!
    2. PlayerWeaponsManager: Equipped weapon (NinjaStarWeapon) has 0 of 3 charges!
    This has me dumbfounded. I'm referencing the same object from two sources, and they're returning different values despite there being no possible change during the interim. If I reference CurrentCharges from within NinjaStarWeapon, then it's fine; no issues! However, if I reference the same value from another script (or even if I call the function by means outside of NinjaStarWeapon; for example, calling it from my otherwise fine HUD script), I get back 0.

    My only thought now is that there's some breakdown in communication between scripts. I mean, that's the only way that this could be happening, right? Do I need to specify something when I'm referencing an object which is a subclass of an abstract class? Thank you guys so much for your help! This problem has really knocked me back on my heels because it's so inexplicable.
     
  10. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,128
    Can you please try to use
    Debug.Log(this.GetInstanceID() + "currently has " + CurrentCharges + " charges and is "+(this.gameObject.activeInHierarchy ? "active" : "inactive")+".");
    , just to confirm it really is called on the same object.

    I have tried it with this Test1 class and it works:
    Code (CSharp):
    1. #if UNITY_EDITOR
    2. using UnityEditor;
    3. #endif
    4. using UnityEngine;
    5.  
    6. public class Test1 : MonoBehaviour
    7. {
    8.     [SerializeField] PlayerWeaponBase weapon;
    9.  
    10.     void Awake()
    11.     {
    12.         Debug.Log(weapon.CurrentCharges); // depends on the order of Awake() calls
    13.         Debug.Break();
    14.     }
    15.  
    16. #if UNITY_EDITOR
    17.     [CustomEditor(typeof(Test1))]
    18.     public class Test1Editor : Editor
    19.     {
    20.         public override void OnInspectorGUI()
    21.         {
    22.             Test1 test1 = (this.target as Test1);
    23.  
    24.             if (GUILayout.Button("TryAttack()"))
    25.             {
    26.                 ((NinjaStarWeapon)test1.weapon).TryAttack(); // prints 3
    27.             }
    28.  
    29.             base.OnInspectorGUI();
    30.         }
    31.     }
    32. #endif
    33. }
    Can you post the code you are using to call TryAttack() from an external class? Maybe there is an issue there.
     
  11. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,572
    My guess is, because that's a common mistake, that you referenced the wrong object you try to "use". Many people accidentally reference the prefab object instead of the actual instance in the scene. While prefabs are also object instances, they do not live in the scene and do not get any callbacks. However they can still be used "manually", i.e. you can call methods on them.

    Try adding a Debug.Log statement like this to your TryAttack method:

    Code (CSharp):
    1. Debug.Log("TryAttack from object "+gameObject.name+" (" + CurrentCharges + " charges)", gameObject);
    Notice the secondary parameter where I passed the gameObject reference. That second parameter is a context object that is stored with the log message. When you click on such a log message in the console, Unity will ping / highlight that object. This should help identifying the object you're actually working with.
     
    StarManta and _geo__ like this.