Inheritance is hard. Give me a hand, please?

So, I have this class:

public class MobileUnit : Unit {
    AidansMovementScript moveConductor;

    void Awake () {
        string prefab = gameObject.name;
        prefab = prefab.Remove(prefab.IndexOf("("));
        prefab = "Sprites/" + prefab;
        if (photonView.Owner.ActorNumber == 1) {
            GetComponent<SpriteRenderer>().sprite = Resources.Load<Sprite>(prefab + "_white");
        }
        else if (photonView.Owner.ActorNumber == 2) {
            GetComponent<SpriteRenderer>().sprite = Resources.Load<Sprite>(prefab + "_orange");
        }
        if (this.GetType() == typeof(MobileUnit)) {
            if (photonView.IsMine) {
                gameObject.AddComponent<MobileUnit_local>();
            }
            else {
                gameObject.AddComponent<MobileUnit_remote>();
            }
            Destroy(this);       
        }
    }

    private void Start() {
        gameState = GameObject.Find("Goliad").GetComponent<GameState>();
        MeatReadout = transform.GetChild(0).GetChild(0).GetChild(0).gameObject;
        moveConductor = gameObject.GetComponent<AidansMovementScript>();   
    }
 
    public virtual void move (Vector3 target, Transform movingTransform = null) {
        moveConductor.setDestination(target, movingTransform);
    }

}

…and then I have this subordinate class:

public class MobileUnit_local : MobileUnit {
    AidansMovementScript moveConductor;

    void Start () {
        gameState = GameObject.Find("Goliad").GetComponent<GameState>();
        MeatReadout = transform.GetChild(0).GetChild(0).GetChild(0).gameObject;
        moveConductor = GetComponent<AidansMovementScript>();
        gameState.enlivenUnit(gameObject);
    }

    public override void activate () {
        gameState.activateUnit(gameObject);
        gameObject.transform.GetChild(0).gameObject.SetActive(true);
        transform.GetChild(0).GetChild(0).GetComponent<RectTransform>().localScale = new Vector3(1,1,1) * (Camera.main.orthographicSize / 5);
    }

    public override void deactivate () {
        gameObject.transform.GetChild(0).gameObject.SetActive(false);
        gameState.deactivateUnit(gameObject);
    }

}

… and I call “move” in the subordinate class like so:

unit.GetComponent<MobileUnit>().move(destination, optionalTransform);
I get a null reference exception: moveConductor hasn't been defined. After probing around, I discovered that the MobileUnit.move call goes to a MobileUnit class, not a MobileUnit_local class. This happens even if I call this.move() from in the MobileUnit_local Start() function. I'm not great at inheritance, but I'm pretty sure that I should be able to define methods in parent methods and have them implicitly present in child methods.

What am I not understanding?

Edit: Ignore this answer and see below

But you never made an instance of a MobileUnit_local class (or at the very least, you seem to have upcasted it). You made/got an instance of MobileUnit class, and called its move()

These should be clear enough as your hints
unit.GetComponent<MobileUnit>()

Why should it ever call a MobileUnit_local move method?
If you have a hard time understanding inheritance, Bunny83 wasn’t as lazy as me to elaborate on it

Btw, this is unrelated, but try to stick to standard recommendation to C# naming. Method and property names should be PascalCase, field and variable names should camelCase. You can do whatever you want, I don’t mind, but being strict with the basic standards such as method and field naming, will likely help you read other people’s code (and vice versa) and perhaps introduce less errors, because you’re still using other people’s APIs in a different writing standard.

@orionsyndrome This is what the gameobject in question looks like when the method is called:

6456643--723700--upload_2020-10-26_0-49-14.png

As you can see, the attached script is a MobileUnit_local, not a MobileUnit.

Edit: Ignore this answer, see the next one

Good thing you’ve showed how exactly you’ve defined it.
But that still doesn’t matter. You’re upcasting it to MobileUnit by doing unit.GetComponent(). (Well, technically you’re not upcasting anything, but are grabbing it’s superclass instead)

Why not GetComponent of correct type?

unit.GetComponent<MobileUnit_local>().move(...)

If you don’t want to, for some reason, maybe it’s a common thing for a bunch of different MobileUnit derivations, okay, then you need to downcast it first, if you know it’s of the right type.

Here

  • you grab MobileUnit component
unit.GetComponent<MobileUnit>();
  • you then downcast it to MobileUnit_local, it works because the object IS in fact MobileUnit_local or a derivation of it, it wouldn’t work otherwise
(MobileUnit_local)unit.GetComponent<MobileUnit>();
  • you call move on that
((MobileUnit_local)unit.GetComponent<MobileUnit>()).move(...);

To make this easier on the eyes

var mobileUnit = unit.GetComponent<MobileUnit>();
var mobileUnitLocal = (MobileUnit_local)mobileUnit;
mobileUnitLocal.move(...);

Ahh naah man you confused me in a roundabout way
Where is your override in MobileUnit_local? You never override your virtual method.
MobileUnit_local has no move at all

Dismiss what I said above, that’s not the problem, and your expectation was right.
But you need to override the method.

The method does relate to your instance in question, but it truly belongs to MobileUnit.
Unless you override it. I kind of fully expected you’ve done this, by mistake.

public class MobileUnit_local : MobileUnit {

  public override move(...) {
    //...
  }

}

Full answer: you need to have moveConductor defined in MobileUnit if you intend to call MobileUnit.move(). If you want moveConductor only in MobileUnit_local, you have to override move in MobileUnit_local.

move from MobileUnit doesn’t know about moveConductor and you can’t access fields like that from derived class instances. it works only upstream, and you need to use protected keyword to be able to do that. The reason being a simple fact that you could have several different derivations of the same class, but there is always one superclass to each of them.

I.e. if moveConductor was in MobileUnit and move was in MobileUnit_local

public class MobileUnit {
  protected GameObject moveConductor; // private but inherited (NOT SHARED) by any subclasses
}

public class MobileUnit_local : MobileUnit {

  public void move(...) {
    moveConductor.whatnot();
  }

}

if moveConductor was in MobileUnit_local and move was in MobileUnit

public class MobileUnit {

  public virtual void move(...) { // must be marked as virtual so that subclasses can override
    //...
  }

}

public class MobileUnit_local {

  private GameObject moveConductor; // field local to this class

  public override void move(...) {
    moveConductor.whatnot();
  }

}

It’s an important thing to notice, as explained by Bunny83 in that post above, that instances of classes that are derived do not share anything with each other, except for being able to repurpose or override the methods as defined with virtual/override. All data is kept in each instance, as usual.

If you think of classes as templates, your MobileUnit_local class simply adds to any existing definition of MobileUnit, and is treated in addition as ‘some kind of MobileUnit’. Instance of the two classes, however, have nothing in common, except for being treated as type-compatible (both are MobileUnits). I have to mention this just in case.

When you override a virtual method, that means that MobileUnit_local practically redefines this method locally, and whenever the existing logic would necessitate calling of that MobileUnit’s method in an instance of MobileUnit_local, MobileUnit_local’s override would be called instead, because it replaced the original MobileUnit’s definition of what is move.

In your case, however, there is no override, so you end up calling MobileUnit’s move implementation which simply cannot access any data that’s outside of that class.

This is so confusing to explain because of your poor choice of names.

@orionsyndrome Thank you for giving so much attention to this issue. What you are saying seems to contradict this other example of inheritance in my project: (sorry about the capitalization; I’ll take your advice and change it on my next refactor)

public class Unit : MonoBehaviourPun {
    protected GameState gameState;
    protected GameObject MeatReadout;
    public int maxMeat = 30;
    public int meat = 10;
    int meatCost = 10;

    void Awake () {
    ...
    }
 
    void Start() {
        gameState = GameObject.Find("Goliad").GetComponent<GameState>();
        MeatReadout = transform.GetChild(0).GetChild(0).GetChild(0).gameObject;
    }

    [PunRPC]
    public virtual void die() {
        gameState.deadenUnit(gameObject);
        GameObject orb = (GameObject)Resources.Load("Orb");
        for (; meat > 0; --meat) {
            Vector3 place = new Vector3(transform.position.x + Random.Range(-.1f, 0.1f), transform.position.y + Random.Range(-.5f, 0.5f), 0);
            GameObject lastOrb = Instantiate(orb, place, transform.rotation);
            lastOrb.GetComponent<Rigidbody2D>().AddForce((lastOrb.transform.position - transform.position).normalized * 2);
        }
        Destroy(gameObject);
    }
}
public class Unit_local : Unit {

    void Start() {
        gameState = GameObject.Find("Goliad").GetComponent<GameState>();
        gameState.enlivenUnit(gameObject);
        MeatReadout = transform.GetChild(0).GetChild(0).GetChild(0).gameObject;
    }

    public override void activate () {
        gameState.activateUnit(gameObject);
        gameObject.transform.GetChild(0).gameObject.SetActive(true);
        transform.GetChild(0).GetChild(0).GetComponent<RectTransform>().localScale = new Vector3(1,1,1) * (Camera.main.orthographicSize / 5);
    }

    public override void deactivate () {
        gameObject.transform.GetChild(0).gameObject.SetActive(false);
        gameState.deactivateUnit(gameObject);
    }

}

So, same deal as before:

  • Virtual method die() in the base class is not overridden in child class.
  • die() refers to things that aren’t defined in the base class.
  • die() called in an instance of child class (not pictured, but I tested it)

This works! What is different here than in the case of move()?

SOLVED: I just forget to name moveConductor as a Public or Protected object. Sorry @orionsyndrome for taking your time with this one; I’m sure you understand that sometimes big dialogues are needed to spot small errors. I guess the real mystery is why VS Code didn’t highlight the error last night, and Unity allowed the scripts to run.

1 Like

There’s no compile time error here. Both classes define AidansMovementScript moveConductor;, so a field with this name is available to both classes. However, they are not the same field, since they are private to each class. Once you change the moveConductor in your base class to be public, you should see at least a warning in your derived class about a local field using the same name as an inherited field. I hope the compiler gives an error, but I’m not sure without testing.

When you have an instance of your derived class, Unity will call the derived class’s Start but not the base class’s.¹ The field local to the derived class is set, but the field local to the base class is not. When Move is called, being in the base class, it only has access to the base classes version of moveConductor, which was not set.

¹ This is one of the shortcomings of the Magic Method approach Unity took on MonoBehaviours. You might not have run into this if it were possible to call base.Start() from the derived version … though you’d be wastefully allocating two pointers to moveConductor, when only one seems to be used.

Initially you’ve managed to confuse me and I gave you a wrong explanation.
I wasn’t focused and didn’t pick up on the nuances. Sorry if this mislead you.

In the end you got a proper explanation and reasons for your error.
Edit: Though I again managed to not see a field. I guess it was just a bad day for coding.

I could’ve sworn there wasn’t a field in the second class. Oh well, bad days for everyone, in terms of cognition.

I had the benefit of AidanofVT’s solution. I could easily have missed the duplicate field too.

The important bit was engaging in discussion. I’m sure Aidanof will agree, having a sounding board is extremely helpful.