Search Unity

Resolved Game runs fine in editor but not in build

Discussion in 'Editor & General Support' started by MidniteOil, Nov 21, 2022.

  1. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    Hello, I am a pretty experienced Unity developer with 3 published games, including 2 on both the Apple App Store and Google Play and I have successfully configured CI/CD pipelines on Unity Cloud Build.

    But I having an issue on a new project that has me stumped. I created a game using the 2D URP template. I am using the new Unity Input System.

    I have a simple title screen and a "main" scene in which I have a simple background and a player ship, some basic sound-effects, etc.

    I'm actually producing a tutorial on how to write Defender in Unity and in my most recent video I showed how to write unit tests, do TDD and setup a CI pipeline on GitLab.

    I noticed that the Windows build I produced on GitLab showed the title screen (minus the fade & zoom in of the logo image via DOTween) but didn't advance to the main screen when pressing Start on the gamepad or any key on the keyboard. (this all works fine in the editor). I built locally just to make sure it wasn't a GitLab issue and had the same problem.

    I thought maybe DOTween was the problem so I commented out all the DOTween stuff but it still isn't advancing to the main screen so it's clearly not recognizing any input.

    I don't know if it's related to this using URP or the new Input System but maybe I'm missing something in my configuration for the build.

    Here's a link to the repo on GitLab:

    Here's a video demonstrating the problem:
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    Hey Midnite... no crashies in the loggies??

    What happens if you flip the order of your input handler attach around so that the input is attached first? If anything in the OnEnable() throws an exception you won't have input (obviously).
     
  3. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    The game doesn't crash. Where would I find log output from the runtime build?

    I'm not clear on where you mean to flip the order of my input handler attach. the title script grabs the UserInput component in Awake() and I don't check for input until Update(). Ther UserInput is a singleton that gets loaded by a bootstrapper script with the RuntimeInitializeOnLoadMethod attribute.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class Bootstrapper : MonoBehaviour
    4. {
    5.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    6.     public static void Initialize()
    7.     {
    8.         var bootstrapPrefab = Resources.Load("--- Persistent Components ---");
    9.         if (bootstrapPrefab == null) return;
    10.         var bootstrapper = Instantiate(bootstrapPrefab);
    11.         bootstrapper.name = "--- Persistent Components ---";
    12.         DontDestroyOnLoad(bootstrapper);
    13.     }
    14. }
    15.  
    upload_2022-11-21_20-22-26.png

    upload_2022-11-21_20-22-39.png

    I wonder if the fact that my Resources folder is under my _defender folder instead of directly under Assets could be the issue?
     
    KyryloKuzyk likes this.
  4. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    Nope. I moved it up into the same Resources folder that DOTween was in and rebuilt and still see the same behavior.
     
  5. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    Google that one my friend.

    I'm gonna guess you're throwing an exception and that somehow it doesn't affect in the editor, likely for timing reasons because one script ran before the other.
     
  6. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    I just installed Backtrace and integrated it into my project ran it and got stuck at the title screen but I am not seeing anything being logged to backtrace.
    Unity Integration Guide – Backtrace Help

    But, I did find the log output and see this:
    NullReferenceException: Object reference not set to an instance of an object
    at TitleScreen.OnDisable () [0x00000] in <d0a7cf4fccd543c19d965160c8f9d260>:0
     
  7. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    But no errors prior to OnDisable() so I'm guess that error occurred after I hit Alt-F4 to kill the game. So no indication of anything failing prior to that.

    Code (CSharp):
    1. using UnityEngine;
    2. using DG.Tweening;
    3. using UnityEngine.SceneManagement;
    4.  
    5. public class TitleScreen : MonoBehaviour
    6. {
    7.     [SerializeField] CanvasGroup _canvasGroup;
    8.     [SerializeField] Transform _logo;
    9.     [SerializeField] GameObject _pressStartToPlay;
    10.  
    11.     IUserInput _userInput;
    12.     bool _startPressed;
    13.  
    14.     void Awake()
    15.     {
    16.         _userInput = FindObjectOfType<UserInput>();
    17.     }
    18.  
    19.     void OnEnable()
    20.     {
    21.         _pressStartToPlay.SetActive(false);
    22.         _canvasGroup.alpha = 0f;
    23.         _canvasGroup.DOFade(1f, 1f);
    24.         _logo.localScale = Vector3.zero;
    25.         _userInput.OnStartPressed += HandleOnStart;
    26.         _logo.DOScale(Vector3.one, 3f)
    27.             .OnComplete(EnablePressStartToPlay);
    28.     }
    29.  
    30.     void OnDisable()
    31.     {
    32.         _userInput.OnStartPressed -= HandleOnStart;
    33.     }
    34.  
    35.     void Update()
    36.     {
    37.         if (_pressStartToPlay.activeSelf && _startPressed)
    38.         {
    39.             SceneManager.LoadScene("Main");
    40.         }
    41.     }
    42.  
    43.     void HandleOnStart()
    44.     {
    45.         _startPressed = true;
    46.     }
    47.  
    48.     void EnablePressStartToPlay()
    49.     {
    50.         _startPressed = false;
    51.         _pressStartToPlay.SetActive(true);
    52.     }
    53. }
    54.  
     
  8. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,934
    This:
    Code (CSharp):
    1.     void Awake()
    2.     {
    3.         _userInput = FindObjectOfType<UserInput>();
    4.     }
    Should probably be in Start?

    There's a high chance the object potentially doesn't exist, as Awake is called as objects are loaded in. This would also be why you get different behaviour between editor and build.

    Remember, self-initialisation in Awake. Initialisation that depends on other stuff in Start.
     
  9. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    I'll try that but like I said up above. UserInput is being loaded as a singleton by a bootstrapper script during subsystem registration. So far as I know that happens before any scene is loaded.


    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class Bootstrapper : MonoBehaviour
    4. {
    5.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    6.     public static void Initialize()
    7.     {
    8.         var bootstrapPrefab = Resources.Load("--- Persistent Components ---");
    9.         if (bootstrapPrefab == null) return;
    10.         var bootstrapper = Instantiate(bootstrapPrefab);
    11.         bootstrapper.name = "--- Persistent Components ---";
    12.         DontDestroyOnLoad(bootstrapper);
    13.     }
    14. }
     
  10. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    Trying to assign it in Start() gave me a null reference exception so I refactored to use a getter which would try to find it if it was null. I created a new exception class to help me isolate the error to trying to access the UserInput component. It's definitely failing to find the UserInput.

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. using DG.Tweening;
    4. using UnityEngine.SceneManagement;
    5.  
    6. public class TitleScreen : MonoBehaviour
    7. {
    8.     [SerializeField] CanvasGroup _canvasGroup;
    9.     [SerializeField] Transform _logo;
    10.     [SerializeField] GameObject _pressStartToPlay;
    11.  
    12.     IUserInput _userInput;
    13.     bool _startPressed;
    14.  
    15.     IUserInput PlayerInput
    16.     {
    17.         get
    18.         {
    19.             _userInput ??= FindObjectOfType<UserInput>();
    20.  
    21.             if (_userInput == null)
    22.             {
    23.                 throw new MissingComponentException();
    24.             }
    25.             return _userInput;
    26.         }
    27.     }
    28.  
    29.     void OnEnable()
    30.     {
    31.         _pressStartToPlay.SetActive(false);
    32.         _canvasGroup.alpha = 0f;
    33.         _canvasGroup.DOFade(1f, 1f);
    34.         _logo.localScale = Vector3.zero;
    35.         PlayerInput.OnStartPressed += HandleOnStart;
    36.         _logo.DOScale(Vector3.one, 3f)
    37.             .OnComplete(EnablePressStartToPlay);
    38.     }
    39.  
    40.     void OnDisable()
    41.     {
    42.         PlayerInput.OnStartPressed -= HandleOnStart;
    43.     }
    44.  
    45.     void Update()
    46.     {
    47.         if (_pressStartToPlay.activeSelf && _startPressed)
    48.         {
    49.             SceneManager.LoadScene("Main");
    50.         }
    51.     }
    52.  
    53.     void HandleOnStart()
    54.     {
    55.         _startPressed = true;
    56.     }
    57.  
    58.     void EnablePressStartToPlay()
    59.     {
    60.         _startPressed = false;
    61.         _pressStartToPlay.SetActive(true);
    62.     }
    63. }
    64.  
    65. public class MissingComponentException : Exception { }
    66.  
    MissingComponentException: Exception of type 'MissingComponentException' was thrown.
    at TitleScreen.get_PlayerInput () [0x00020] in <6f28d40870ca40b9a135f1406a5f3e57>:0
    at TitleScreen.OnEnable () [0x00042] in <6f28d40870ca40b9a135f1406a5f3e57>:0

    It would appear that my bootstrapper script is not running, or that it is failing to load the assets from Resources.
     
  11. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    BAYAM!!

    That my friend is probably why it's not working.

    And you DO know the only three steps to fix a nullref, right?

    You say that but... slap in a metric buttload of
    Debug.Log( "Uncle Kurt wuz heah #32");
    kinda messages and PROVE it!

    And then try it on the build... OnEnable() and Awake() are both SUPER early lifecycle things. Start() always gives everybody else a chance to shower and shave before you get after nagging them.

    Here is some timing diagram help:

    https://docs.unity3d.com/Manual/ExecutionOrder.html
     
  12. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    Hol' up... is that really the name of your Asset file under Resources?!!!

    A through Z and 0 through 9 and if you're feeling SUPER SUPER SUPER SUPER lucky, throw in an underscore.

    Anything else and you are a test pilot.

    And either way, if it is execution order, regardless of what the docs promise, you still gotta make it work.

    Remember just because you think the docs promised it, it does not mean that they did.

    - the docs might be in error or out of date
    - the docs might be non-obviously ambiguous
    - you might have interpreted the docs wrong
    - you might be looking at the wrong docs
    - any one of another bazillion reasons of which I have seen approximately a third of them personally

    Adapt, overcome, find your stuff and fill out your pointers with non-null values. Erhg, nvm... by pointers I actually mean => references. Sigh my old K&R C past is still shining through...
     
  13. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,934
    Well hey we're onto something. But similar as Kurt, the scene isn't exactly completely loaded until
    Start()
    is being called on your components. (Personally I only ever use Awake/Start. OnEnable is fickle).

    So you may have to refactor more of your code to happen in Start and not be expecting things of other objects so early.
     
  14. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    The issue seems to be in the bootstrapper. I see this in the log:
    DontDestroyOnLoad only works for root GameObjects or components on root GameObjects.

    But it seems to me that it should either work or not. I shouldn't see different behavior in the editor.
    So, following Kurts recommendation I've renamed the component to PersistentObjects and am now trying to load it by that name and threw in some Debug.Log() statements. It still runs in the Editor so I didn't break anything.

    But it still doesn't work standalone. It doesn't like the DontDestroyOnLoad so the UserInput must not be sticking around.

    Here's the refactored code:
    upload_2022-11-21_21-44-6.png
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class Bootstrapper : MonoBehaviour
    4. {
    5.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    6.     public static void Initialize()
    7.     {
    8.         Debug.Log("Uncle Kurt is loading PersistentComponents from resources.");
    9.         var bootstrapPrefab = Resources.Load("PersistentComponents");
    10.         if (bootstrapPrefab == null) return;
    11.         Debug.Log("instantiating bootstrapper from prefab...");
    12.         GameObject bootstrapper = (GameObject)GameObject.Instantiate(bootstrapPrefab);
    13.         bootstrapper.name = "--- Persistent Components ---";
    14.         Debug.Log("Calling DontDestroyOnLoad on bootstrapper");
    15.         DontDestroyOnLoad(bootstrapper);
    16.     }
    17. }
    18.  
    and the log output:
    Uncle Kurt is loading PersistentComponents from resources.
    instantiating bootstrapper from prefab...
    DontDestroyOnLoad only works for root GameObjects or components on root GameObjects.
    Calling DontDestroyOnLoad on bootstrapper

    Does anyone else find it odd that the log message saying that DontDestroyOnLoad only works for root GameObjects appears BEFORE my Debug.Log saying I'm about to call DontDestroyOnLoad?

    I am going to temporarily punt on my fancy bootstrapper and just add these to all my scenes for now but this is supposed to work.
     
    KyryloKuzyk likes this.
  15. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    Punting fixed it but I wonder if the bootstrapper issue was that I didn't have a Monobehavior script on my PersistentComponents object?

    Also, the out-of-order log statement was most-likely due to DontDestroyOnLoad being called on the UserInput child of the PersistentComponents object. I've added a script to PersistentComponents and made it the Singleton.

    I also refactored my TitleScreen to do everything in Start() since it looked like OnEnable() got called before OnStart() according to my log statements. The problem with this approach is I typically subscribe to events in OnEnable() and unsubscribe in OnDisable().

    At any rate, I'm still unable to get the UserInput. It's null when I try to get it from Start().
    Uncle Kurt is loading PersistentComponents from resources.
    instantiating bootstrapper from prefab...
    Calling DontDestroyOnLoad on bootstrapper
    <RI> Initializing input.

    New input system (experimental) initialized
    Using XInput
    <RI> Input initialized.

    <RI> Initialized touch support.

    UnloadTime: 3.326100 ms
    The character used for Underline is not available in font asset [Arcade SDF].
    The character used for Underline is not available in font asset [Arcade SDF].
    The character used for Underline is not available in font asset [Arcade SDF].
    The character used for Underline is not available in font asset [Arcade SDF].
    TitleScreen.Start(). _userInput is null.

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. using DG.Tweening;
    4. using UnityEngine.SceneManagement;
    5.  
    6. public class TitleScreen : MonoBehaviour
    7. {
    8.     [SerializeField] CanvasGroup _canvasGroup;
    9.     [SerializeField] Transform _logo;
    10.     [SerializeField] GameObject _pressStartToPlay;
    11.  
    12.     IUserInput _userInput;
    13.     bool _startPressed;
    14.  
    15.     IUserInput PlayerInput
    16.     {
    17.         get
    18.         {
    19.             _userInput ??= FindObjectOfType<UserInput>();
    20.  
    21.             if (_userInput == null)
    22.             {
    23.                 Debug.Log("_userInput is null.");
    24.             }
    25.             return _userInput;
    26.         }
    27.     }
    28.  
    29.     void Start()
    30.     {
    31.         _userInput = FindObjectOfType<UserInput>();
    32.         if (_userInput == null)
    33.         {
    34.             Debug.Log("TitleScreen.Start(). _userInput is null.");
    35.             return;
    36.         }
    37.  
    38.         _pressStartToPlay.SetActive(false);
    39.         _canvasGroup.alpha = 0f;
    40.         _canvasGroup.DOFade(1f, 1f);
    41.         _logo.localScale = Vector3.zero;
    42.         _userInput.OnStartPressed += HandleOnStart;
    43.         _logo.DOScale(Vector3.one, 3f)
    44.             .OnComplete(EnablePressStartToPlay);
    45.     }
    46.  
    47.     void OnDisable()
    48.     {
    49.         if (_userInput == null) return;
    50.         _userInput.OnStartPressed -= HandleOnStart;
    51.     }
    52.  
    53.     void Update()
    54.     {
    55.         if (_pressStartToPlay.activeSelf && _startPressed)
    56.         {
    57.             SceneManager.LoadScene("Main");
    58.         }
    59.     }
    60.  
    61.     void HandleOnStart()
    62.     {
    63.         _startPressed = true;
    64.     }
    65.  
    66.     void EnablePressStartToPlay()
    67.     {
    68.         _startPressed = false;
    69.         _pressStartToPlay.SetActive(true);
    70.     }
    71. }
    72.  
    73.  
     
  16. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    I am really at a loss here. I've refactored this bootstrapper every way to Sunday and it works perfectly in the editor. I even instantiate a parent manually and load & instantiate each child (SoundManager, UserInput) separately and parent them under the PersistentObject node.

    Code (CSharp):
    1.     [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    2.     public static void Initialize()
    3.     {
    4.         Debug.Log("Uncle Kurt is loading PersistentComponents from resources.");
    5.         //var bootstrapPrefab = Resources.Load("PersistentComponents");
    6.         //if (bootstrapPrefab == null) return;
    7.         // Debug.Log("instantiating bootstrapper from prefab...");
    8.         // var bootstrapper = GameObject.Instantiate(bootstrapPrefab);
    9.         // bootstrapper.name = "--- Persistent Components ---";
    10.         GameObject bootstrapper = new GameObject("Persistent Objects");
    11.         Debug.Log("Calling DontDestroyOnLoad on bootstrapper");
    12.         DontDestroyOnLoad(bootstrapper);
    13.         var soundManagerPrefab = Resources.Load("SoundManager");
    14.         GameObject soundManager = GameObject.Instantiate(soundManagerPrefab) as GameObject;
    15.         soundManager?.transform.SetParent(bootstrapper.transform);
    16.         var userInputPrefab = Resources.Load("UserInput");
    17.         GameObject userInput = GameObject.Instantiate(userInputPrefab) as GameObject;
    18.         userInput?.transform.SetParent(bootstrapper.transform);
    19.         int i = 0;
    20.         foreach (Transform child in bootstrapper.transform)
    21.         {
    22.             Debug.Log($"bootStrapper child {++i} is {child.name}");
    23.         }
    24.     }
    upload_2022-11-21_22-30-28.png

    But when running the standalone build, FindObjectOfType<UserInput> always returns null.

    According to the log output, I have successfully instantiated and parented those objects:
    Uncle Kurt is loading PersistentComponents from resources.
    Calling DontDestroyOnLoad on bootstrapper
    bootStrapper child 1 is SoundManager(Clone)
    bootStrapper child 2 is UserInput(Clone)
    <RI> Initializing input.

    New input system (experimental) initialized
    Using XInput
    <RI> Input initialized.

    <RI> Initialized touch support.

    UnloadTime: 3.393600 ms
    The character used for Underline is not available in font asset [Arcade SDF].
    The character used for Underline is not available in font asset [Arcade SDF].
    The character used for Underline is not available in font asset [Arcade SDF].
    The character used for Underline is not available in font asset [Arcade SDF].
    TitleScreen.Start(). _userInput is null.
     
  17. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    Okay, out of desperation I decided to change my [RuntimeInitializeOnLoadMethod] attribute to BeforeSceneLoad instead of SubsystemRegistration and lo-and-behold IT WORKED!!!

    I need to do some research to find out why.
     
    KyryloKuzyk likes this.
  18. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,934
    MidniteOil likes this.
  19. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    Thanks! I’ll look into that.
     
  20. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    Oh my goodness, there's your problem... that decorator above happens EVEN EARLIER!!

    Bootstrappers are really not suitable in Unity for the simple reason that your code is NOT the app. Unity is the app... your code is just a minor guest at the party, so you gotta play by a different set of rules.

    Just use simple stuff like these:

    Simple Singleton (UnitySingleton):

    Some super-simple Singleton examples to take and modify:

    Simple Unity3D Singleton (no predefined data):

    https://gist.github.com/kurtdekker/775bb97614047072f7004d6fb9ccce30

    Unity3D Singleton with a Prefab (or a ScriptableObject) used for predefined data:

    https://gist.github.com/kurtdekker/2f07be6f6a844cf82110fc42a774a625

    These are pure-code solutions, DO NOT put anything into any scene, just access it via .Instance!

    If it is a GameManager, when the game is over, make a function in that singleton that Destroys itself so the next time you access it you get a fresh one, something like:

    Code (csharp):
    1. public void DestroyThyself()
    2. {
    3.    Destroy(gameObject);
    4.    Instance = null;    // because destroy doesn't happen until end of frame
    5. }
    There are also lots of Youtube tutorials on the concepts involved in making a suitable GameManager, which obviously depends a lot on what your game might need.

    OR just make a custom ScriptableObject that has the shared fields you want for the duration of many scenes, and drag references to that one ScriptableObject instance into everything that needs it. It scales up to a certain point.

    If you really insist on a barebones C# singleton, here's a highlander (there can only be one):

    https://gist.github.com/kurtdekker/b860fe6734583f8dc70eec475b1e7163

    And finally there's always just a simple "static locator" pattern you can use on MonoBehaviour-derived classes, just to give global access to them during their lifecycle.

    WARNING: this does NOT control their uniqueness.

    WARNING: this does NOT control their lifecycle.

    Code (csharp):
    1. public static MyClass Instance { get; private set; }
    2.  
    3. void OnEnable()
    4. {
    5.   Instance = this;
    6. }
    7. void OnDisable()
    8. {
    9.   Instance = null;     // keep everybody honest when we're not around
    10. }
    Anyone can get at it via
    MyClass.Instance.
    , and only while it exists.
     
  21. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    Thanks Kurt, you are certainly a fount of information. I am quite familiar with Singletons. The only difference it seems, between my implementation and yours, is that you are loading/instantiating the components via their Instance getter whereas I was doing in via the RuntimeInitializeOnLoad. I saw that approach put forward by both Jason Weimann (in his Master Architect course) and Tarodev in this video.
     
  22. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    Just to pile on I think the issue I was having might have to do with something call domain reloading which clears static references which someone else encountered. Not surprisingly, the recommended solution was basically what you suggested Kurt.
     
    Kurt-Dekker likes this.
  23. KyryloKuzyk

    KyryloKuzyk

    Joined:
    Nov 4, 2013
    Posts:
    1,145
    I'm experiencing the same issue with Unity 2021.3.16f.

    Steps to reproduce the issue:
    1. Create an object in a method with the [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] attribute.
    2. Call DontDestroyOnLoad on the created game object.
    3. Load a new scene.
    4. The created game object will be destroyed.
     
  24. MidniteOil

    MidniteOil

    Joined:
    Sep 25, 2019
    Posts:
    345
    Yeah, it seems like SubsystemRegistration allows those objects to be cleared. It works if I use BeforeSceneLoad.
     
    KyryloKuzyk likes this.