Unity not saving scriptable object changes in project files with custom inspector

So i understand your not suppose to modify values directly, but in this case i need to. im using a bitfield for memory efficient bool storage, and i need to edit every value individually without doing bitmath for 2^32
I just assume im using the wrong method, but i dont know what im suppose to call.

using System;
using System.Collections;
using Unity.Collections;
using UnityEngine;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;

[CustomEditor(typeof(GroundTile))]
public class GroundTileEditor : Editor
{
    SerializedProperty sprite;

    public void OnEnable()
    {
        sprite = serializedObject.FindProperty("sprite");
        Debug.Log(((GroundTile)target).data.Value);
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        BitField32 bits = ((GroundTile)target).data;

        EditorGUI.BeginChangeCheck();

        var nwMoveX = bits.IsSet(0);
        var nwMoveY = bits.IsSet(1);
        var nwMoveZ = bits.IsSet(2);
        var nwUp = bits.IsSet(3);

        var neMoveX = bits.IsSet(4);
        var neMoveY = bits.IsSet(5);
        var neMoveZ = bits.IsSet(6);
        var neUp = bits.IsSet(7);

        var swMoveX = bits.IsSet(8);
        var swMoveY = bits.IsSet(9);
        var swMoveZ = bits.IsSet(10);
        var swUp = bits.IsSet(11);

        var seMoveX = bits.IsSet(12);
        var seMoveY = bits.IsSet(13);
        var seMoveZ = bits.IsSet(14);
        var seUp = bits.IsSet(15);

        var isNWValid = bits.IsSet(16);
        var isNEValid = bits.IsSet(17);
        var isSWValid = bits.IsSet(18);
        var isSEValid = bits.IsSet(19);

        var unmountedMovementCost = bits.GetBits(20, 3);
        var mountedMovementCost = bits.GetBits(23, 3);

        if (isNWValid = EditorGUILayout.Foldout(isNWValid,"NorthWest Valid?"))
        {
            EditorGUI.indentLevel++;
            nwMoveX = EditorGUILayout.Toggle("Move Along X?",nwMoveX);
            nwMoveY= EditorGUILayout.Toggle("Move Along Y?",nwMoveY);
            nwMoveZ = EditorGUILayout.Toggle("Move Along Z?", nwMoveZ);
            nwUp = EditorGUILayout.Toggle("True for Up", nwUp);
            EditorGUI.indentLevel--;
        }
        if (isNEValid = EditorGUILayout.Foldout(isNEValid, "NorthEast Valid?"))
        {
            EditorGUI.indentLevel++;
            neMoveX = EditorGUILayout.Toggle("Move Along X?", neMoveX);
            neMoveY = EditorGUILayout.Toggle("Move Along Y?", neMoveY);
            neMoveZ = EditorGUILayout.Toggle("Move Along Z?", neMoveZ);
            neUp = EditorGUILayout.Toggle("True for Up", neUp);
            EditorGUI.indentLevel--;
        }
        if (isSWValid = EditorGUILayout.Foldout(isSWValid, "SouthEast Valid?"))
        {
            EditorGUI.indentLevel++;
            seMoveX = EditorGUILayout.Toggle("Move Along X?", seMoveX);
            seMoveY = EditorGUILayout.Toggle("Move Along Y?", seMoveY);
            seMoveZ = EditorGUILayout.Toggle("Move Along Z?", seMoveZ);
            seUp = EditorGUILayout.Toggle("True for Up", seUp);
            EditorGUI.indentLevel--;
        }
        if (isSEValid = EditorGUILayout.Foldout(isSEValid, "SouthWest Valid?"))
        {
            EditorGUI.indentLevel++;
            swMoveX = EditorGUILayout.Toggle("Move Along X?", swMoveX);
            swMoveY = EditorGUILayout.Toggle("Move Along Y?", swMoveY);
            swMoveZ = EditorGUILayout.Toggle("Move Along Z?", swMoveZ);
            swUp = EditorGUILayout.Toggle("True for Up", swUp);
            EditorGUI.indentLevel--;
        }

        EditorGUILayout.LabelField("Movement Costs");
        EditorGUI.indentLevel++;
        unmountedMovementCost = (uint)EditorGUILayout.IntSlider("On Foot",(int)unmountedMovementCost, 0, 7);
        mountedMovementCost = (uint)EditorGUILayout.IntSlider("Mounted",(int)mountedMovementCost, 0, 7);
        EditorGUI.indentLevel--;

        EditorGUILayout.PropertyField(sprite);

        bits.SetBits(0,nwMoveX);
        bits.SetBits(1, nwMoveY);
        bits.SetBits(2, nwMoveZ);
        bits.SetBits(3, nwUp);

        bits.SetBits(4, neMoveX);
        bits.SetBits(5, neMoveY);
        bits.SetBits(6, neMoveZ);
        bits.SetBits(7, neUp);

        bits.SetBits(8, swMoveX);
        bits.SetBits(9, swMoveY);
        bits.SetBits(10, swMoveZ);
        bits.SetBits(11, swUp);

        bits.SetBits(12, seMoveX);
        bits.SetBits(13, seMoveY);
        bits.SetBits(14, seMoveZ);
        bits.SetBits(15, seUp);

        bits.SetBits(16, isNWValid);
        bits.SetBits(17, isNEValid);
        bits.SetBits(18, isSWValid);
        bits.SetBits(19, isSEValid);

        //Black Magic
        var bit1 = (unmountedMovementCost & (1 << 1 - 1)) != 0;
        var bit2 = (unmountedMovementCost & (1 << 2 - 1)) != 0;
        var bit3 = (unmountedMovementCost & (1 << 3 - 1)) != 0;

        bits.SetBits(20, bit1);
        bits.SetBits(21, bit2);
        bits.SetBits(22, bit3);
       
        bit1 = (mountedMovementCost & (1 << 1 - 1)) != 0;
        bit2 = (mountedMovementCost & (1 << 2 - 1)) != 0;
        bit3 = (mountedMovementCost & (1 << 3 - 1)) != 0;

        bits.SetBits(23, bit1);
        bits.SetBits(24, bit2);
        bits.SetBits(25, bit3);

        if (((GroundTile)target).data.Value != bits.Value)
        {
            ((GroundTile)target).data = bits;

            Undo.RecordObject(target,"Changed Tile Properties");
        }
        EditorUtility.SetDirty(target);

        EditorGUI.EndChangeCheck();
        serializedObject.ApplyModifiedProperties();
    }
}

I dont think you want to call:

serializedObject.ApplyModifiedProperties();

That applies the values of the property fields back to the asset, so your setting the bits above then call that which reads the value in the serialized property incorrectly setting it back to what it was. In my experience EditorUtility.SetDirty should be enough on its own.

Unfortuately this fix didnt work, I have gotten around the problem by printing the number it should be, and making a new field with the value then copy pasting. But i really wish unity would provide better tools for custom UI

Also, i dont know if this matters, but my data was never a propertyfield. i didnt convert it because unity wont convert this for some reason

Well, the problem is not the tools. There are multiple problem here. One is that you mix two completely different approaches which clash with each other. The SerializedObject / SerializedProperty approach is a mediating layer between your UI code and the actual serialized data. It handles many things for you like multi object editing, saving the changes and creating undo entries. However you also mess with the main target object manually. That would never work out.

When serializedObject.Update(); is called, Unity would copy all the serialized data into the SerializedObject / SerializedProperty structure. A serialized object can actually represent multiple objects at the same time (of course only multiple objects of the same type). The SerializedProperties are wrappers for the actual serialized values. They have no connection to the C# classes that may define the values. When you “edit” values of a SerializedProperty, you just edit the state of that copy inside the wrapper. Only when calling ApplyModifiedProperties on the serialized object those changes would be applied to all the objects that are represented by the serialized object.

Since you mix and criss-cross doing direct manipulation and also using the serialized object you overwrite any changes you had made directly by calling ApplyModifiedProperties at the end because this method applies the internal state of the SerializedProperties.

However your main issue is that the BitField32 struct is not marked as a Serializable type. Therefore it can not be serialized at all. If you want to use a bitmask in your GroundTile, you have to use an uint variable for now. You can still use the BitField32 struct to do your bit manipulations inside the editor and even at runtime. Since BitField32 is a struct you can always create one by passing the stored uint value into its constructor. This has no memory overhead (since it’s a struct).

I just noticed that the BitField32 struct is lacking a lot of important methods. Like actually setting bits from a bitmask. It has the GetBits method but no SetBits / ReplaceBits that takes a bitmask as input. That’s why you had to use your “Black Magic”.

So assuming you replaced your public BitField32 data with a public uint data, you can use this editor:

using Unity.Collections;
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(GroundTile)), CanEditMultipleObjects]
public class GroundTileEditor : Editor
{
    SerializedProperty sprite;
    SerializedProperty data;

    public void OnEnable()
    {
        sprite = serializedObject.FindProperty("sprite");
        data = serializedObject.FindProperty("data");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        BitField32 bits = new BitField32((uint)data.longValue);

        EditorGUI.BeginChangeCheck();

        var nwMoveX = bits.IsSet(0);
        var nwMoveY = bits.IsSet(1);
        var nwMoveZ = bits.IsSet(2);
        var nwUp = bits.IsSet(3);

        var neMoveX = bits.IsSet(4);
        var neMoveY = bits.IsSet(5);
        var neMoveZ = bits.IsSet(6);
        var neUp = bits.IsSet(7);

        var swMoveX = bits.IsSet(8);
        var swMoveY = bits.IsSet(9);
        var swMoveZ = bits.IsSet(10);
        var swUp = bits.IsSet(11);

        var seMoveX = bits.IsSet(12);
        var seMoveY = bits.IsSet(13);
        var seMoveZ = bits.IsSet(14);
        var seUp = bits.IsSet(15);

        var isNWValid = bits.IsSet(16);
        var isNEValid = bits.IsSet(17);
        var isSWValid = bits.IsSet(18);
        var isSEValid = bits.IsSet(19);

        var unmountedMovementCost = bits.GetBits(20, 3);
        var mountedMovementCost = bits.GetBits(23, 3);

        if (isNWValid = EditorGUILayout.Foldout(isNWValid, "NorthWest Valid?"))
        {
            EditorGUI.indentLevel++;
            nwMoveX = EditorGUILayout.Toggle("Move Along X?", nwMoveX);
            nwMoveY = EditorGUILayout.Toggle("Move Along Y?", nwMoveY);
            nwMoveZ = EditorGUILayout.Toggle("Move Along Z?", nwMoveZ);
            nwUp = EditorGUILayout.Toggle("True for Up", nwUp);
            EditorGUI.indentLevel--;
        }
        if (isNEValid = EditorGUILayout.Foldout(isNEValid, "NorthEast Valid?"))
        {
            EditorGUI.indentLevel++;
            neMoveX = EditorGUILayout.Toggle("Move Along X?", neMoveX);
            neMoveY = EditorGUILayout.Toggle("Move Along Y?", neMoveY);
            neMoveZ = EditorGUILayout.Toggle("Move Along Z?", neMoveZ);
            neUp = EditorGUILayout.Toggle("True for Up", neUp);
            EditorGUI.indentLevel--;
        }
        if (isSWValid = EditorGUILayout.Foldout(isSWValid, "SouthEast Valid?"))
        {
            EditorGUI.indentLevel++;
            seMoveX = EditorGUILayout.Toggle("Move Along X?", seMoveX);
            seMoveY = EditorGUILayout.Toggle("Move Along Y?", seMoveY);
            seMoveZ = EditorGUILayout.Toggle("Move Along Z?", seMoveZ);
            seUp = EditorGUILayout.Toggle("True for Up", seUp);
            EditorGUI.indentLevel--;
        }
        if (isSEValid = EditorGUILayout.Foldout(isSEValid, "SouthWest Valid?"))
        {
            EditorGUI.indentLevel++;
            swMoveX = EditorGUILayout.Toggle("Move Along X?", swMoveX);
            swMoveY = EditorGUILayout.Toggle("Move Along Y?", swMoveY);
            swMoveZ = EditorGUILayout.Toggle("Move Along Z?", swMoveZ);
            swUp = EditorGUILayout.Toggle("True for Up", swUp);
            EditorGUI.indentLevel--;
        }

        EditorGUILayout.LabelField("Movement Costs");
        EditorGUI.indentLevel++;
        unmountedMovementCost = (uint)EditorGUILayout.IntSlider("On Foot", (int)unmountedMovementCost, 0, 7);
        mountedMovementCost = (uint)EditorGUILayout.IntSlider("Mounted", (int)mountedMovementCost, 0, 7);
        EditorGUI.indentLevel--;

        EditorGUILayout.PropertyField(sprite);

        bits.SetBits(0, nwMoveX);
        bits.SetBits(1, nwMoveY);
        bits.SetBits(2, nwMoveZ);
        bits.SetBits(3, nwUp);

        bits.SetBits(4, neMoveX);
        bits.SetBits(5, neMoveY);
        bits.SetBits(6, neMoveZ);
        bits.SetBits(7, neUp);

        bits.SetBits(8, swMoveX);
        bits.SetBits(9, swMoveY);
        bits.SetBits(10, swMoveZ);
        bits.SetBits(11, swUp);

        bits.SetBits(12, seMoveX);
        bits.SetBits(13, seMoveY);
        bits.SetBits(14, seMoveZ);
        bits.SetBits(15, seUp);

        bits.SetBits(16, isNWValid);
        bits.SetBits(17, isNEValid);
        bits.SetBits(18, isSWValid);
        bits.SetBits(19, isSEValid);

        bits.ReplaceBits(20, unmountedMovementCost, 3);
        bits.ReplaceBits(23, mountedMovementCost, 3);

        data.longValue = bits.Value;

        if (EditorGUI.EndChangeCheck())
            serializedObject.ApplyModifiedProperties();
    }
}

public static class BitField32_ReplaceBits
{
    public static void ReplaceBits(this ref BitField32 aBitField, int pos, uint value, int numBits)
    {
        var mask = 0xffffffffu >> (32 - numBits);
        var tmp = (value & mask) << pos;
        aBitField.Value = (aBitField.Value & ~(mask << pos)) | tmp;
    }
}

Note that BitField extension method at the bottom. It adds the “missing” ReplaceBits method. Thanks to C# now supporting “ref” extension methods for structs it becomes much cleaner. Though I hope Unity would add a method like this to the BitField structs directly as well as make the struct serializable. Since the whole Collections package is still in preview, maybe they change it before it goes live.

2 Likes