Correctly track changes to SerializedProperty in Inspector

Problems with tracking SerializedProperty updates:

I’ve run into some situations where developers are having trouble tracking changes to SerializedProperties and I present a working solution here, as well as an open-source library to help deal with these issues.

The first thing developers are told to use is often Unity’s BeginChangeCheck() and EndChangeCheck() methods. If any UI was interacted with between these two calls, then the second method will return true. However, this fails to detect complicated updates made by the user, including paste, revert, reset, and undo operations. It also erroneously returns true if a non-essential foldout is toggled, or if an int-slider is interacted with (even without reaching the next notch in the int slider)

The second option is to use SerializedObject.hasModifiedProperties to tell if data has been changed. Unfortunately, this also fails to catch changes caused by paste, undo, revert, and reset.

Solution:

The only way I’ve found to correctly track updates to SerializedProperties is to individually track their previous values and directly compare for changes. This also requires that the original value of Property.hasMultipleDifferentValues is tracked and compared, because .hMDV can change from true to false (or vice versa) from user interaction. In these cases, the primary value for the SerializedProperty being updated can (and will often) remain the same.

So for a given SerializedProperty:

private SerializedProperty enablePreviewProp;

We’ll also need to define:

private bool prev_enablePreview = false;
private bool prevMulti_enablePreview = false;

We track the previous values of enablePreviewProp:

prev_enablePreview = enablePreviewProp.boolValue;
prevMulti_enablePreview = enablePreviewProp.hasMultipleDifferentValues;

And later, we do the comparison

updated_previewEnabled |= prev_enablePreview != enablePreviewProp.boolValue;
updated_previewEnabled |= prevMulti_enablePreview != enablePreviewProp.hasMultipleDifferentValues;

If an update is detected, then the two “prev_” values need to be set to the new values so that we can check for more changes in the future.

If you run this comparison AFTER drawing the UI (and making any automatic changes to property values), then you can effectively measure all changes to your SerializedProperties, including those made from Undo, Redo, Paste, Revert, and Reset.

Examples:

Examples are included in the BetterEditor library I’ve published, more on this below.

The Demo scene contains 6 stages.

  • Stage 1 has an example of EndChangeCheck() failing
  • Stage 2 has an example of hasModifiedProperties failing
  • Stage 3 shows this approach applied and working correctly.

I’ve also made a video (with timestamps) that’s more of a tutorial, but it explains these issues in detail alongside the demo scene. It also includes some more explanation on the subject for beginners, and a first-look at how to use the BetterEditor library to help with these issues.

If this stuff interests you at all, I hope you’ll check out the video or library

BetterEditor Library:

I also present BetterEditor, a new open-source library I’ve been working on. It includes “Trackers” to help deal with our issue from before, these replace SerializedProperty as a wrapper for each Property.

Instead of creating three variables for each SerializedProperty, we replace the property with a Tracker. (The target property name
is given to the Tracker immediately)

private Tracker enablePreviewTracker = new( nameof(_demoComponent.enablePreview) );

The tracker (and its internal SerializedProperty) are initialized via the Track Method:

enablePreviewTracker.Track(serializedObject.AsSource());

Now we can quickly tell if the property was updated, with full logging support

bool updated = Tracker.WasUpdated()
updated = Tracker.WasUpdated( ETrackLog.LogIfUpdated )
updated = Tracker.WasUpdated( ETrackLog.Log )

And to refresh Tracking, we can call either:

Tracker.RefreshTracking()
enablePreviewTracker.Track(serializedObject.AsSource()); (again)

Advantages of Using Tracker

  • Single variable per Property (instead of 3)
  • Tracker provides a .prop value to get instant access to the original SerializedProperty.
  • Tracker provides a .content GUIContent which is automatically generated from [Tooltip] and [DisplayName]
  • Trackers are type-agnostic (you can change the type of the original property they refer to without any other code changes)
  • Tracker comparisons are safe - floating point values are compared using the proper methods
  • Trackers provide detailed logging quickly
  • ListTrackers can be used to track updates to array-type SerializedProperties.

TrackerGroups:

TrackerGroups HashSet<ITrack> can have multiple Trackers added to them, such that TrackerGroup.WasUpdated() will return true if any member of the group was updated. Groups can be populated via reflection easily, and TrackerGroup.Track(SerializedObject.AsSource()) will cause all child Trackers to begin tracking from the provided SerializedObject.

TrackerGroup can be made relative using SetAsRelativeTracker(string PropName) such that its children Trackers are populated relative to a given property. This is shown in the 5th step of the demo, and used to make a “mini-Editor” for a commonly reused sub-class

More Advantages of BetterEditor Library:

BetterEditor provides a bunch of missing GUI methods, a custom-row builder, a full wrapper for Unity’s Undo, and more. Checkout the full readme on github for a full list.

Closing Thoughts:

I hope this work can be helpful to more people. I haven’t found any talk about these issues, so I’ve mostly been doing this in the dark. GPT4 had no idea about any of these issues or how to solve them, which was the main inspiration. I’ve been able to apply this code successfully to the massive components in the Rayfire Destruction Store asset (which is a lot of fun by the way)

The demo, video, and library contain a lot of other cool tricks, and I’d be happy to do a deeper dive into any of that. As of posting, BetterEditor has NOT been code reviewed at all. If you have other questions about the Editor which you think I’d be able to help with, I’m happy to take them here, or to discuss any of this! Cheers!

Or in UI Toolkit land, just: Unity - Scripting API: UIElements.BindingExtensions.TrackPropertyValue

1 Like

Definitely going to check some of that out