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. Dismiss Notice

Resolved AddComponent() instantiates the MonoBehavior provided then later destroys it and re-instantiates it?

Discussion in 'Scripting' started by SearousK, Sep 23, 2020.

  1. SearousK

    SearousK

    Joined:
    Mar 21, 2020
    Posts:
    6
    [SOLUTION]: Destroy() doesn't destroy the referenced object immediatly, and GetComponent() doesn't consistantly return components in any particular order.

    Therefore, when destroying a component of Type T then adding a new component of Type T to the same GameObject, you MUST ensure that if you want to set any data on the new component you save the reference to it return by GetComponent().

    I'm more so curious about this than wanting help. So, I'm destroying a MonoBehavior, then adding a new one to the same GameObject that derives from the one destroyed (sometimes) from a MonoScript[] defined in the inspector. I want to save a reference to the new one for convenience, but my references kept getting nulled. I check every line of code in the project and nothing ever for any reason sets the references to null or destroys the MonoBehaviors without resetting the references to new ones.

    Here's the relevent lines where I destroy then recreate the MonoBehaviors:
    Code (CSharp):
    1. // Load JSON
    2. string jsonString = File.ReadAllText(path);
    3. LevelData levelData = JsonUtility.FromJson<LevelData>(jsonString);
    4.  
    5. // Setup nodes
    6. for(int i = 0; i < transform.childCount; i++) {
    7.     Node node = transform.GetChild(i).GetComponent<Node>();
    8.  
    9.     Destroy(node);
    10.     node = (Node)transform.GetChild(i).gameObject.AddComponent(nodeBehaviors[levelData.grid[i]].GetClass());
    11. }
    (LevelData is a simple, data-holding type that contains the decoded JSON.)

    I stepped through this server times. The references only get reset whenever the Start() methods are called on the respective Nodes. For reference, here is that code as well:
    Code (CSharp):
    1. protected virtual void Start() {
    2.     _player = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerController>();
    3.     if(_player == null) {
    4.         Debug.LogError("Player Controller not fount! Check that the right tag was selected, and that the object has the PlayerController behavior attacheched and enabled!");
    5.     }
    6.     _spriteGraphics = transform.GetChild(0).gameObject;
    7.     _sprite = _spriteGraphics.GetComponent<SpriteRenderer>();
    8.     MainCollider = GetComponent<Collider2D>();
    9.  
    10.     UpdateVisuals();
    11. }
    (Currently, no classes deriving from Node hide or override Start(). The UpdateVisuals() method simply sets the color of the sprite in a single line.)

    My solution to get around this was to throw my code that sets my references into a method, then pass it into Invoke():
    Code (CSharp):
    1. private void SetupNodeGrid() {
    2.     NodeGrid.Clear();
    3.     for(int i = 0; i < transform.childCount; i++) {
    4.         NodeGrid.Add(transform.GetChild(i).GetComponent<Node>());
    5.     }
    6. }
    (NodeGrid is a List<Node> which holds the references to my MonoBehaviors. It's a static property:
    public static List<Node> NodeGrid { get; } = new List<Node>(9);
    )
    Code (CSharp):
    1. Invoke("SetupNodeGrid", 0.1f);
    (This Invoke() call happens right after the first for for loop above, where I re-instantiated my MonoBehaviors.)

    I should also note that I've had similar issues trying to set variables on these newly instantiated MonoBehaviors. Those values also get reset. Therefore, this leads me to believe that my MonoBehaviors are being instantiated, destroyed, then instantiated again for some reason.

    I've only been working with Unity/C# for a month or two. It's very possible there's just something about the engine or language I don't yet understand.

    Any clue as to why this might be?
     
    Last edited: Sep 24, 2020
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,697
    I think your problem is that Destroy actually doesn't destroy the object (in this case your old node component) until the end of the current frame. Read here: https://docs.unity3d.com/ScriptReference/Object.Destroy.html

    So your code here:
    Code (CSharp):
    1. private void SetupNodeGrid() {
    2.     NodeGrid.Clear();
    3.     for(int i = 0; i < transform.childCount; i++) {
    4.         NodeGrid.Add(transform.GetChild(i).GetComponent<Node>());
    5.     }
    6. }
    May be picking up the Nodes that are marked for deletion at the end of frame, Rather than the "good" new Nodes.

    One workaround, as you found, is to wait until at least the next frame when the old nodes are deleted. Another workaround would be to manually keep a List<Node> that you clear out when deleting the old nodes and add the new nodes to when you instantiate them. A third could be to mark the "to-be-deleted" nodes in a HashSet or something similar, and use GetComponents (plural) instead of GetComponent, then filter by which nodes are not in the to-be-deleted set.
     
  3. SearousK

    SearousK

    Joined:
    Mar 21, 2020
    Posts:
    6
    Would that also mean that Destroy() doesn't destroy the referenced Component, but instead destroys the one in the "slot" the referenced one was in? (or so to speak?) If that's the case, then what's happening is when I add the new component and then grab it again later, the one being grabbed is the old one as it was created first. Therefor, another workaround might be to update the nodes in the for loop where they are destroyed then re-instantiated immediately, using the reference returned by AddComponent()
     
  4. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,697
    I'm not really sure what you mean by "the referenced one" in this context.

    Code (CSharp):
    1.     Node node = transform.GetChild(i).GetComponent<Node>();
    2.     Destroy(node);
    This code will destroy whatever came back from
    transform.GetChild(i).GetComponent<Node>();
    . If there's only one Node component on that object, that's the one that will be destroyed. If there is more than one, well, the docs for GetComponent say that the order is not defined. So it could be any one of them. See https://docs.unity3d.com/ScriptReference/GameObject.GetComponent.html
     
  5. SearousK

    SearousK

    Joined:
    Mar 21, 2020
    Posts:
    6
    That's really not the behavior I would expect, even if Destroy() doesn't destroy things immediately. You'd think it would destroy the exact component passed into it, not just any of them on that GameObject.

    What I mentioned seems to work though. I'm calling Destroy() on the old component before instantiating it again. So, when I call Destroy() there's only one copy of Node on the GameObject. I would assume that Unity keeps track of what needs to be destroyed exactly, rather than "destroy a component of Type T on on GameObject GO". That just seems a little silly to me. Otherwise, it would be clearer to have Destroy() take GameObject and Type rather than any Unity.Object derivative.

    Regardless, I wouldn't have even consider it being related to Destroy() or GetComponent() on my own. My understanding has improved greatly, thank you!
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,336
    It definitely does destroy only the one passed in. If you are finding it destroys something else, then either that something else IS what got passed in, or you're running this code more than once, or somehow something else is destroying it.

    Keep in mind, if you Destroy() a GameObject, you can add Monobehaviors to it all you like until end of frame, and then ALL of them get destroyed along with the GameObject. Destroy() is sort of like an irrevocable mark of Cain at the GameObject level. It's doomed.
     
    PraetorBlue likes this.
  7. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,697
    Destroy does destroy the exact component passed to it. It's just that GetComponent returns essentially a "random" component if there is more than one of the given type, so if you Destroy() the result of a GetComponent call, and there is more than one component of that type on the object, it's essentially destroying a random one.

    Anyway, glad you were able to get things working.
     
  8. SearousK

    SearousK

    Joined:
    Mar 21, 2020
    Posts:
    6
    Ya know, it's kinda funny. I would've stumbled across this revilation on my own as I just rewrote some things to use references to Nodes instead of the GameObjects they are on.