NullReferenceException on Prefab inside ScriptableObject only on Build

I’m building a level editor for my rhythm game. Each level is a ScriptableObject named LevelData that can be saved as a .lvl file using the JSONUtility methods.

LevelData class:

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

[Serializable]
[CreateAssetMenu(fileName = "New Level Data")]
public class LevelData : ScriptableObject
{
    public string fileName = "new_level";
    public AudioClip songClip;
    public int totalBars;
    public float bpm;

    [SerializeReference] public List<ObstacleEvent> obstacleEvents = new List<ObstacleEvent>();

    public void Reset()
    {
        fileName = "new_level";
        songClip = null;
        totalBars = 0;
        bpm = 0;

        obstacleEvents.Clear();
    }
}

LevelData when saved example:
Example

To manage the LevelData files, I have a LevelSelector that lists all .lvl files inside a “levels” folder.

LevelSelector print:

When the user clicks on the level “EDIT” button, a .lvl file is loaded, it overwrites a LevelData instance and gets passed to a LevelEditor scene.

Loading and overwriting LevelData process:

    public async Task<string> GetLevelDataAsString(string levelFileName)
    {
        string result;

        using (var reader = File.OpenText($"{Application.persistentDataPath}/levels/{levelFileName}.lvl"))
        {
            result = await reader.ReadToEndAsync();
        }

        return result;
    }

    public void OverwriteLevelData(LevelData oldLevelData, string newData)
    {
        JsonUtility.FromJsonOverwrite(newData, oldLevelData);
    }

Everything works fine on Unity, but after building to the .exe file, a strange thing happens.

On build: When I try to edit an existing level before doing anything, a NullReferenceException gets thrown and part of the LevelData does not load properly on the LevelEditor scene (I made it so logs get printed on the build btw). But, if I try to open the same level after returning to the LevelSelector scene, everything works as expected and no exceptions appear.

The logged exception (first time opening):
7536305--930797--upload_2021-9-30_14-53-39.png

Second time opening (nothing goes wrong, thats the obstaclePrefab.name):
7536305--930800--upload_2021-9-30_14-53-59.png

The Start method where the exception gets thrown (inside the LevelEditor script):

    private void Start()
    {
        levelData = VariablesManager.Instance.currentLevelData;

        if (levelData.obstacleEvents.Count > 0)
            Debug.Log(levelData.obstacleEvents[0].obstaclePrefab.name);

        levelName.text = $"<b>Current loaded level:</b> {levelData.fileName}";
        timingDisplay = FindObjectOfType<TimingDisplay>();
        currentTiming = new Timing(1, 1);
        UpdateAll();
    }

(without this Debug.Log call, the exception doesn’t get thrown)

I’ve been searching for two days and found nothing helpful or at least no situation that looks like mine.
Thanks for taking your time <3

In your Start() above, why are you creating the ScriptableObject in line 3, then immediately overwriting it in line 4?

At a minimum you can freely delete line 3… that SO is just a fart in a fan factory and it’s gone, unless you go digging for it with something global, and AFAIK, who knows if it will have been discarded by then or not, since there are no remaining references to it.

That line 3 was not there originally, I just figured it would probably solved the problem (it didn’t) but I forgot to remove it.

Be aware - in a build the game objects’ Start methods may all run in a different order than the do in editor playmode. Check if any of the objects that are referenced in that Start are initialized in another GameObject’s Start method. It may be initializing before your level editor’s Start in the editor but not in the build.

2 Likes

Ok so, I’ve tried changing the Script Execution Order of the singleton that contains the “currentLevelData” LevelData (which I use to overwrite a local LevelData variable):
7538744--931121--upload_2021-10-1_12-32-19.png

Unfortunately this didn’t work and the exception is still being thrown upon loading a level for the first time in the Editor scene.

This is the VariablesManager script btw:

using UnityEngine;

public class VariablesManager : MonoBehaviour
{
    #region singleton
    public static VariablesManager Instance { get; private set; }

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);

            if (currentLevelData == null)
                currentLevelData = ScriptableObject.CreateInstance<LevelData>();
        }
        else
            Destroy(gameObject);
    }
    #endregion

    public LevelData currentLevelData;
}