0
\$\begingroup\$

I'm working on a game, where the map is generated, and:

  • The map is generated in an Awake(), so when an object's Start() is called, the map should be fully initialized.

  • But the scene's physics world won't be updated till the next FixedUpdate.

  • So when Start() is called, there is a high chance, that physics operations are done in an incorrect world (like raycasting)

I tried using Physics.SyncTransforms, but it didn't solve the issue for some reason. (Why?)

My next approach was to start the objects with a delay. Precisely: after a FixedUpdate.

  • So I started a Coroutine in the Start() method, which waits for a FixedUpdate, then starts the object.

  • But this caused that for example: ObjectA's Update() is called before ObjectB's Start coroutine finishes.

How could I achieve that scenes won't be started until the scene is fully initialized?

Is there a way to tell Unity to call every Start() i.e. 1 frame later? Or how else could I fix this? What is the best solution?

\$\endgroup\$
4
\$\begingroup\$

The neatest way to do this is probably with an event. Content that needs to initialize after the map's physics are ready can subscribe to the event.

Using a generator method like the one below, this subscription can be as simple as:

void Start() {
    MapGenerator.DeferUntilPhysicsReady(InitializeAfterPhysics);
}

void InitializeAfterPhysics() {
   // Do the thing.
}

Here's a sketch of a generator script that can handle this deferred initialization:

public class MapGenerator : MonoBehaviour
{
    static MapGenerator _instance;
    public static MapGenerator instance { get {
            if (_instance == null)
                _instance = FindObjectOfType<MapGenerator>();
            return _instance;
        } }

    public static void DeferUntilPhysicsReady(System.Action callback) {
        var generator = instance;
        if (generator == null) {
            Debug.LogError("Trying to defer in scene with no Map Generator.");
            return;
        }

        if(generator._isPhysicsReady) {
            // Handle late-spawning scripts that can safely initialize now.
            callback();
        } else {
            // Otherwise, wait until physics is ready, then call it.
            generator.OnPhysicsReady += callback;
        }
    }    

    public event System.Action OnPhysicsReady;
    bool _isPhysicsReady;

    IEnumerator Start()
    {
        GenerateMap();

        // Wait until just before the next physics step.
        yield return new WaitForFixedUpdate();
        // Let physics step at least once, then resume after Update().
        yield return null;

        _isPhysicsReady = true;

        // Physics are all up to date:
        // ready to call our deferred initialization methods.
        if (OnPhysicsReady != null)
            OnPhysicsReady();
    }

    void GenerateMap() {
        // Do the thing.
    }
}

You can even offload this subscription logic to a separate standalone component that wakes the rest. Just spawn you object with the sensitive scripts initially disabled, and use a script like this to wake them and start ticking their Start / Updates on demand:

public class WakeAfterMapReady : MonoBehaviour {

    public Behaviour[] disabledBehaviours;

    void Start() {
        MapGenerator.DeferUntilPhysicsReady(InitializeAfterPhysics);
    }

    void InitializeAfterPhysics() {
         foreach (var behaviour in disabledBehaviours)
             behaviour.enabled = true;
    }
}
\$\endgroup\$
  • \$\begingroup\$ But in this way, Update() could run while not every object is initialized, couldn't it? So now in Update() I can't rely on that every object is fully initialized. I could add 2 static counters, one of them would be increased in the Start() methods, and the other would be increased at the ends of InitializeAfterPhysics. And there would be an Update() in this wrapper class with: if(counter2 < counter1) return; But isn't this too much boilerplate? \$\endgroup\$ – Tudvari Apr 4 at 6:52
  • \$\begingroup\$ If you want to delay Update calls, just spawn your scripts disabled and wake them up when you're ready, as shown in the edit. \$\endgroup\$ – DMGregory Apr 4 at 8:54
2
\$\begingroup\$

Ordering dependencies in Unity is a pretty complex thing, once you have too many different game objects in the scene. In my experience it's better not do things like raycasting in Start, simply because you don't know the initialization state of each object. You cannot ensure when or in which order will Start() be called. GameObjects are designed to be independent, not reliant on the state of other objects.

I find that the best solution is to sort out your dependencies in the design stage, in order to have your code properly reflect them. For example, if I understand your description correctly, you have the following order of stages:

  1. generate map
  2. ensure that the Physics state of each object has been set (I.E. each object has ran FixedUpdate at least once).
  3. Setup each object.

Assuming this is correct, it seems pretty obvious that what you need is to implement a barrier for stage 2, and only after the barrier has been entered by all the objects can stage 3 start. It can be as simple as having a global counter that each object increments in Start(), and then have check in Update():

void Start() {
    counter++;

    // Other non-dependant logic
}

void Update() {
    if (counter < targetNumber) {
        return;
    }

    // Implement dependant logic
}
\$\endgroup\$
  • \$\begingroup\$ That counting solution came to me as well, but I think you are right, that I should reconsider using Physics in Start. Maybe my initializer raycast should be just moved to update, inside an if statement. And that would save me a lot of hassle and boiler plate code. \$\endgroup\$ – Tudvari Apr 4 at 10:13
  • 1
    \$\begingroup\$ Oh, wait, that would cause issues again, because Update may use an "unupdated" object. My new idea is to just implement that raycasting in lazy initialization: the field is first initialized when its getter is called but it's not yet initialized. \$\endgroup\$ – Tudvari Apr 4 at 10:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.