Search Unity

Is there a way to check if objects has changed ?

Discussion in 'Scripting' started by SharonL75, Oct 2, 2020.

  1. SharonL75

    SharonL75

    Joined:
    Aug 13, 2020
    Posts:
    91
    At the top of a script :

    Code (csharp):
    1.  
    2. public Button saveButton;
    3.  
    In the Update :

    Code (csharp):
    1.  
    2. private void Update()
    3.     {
    4.         foreach(Transform objectToSave in objectsToSave)
    5.         {
    6.             if(objectToSave.hasChanged)
    7.             {
    8.                 saveButton.enabled = true;
    9.             }
    10.         }
    11.     }
    12.  
    Two problems :

    If the List objectsToSave have a lot of objects ? It will get slowly loop over it all the time in the Update.

    And how can I make that if none of the objects has changed then keep or disable the saveButton ?
    I want that if one object or more has changed enable true the button but if none of the objects has changed enable false the button.
     
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,914
    Code (CSharp):
    1.         bool anythingChanged = false;
    2.         foreach(Transform objectToSave in objectsToSave)
    3.         {
    4.             if(objectToSave.hasChanged)
    5.             {
    6.                 anythingChanged = true;
    7.                 // Exit the loop early
    8.                 break;
    9.             }
    10.         }
    11.  
    12.         saveButton.enabled = anythingChanged;
    Isn't that the goal here?
     
    SharonL75 likes this.
  3. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,605
    You are working on a save system. Thus you know what kind of data you do save. Use properties to access this data, and when writing to it notify your SaveManager or whatever that something changed, which will then take care of managing whether or not the button is active.
     
  4. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    It is highly unlikely you actually need the Save button to activate on the very next frame a change can be saved. Wouldn't it be just fine if the save button appeared up to a half a second later? If so the below simple change would reduce the time you spend in this foreach loop by about 96% at 60 FPS. Also, if saveButton.enabled is already true, there is no point in running through the foreach loop at all, so skip it. I'm going to guess the typical player behavior is to leave the Save button active without clicking it for a good portion of the game (every once in a while they click it, but it probably gets reactivated quickly as they keep playing and they just leave it active for a few minutes). If so that would then mean most of the time you don't even run through the loop.

    So I'd probably just do this and move on for now instead of redesigning for a performance problem you don't even know yet exists. Come back to this if you actually encounter a performance problem. YMMV my 2 cents


    Code (csharp):
    1. float nextCheck = 0f;
    2.  
    3. private void Update()
    4.     {
    5.         if ((saveButton.enabled == false) && (Time.time >= nextCheck))
    6.         {
    7.             foreach(Transform objectToSave in objectsToSave)
    8.             {
    9.                 if(objectToSave.hasChanged)
    10.                 {
    11.                     saveButton.enabled = true;
    12.                 }
    13.             }
    14.             nextCheck = Time.time + 0.5f;
    15.         }
    16.     }
    17.  
     
    Last edited: Oct 2, 2020
    PraetorBlue likes this.
  5. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    Transform.hasChanged is really weird. First, this flag is always true by default and you have to set it to false yourself. Meaning, that you listen for hasChanged to become true, then set it back to false yourself. This means that only a single script can use this flag, since multiple systems could try to set hasChanged to different values depending on the timing. Also the flag is set whenever somebody accesses the transform (sets position, rotation, scale or parent), but without comparing the values for an actual change.

    However, let's go with this as a start. At the beginning, disable the save button, because nothing needs to be saved. Then get a list of all transforms in the scene or everything that can move or should be saved. Then we check for hasChanged. If any is true, we know that the button should be enabled, but we also need to reset all other hasChanged flags to prevent the button from being enabled right after saving for each object that was set dirty previously.

    Here is some code with micro optimizations and it's really fast with 1000 GameObjects in the scene. It's also much more work than one might think at first:

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using System.Linq;
    3. using UnityEngine;
    4. using UnityEngine.UI;
    5.  
    6. public class SavePrompt : MonoBehaviour
    7. {
    8.     public Button saveButton;
    9.  
    10.     private Transform[] transforms;
    11.     private int transformCount;
    12.     private bool dirty;
    13.  
    14.     private void Start()
    15.     {
    16.         // Assuming that the transforms are all present
    17.         // at the start of the scene and non are instantiated later.
    18.         List<Transform> list = FindObjectsOfType<Transform>().ToList();
    19.  
    20.         // Remove the button from the list of transforms to be checked
    21.         // because it changes when being disabled, but we don't want
    22.         // to trigger another save because of this.
    23.         list.Remove(saveButton.transform);
    24.  
    25.         transforms = list.ToArray();
    26.  
    27.         // Micro optimization: cache length instead of accessing property.
    28.         transformCount = transforms.Length;
    29.  
    30.         for (int i = 0; i < transformCount; i++)
    31.             transforms[i].hasChanged = false;
    32.  
    33.         // If dirty is true, the button needs to be shown.
    34.         dirty = false;
    35.  
    36.         // At the start, hide the button.
    37.         HideButton();
    38.  
    39.         // Whenever the player presses the button,
    40.         // assume that everything was saved and the button
    41.         // is no longer needed. Better check with the SaveManager
    42.         // if saving has actually succeeded.
    43.         saveButton.onClick.AddListener(HideButton);
    44.     }
    45.  
    46.     private void OnDestroy()
    47.     {
    48.         if (saveButton != null)
    49.             saveButton.onClick.RemoveListener(HideButton);
    50.     }
    51.  
    52.     private void HideButton()
    53.     {
    54.         dirty = false;
    55.         saveButton.gameObject.SetActive(false);
    56.     }
    57.  
    58.     private void ShowButton()
    59.     {
    60.         dirty = true;
    61.         saveButton.gameObject.SetActive(true);
    62.     }
    63.  
    64.     private void Update()
    65.     {
    66.         // Fake something being moved by script for testing.
    67.         if (Time.frameCount % 300 == 0)
    68.             transforms[Random.Range(0, transformCount)].position += new Vector3(0.01f, 0f, 0f);
    69.     }
    70.  
    71.     private void LateUpdate()
    72.     {
    73.         // Don't check for changes if we already know
    74.         // and the button is enabled.
    75.         if (dirty == true)
    76.             return;
    77.  
    78.         bool anyTransformChanged = false;
    79.  
    80.         for (int i = 0; i < transformCount; i++)
    81.         {
    82.             if (transforms[i].hasChanged)
    83.             {
    84.                 anyTransformChanged = true;
    85.  
    86.                 // You have to manually reset this flag or else
    87.                 // it will always stay true.
    88.                 transforms[i].hasChanged = false;
    89.  
    90.                 // We could try to break out of here, but then
    91.                 // the save button would appear right after saving again
    92.                 // because some other transform still returns hasChanged as true.
    93.                 // So instead, set all of them to false.
    94.             }
    95.         }
    96.  
    97.         if (anyTransformChanged)
    98.         {
    99.             // Checking the c# bool variables is faster than
    100.             // checking saveButton.enabled because the latter
    101.             // is a property and also marshalled to Unity's
    102.             // C++ representation of the button.
    103.             if (dirty == false)
    104.             {
    105.                 ShowButton();
    106.             }
    107.         }
    108.     }
    109. }
    110.  
    So yea, this is one way this could work and it seems to be fast enough for common use cases. However, I think it's a little awkward. Especially considering, that changing a transform is not the only reason for wanting to save, so you would need other systems to check for that anyway. This example also assumes that the SaveSystem will automatically find and save all required objects and variables. These systems usually scan the entire scene and serialize everything.

    If the project is small enough it could make sense to try and ensure that every script that changes a saveable value, notifies the SaveManager somehow. This would avoid checking objects that haven't changed and may also be a straightforward easy implementation.

    However, often, each script must tell the SaveManager which data so save anyway, so there is no need for checking for change. Instead, simply set the SaveSystem dirty whenever some other script writes something relevant to the save data buffer. This implementation can work out nicely if there are not too many components involved. I'd also try to separate things a little more like this:

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. using UnityEngine.UI;
    4.  
    5. public class MoverScript : MonoBehaviour
    6. {
    7.     public string saveKey = "Object123";
    8.  
    9.     private void OnEnable()
    10.     {
    11.         // Some way of loading the saved data after scene load.
    12.         transform.position = SaveManager.Instance.GetVector3(saveKey);
    13.     }
    14.  
    15.     private void Update()
    16.     {
    17.         // This script moves some object.
    18.         transform.Translate(Time.deltaTime, 0f, 0f);
    19.  
    20.         // But it also tells the save manager that something needs to be saved.
    21.         // While we're at it, we can already send the current value.
    22.         SaveManager.Instance.SetVector3(saveKey, transform.position);
    23.     }
    24. }
    25.  
    26. public class SaveManager : MonoBehaviour
    27. {
    28.     public static SaveManager Instance;
    29.  
    30.     private void Awake()
    31.     {
    32.         // Fake singleton is not the best solution, but will do for this demo.
    33.         Instance = this;
    34.     }
    35.  
    36.     public bool Dirty { get; private set; }
    37.  
    38.     public event Action DirtyChanged;
    39.     public event Action DataSaved;
    40.  
    41.     public Vector3 GetVector3(string saveKey)
    42.     {
    43.         // Just a bad example.
    44.         return new Vector3(PlayerPrefs.GetFloat(saveKey), 0f, 0f);
    45.     }
    46.  
    47.     public void SetVector3(string saveKey, Vector3 value)
    48.     {
    49.         // Just a bad example.
    50.         PlayerPrefs.SetFloat(saveKey, value.x);
    51.  
    52.         if (Dirty == false)
    53.         {
    54.             // But this is important.
    55.             Dirty = true;
    56.  
    57.             if (DirtyChanged != null)
    58.                 DirtyChanged.Invoke();
    59.         }
    60.     }
    61.  
    62.     public void Save()
    63.     {
    64.         // TODO
    65.         // ...
    66.  
    67.         if (DataSaved != null)
    68.             DataSaved.Invoke();
    69.     }
    70. }
    71.  
    72. public class SavePrompt : MonoBehaviour
    73. {
    74.     public Button saveButton;
    75.  
    76.     private void Start()
    77.     {
    78.         // At the start, hide the button.
    79.         HideButton();
    80.  
    81.         // Whenever the player presses the button,
    82.         // assume that everything was saved and the button
    83.         // is no longer needed. Better check with the SaveManager
    84.         // if saving has actually succeeded.
    85.         saveButton.onClick.AddListener(OnButtonClicked);
    86.  
    87.         SaveManager.Instance.DirtyChanged += OnDirtyChanged;
    88.         SaveManager.Instance.DataSaved += OnDataSaved;
    89.     }
    90.  
    91.     private void OnDataSaved()
    92.     {
    93.         HideButton();
    94.     }
    95.  
    96.     private void OnDirtyChanged()
    97.     {
    98.         if (SaveManager.Instance.Dirty)
    99.             ShowButton();
    100.     }
    101.  
    102.     private void OnButtonClicked()
    103.     {
    104.         SaveManager.Instance.Save();
    105.     }
    106.  
    107.     private void HideButton()
    108.     {
    109.         saveButton.gameObject.SetActive(false);
    110.     }
    111.  
    112.     private void ShowButton()
    113.     {
    114.         saveButton.gameObject.SetActive(true);
    115.     }
    116. }
    117.  
    Just some ideas, there are many ways to handle saving and dirty flags, especially when it comes to optimizations...
     
    SharonL75 likes this.