[Interim solution] Instances share the same HashCode

Hi everyone!

In my current project, I’m keeping track of all existing objects with some public static Dictionaries. When the user creates a new object, I add the new GameObject to one Dictionary, the instance of the corresponding class in an other Dictionary and a reference between those two in a third Dictionary.

public class EnvironmentData
{

   [...] // some irrelevant references

    private static Dictionary<int, Item> items = new Dictionary<int, Item> ();
    private static Dictionary<Item, int> itemsReverse = new Dictionary<Item, int> ();
    private static Dictionary<int, GameObject> gameObjects = new Dictionary<int, GameObject> ();

    // ===================================
      
    public static void AddItem (int objID, Item item)
    {
        items.Add (objID, item);
        itemsReverse.Add (item, objID);
    }

    public static void AddGameObject (int objID, GameObject gameObject)
    {
        gameObjects.Add (objID, gameObject);
    }

    // ===================================

    public static Item GetItem (int id)
    {
        Item item;
        items.TryGetValue (id, out item);

        return item;
    }

    public static int GetID (Item item)
    {
        int id;
        itemsReverse.TryGetValue (item, out id);

        return id;
    }

    public static GameObject GetGameObject (int id)
    {
        GameObject go;
        gameObjects.TryGetValue (id, out go);

        return go;
    }

    // ===================================

    public static void ClearItems ()
    {
        items.Clear ();
        itemsReverse.Clear ();
    }

    public static void ClearGameObjects ()
    {
        foreach (GameObject go in gameObjects.Values) {
            GameObject.Destroy (go);
        }

        gameObjects.Clear ();
    }
}

Now, when I do this during normal runtime, there’s no problem at all. Everything is working perfectly fine. However, I also offer the possibility to save and load those setups. And for the most part even that works. But I have 1 Items whose instances are recognized as identical by Unity and even share the same HashCode, after loading them. Again: This only happens after clearing everything and loading those instances, not when manually creating them at runtime.
So, when trying to load such a setup Unity gives me this Error message:

ArgumentException: An element with the same key already exists in the dictionary.
System.Collections.Generic.Dictionary`2[Item,System.Int32].Add (.Item key, Int32 value) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Collections.Generic/Dictionary.cs:404)
EnvironmentData.AddItem (Int32 objID, .Item item) (at Assets/Scripts/StaticScripts/EnvironmentData.cs:20)

which corresponds with line 12 ( itemsReverse.Add (item, objID); ) in this snippet.

I know which Item is affected, but I don’t know why. It’s declared the exact same way as all the other Items, so I’m kinda lost here.

Cheers

Thats how Hashes work. There is no guarantee that every object gets its own unique hash. Thats why dictionaries put objects in “buckets” when more objects share the same internal hash (which is totally valid and hidden from you). Its called “collision”. Have a read about the details here.
A possible strategy is to set the capacity of your dictionary “way” above the required amount (this also prevents frequent reallocations when growing). This also increases the range of possible hashes and thus decreases the chance of collisions. But it’s no guarantee either.
You say 2 items share the same hashcode after loading them. Whats the situation before? Maybe you should also display the saving/loading code. Do you use your own “ID” or is this Unity’s ID/Hash?

Could you elaborate why you need this cross referencing? Shouldn’t there be just one access method (like ID) and this links to a struct which in turn links to Item, GameObject etc.?

Now that you mention it, I even remember hearing about that before. facepalm Totally forgot about that.

I guess that would be a viable strategy if the HashCodes (=the Dictionary keys I use) weren’t equal. Unfortunately they are.

I’m using the default built-in GetHashCode () function. When creating the objects, those HashCodes differ. I don’t store them in the save file, though, so I have to create new ones when I load the file. And after doing that, all instances of that one particular Item return the same HashCode - every single time.
It’s not the very same number every time, but for all those instances the HashCode is identical.

The construct I’m saving is a Map, which is basically a two-dimensional array of Tiles (plus some irrelevant additional information). Every Tile has 3 distinct spots, were Items can be placed/stored, and a List for additional Items that are not meant to be as displayed as the others. (Those Items’ presentations with GameObjects are rather mushed into each other but that shoudl be irrelevant here.)

So the (oversimplified) code looks like this:

public class Map {
    public Tile[][] tiles;
}

public class Tile {
    public Item itemA;
    public Item itemB;
    public Item itemC;
    public List<Item> otherItems;
}

public class Item {
    public string name;
    public string category;
    public Tile currentTile;
}

Now, the thing is that this Map can be manipulated from both ways - manually by the user and automatically by the program. The user accomplishes that by clicking on the Item’s GameObject he wants to change. That gives him its InstanceID (which is unique in Unity) and that is used to look up the corresponding Item in the Dictionary to write the changes to. The program, however, has to make this decision via its Map data structure, so it can only look at that, choose one of the Tiles and choose an Item from there. And then I use the Item’s HashCode to look up the InstanceID of the corresponding GameObject in the itemReverse Dictionary, which I then use to get the GameObject itself from the third Dictionary.
Long story short: I need a unique ID to link the Item to its GameObject and I have to do both ways instead of a single struct so I can access them from both directions. At least, I couldn’t come up with anything else yet.

Thank you for digging into this.

Yeah, this helps only when the Hashes of the internal hashmap are the same for different keys. This wasn’t clear to me from your initial explanation.

Thats why I asked for the saving/loading code. I guess the issue is there. But when you create new objects and assign the data you loaded they should be different.
Are the instance different anyway? So when you modify one instance do the others stay the same? Or do you just have one instance that is referenced from several Tiles and thus they have the same hash?

Yes, all the instances are different. Modifying one leaves the other ones unchanged.

Okay, I’m trying to cut down the code for saving/loading to its core without removing any important lines of code.

public static class SaveLoad
{

    public static bool Save ()
    {
        // this opens an explorer window where you can select a path (string) to write to
        string path = FileBrowser.SaveFilePanel ("Save File", "", "newMap", "map");
  
        // [...] some error detection and handling

        // actually saving
        MapSaveFile mapToSave = new MapSaveFile ();
        BinaryFormatter bf = new BinaryFormatter ();
        FileStream fs = File.Create (path);
        bf.Serialize (fs, mapToSave);
        fs.Close ();

        return true;
    }



    public static MapSaveFile LoadFromExplorer ()
    {
        // this opens an explorer window where you can select a path (string) to read from
        string path = FileBrowser.OpenFilePanel ("Open File", "", "map", false) [0];

        // [...] some error detection and handling

        // actually loading
        BinaryFormatter bf = new BinaryFormatter ();
        FileStream fs = File.Open (path, FileMode.Open);
        MapSaveFile mapToLoad = new MapSaveFile ();

        bool corrupt = false;
        try {
            mapToLoad = (MapSaveFile)bf.Deserialize (fs);
        } catch (SerializationException) {
            corrupt = true;
        }
        fs.Close ();

        if (corrupt) {
            throw new SerializationException ("File is corrupt and can't be loaded!");
        }

        fileName = path;

        return mapToLoad;
    }
}
public class MapSaveFile
{
    // Field for storing the Map (the data format I already posted earlier)
    public Map map;

    // [...] some irrelevant additional information

    public MapSaveFile ()
    {
        map = MapData.map;

        // [...] assigning irrelevant additional information
    }
}

This code is called from a MonoBehaviour that controls my UI:

public class MenuController : MonoBehaviour
{
    GameObject hexPrefab; // hexagonal tile prefab
    GameObject squarePrefab; // square tile prefab

    private EnvironmentGenerator envGen;


    // Use this for initialization
    void Start ()
    {
        hexPrefab = Resources.Load ("Map/Hex", typeof(GameObject)) as GameObject;
        squarePrefab = Resources.Load ("Map/Square", typeof(GameObject)) as GameObject;

        envGen = this.gameObject.GetComponent<EnvironmentGenerator> ();
    }



    public void Save ()
    {
        if (SaveLoad.Save ())
            // go back to game
    }



    public bool LoadFromExplorer ()
    {
        MapSaveFile mapToLoad;
        try {
            mapToLoad = SaveLoad.LoadFromExplorer ();
        } catch (SystemException exc) {
            // [...] error handling
            return false;
        }

        if (mapToLoad == null)
            return false;

        StartCoroutine (LoadCoroutine (mapToLoad));
        return true;
    }



    private IEnumerator LoadCoroutine (MapSaveFile file)
    {
        // [...] initiating a splashScreen while loading

        // [...] clearing all existing data and destrying the GameObjects

        // loading the map from the file
        MapData.map = file.map;

        // Parent GameObject for all Tiles
        GameObject mapGO = MapData.GetMapContainer ();

        // creating a GameObject for each Tile in the Map
        for (int x = 0; x < file.map.Tiles.GetLength (0); x++) {
            for (int z = 0; z < file.map.Tiles.GetLength (1); z++) {

                if (file.map.Tiles [x, z] == null)
                    continue;

                Tile tile = file.map.Tiles [x, z];
          
                // [...] determining position in Unity --> xPos, zPos

                GameObject tileGO =
                    (GameObject)Instantiate (tilePrefab, new Vector3 (xPos, 0, zPos), Quaternion.identity, mapGO.transform);

                // Add it to the intern list
                MapData.AddTile (tileGO.GetInstanceID (), tile);
                MapData.AddGameObject (tileGO.GetInstanceID (), tileGO);


                // referencing the Tile's GameObject where to create Items on
                EnvironmentData.SetInteractionSpot (tileGO);

                // If the loaded Tiles also contain Items, we have to load those too
                if (tile.itemA != null) {
                    envGen.CreateItem_LoadingMap (tile.MainItem);
                }
                if (tile.itemB != null) {
                    envGen.CreateItem_LoadingMap (tile.BonusItem);
                }
                if (tile.itemC != null) {
                    envGen.CreateItem_LoadingMap (tile.PathItem);
                }
                if (tile.otherItems != null && tile.otherItems.Count >= 1) {
                    foreach (Item i in tile.otherItems.ToArray ()) {
                        Debug.Log ("Loading: " + i.GetHashCode ());
                        envGen.CreateItem_LoadingMap (i);
                    }
                }
            }
        }
    }
}

And lastly, this is the Generator to create GameObjects for Items:

public class EnvironmentGenerator : MonoBehaviour
{
    // Parent GameObject for all Tiles, assigned in the inspector
    public GameObject environment;



    public void CreateItem_LoadingMap (Item newItem)
    {
        GameObject targetTile = EnvironmentData.GetInteractionSpot ();
        Tile tile = MapData.GetTile (targetTile.GetInstanceID ());

        // Creating the GameObject for this Item
        // [...] load correct AssetBundle and stuff
        GameObject newObject = Instantiate (assetBundle.LoadAsset <GameObject> (newItem.item));
        newObject.transform.position = targetTile.transform.position;


        // Add it to the intern list
//        Debug.Log ("Object: " + newObject.name + " (ID: " + newObject.GetInstanceID () + ")\nItem: " + newItem.name + " (Hash: " + newItem.GetHashCode () + ")");
        EnvironmentData.AddItem (newObject.GetInstanceID (), newItem); // THIS LINE PRODUCES THE ERROR (see inital post for code)
        EnvironmentData.AddGameObject (newObject.GetInstanceID (), newObject);
    }
}

Ok I have tried to understand your code. It’s a bit hard maybe due to the “simplifying” process.

        for (int x = 0; x < file.map.Tiles.GetLength (0); x++)
   {
            for (int z = 0; z < file.map.Tiles.GetLength (1); z++)
       {
                GameObject tileGO = (GameObject)Instantiate (tilePrefab, new Vector3 (xPos, 0, zPos), Quaternion.identity, mapGO.transform);
                // Add it to the intern list
                MapData.AddTile (tileGO.GetInstanceID (), tile);
                MapData.AddGameObject (tileGO.GetInstanceID (), tileGO);
       }
   }

vs

    public static void AddItem (int objID, Item item)
    {
        items.Add (objID, item);
        itemsReverse.Add (item, objID);
    }

So “item” in the second snippet from your first post is a tile? I confused it with the items within a tile.

From the documentation:

So this is weird since every prefab instance you create SHOULD have its own InstanceID. Maybe you should file a bug report with the project attached for reproduction if we can’t find the issue in your code.

You use the instance id of the gameobject as key for your dictionaries? Thats where 2 (or more) different gameobjects return the same ID? I can’t find an error in your code but I also can’t believe that Unity is faulty in this regard. So first let us circle the problem.
When they are gameobjects you can select them when player is paused and show their id. In the inspector upper right corner click on the list symbol beside the lock symbol then select Debug. This will show the instance ID. Then lock it. Open another inspector, select another gameobject which has the same ID as you suggest and make a screenshot and post it here please.

No, you didn’t confuse them, you just picked the wrong code snippet. :wink: The second snippet you quoted is right, but the first one (the double for-loop) is not the correct extract. We just need this part instead:

        EnvironmentData.AddItem (newObject.GetInstanceID (), newItem); // THIS LINE PRODUCES THE ERROR (see inital post for code)
        EnvironmentData.AddGameObject (newObject.GetInstanceID (), newObject);

That’s the very bottom of the very last script I posted.

I’m using the GameObject’s InstanceID for one Dictionary and the Item’s HashCode for the other Dictionary. The InstanceIDs are unique and work fine, the HashCodes are not (as I mentioned, but only after loading) and therefore, when adding a new one to the second Dictionary, an ArgumentException is thrown.

I solved my problem by changing my internal data structures.
Instead of having a second Dictionary, now every single Items stores the reference to its corresponding GameObject by itself with something like this:

public class Item {
    public string name;
    public string category;
    public Tile currentTile;

    // new code below
    public int gameObjectID; // use this for referencing the GameObject
}

It feels kinda dirty to me doing it like this instead of having a single global lookup table, but it works and that’s more important right now.

Glad you have found a solution.

Just to understand you correctly. The multiple hashcodes come directly from the deserialization? If so, there would not be much to do for you anyway I guess when this happens on Unity’s side.

Yes, exactly. I had hoped I could force a new HashCode for every Item but obviously not.
Thank you for your input.

Don’t mention it.