Be careful about run order of Awake functions

I think controllers are a great idea in general. But I’ve also come up with this solution that helps with values that need initializing after Awake.

First of all I have a custom wrapper for the state called LazyInit. It allows me to declare a variable like this in Awake:

currentWeapon = new LazyInit<Weapon>(InitDefaultWeapon);

Then I can use that currentWeapon anywhere in the knowledge that it will be initialized on first use.

If I need it initialized before rendering I do this:

private void Start() {
    currentWeapon.ForceInit();
}

The full code for LazyInit is here:

public class LazyInit<T>
{
    private T _value;
    private bool _initialized = false;
    private InitializerDelegate _initializer;

    public T v
    {
        get
        {
            ForceInit();
            return _value;
        }
        set
        {
            _initialized = true;
            _value = value;
        }
    }
 
    public delegate T InitializerDelegate();

    public LazyInit(InitializerDelegate initializer)
    {
        _initializer = initializer;
    }

    public void ForceInit()
    {
        if (!_initialized)
        {
            _value = _initializer();
            _initialized = true;
        }
    }
}

I’m wondering why a Start() method is being called before an Awake() method in my mobile game; as I understand it, the Awake() call order is undefined but all of them are indeed called before any Start() methods begin being called.

My setup is pretty simple; I’m just following along a CodeMonkey video trying to make a UI popup in my game.

There’s a Testing.cs script attached to my “Managers” empty GameObject (where my GameManager.cs script is also attached):

void Start()
    {
        CurrentGameManager gameManager = GameObject.Find("Managers").GetComponent<CurrentGameManager>();

        PlopDistance.Create(new Vector3(0, 25, 0), 199.1f, gameManager);
    }

The Start() method in that Testing.cs script is being called before the Awake() method in my Popup script (this popup is a prefab called PlopDistance and the script is attached to it:

public class PlopDistance : MonoBehaviour
{
    private TextMeshPro plopDistanceText;

    public void Setup(float plopDistance)
    {
        plopDistanceText.SetText(string.Format("{0} m", plopDistance));
    }

    public static PlopDistance Create(Vector3 spawnPosition, float plopDistance, CurrentGameManager gameManager)
    {
        Transform plopDistanceTransform = Instantiate(gameManager.plopDistancePopup, spawnPosition, Quaternion.identity);
     
        PlopDistance plopDistanceScript = plopDistanceTransform.GetComponent<PlopDistance>();
        plopDistanceScript.Setup(plopDistance);

        return plopDistanceScript;
    }

    void Awake()
    {
        plopDistanceText = transform.GetComponent<TextMeshPro>();
    }
}

I think it better to create your own Initialize logic. In my game i create an Initializer.cs like this:

public class Initializer : MonoBehaviour {
    [SerializeField] private AdManager adManager;
    [SerializeField] private IAPManager iAPManager;

    [SerializeField] private ProjectData projectData;
    [SerializeField] private GameController gameController;
    [SerializeField] private UIController uIController;
    [SerializeField] private AudioController audioController;
    [SerializeField] private LevelController levelController;
    [SerializeField] private PlayerController playerController;
    [SerializeField] private CameraHolder cameraHolder;
    [SerializeField] private MaterialController materialController;

    private void Start() {
        adManager.Initialize();
        iAPManager.Initialize();

        projectData.InitializeSelf();
        gameController.InitializeSelf();
        uIController.InitializeSelf();
        audioController.InitializeSelf();
        levelController.InitializeSelf();
        playerController.InitializeSelf();
        cameraHolder.InitializeSelf();
        materialController.InitializeSelf();

        gameController.Initialize();
        uIController.Initialize();
        playerController.Initialize();
        cameraHolder.Initialize();

        GameController.Setup(GameController.Mode.TwoD);
    }
}
2 Likes

if two objects are created at the same time, specifically within the same frame, both their “Awakes” will fire the moment they are instantiated, then on the next frame their “Starts” fire.

if two objects are created at different times, say one now and the next two seconds later, the first one will undoubtedly fire both its Awake and Start long before the other object could. The “all awakes before all starts” rule only counts on a per frame basis. if an object wasn’t created on that frame yet, it has to do its awake/start timing on later frames.

thus if you testing script existed in the scene as its was loading, but your PlopDistance prefab was instantiated in at a later time, even by a single frame, then the Awake/Start call order across both scripts cannot be guaranteed.

1 Like

That’s because your PlopDistance object is instantiated after a call to Test.Start method. It is not possible to call Awake for object what does not exists yet. This is what’s happening in your scene:

  • Scene is loaded
  • All scripts Awake called
  • All scripts Start called
  • Popup instantiated
  • Popup Awake called
1 Like

Ahhhh yeah that makes sense. Duh. Jeez. I are Unity nub :frowning:

Now I just need to figure out the necessary Quaternion for the UI text that spawns. It looks like it’s rotated 90 degrees too far clockwise maybe. Thankfully my camera faces the same direction all the time; just need to have the Quaternion face the camera I think.

Man, just about every single little thing in this game is a whole thing to figure out and I’m constantly stuck. I guess it’s just a fact of learning something new. Thanks for your help y’all, I really appreciate it!

To make any object face the camera, just copy camera’s own rotation:

transform.rotation = Camera.main.transform.rotation;

Thanks for this. It really helped a lot of my time.

1 Like