Why is my C# array of objects fully populated when some of them should be null?

Hi All

Aplogies if this is more a c# query, but I can’t find any details generally so far.

I have a 2D map that represents legal positions for creatures and players in the world. The world is 3d but movement is tile based. I have a serializable class for each tile and a 1d array I treat as 2d via a GetTile() method. Some of my tiles are not accessible and so I wanted to set the array location to null, but it doesn’t appear to work - when I inspect the array on load, each index contains a tile class, although where the tiles should be null they are only partially constructed?

Any help greatly apprechiated!

[Edit]
Thanks for all the help so far. Here’s the code with the bodies edited out

[System.Serializable]
public class BVMapTile
{  // [EDIT] clearly a tile isn't empty but the contents probably don't tell you much
}

[System.Serializable]
public class BVMap2D : MonoBehaviour
{   // map data
    public int MaxX = 0;
    public int MaxZ = 0;
    public BVMapTile[] Tiles = null; // array is single but treated like 2D

    // gets a tile
    public BVMapTile GetTile(int x, int z)
    {  // return the tile specified
       return Tiles[x * MaxZ + z];
    }
    
    public void SetTile(int x, int z, BVMapTile tile)
    {  // set the tile
       Tiles[x * MaxZ + z] = tile;
    }
}

[CustomEditor(typeof(BVMap2D))]
public class BVMap2DEditor : Editor
{
    static bool m_render_boxes = true;
    
    void OnSceneGUI()
    {  // [EDIT] I'm drawing some handles here
    }
    
    [DrawGizmo(GizmoType.NotSelected)]
    static void RenderLevelHullGizmo(GameObject obj, GizmoType type)
    {  // if the selection is same we might care
       if(!Selection.Contains(obj)) return;
        
       // [EDIT] I'm drawing some gizmos here, I look for the component type on the GameObject and bail if its not there
    }
    
    Vector3 m_move_height = new Vector3(0, 1, 0);   // if we collide at this height we cannot pass between squares
    Vector3 m_sight_height = new Vector3(0, 1.5f, 0);  // if we collide at this height we cannot see between squares
    
    // map of calculated grounds positions
    Dictionary<BVMapTile, Vector3> m_calculated_ground_positions = new Dictionary<BVMapTile, Vector3>();
    
    public override void OnInspectorGUI()
    {  // get the map
        BVMap2D map = target as BVMap2D;
        Transform t = map.transform;
        
        // build map button
        if(GUILayout.Button("Build Map"))
        {  // warning!
           // clear map of ground positions
           m_calculated_ground_positions.Clear();
            
           // calculate the size of the map, from the handles in the scene
           Vector3 size = map.UpperBounds;
           size -= map.LowerBounds;
           int max_x = (int) ((size.x + 0.2f) / BVMap2DConstants.TileSize);
           int max_z = (int) ((size.z + 0.2f) / BVMap2DConstants.TileSize);
            
           // create a new map!
           map.Tiles = new BVMapTile[max_x * max_z];
           map.MaxX = max_x;
           map.MaxZ = max_z;
            
           // [EDIT]
           // *** The Relevant Collision Models are parsed to decide whether the tile position is accessible. ***
            
           // the final pass will decides whether the tile is closed
           for(int x=0; x<max_x; ++x)
           {  // for each z tile
              for(int z=0; z<max_z; ++z)
              {  // get  the tile we are on
                 BVMapTile tile = map.GetTile(x, z);
                 tile.DetermineClosed();
                    
                 // if the tile has an entry, set the ground position
                 tile.HaveGroundPosition = tile.HaveGroundPosition || m_calculated_ground_positions.TryGetValue(tile, out tile.GroundPosition);
                 if(!tile.HaveGroundPosition || !tile.TileOpen)
                 {  // this sets the tile in the array to null
                    map.SetTile(x,z, null);
                 }
              }
           }
        }
        
        if(GUI.changed)
            EditorUtility.SetDirty(target);
    }
}

Apologies if the code is a bit odd looking - I have trouble pasting code into Unity Answers. Hopefully I didn’t edit away too much.

Some points to note:

  • I am using a public 1d array to benefit from Unity’s serialisation (2d arrays don’t get serialised)
  • It seems I may not want to benefit from Unity’s in built serialisation given answers to date…
  • The tiles are serializable classes
  • I’m setting the contents of the array in a custom editor which boils collision models within an array defined by two handles down to a 2D grid.

This looks like so in the editor for anyone who might be interested:

Editor Screenshot

I’ll respond to comments below, but it looks like the Unity default serialisation is getting in my way a little - how can I work around this?

Thanks
Ian H

If you’re using a struct, then you can’t have null items. If you’re using a class, then all entries are null until initialized. You’d probably make things easier on yourself if you used a 2D array.

Well, it would help to see your actual definition of your array / Tile-class. Your problem might be that you use a public 1-dimensional native array and your Tile class is serializable. In this case Unity will automatically create the members of the array. You can also resize the array in the inspector. The only way to prevent this is to disable the serialization of Unity (by making it private / protected or nonserialized).

The serialization in Unity is a bit annoying. It also doesn’t support inheritance. So if you have an array of a base type and populate it with derived classes they are “converted” into base-class-instances after deserialization (which happens quite often: enter playmode, enter editmode, loadlevel, …)

There is a little exception that works: MonoBehaviour! Because it’s a Component, Unity will just hold the reference to the real component. The disadvantage is that all your instances have to be attached to a GameObject (can be the same GO for all). An array that holds a base-class that is derived from MonoBehaviour works very well. Unity will also “remember” the true type of the instances since they are serialized seperately as Components.

It’s not a nice setup, but it’s the only way to use the built-in serialization.

each collection class is programmed differently, some allow nulls, some don’t. When you try setting some value, the class could do all kinds of things behind the scenes you don’t know about.

I’m assuming that you are using the javascript “Array” class to hold your tiles right? My guess is that it refuses to take null values, or it interprets them as not a true index of the array. Unfortunately the documentation on “Array” does not say. Quite typical of Unity, leaving us uncertain, forced into trial and error. Some ideas:

  1. You could easily switch to use the .net class List. I’m pretty sure that it will accept null values.
    List<T> Class (System.Collections.Generic) | Microsoft Learn
  2. You could add a special tile class to the array as a place marker. Something that is not literally null, but represents a null value.