Custom editor script throwing a null reference error to some objects but not all :S

So i’ve been trying to learn how to build my own custom editor tools by building a 2d level map editor and it seems to be working fine when i run it but if i make any changes, even commenting out a line in the editor script, or press the play button, i get an error from the TileGrid() in my editor script that says GetTile() in the level script has a null reference, but any new levels i make work perfectly fine. I know i’m probably missing something extremely simple to do with building an editor or a scriptable object but i don’t know what. If anyone can point me in the right direction as to where i’ve gone wrong, that’d be great!

TileBase

using UnityEngine;
using System.Collections;



public class TileBase  {

    private int tileValue;

    public TileBase(){
        tileValue = 0;
    }

    public int TileValue {
        get {
            return tileValue;
        }
        set {
            tileValue = value;
        }
    }

}

Level Script

using UnityEngine;
using System.Collections;

[System.Serializable]
public class Level {

    [SerializeField]private string levelName;
    [SerializeField]private int levelWidth;
    [SerializeField]private int levelHeight;
    private TileBase[,] levelMap;
    private int minSize = 5;
    private int maxSize = 20;

    public Level(string name, int width, int height){
        levelName = name;
        levelWidth = Mathf.Clamp(width, minSize, maxSize);
        levelHeight =  Mathf.Clamp(height,minSize,maxSize);
        ResetTiles();
    }

    /// <summary>
    /// Gets or sets the width of the level and then resets the map int array.
    /// </summary>
    /// <value>The width of the level.</value>
    public int LevelWidth {
        get {
            return levelWidth;
        }
        set {
            if(value != levelWidth){
                levelWidth = value;
                ResetTiles();
            }
        }
    }

    /// <summary>
    /// Gets or sets the height of the level and then resets the map int array.
    /// </summary>
    /// <value>The height of the level.</value>
    public int LevelHeight {
        get {
            return levelHeight;
        }
        set {
            if(value != levelHeight){
                levelHeight = value;
                ResetTiles();
            }
        }
    }

    /// <summary>
    /// Gets the tile.
    /// </summary>
    /// <returns>The tile.</returns>
    /// <param name="x">The x coordinate.</param>
    /// <param name="y">The y coordinate.</param>
    public TileBase GetTile(int x, int y){
        TileBase temp = levelMap [x, y]; // <-----Unity flags this line as causing an error
        return temp;
    }

    /// <summary>
    /// Resets the tiles.
    /// </summary>
    public void ResetTiles(){
        levelMap = new TileBase[levelWidth,levelHeight];
        for (int x = 0; x < levelWidth; x++) {
            for(int y = 0; y < levelHeight; y++){
                levelMap[x,y] = new TileBase();
            }
        }
        Debug.Log ("Tiles Reset");
    }

All the levels are added to this level storage
Level Storage

public class LevelStorage : ScriptableObject {

    [SerializeField]
    private List<Level> allLevels;
    private int numberOfLevels;

    /// <summary>
    /// Initializes the list.
    /// </summary>
    public void InitializeList(){
        allLevels = new List<Level> ();
    }

    /// <summary>
    /// Gets the number of levels.
    /// </summary>
    /// <value>The number of levels.</value>
    public int NumberOfLevels {
        get {
            numberOfLevels = allLevels.Count;
            return numberOfLevels;
        }
    }

    /// <summary>
    /// Gets the level.
    /// </summary>
    /// <returns>The level.</returns>
    /// <param name="levelNumber">Level number.</param>
    public Level GetLevel(int levelNumber){
        Level temp;
        if (numberOfLevels == 0) {
            temp = new Level ("Default", 10,10);
            allLevels.Add(temp);
            return temp;
        }
        if (levelNumber < allLevels.Count && levelNumber > -1) {
            Debug.Log("test");
            if(allLevels[levelNumber] != null){
                temp = allLevels [levelNumber];
                return temp;
            } else {
                Debug.Log("error");
                return null;
            }
        } else {
            Debug.Log("Level:" + levelNumber.ToString() + " Does not exist");
            return allLevels[allLevels.Count-1];
        }
    }

    /// <summary>
    /// Adds the new level.
    /// </summary>
    /// <param name="newLevel">New level.</param>
    public void AddNewLevel(Level newLevel){
        allLevels.Add (newLevel);
        newLevel.ResetTiles ();
        numberOfLevels = allLevels.Count;
    }

    /// <summary>
    /// Removes the level.
    /// </summary>
    /// <param name="levelToRemove">Level to remove.</param>
    public void RemoveLevel(int levelToRemove){
        allLevels.RemoveAt (levelToRemove);
        numberOfLevels = allLevels.Count;
    }
}

This is my editor script at the moment
Custom Editor Script

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

public class LevelEditor : EditorWindow {

    private LevelStorage levelStorage;
    private string database = "Assets/Resources/LevelDatabase.Asset";
    private Level openLevel;
    private Vector2 levelSelectScrollPos;

    [MenuItem("Level Editor/Open Editor")]
    public static void Init(){
        LevelEditor levelEditor = EditorWindow.GetWindow<LevelEditor> ();
        levelEditor.Show ();
    }

    void OnEnable(){
        levelStorage = AssetDatabase.LoadAssetAtPath (database, typeof(LevelStorage))as LevelStorage;
        if (levelStorage == null) {
            levelStorage = ScriptableObject.CreateInstance<LevelStorage>();
            AssetDatabase.CreateAsset(levelStorage,database);
            AssetDatabase.SaveAssets();
            AssetDatabase.Refresh();
            levelStorage.InitializeList();
            Level temp = new Level("Default",5,5);
            levelStorage.AddNewLevel (temp);
            RefreshAndSave();
        }
        openLevel = levelStorage.GetLevel (0);
    }

    void OnLostFocus(){
    }

    void OnFocus(){
        RefreshAndSave ();
        openLevel = levelStorage.GetLevel (0);
    }

    void OnGUI(){
        GUILayout.BeginHorizontal ();
        GUILayout.BeginVertical (GUILayout.Width(150));
        GUILayout.Label ("# of Levels:" + levelStorage.NumberOfLevels.ToString());
        LevelSelector ();
        GUILayout.EndVertical ();
        GUILayout.BeginVertical ();
        LevelDetails ();
        TileGrid ();
        GUILayout.EndVertical ();
        GUILayout.EndHorizontal ();
    }

    void RefreshAndSave(){
        AssetDatabase.Refresh();
        EditorUtility.SetDirty(levelStorage);
        AssetDatabase.SaveAssets();
    }

    void LevelSelector(){
        GUILayout.Label ("Levels");
        levelSelectScrollPos = GUILayout.BeginScrollView (levelSelectScrollPos,false,true);
        for (int i = 0; i < levelStorage.NumberOfLevels; i++) {
            GUILayout.BeginHorizontal ();
            if(GUILayout.Button ("Level: " + i.ToString(),GUILayout.Width(100),GUILayout.Height(20))){
                if(openLevel == levelStorage.GetLevel(i)){
//                    Debug.Log("same level selected ");
                } else {
                    openLevel = levelStorage.GetLevel(i);
                    //                    Debug.Log("different level selected ");
                }
//                Debug.Log ("Press");
//                Debug.Log (openLevel.GetTile(0,0).TileValue.ToString());
            }
            if(GUILayout.Button ("X",GUILayout.Width(20),GUILayout.Height(20))){
                levelStorage.RemoveLevel(i);
            }
            GUILayout.EndHorizontal ();
        }
        if(GUILayout.Button ("+",GUILayout.Width(100),GUILayout.Height(20))){
            AddNewLevel();
        }

        GUILayout.EndScrollView ();
    }

    void AddNewLevel(){
        Level temp = new Level (levelStorage.NumberOfLevels.ToString(), 5, 5);
        temp.ResetTiles ();
        levelStorage.AddNewLevel (temp);
        RefreshAndSave ();
    }

    void LevelDetails(){
        GUILayout.BeginVertical ();
        GUILayout.BeginHorizontal ();
        GUILayout.Label (openLevel.LevelName);
        GUILayout.EndHorizontal ();
        GUILayout.BeginHorizontal ();
        GUILayout.Label ("Width: " + openLevel.LevelWidth,GUILayout.Width(100));
        openLevel.LevelWidth = (int)GUILayout.HorizontalSlider(Mathf.RoundToInt(openLevel.LevelWidth),openLevel.MinSize,openLevel.MaxSize);
        GUILayout.EndHorizontal ();
        GUILayout.BeginHorizontal ();
        GUILayout.Label ("Height: " + openLevel.LevelHeight,GUILayout.Width(100));
        openLevel.LevelHeight = (int)GUILayout.HorizontalSlider (Mathf.RoundToInt(openLevel.LevelHeight), openLevel.MinSize, openLevel.MaxSize);
        GUILayout.EndHorizontal ();
        GUILayout.EndVertical ();
    }

    void TileGrid(){
        for (int y = openLevel.LevelHeight - 1; y > -1; y--) {
            GUILayout.BeginHorizontal();
            for(int x = 0; x < openLevel.LevelWidth; x++){
                if(openLevel.GetTile(x,y).TileValue != null){
                    if(GUILayout.Button(openLevel.GetTile(x,y).TileValue.ToString(),GUILayout.Width(25),GUILayout.Height(25))){
                        openLevel.GetTile(x,y).TileValue = openLevel.GetTile(x,y).TileValue + 1;
                        RefreshAndSave();
                    }
                }
            }
            GUILayout.EndHorizontal();
        }
    }

}

Line number?

oops forgot to add the error:
NullReferenceException: Object reference not set to an instance of an object
Level.GetTile (Int32 x, Int32 y) (at Assets/Scripts/Level.cs:60)
LevelEditor.TileGrid () (at Assets/Scripts/LevelEditor.cs:116)
LevelEditor.OnGUI () (at Assets/Scripts/LevelEditor.cs:50)
System.Reflection.MonoMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[ ] parameters, System.Globalization.CultureInfo culture) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:222)

For first one when sending x and y send them reduced by 1 like x-1 and y-1 because it may be sending x32 and 32 is stored under 31

for others iam not sure so i wont comment

1 Like

Phoda is correct, you want to subtract one from the x and y when accessing the map information.
This is due to how the elements in arrays work.
Keep in mind, that the first element in an array is at index [0], so if you have 32 elements in an array, the 32nd element is actually going to be at index 31, because the first element is going to be at index [0], not index [1](which is actually the second element).

Doing this should fix the other errors, if not, let us know! :slight_smile:

When i call GetTile i’m calling it from the nested for loop in TileGrid and thats sending it all the locations starting from zero in both axis.

void TileGrid(){
        for (int y = openLevel.LevelHeight - 1; y > -1; y--) {
            GUILayout.BeginHorizontal();
            for(int x = 0; x < openLevel.LevelWidth; x++){
                if(openLevel.GetTile(x,y).TileValue != null){
                    if(GUILayout.Button(openLevel.GetTile(x,y).TileValue.ToString(),GUILayout.Width(25),GUILayout.Height(25))){
                        openLevel.GetTile(x,y).TileValue = openLevel.GetTile(x,y).TileValue + 1;
                        RefreshAndSave();
                    }
                }
            }
            GUILayout.EndHorizontal();
        }
    }

The for loop is working when i initially save the script and run the editor, its if i edit any part of the script once the editor has created the asset file and i then go back and tweak anything at all that it breaks.

Have tried to delete the old asset file before making changes, and restarting Unity?

yea, still gives the same errors

I found out what was causing this error so i’m going to add it on here in case anyone else has this problem and finds this thread. Turns out that scriptable objects can’t save multi dimensional arrays, so i’ve rewritten my level script to store all the tilebases in one long array and changed my set and get tile functions to access the correct tile from there. Everything is now working exactly as intended

So you were serializing hahahahahha. Yeah multi-arrays can’t be saved. If you are interested more about saving I wrote thread about it about other methods for comparison.