Search Unity

Values in script reset to null after loading

Discussion in 'Scripting' started by Karrzun, Nov 1, 2017.

  1. Karrzun

    Karrzun

    Joined:
    Oct 26, 2017
    Posts:
    129
    Hi everyone,

    I've got a little problem with my current project when it comes to saving and loading.
    In general, it's possible to generate random hex-tile-maps and place some (what I call) InteractableObjects on it. If you really like the map, you can save it including all placed InteractableObjects so that you can reuse it some other time, if you want to. Generating and interacting works very well, saving isn't a problem either and even loading seems to work fine at first glance. The problem is, after loading a map and maybe working around with it a little (you know, like adding some more InteractableObjects, etc), when you want to save the map again, it throws an error that basically tells you that the values in the scripts on the InteractableObjects, that you had loaded before, are now reset to null - even though they are set properly after loading them. And I just can't see how that happens.

    For better visualization here's a screenshot of the programm at runtime. At the top, you can set your preferences for a randomly generated map. At the bottom, there's a selection to create new InteractableObjects. And on the right border, the two points of interest are the buttons for saving and loading.
    HexMaps.PNG


    This is basically the code used in saving and loading:

    Code (CSharp):
    1. // static script for calling the save or load method - works fine
    2. public static class SaveLoad {
    3.  
    4.     public static void Save(){
    5.         string path = EditorUtility.SaveFilePanel("Save Hexmap to...", "", "newHexmap", "hexmap");
    6.  
    7.        [...] // exception handling
    8.  
    9.         Scenario scenario = new Scenario ();
    10.         BinaryFormatter bf = new BinaryFormatter ();
    11.         FileStream fs = File.Create (path);
    12.         bf.Serialize(fs, scenario);
    13.         fs.Close();
    14.     }
    15.  
    16.     public static void Load(){
    17.         string path = EditorUtility.OpenFilePanel ("Select a Hexmap", "", "hexmap");
    18.  
    19.        [...] // exception handling
    20.  
    21.         BinaryFormatter bf = new BinaryFormatter ();
    22.         FileStream fs = File.Open (path, FileMode.Open);
    23.         Scenario scenario = (Scenario) bf.Deserialize (fs);
    24.         GameObject.FindGameObjectWithTag ("ManagerHolder").GetComponent<GameManager> ().LoadScenario(scenario);
    25.         fs.Close ();
    26.     }
    27. }
    28.  
    29.  
    30. [System.Serializable]
    31. public class Scenario {
    32.  
    33.     public List<HexSaveInfo> hexes;
    34.     public List<InteractableSaveInfo> interactables;
    35.  
    36.     public Scenario(){
    37.         hexes = new List<HexSaveInfo> ();
    38.         interactables  = new List<InteractableSaveInfo> ();
    39.  
    40.         foreach (Hex h in MapData.GetHexes ()){
    41.             hexes.Add (new HexSaveInfo (h));
    42.         }
    43.         foreach (InteractableObject i in MapData.GetInteractables ()) {
    44.             interactables.Add (new InteractableSaveInfo (i));
    45.         }
    46.     }
    47. }
    Code (CSharp):
    1.  // script used to instantiate loaded elements - here the error becomes visible when debugging
    2. public class GameManager : MonoBehaviour {
    3.  
    4. public HexData terrainMaterials;
    5.  
    6. [...]
    7.  
    8. public void LoadScenario(Scenario sc){
    9.        // deletes all currently existent hex tiles and InteractableObjects
    10.         MapData.DestroyHexes ();
    11.         MapData.DestroyInteractables ();
    12.  
    13.        // instantiates the saved hex tiles - this works fine
    14.         foreach (HexSaveInfo h in sc.hexes) {
    15.             GameObject obj = (GameObject) Instantiate (hexPrefab);
    16.             obj.GetComponentInChildren<Hex> ().SetTo (h, terrainMaterials.terrain [(int)h.terrainType].height [h.height]);
    17.             MapData.AddHex (obj.GetComponentInChildren<Hex> ());
    18.         }
    19.  
    20.        // instantiates the saved InteractableObjects - this produces the error
    21.         foreach (InteractableSaveInfo i in sc.interactables) {
    22.             GameObject obj = (GameObject) Instantiate (Resources.Load ("Interactables/" + i.prefabName + "Prefab", typeof(GameObject)) as GameObject);
    23.             // in SetTo() the values are correctly set and at the end of the method everything is ok, but...
    24.             obj.GetComponentInChildren<InteractableObject> ().SetTo (i);
    25.        
    26.             // here, when adding the object to the intern list of all existent objects, the values are reset to null. Why?
    27.             MapData.AddInteractable (obj.GetComponentInChildren<InteractableObject> ());
    28.         }
    29.     }
    30.  
    31. }
    Code (CSharp):
    1. // class construct to save the data of a MonoBehaviour script called "InteractableObject"
    2. [System.Serializable]
    3. public class InteractableSaveInfo {
    4.  
    5.     public string prefabName;
    6.     public int hexX, hexY;
    7.     public float positionX, positionY, positionZ;
    8.     public float standardColorR, standardColorG, standardColorB;
    9.     public int colorNumber;
    10.     public List<float> colorList;
    11.  
    12.     public InteractableSaveInfo(InteractableObject i){
    13.         this.prefabName = i.prefabName;
    14.         this.hexX = i.currentTile.GetComponentInChildren<Hex> ().x;
    15.         this.hexY = i.currentTile.GetComponentInChildren<Hex> ().y;
    16.         this.positionX = i.currentTile.transform.position.x;
    17.         this.positionY = i.currentTile.transform.position.y;
    18.         this.positionZ = i.currentTile.transform.position.z;
    19.         this.standardColorR = i.standardColor.r;
    20.         this.standardColorG = i.standardColor.g;
    21.         this.standardColorB = i.standardColor.b;
    22.         this.colorNumber = i.colorNumber;
    23.         this.colorList = (List<float>) ConvertColorList(i.colorList);
    24.     }
    25.  
    26.     private ICollection<float> ConvertColorList(ICollection<Color> colorList){
    27.         List<float> convertedList = new List<float> ();
    28.  
    29.         foreach (Color c in colorList) {
    30.             convertedList.Add (c.r);
    31.             convertedList.Add (c.g);
    32.             convertedList.Add (c.b);
    33.         }
    34.  
    35.         return convertedList;
    36.     }
    37. }

    Code (CSharp):
    1.  // script used to interact with objects at runtime
    2. public abstract class InteractableObject : MonoBehaviour {
    3.  
    4.     public string prefabName;
    5.     public GameObject currentTile;
    6.     public Color standardColor;
    7.     public int colorNumber;
    8.     public List<Color> colorList;
    9.  
    10. [...]
    11.  
    12. public void SetTo(InteractableSaveInfo i){
    13.         this.prefabName = i.prefabName;
    14.         this.currentTile = GameObject.Find ("Hex_" + i.hexX + "_" + i.hexY);
    15.         this.transform.position = new Vector3 (i.positionX, i.positionY, i.positionZ) + currentTile.GetComponent<Hex> ().GetHeightOffset ();
    16.         this.standardColor = new Color (i.standardColorR, i.standardColorG, i.standardColorB);
    17.         this.colorNumber = i.colorNumber;
    18.         this.colorList = (List<Color>) ConvertColorList(i.colorList);
    19.     }
    20.  
    21. private ICollection<Color> ConvertColorList(List<float> floatList){
    22.         List<Color> convertedList = new List<Color> ();
    23.  
    24.         while (floatList.Count > 2) {
    25.             float r = floatList [0];
    26.             floatList.RemoveAt (0);
    27.             float g = floatList [0];
    28.             floatList.RemoveAt (0);
    29.             float b = floatList [0];
    30.             floatList.RemoveAt (0);
    31.  
    32.             Color c = new Color (r, g, b, 255f);
    33.             convertedList.Add (c);
    34.         }
    35.  
    36.         return convertedList;
    37.     }
    38. }
    I hope that's enough code to make my problem comprehensible.

    Thank you in advance for your help!



    P.S: For the sake of completeness, here's the thrown error:
    Code (CSharp):
    1. MissingReferenceException: The object of type 'GameObject' has been destroyed but you are still trying to access it.
    2. Your script should either check if it is null or you should not destroy the object.
    3. UnityEngine.GameObject.GetComponentInChildren[Hex] (Boolean includeInactive) (at C:/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineGameObjectBindings.gen.cs:78)
    4. UnityEngine.GameObject.GetComponentInChildren[Hex] () (at C:/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineGameObjectBindings.gen.cs:73)
    5. InteractableSaveInfo..ctor (.InteractableObject i) (at Assets/Scripts/InteractableSaveInfo.cs:23)
    6. Scenario..ctor () (at Assets/Scripts/SaveLoad.cs:69)
    7. SaveLoad.Save () (at Assets/Scripts/SaveLoad.cs:22)
    8. GameManager.Save () (at Assets/Scripts/Managers/GameManager.cs:38)
    9. UnityEngine.Events.InvokableCall.Invoke (System.Object[] args) (at C:/buildslave/unity/build/Runtime/Export/UnityEvent.cs:153)
    10. UnityEngine.Events.InvokableCallList.Invoke (System.Object[] parameters) (at C:/buildslave/unity/build/Runtime/Export/UnityEvent.cs:634)
    11. UnityEngine.Events.UnityEventBase.Invoke (System.Object[] parameters) (at C:/buildslave/unity/build/Runtime/Export/UnityEvent.cs:769)
    12. UnityEngine.Events.UnityEvent.Invoke () (at C:/buildslave/unity/build/Runtime/Export/UnityEvent_0.cs:53)
    13. UnityEngine.UI.Button.Press () (at C:/buildslave/unity/build/Extensions/guisystem/UnityEngine.UI/UI/Core/Button.cs:35)
    14. UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at C:/buildslave/unity/build/Extensions/guisystem/UnityEngine.UI/UI/Core/Button.cs:44)
    15. UnityEngine.EventSystems.ExecuteEvents.Execute (IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at C:/buildslave/unity/build/Extensions/guisystem/UnityEngine.UI/EventSystem/ExecuteEvents.cs:52)
    16. UnityEngine.EventSystems.ExecuteEvents.Execute[IPointerClickHandler] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.EventFunction`1 functor) (at C:/buildslave/unity/build/Extensions/guisystem/UnityEngine.UI/EventSystem/ExecuteEvents.cs:269)
    17. UnityEngine.EventSystems.EventSystem:Update()
     
    Last edited: Nov 1, 2017
  2. McDev02

    McDev02

    Joined:
    Nov 22, 2010
    Posts:
    664
    Check the line "this.currentTile = GameObject.Find ("Hex_" + i.hexX + "_" + i.hexY);" in InteractableObject.SetTo(InteractableSaveInfo i) and see after that assignment if this.currentTile is null. If it is you can check in the editor if that object is there or not or if something whent wrong. Otherwise see if that line is called or not after the map was generated.

    Edit OK I see that you spot that already and say it is reset somehow. I really can't see something from here. Check all assignments to currentTile and see if those are called for some reason. Maybe you have soemthign in the Start method?

    Just a small detail and I am not sure if it makes a change but try to store the interactible. Also I added error handlers. So according to you the second Log should be called?
    Code (CSharp):
    1. foreach (InteractableSaveInfo i in sc.interactables)
    2. {
    3.     GameObject obj = (GameObject)Instantiate(Resources.Load("Interactables/" + i.prefabName + "Prefab", typeof(GameObject)) as GameObject);
    4.     // in SetTo() the values are correctly set and at the end of the method everything is ok, but...
    5.     var interactible = obj.GetComponentInChildren<InteractableObject>();
    6.     interactible.SetTo(i);
    7.     if (interactible.currentTile == null) Debug.LogError("tile is null in 1st call");
    8.     // here, when adding the object to the intern list of all existent objects, the values are reset to null. Why?
    9.     MapData.AddInteractable(interactible);
    10.     if (interactible.currentTile == null) Debug.LogError("tile is null in 2nd call");
    11. }
     
    Last edited: Nov 1, 2017
  3. Karrzun

    Karrzun

    Joined:
    Oct 26, 2017
    Posts:
    129
    First of all, thank you for your reply.

    In all of my code, there are only two assignments to currentTile. One, of course, is here when loading from an external file. The other one is when right-clicking an empty hex tile to create a new InteractableObject (for reference, that's what the bottom panel in the screenshot is good for). I can see no situation in which those two lines of code could ever produce the current situation.
    Also, initiating the InteractableObject and assigning its values are two seperate steps in my code, so the Start method should already be finished when I start assinging the values, shouldn't it?

    I tried storing the script in a variable but unfortunately it didn't make a difference. When it comes to the exception handlers, actually none of them is called. Yet when I try to reference that field later (to be exact, when saving again), it tells me that it's null.
    Maybe I should have been more precide about this right from the beginning, but the error occurs in class "InteractableSaveInfo" in line 14. I already ensured that it's the "currentTile" field, which is null, and none of its called components. However, the last and only time this field is assigned, is at former mentioned "SetTo()" method. And as I mentioned before (and as your Exception handlers show), it's not null there.

    So what is happening in there? o_O
     
  4. McDev02

    McDev02

    Joined:
    Nov 22, 2010
    Posts:
    664
    This is good then, because this is expected. Do you maybe delete the tile before you save it? As the error says the GameObject does no longer exist? That would be the only thign that makes sense, that the GameObject istself simply gets destroyed somewhere in your code. Do you have code which makes you able to delete a tile? Or you replace a tile bydeleting the old one and instantiating another one? Then you would have to handle the references.
     
  5. Karrzun

    Karrzun

    Joined:
    Oct 26, 2017
    Posts:
    129
    There is only one method that destroys tiles, which is "DestroyHexes()". This method, however, destroys all tiles at once and is only called before generating a new random map or before reading the input stream from an external file.

    Code (CSharp):
    1. public static class MapData {
    2.  
    3.     private static List<Hex> allHexes = new List<Hex>();
    4.     private static List<InteractableObject> allInteractables = new List<InteractableObject>();
    5.  
    6.     private static GameObject selectedInteractableObject;
    7.     private static GameObject creationSpot;
    8.  
    9.     // Adds a new hex to the intern list of all hexes
    10.     public static void AddHex(Hex hex){
    11.         allHexes.Add (hex);
    12.     }
    13.  
    14.     // Returns all currently existent hexes
    15.     public static ICollection GetHexes(){
    16.         return allHexes;
    17.     }
    18.  
    19.     // Clears the list of all hexes
    20.     public static void DestroyHexes(){
    21.         while (allHexes.Count > 0) {
    22.             Hex hex = allHexes [0];
    23.             allHexes.RemoveAt (0);
    24.             hex.Destroy ();
    25.         }
    26.     }
    27.  
    28.     // Adds a new InteractableObject to the intern list of all InteractableObjects
    29.     public static void AddInteractable(InteractableObject interactable){
    30.         allInteractables.Add (interactable);
    31.     }
    32.  
    33.     // Removes an InteractableObject form the intern list of all InteractableObjects
    34.     public static void RemoveInteractable(InteractableObject interactable){
    35.         if(allInteractables.Contains(interactable)){
    36.             allInteractables.Remove (interactable);
    37.         }
    38.     }
    39.  
    40.     public static ICollection GetInteractables(){
    41.         return allInteractables;
    42.     }
    43.  
    44.     // Clears the list of all InteractableObjects
    45.     public static void DestroyInteractables(){
    46.         while (allInteractables.Count > 0) {
    47.             InteractableObject obj = allInteractables [0];
    48.             allInteractables.RemoveAt (0);
    49.             obj.Destroy ();
    50.         }
    51.     }
    52.  
    53.     [...]
    54.  
    55. }
    I'd like to claim, that this is not the case. A quick look into my code didn't lead to anything but the project's grown quit a bit by now so maybe I overlooked something in there. I doubt that, though, to be honest, as I separate my code pretty strictly and there should be no such functionality in my code.
    I'll keep an eye on that as long as I don't have any other solutions, but I'd be glad to hear other suggestions.
     
  6. Karrzun

    Karrzun

    Joined:
    Oct 26, 2017
    Posts:
    129
    Little update regarding this problem.

    I tried a few more things by now and decided to add a small method for debugging. This method doesn't do much except going through the list of all currently existent InteractableObjects and write them into the console by their name and their currentTile value, so that we can see which value is saved to which object.
    Code (CSharp):
    1. public static void PrintAllOccupiedTiles(){
    2.         Debug.Log ("ALL OCCUPIED TILES:");
    3.         foreach (InteractableObject obj in GetInteractables ()) {
    4.             Debug.Log (obj.name + " " + obj.currentTile);
    5.         }
    6.     }
    I added the method to the button that triggers the Load method. And the thing is, when being called immediatly after the Load method like this, it even displays the correct values.

    Debug01.PNG

    However, when called one frame later via a seperate button, that I created for debugging purposes, the currentTiles values are reset to null once again.

    Debug02.PNG

    All of that makes me even more clueless than at the beginning.
    The Update method isn't used in InteractableObject, nor does any other script access InteractableObject in its Update method (besides my mouse manager, that's looking if I'm selecting something). So how is that possible?
    It shouldn't be about assigning those non-static values by a non-static method, which is called by a static method, or is it?
     
    Last edited: Nov 1, 2017
  7. Karrzun

    Karrzun

    Joined:
    Oct 26, 2017
    Posts:
    129
    And I'm in for the next little update.

    I did some further testing and found out that not all InteractableObjects' currentTile value is reset to null after loading (unlike assumed by me until now). I generated a bigger map with lots and lots of trees on it, saved and loaded that and it seems that "only" about 60-70% are missing their currentTile value afterwards. The others still have their correctly referenced tile. Unfortunately, I can't seem to find any sort of pattern, which objects got to keep their value and which didn't, but after reloading the same map several times I can record that it's always the same objects, that retain their value - so at the very least, it's deterministic and thus hopefully a problem that can be fixed and not just something that happens by chance.
     
  8. McDev02

    McDev02

    Joined:
    Nov 22, 2010
    Posts:
    664
    That is unlikely to be the case. Often this is caused by simple dump mistakes in code, I can tell from experience. But without the project at hand this is impossible to track it down, you are on your own I guess. Hours of debugging is something that is not uncommon. The good side is that you then understand your own code better.
     
  9. Karrzun

    Karrzun

    Joined:
    Oct 26, 2017
    Posts:
    129
    Yeah, I've never experienced an error by chance either, but by now I've learned to expect everything so I didn't want to exclude this possibility right away.

    I know I'm asking for a lot here, but if you are interested in the project, would you be willing to take a look at it, if I send it to you as a whole?
    Of course you can keep it, use it at will, or even develop it autonomosly.
     
  10. Karrzun

    Karrzun

    Joined:
    Oct 26, 2017
    Posts:
    129
    I'd like to give an other update regarding this.
    I found that the refernces to the currentTiles are only reset, if an other map has existed before (regardless whether it was loaded or generated). If the first thing you do, when starting the program, is to load a map, it works perfectly fine.

    It's not solved yet, but maybe someone with similiar problems in the future can take something from it.
     
  11. Karrzun

    Karrzun

    Joined:
    Oct 26, 2017
    Posts:
    129
    Late reply for this one but I finally was able to solve it some weeks ago. So in case anyone else has the same problem I'll post my solution here.

    Until now, when loading a map, I did it like this: I told a (static) script to find the save file, deserialize it and then that static script would tell a generator to instantiate all the necessary objects.
    Now, I tell the generator to tell the static script to load and deserialize a file and the static script returns it as a custom data type to the generator.

    No errors since then.