- Home /
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:
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
I'll see if I can post an edited version as it's several files... $$anonymous$$orrow tho as it's very late here!
Ins$$anonymous$$d of setting your inaccessible tiles to 'null', you should put a boolean in them which deter$$anonymous$$es whether they are walkable or not. This neatly sidesteps the problem you have been having, and still allows you to use serialization. As an added bonus, you could put in a cast definition which returns the 'walkable' boolean if you refer to the tile as a bool (not quite sure how to do this, assu$$anonymous$$g it's possible).
@syclamoth - that's essentially what I have now but there's no point having a structure exist that I won't use. I'm deploying for iOS so I want to keep memory usage low, having a smaller data structure to serialise and deserialize will also speed up loading times. At present the structures are too heavy weight so I need to boil them down to using enum flags where possible which should greatly reduce the overhead and perhaps mean that there is less necessity to have the array scarcely populated.
Null is still the cheapest and most efficient option however.
Boiling the tile data down it amounts to 36 bytes per tile. Each valid tile piggy backs a structure of 16 bytes per concurrent path we can be calculating. I'm doing this because it makes it easy to find the data associated with a tile for a given path and avoids allocation or pooling of this path information. The number of concurrent paths is configurable so where little or no pathing will occur on a map, 1 (or even 0) path data structures are allocated. a 256x256 map will eat nearly 9 megs if I allow 4 concurrent paths to be calculated. 256x256 I would consider a HUGE map. Naturally on a desktop 9 megs is nothing...
I imagine about 50% of tiles are in-use typically nulls would halve the memory footprint.
It was actually more like 44, but I've shrunk it back to 36 and 12 bytes per concurrent path. I can shrink it to 20 bytes + 12 per concurrent path I think but that's about as right as it'll go but it's still probably worth doing. Those numbers would be 4.5$$anonymous$$B @256x256 fully populated not counting the array itself, and 2.25$$anonymous$$B for part population and around 600$$anonymous$$ for 128x128 which is a big map anyway.
Answer by Bunny83 · Sep 27, 2011 at 10:52 PM
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.
I see, so it seems the 1d solution is giving me serialisation but that the serialisation isn't great. What can I do to override what gets serialized? I tried to add the ISerializable interface to the BV$$anonymous$$ap2D class but it didn't seem to call any of my serialization methods :o/
The map itself is a $$anonymous$$onoBehaviour subclass - I could do the same for tiles, but that's a lot of overhead on a give GO. I had thought of making it generic but adding it in the inspector wouldn't work unless I derived a concrete type (which I could have done) but feeling this might not work I just exposed a Data property which any object can be shoved into. A Generic type would be cleaner perhaps but maybe less runtime performant.
Answer by Eric5h5 · Sep 27, 2011 at 10:39 PM
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.
2D arrays suffer resizing issues, they also lack a ton of useful functionality that the collections have built into them. I would not use them unless you know the initial size before hand and that it will never change. Use a list or map collection or some other collection implementation. Personally I think Dictionary is best, it will give you the best runtime performance because of its indexing.
@msknapp: The OP is using a 1D array so it has the same advantages / disadvantages. Sure i love the container classes too but a level-map doesn't change it's size, so a native array is the best choice. A 2D array has the "advantage" over a 1D array that it isn't serialized by Unity, but i'm not sure if that is intended. It would solve the problem by disabling the automatic initialization of the items by Unity.
It's highly unlikely the game world is going to change size.
@Eric5h5 Sure it was a 2d array of classes but Unity was happily throwing that away, hence the 1d array faked up to use as a 2d array. The game world will not change size and if it needed to I could fake that up by having additional maps that are linked or editing edge information on tiles to expand the accessible area - which is how doors currently work.
In the mono debugger I can see my array contains nulls in the right places but as mentioned by someone else serialization seems to be happening and moments later - i.e. when running the game - those nulls are replaced with partially constructed BV$$anonymous$$apTiles.
@msknapp Given the world does not change size the most efficient thing is to have a 2d array (serialization allowing). There's no useful functionality I am missing and in this instance an array feels like the most appropriate collection. I just don't want to have classes hanging about for tiles that don't need the detail - a null tile means it can be discarded from my A* pathing calculations and considered blocking for player movement.
@Bovine: "throwing it away" doesn't really make any sense. If you can use a 1D array, you can use a 2D array.
Answer by msknapp · Sep 27, 2011 at 10:30 PM
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:
You could easily switch to use the .net class List. I'm pretty sure that it will accept null values.
http://msdn.microsoft.com/en-us/library/6sh2ey19.aspxYou could add a special tile class to the array as a place marker. Something that is not literally null, but represents a null value.
Clearly he's not using the JS Array class when the question specifically mentions C#.
Yep it is a C# array but thanks for the effort. I've edited the title to make it clear.
I'm actually having to infer a tile is null because the tiles that are null have particular properties, rather like your suggestion 2.
As I said above, you may as well make it explicit- just have a bool value which you use like you would use null status before.
@syclamoth Given that I already have other data that infers the same thing, I am not going to bloat the structure with an additional bool.
It seems that you are trading between cpu time and memory usage- both things that are at a premium on iOS devices! I really can't claim to know much about iPhone optimisation, I'm afraid.