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…