SetWaveConfig() after Instantiate() in EnemySpawner.cs

I have a doubt about the execution order in the EnemySpawner script:

for (int enemyCount = 0; enemyCount < waveConfig.GetNumberOfEnemies(); enemyCount++)
        {
            var newEnemy = Instantiate(waveConfig.GetEnemyPrefab(), waveConfig.GetWayPoints()[0].position, Quaternion.identity);
            newEnemy.GetComponent<EnemyPathing>().SetWaveConfig(waveConfig);
            yield return new WaitForSeconds(waveConfig.GetTimeBetweenSpawns());
        }

I understand that we instantiate a new Enemy prefab object, and assign it to a variable newEnemy. Inside Instantiate() , we already set the enemy prefab via GetEnemyPrefab(), but we donā€™t assign a path prefab yet.
We do that (assign the path prefab) on the next line , getting the EnemyPathing component of the enemy instance in ā€˜newEnemyā€™ and setting its path via SetWaveConfig().

What Iā€™m not getting is, to my understanding, as soon as we Instantiate something (assigning it to a variable or not), Unity ā€˜runsā€™ the object code, which in this case makes the enemy appear on the 2D plane. But if this is true, at this point the EnemyPathing component would be ā€˜nullā€™ since nothing is assigned to it yet, which would happen only on the next line.

But it works, and in fact, if I comment the SetWaveConfig() unity throws a:

NullReferenceException: Object reference not set to an instance of an object
EnemyPathing.Start () (at Assets/Scripts/EnemyPathing.cs:14)

for every enemy it tried to instantiate on the line above.

In my mind, itā€™s like Instantiate() is waiting for the next line to be executed to have all necessary information (enemy AND path prefabs) to launch an enemy, which obviously is not true. What Iā€™m missing here? Please help.

Thanks!

1 Like

Hi,

Those are great questions and a good opportunity to explore Unity a bit. Add a Debug.Log with a meaningful message to all relevant methods, for example, Awake(), Start() and SetWaveConfig(). Time.frameCount could be interesting in the message. Then run your game. Ideally, the messages in your console will tell you in which order the methods got executed. :slight_smile:


See also:

Hi Nina, I did as you suggested and I think I have an idea of what happened.

First, I littered (lol) my code with lots of Debug messages:

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

public class EnemySpawner : MonoBehaviour
{
    /*[SerializeField]*/ List<WaveConfig> waveConfigs;
    int startingWave = 0;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("Begin of Start() at " + Time.frameCount);
        waveConfigs = new List<WaveConfig> (Resources.FindObjectsOfTypeAll<WaveConfig>());
        Debug.Log("Created list of waveConfigs in Start() at " + Time.frameCount);

        var currentWave = waveConfigs[startingWave];
        Debug.Log("Before calling coroutine in Start() at " + Time.frameCount);
        StartCoroutine(SpawnAllEnemiesInWave(currentWave));
        Debug.Log("After calling coroutine in Start() at " + Time.frameCount);
    }

    private IEnumerator SpawnAllEnemiesInWave(WaveConfig waveConfig)
    {
        Debug.Log("Begin of coroutine at " + Time.frameCount);
        Debug.Log("Before loop in coroutine at " + Time.frameCount);
        for (int enemyCount = 0; enemyCount < waveConfig.GetNumberOfEnemies(); enemyCount++)
        {
            var newEnemy = Instantiate(waveConfig.GetEnemyPrefab(), waveConfig.GetWayPoints()[0].position, Quaternion.identity);

            var anotherEnemyInstantiation = Instantiate(waveConfig.GetEnemyPrefab(), waveConfig.GetWayPoints()[0].position, Quaternion.identity);
            Debug.Log("Created new newEnemy object in coroutine at " + Time.frameCount);
            newEnemy.GetComponent<EnemyPathing>().SetWaveConfig(waveConfig);
            Debug.Log("Called setwaveconfig for newEnemy object in coroutine at " + Time.frameCount);
            Debug.Log("Before yelding in coroutine at " + Time.frameCount);
            Destroy(anotherEnemyInstantiation);
            yield return new WaitForSeconds(waveConfig.GetTimeBetweenSpawns());
            Debug.Log("After yelding in coroutine at " + Time.frameCount);
        }
        Debug.Log("After loop in coroutine at " + Time.frameCount);
    }
}

And what I saw in execution order was what I expected in terms of the coroutine yelding execution back to the Start() method. BUT this didnā€™t explained my original question, of why SetWaveConfig() worked to change the enemy path after the enemy was instantiated.

What I did then: I created a second object INSIDE the coroutine, called SetWaveConfig() for it and immediately destroyed it before yelding, and the object was NOT drawn on the screen:

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

public class EnemySpawner : MonoBehaviour
{
    /*[SerializeField]*/ List<WaveConfig> waveConfigs;
    int startingWave = 0;

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("Begin of Start() at " + Time.frameCount);
        waveConfigs = new List<WaveConfig> (Resources.FindObjectsOfTypeAll<WaveConfig>());
        Debug.Log("Created list of waveConfigs in Start() at " + Time.frameCount);

        var currentWave = waveConfigs[startingWave];
        Debug.Log("Before calling coroutine in Start() at " + Time.frameCount);
        StartCoroutine(SpawnAllEnemiesInWave(currentWave));
        Debug.Log("After calling coroutine in Start() at " + Time.frameCount);
    }

    private IEnumerator SpawnAllEnemiesInWave(WaveConfig waveConfig)
    {
        Debug.Log("Begin of coroutine at " + Time.frameCount);
        Debug.Log("Before loop in coroutine at " + Time.frameCount);
        for (int enemyCount = 0; enemyCount < waveConfig.GetNumberOfEnemies(); enemyCount++)
        {
            var newEnemy = Instantiate(waveConfig.GetEnemyPrefab(), waveConfig.GetWayPoints()[0].position, Quaternion.identity);
            Debug.Log("Created new newEnemy object in coroutine at " + Time.frameCount);
            newEnemy.GetComponent<EnemyPathing>().SetWaveConfig(waveConfig);
            Debug.Log("Called setwaveconfig for newEnemy object in coroutine at " + Time.frameCount);

            // Creating another wave of enemies....
            var anotherEnemyInstantiation = Instantiate(waveConfig.GetEnemyPrefab(), waveConfig.GetWayPoints()[1].position, Quaternion.identity);
            anotherEnemyInstantiation.GetComponent<EnemyPathing>().SetWaveConfig(waveConfigs[1]);
            // And destrying them BEFORE yelding, they're not draw on screen.
            // Meaning, the drawing only occurs after returning to who called
            // the coroutine, regardless of the instantiation inside the coroutine.
            // IS THIS RIGHT?
            Destroy(anotherEnemyInstantiation);

            Debug.Log("Before yelding in coroutine at " + Time.frameCount);
            
            yield return new WaitForSeconds(waveConfig.GetTimeBetweenSpawns());
            Debug.Log("After yelding in coroutine at " + Time.frameCount);
        }
        Debug.Log("After loop in coroutine at " + Time.frameCount);
    }
}

Conclusion : Regardless of the instantiation and setting of properties, components, etc inside the coroutine, the real ā€œplacementā€ of an object on the game only happens after returning to the method who called it, in this case, Start(). I can do whatever I want with the newly created object inside the coroutine, but it is only inserted in the game after the coroutine yelding.

IS THIS RIGHT? Please confirm :slight_smile:

Thank you!

2 Likes

Sounds right, and if the messages in your console verify your claims, your conclusion is probably right.

Since most parts of Unity are not open-source, we rarely know whatā€™s going on behind the scenes, so I often hesitate to claim that something is generally right. However, we can get an idea of what is happening by ā€œlitteringā€ code with Debug.Logs and by interpreting the output like you did. :slight_smile:

Hi Nina!

Well, thatā€™s not exactly a confirmation of my idea, but I understand that, like you said, itā€™s not really possible to know the exact place where the instantiation translates to a ā€œplacementā€ of the object on the game, Unity being closed-source.

But in my opinion, if itā€™s possible at all, this lecture really should be one of the Rickā€™s ā€˜back from the futureā€™ moments where he at least show and comment that the object appears only on return to the ā€œofficialā€ Unity method, whichever called the coroutine.

The fact that one line instantiates the ship with a path assigned on the inspector, and another line immediatelly changes the path, and this change happens, seens to completely break the notion of ā€˜immediate executionā€™ of instructions, unless we understand that ā€œinstantiation < > placement of object on the gameā€. I think that should warrant further explanation on the lecture.

My two cents. :slight_smile:

Anyway, thanks again for your help. Have a great day! :slight_smile:

1 Like

Thanks a lot for your input, Luis. Iā€™m forwarding it to Rick. Maybe heā€™ll edit the video and add a more extended explanation, but I cannot promise anything because Iā€™m just a teaching assistent.

Have a nice day, and please do continue sharing your feedback with us! :slight_smile:

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms