Is there a way to check if objects has changed ?

At the top of a script :

public Button saveButton;

In the Update :

private void Update()
    {
        foreach(Transform objectToSave in objectsToSave)
        {
            if(objectToSave.hasChanged)
            {
                saveButton.enabled = true;
            }
        }
    }

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.

        bool anythingChanged = false;
        foreach(Transform objectToSave in objectsToSave)
        {
            if(objectToSave.hasChanged)
            {
                anythingChanged = true;
                // Exit the loop early
                break;
            }
        }

        saveButton.enabled = anythingChanged;

Isn’t that the goal here?

1 Like

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.

3 Likes

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

float nextCheck = 0f;

private void Update()
    {
        if ((saveButton.enabled == false) && (Time.time >= nextCheck))
        {
            foreach(Transform objectToSave in objectsToSave)
            {
                if(objectToSave.hasChanged)
                {
                    saveButton.enabled = true;
                }
            }
            nextCheck = Time.time + 0.5f;
        }
    }
1 Like

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:

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;

public class SavePrompt : MonoBehaviour
{
    public Button saveButton;

    private Transform[] transforms;
    private int transformCount;
    private bool dirty;

    private void Start()
    {
        // Assuming that the transforms are all present
        // at the start of the scene and non are instantiated later.
        List<Transform> list = FindObjectsOfType<Transform>().ToList();

        // Remove the button from the list of transforms to be checked
        // because it changes when being disabled, but we don't want
        // to trigger another save because of this.
        list.Remove(saveButton.transform);

        transforms = list.ToArray();

        // Micro optimization: cache length instead of accessing property.
        transformCount = transforms.Length;

        for (int i = 0; i < transformCount; i++)
            transforms[i].hasChanged = false;

        // If dirty is true, the button needs to be shown.
        dirty = false;

        // At the start, hide the button.
        HideButton();

        // Whenever the player presses the button,
        // assume that everything was saved and the button
        // is no longer needed. Better check with the SaveManager
        // if saving has actually succeeded.
        saveButton.onClick.AddListener(HideButton);
    }

    private void OnDestroy()
    {
        if (saveButton != null)
            saveButton.onClick.RemoveListener(HideButton);
    }

    private void HideButton()
    {
        dirty = false;
        saveButton.gameObject.SetActive(false);
    }

    private void ShowButton()
    {
        dirty = true;
        saveButton.gameObject.SetActive(true);
    }

    private void Update()
    {
        // Fake something being moved by script for testing.
        if (Time.frameCount % 300 == 0)
            transforms[Random.Range(0, transformCount)].position += new Vector3(0.01f, 0f, 0f);
    }

    private void LateUpdate()
    {
        // Don't check for changes if we already know
        // and the button is enabled.
        if (dirty == true)
            return;

        bool anyTransformChanged = false;

        for (int i = 0; i < transformCount; i++)
        {
            if (transforms[i].hasChanged)
            {
                anyTransformChanged = true;

                // You have to manually reset this flag or else
                // it will always stay true.
                transforms[i].hasChanged = false;

                // We could try to break out of here, but then
                // the save button would appear right after saving again
                // because some other transform still returns hasChanged as true.
                // So instead, set all of them to false.
            }
        }

        if (anyTransformChanged)
        {
            // Checking the c# bool variables is faster than
            // checking saveButton.enabled because the latter
            // is a property and also marshalled to Unity's
            // C++ representation of the button.
            if (dirty == false)
            {
                ShowButton();
            }
        }
    }
}

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:

using System;
using UnityEngine;
using UnityEngine.UI;

public class MoverScript : MonoBehaviour
{
    public string saveKey = "Object123";

    private void OnEnable()
    {
        // Some way of loading the saved data after scene load.
        transform.position = SaveManager.Instance.GetVector3(saveKey);
    }

    private void Update()
    {
        // This script moves some object.
        transform.Translate(Time.deltaTime, 0f, 0f);

        // But it also tells the save manager that something needs to be saved.
        // While we're at it, we can already send the current value.
        SaveManager.Instance.SetVector3(saveKey, transform.position);
    }
}

public class SaveManager : MonoBehaviour
{
    public static SaveManager Instance;

    private void Awake()
    {
        // Fake singleton is not the best solution, but will do for this demo.
        Instance = this;
    }

    public bool Dirty { get; private set; }

    public event Action DirtyChanged;
    public event Action DataSaved;

    public Vector3 GetVector3(string saveKey)
    {
        // Just a bad example.
        return new Vector3(PlayerPrefs.GetFloat(saveKey), 0f, 0f);
    }

    public void SetVector3(string saveKey, Vector3 value)
    {
        // Just a bad example.
        PlayerPrefs.SetFloat(saveKey, value.x);

        if (Dirty == false)
        {
            // But this is important.
            Dirty = true;

            if (DirtyChanged != null)
                DirtyChanged.Invoke();
        }
    }

    public void Save()
    {
        // TODO
        // ...

        if (DataSaved != null)
            DataSaved.Invoke();
    }
}

public class SavePrompt : MonoBehaviour
{
    public Button saveButton;

    private void Start()
    {
        // At the start, hide the button.
        HideButton();

        // Whenever the player presses the button,
        // assume that everything was saved and the button
        // is no longer needed. Better check with the SaveManager
        // if saving has actually succeeded.
        saveButton.onClick.AddListener(OnButtonClicked);

        SaveManager.Instance.DirtyChanged += OnDirtyChanged;
        SaveManager.Instance.DataSaved += OnDataSaved;
    }

    private void OnDataSaved()
    {
        HideButton();
    }

    private void OnDirtyChanged()
    {
        if (SaveManager.Instance.Dirty)
            ShowButton();
    }

    private void OnButtonClicked()
    {
        SaveManager.Instance.Save();
    }

    private void HideButton()
    {
        saveButton.gameObject.SetActive(false);
    }

    private void ShowButton()
    {
        saveButton.gameObject.SetActive(true);
    }
}

Just some ideas, there are many ways to handle saving and dirty flags, especially when it comes to optimizations…

1 Like