Child override not running

Hey everyone! I’ve been at this for hours, but for the life of me I can’t get a simple override to work. I’m nearly positive I’m doing it right. Anyone know what’s going on here and why my override isn’t running?

I want the DealDamage function in my EnemyStats class to run, instead of the DealDamage from the Damageable class.

public class EnemyStats : CharacterStats
{
    Animator animator;

    private void Awake()
    {
        animator = GetComponentInChildren<Animator>();
    }
    // Start is called before the first frame update
    void Start()
    {
        maxHealth = SetMaxHealthFromHealthLevel();
        currentHealth = maxHealth;
    }

    public override void DealDamage(float damage)
    {
        currentHealth -= damage;
        animator.Play("Damage_01");
        Debug.Log("Attacked!");
        if (currentHealth <= 0)
        {
            Debug.Log("Killed!");
            currentHealth = 0;
            animator.Play("Damage_01");
            //Handle Death
        }
    }

Which inherits:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CharacterStats : BNG.Damageable
{
    public float healthLevel = 10;
    public float maxHealth;
    public float currentHealth;

    public float staminaLevel = 10;
    public float maxStamina;
    public float currentStamina;

    // Update is called once per frame
    void Update()
    {
       
    }
}

which then inherits:

namespace BNG {
    /// <summary>
    /// A basic damage implementation. Call a function on death. Allow for respawning.
    /// </summary>
    public class Damageable : MonoBehaviour {

        public float Health = 100;
        private float _startingHealth;

        [Tooltip("If specified, this GameObject will be instantiated at this transform's position on death.")]
        public GameObject SpawnOnDeath;

        [Tooltip("Activate these GameObjects on Death")]
        public List<GameObject> ActivateGameObjectsOnDeath;

        [Tooltip("Deactivate these GameObjects on Death")]
        public List<GameObject> DeactivateGameObjectsOnDeath;

        [Tooltip("Deactivate these Colliders on Death")]
        public List<Collider> DeactivateCollidersOnDeath;

        /// <summary>
        /// Destroy this object on Death? False if need to respawn.
        /// </summary>
        [Tooltip("Destroy this object on Death? False if need to respawn.")]
        public bool DestroyOnDeath = true;

        [Tooltip("If this object is a Grabbable it can be dropped on Death")]
        public bool DropOnDeath = true;

        /// <summary>
        /// How long to wait before destroying this objects
        /// </summary>
        [Tooltip("How long to wait before destroying this objects")]
        public float DestroyDelay = 0f;

        /// <summary>
        /// If true the object will be reactivated according to RespawnTime
        /// </summary>
        [Tooltip("If true the object will be reactivated according to RespawnTime")]
        public bool Respawn = false;

        /// <summary>
        /// If Respawn true, this gameObject will reactivate after RespawnTime. In seconds.
        /// </summary>
        [Tooltip("If Respawn true, this gameObject will reactivate after RespawnTime. In seconds.")]
        public float RespawnTime = 10f;

        /// <summary>
        /// Remove any decals that were parented to this object on death. Useful for clearing unused decals.
        /// </summary>
        [Tooltip("Remove any decals that were parented to this object on death. Useful for clearing unused decals.")]
        public bool RemoveBulletHolesOnDeath = true;

        [Header("Events")]
        [Tooltip("Optional Event to be called when receiving damage. Takes damage amount as a float parameter.")]
        public FloatEvent onDamaged;

        [Tooltip("Optional Event to be called once health is <= 0")]
        public UnityEvent onDestroyed;

        [Tooltip("Optional Event to be called once the object has been respawned, if Respawn is true and after RespawnTime")]
        public UnityEvent onRespawn;

#if INVECTOR_BASIC || INVECTOR_AI_TEMPLATE
        // Invector damage integration
        [Header("Invector Integration")]
        [Tooltip("If true, damage data will be sent to Invector object using 'ApplyDamage'")]
        public bool SendDamageToInvector = true;
#endif

        bool destroyed = false;

        Rigidbody rigid;
        bool initialWasKinematic;

        private void Start() {
            _startingHealth = Health;
            rigid = GetComponent<Rigidbody>();
            if (rigid) {
                initialWasKinematic = rigid.isKinematic;
            }
        }

        public virtual void DealDamage(float damageAmount) {
            DealDamage(damageAmount, transform.position);
        }

        public virtual void DealDamage(float damageAmount, Vector3? hitPosition = null, Vector3? hitNormal = null, bool reactToHit = true, GameObject sender = null, GameObject receiver = null) {

            if (destroyed) {
                return;
            }

            Health -= damageAmount;

            onDamaged?.Invoke(damageAmount);

            // Invector Integration
#if INVECTOR_BASIC || INVECTOR_AI_TEMPLATE
            if(SendDamageToInvector) {
                var d = new Invector.vDamage();
                d.hitReaction = reactToHit;
                d.hitPosition = (Vector3)hitPosition;
                d.receiver = receiver == null ? this.gameObject.transform : null;
                d.damageValue = (int)damageAmount;

                this.gameObject.ApplyDamage(new Invector.vDamage(d));
            }
#endif

            if (Health <= 0) {
                DestroyThis();
            }
        }

        public virtual void DestroyThis() {
            Health = 0;
            destroyed = true;

            // Activate
            foreach (var go in ActivateGameObjectsOnDeath) {
                go.SetActive(true);
            }

            // Deactivate
            foreach (var go in DeactivateGameObjectsOnDeath) {
                go.SetActive(false);
            }

            // Colliders
            foreach (var col in DeactivateCollidersOnDeath) {
                col.enabled = false;
            }

            // Spawn object
            if (SpawnOnDeath != null) {
                var go = GameObject.Instantiate(SpawnOnDeath);
                go.transform.position = transform.position;
                go.transform.rotation = transform.rotation;
            }

            // Force to kinematic if rigid present
            if (rigid) {
                rigid.isKinematic = true;
            }

            // Invoke Callback Event
            if (onDestroyed != null) {
                onDestroyed.Invoke();
            }

            if (DestroyOnDeath) {
                Destroy(this.gameObject, DestroyDelay);
            }
            else if (Respawn) {
                StartCoroutine(RespawnRoutine(RespawnTime));
            }

            // Drop this if the player is holding it
            Grabbable grab = GetComponent<Grabbable>();
            if (DropOnDeath && grab != null && grab.BeingHeld) {
                grab.DropItem(false, true);
            }

            // Remove an decals that may have been parented to this object
            if (RemoveBulletHolesOnDeath) {
                BulletHole[] holes = GetComponentsInChildren<BulletHole>();
                foreach (var hole in holes) {
                    GameObject.Destroy(hole.gameObject);
                }

                Transform decal = transform.Find("Decal");
                if (decal) {
                    GameObject.Destroy(decal.gameObject);
                }
            }
        }

Unity’s serialization does not support polymorphism. And it shouldn’t.

You should abandon the idea of treating MonoBehaviours as if this is a normal OO programming.
Unity itself is not OO (even though C# is), and MonoBehaviours should be treated with care. They are half-submersed in the engine itself, half-exposed for your coding convenience.

They do not have constructors and you cannot instantiate a MonoBehaviour by using new. These two facts alone should serve as a hint that something special is going on.

For one you’re only overriding one of the overloads… is that your problem? You didn’t show the call site.

Please forgive my ignorance, I’ve only been playing with Unity for a few days now. If I’m understanding correctly, functions can’t override if the parent class inherits MonoBehavior? The Damageable class is part of a VR development framework. Could I adapt it to play nicely without MonoBehavior?

Hey Kurt, as per your tip I added an override in the CharacterStats class that calls base.DealDamage(damage), but to no avail :frowning:

Also, I’m still fairly new to Unity. How would I go about finding the call site?

Nobody forbids you to do whatever you want with MonoBehaviours, as long as the code compiles, and oh btw, what you did isn’t explicitly a bad practice, and it’s supposed to work. To an extent.

I’m just saying that by going down this path, you’re going to have a harder time learning Unity, because you’re opening a very painful realm of some very obscure shenanigans by doing this. As I said, serialization does not support polymorphism. This doesn’t necessarily mean you’re having problem with this right now, but you surely will at some point in the future. That’s just a minor thing that can happen to you, and one that is easily caught and fixed, but it surely sends you back to the drawing board.

In my opinion it is simpler not to go down this path. There are many other patterns and it’s more practical to learn how Unity likes to be used, instead of pushing the standard OO paradigm. After a while you’ll get a feel what is safe and what’s not, and what’s the fearsome grey area. This pattern in particular, where you inherit from MonoBehaviour and then override selectively, is something I have always avoided as a grey area, and to a good result so far.

If you’re really sure you want this, pay attention that you cannot just call base and there is no constructor in your base class. (I mean you can, but there is no guarantee that the reference is alive, because the holder of the reference is out of scope.*) There is only Start method (also called a Message because it’s poked by the engine), and it doesn’t behave at all like a constructor. There are a lot of issues with this design.

*My head hurts. What IS the base instance when the base MonoBehaviour doesn’t exist in the engine because it was never properly registered through instantiation. It should be there, but then how would you expect to construct the values properly, when there is no ctor and its Start was never called…

In general, I don’t know, try to stick with a simple rule, don’t mix UnityEngine.Objects with ordinary C# objects. You are free to do whatever you want, just keep in mind that MBs like to be used as first-class elements of your scene. Use them as renderers, as data containers, use them for logic resolution, as state signals, whatever, just don’t mingle them with OO. You can aggregate proper OO through them however. And this is what gave rise to an epidemy of all kinds of managers. Unity, at least at the basic level, likes managers.

This way you are likely to avoid weirdness that lives beneath the hub. MonoBehaviours are created weirdly and are collated somewhere out of scope, their life cycle is not yours to handle, and meshing them with inheritance and polymorphism will inevitably produce unexpected behavior.

That said, is it too much to ask of you to produce a miniature prototype of the desired behavior? I can’t possibly detect an error in so much code. It definitely looks like it should work.

Thanks for all the notes, Orion. I definitely prefer to use things the way they’re designed to be used, to avoid headaches in the future. I’m just now wondering how to restructure the code so as to avoid using polymorphism with an MB. The framework I’m using actually gave me the idea in it’s documentation: Dealing and Taking Damage | VRIF Wiki (beardedninjagames.com)

but it isn’t working as it should. What do you mean by prototype? Essentially I want Damage to be calculated for all objects – players, npcs, and props – in the Damageable class. I can then fine tune how damage is handled in each object’s respective class. In my example, the EnemyStats class.