Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Serializing ScriptableObjects when using CustomEditor

Discussion in 'Scripting' started by Vallsten, Jul 3, 2018.

  1. Vallsten

    Vallsten

    Joined:
    Apr 3, 2016
    Posts:
    9
    Im working on a prototype for a Match Three game and I want to make an easy to use board editor.
    I've been using ScriptableObjects to hold "Width, height" of the board and an array of enums (For normal tiles/block tiles/empty tiles) used to initialize the board. The way the current editor handles arrays is quite hard to get an overview of how the board looks so I started working on a custom editor.

    I got the idea from this blogpost:
    http://www.ryan-meier.com/blog/?p=87

    The visual representation of the array works as intended but when I close and open Unity all saved stats in the ScriptableObject is lost and I figured this is a Serializing issue.
    The standard editor serializes all the values in the ScriptableObject when changed but I'm very unfamiliar with serializing myself and would like some help what I'm doing wrong.

    Here's the BoardSettings ScriptableObject:

    Code (CSharp):
    1.  
    2. using System;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5. using UnityEditor;
    6. using UnityEngine;
    7.  
    8. [Serializable]
    9. public enum BoardTileType
    10. {
    11.     EMPTY,
    12.     BLOCK,
    13.     NORMAL
    14. }
    15.  
    16. [Serializable][CreateAssetMenu()]
    17. public class BoardSettings : ScriptableObject
    18. {
    19.     public int width;
    20.     public int height;
    21.  
    22.     public float tileWidth;
    23.     public float tileHeight;
    24.  
    25.     public BoardTileType[] boardStatus;
    26.  
    27.     public void UpdateBoard()
    28.     {
    29.         for (int y = 0; y < height; y++)
    30.         {
    31.             BoardTileType type = BoardTileType.NORMAL;
    32.             for(int x = 0; x<width; x++)
    33.             {
    34.                 boardStatus[GetIndex(x,y)] = type;
    35.             }
    36.         }
    37.     }
    38.  
    39.     private int GetIndex(int x, int y)
    40.     {
    41.         return (x*width)+y;
    42.     }
    43.  
    44.     void OnEnable()
    45.     {
    46.         GUI.enabled = true;
    47.         if(boardStatus == null)
    48.         {
    49.             boardStatus = new BoardTileType[81];
    50.         }
    51.         hideFlags = HideFlags.HideAndDontSave;
    52.     }
    53.  
    54.     public BoardTileType GetTileType(int x, int y)
    55.     {
    56.         return boardStatus[GetIndex(x,y)];
    57.     }
    58.  
    59.     public void SetTileType(int x, int y, BoardTileType type)
    60.     {
    61.         boardStatus[GetIndex(x,y)] = type;
    62.     }
    63. }
    Here's my custom editor script:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEditor;
    5. using System;
    6.  
    7. [CustomEditor(typeof(BoardSettings))]
    8. public class BoardSettingsEditor : Editor
    9. {
    10.     private static GUIStyle _selectedButton = null;
    11.     private BoardTileType selectedTileType = BoardTileType.EMPTY;
    12.  
    13.     private static GUIStyle selectedButton
    14.     {
    15.         get
    16.         {
    17.             if (_selectedButton == null)
    18.             {
    19.                 _selectedButton = new GUIStyle(GUI.skin.button);
    20.                 _selectedButton.normal.background = _selectedButton.active.background;
    21.                 _selectedButton.normal.textColor = _selectedButton.active.textColor;
    22.             }
    23.             return _selectedButton;
    24.         }
    25.     }
    26.  
    27.     public override void OnInspectorGUI()
    28.     {
    29.         GUI.enabled = true;
    30.         BoardSettings boardSettings = target as BoardSettings;
    31.      
    32.         boardSettings.width = EditorGUILayout.IntSlider("Width", boardSettings.width,4,9);
    33.         boardSettings.height = EditorGUILayout.IntSlider("Height", boardSettings.height,4,9);
    34.      
    35.         boardSettings.tileWidth = EditorGUILayout.FloatField("TileWidth", boardSettings.tileWidth);
    36.         boardSettings.tileHeight = EditorGUILayout.FloatField("TileHeight", boardSettings.tileHeight);
    37.  
    38.         //If the board doesnt contain any "NORMAL" tiles, update the board
    39.         if(!ArrayUtility.Contains(boardSettings.boardStatus,BoardTileType.NORMAL))
    40.         {
    41.             Debug.Log("Update board boardStatus");
    42.             boardSettings.UpdateBoard();
    43.             //EditorUtility.SetDirty(boardSettings);
    44.         }
    45.  
    46.         Color oldColor = GUI.backgroundColor;
    47.  
    48.         //Show a button for each BoardTileType enum
    49.         EditorGUILayout.BeginHorizontal();
    50.         foreach (BoardTileType tile in Enum.GetValues(typeof (BoardTileType)))
    51.         {
    52.             if (GUILayout.Button(tile.ToString(), (tile == selectedTileType ? selectedButton : GUI.skin.button)))
    53.             {
    54.                 selectedTileType = tile;
    55.             }
    56.         }
    57.         GUI.backgroundColor = oldColor;
    58.         EditorGUILayout.EndHorizontal();
    59.  
    60.         EditorGUILayout.Space();
    61.         EditorGUILayout.BeginVertical();
    62.         Color buttonColor = Color.white;
    63.         for (int y = 0; y < boardSettings.height; y++)
    64.         {
    65.             EditorGUILayout.BeginHorizontal();
    66.             for (int x = 0; x < boardSettings.width; x++)
    67.             {
    68.                 if(boardSettings.GetTileType(x, y) == BoardTileType.EMPTY)
    69.                     buttonColor = Color.red;
    70.                 else if(boardSettings.GetTileType(x, y) == BoardTileType.NORMAL)
    71.                     buttonColor = Color.green;
    72.                 else if(boardSettings.GetTileType(x, y) == BoardTileType.BLOCK)
    73.                     buttonColor = Color.cyan;
    74.              
    75.                 GUI.backgroundColor = buttonColor;
    76.                 if (GUILayout.Button(boardSettings.GetTileType(x, y).ToString(), GUILayout.Width(70), GUILayout.Height(70)))
    77.                 {
    78.                     boardSettings.SetTileType(x, y, selectedTileType);
    79.                 }
    80.             }
    81.             EditorGUILayout.EndHorizontal();
    82.         }
    83.         GUI.backgroundColor = oldColor;
    84.         EditorGUILayout.EndVertical();
    85.      
    86.         AssetDatabase.SaveAssets();
    87.      
    88.     }
    89. }
    My custom editor looks like this:


    When closing Unity and open it up again width and height are both "4" and all the tiles in the Array are "NORMAL". Any idea where to start?
    I found this forum thread about "Serialization best practices"
    https://forum.unity.com/threads/serialization-best-practices-megapost.155352/
    but not quite sure how to implement this in my project.
    Thanks in advance!
     
    Last edited: Jul 3, 2018
  2. FernandoHC

    FernandoHC

    Joined:
    Feb 6, 2018
    Posts:
    338
  3. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    You need to use serializedObject.Update() / serializedObject.ApplyModifiedProperties(). Like so:
    Code (CSharp):
    1. public override void OnInspectorGUI()
    2.     {
    3.         serializedObject.Update();
    4.         ...
    5.         serializedObject.ApplyModifiedProperties();
    6.     }
    Otherwise any changes to serializedObject won't be applied.
     
    FernandoHC likes this.
  4. Vallsten

    Vallsten

    Joined:
    Apr 3, 2016
    Posts:
    9
    EditorUtility.SetDirty(boardSettings) works to save the ScriptableObject but I've read in various threads (And in the docs you just linked) that this is bad practice and you shouldn't use it. That's why I commented it out and wanted to find a better way, the proper way, to serialize a list.

    I've been trying to use serializedObject.Update() and ApplyModifiedProperties() but havent been able to get it to work. Do I have to change my code to use PropertyField instead of accessing the variables from ex "boardSettings.width", how do I get this code to work with my array as an propertyfield instead of changing it through boardSettings.boardStatus?
     
  5. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,296
    That's most likely because in most cases it should be replaced with Undo.RecordObject();
    https://docs.unity3d.com/ScriptReference/Undo.RecordObject.html

    Yes, SerializedObject.ApplyModifiedProperties() works only on SerializedProperties. If you're modifying object directly, you need to either notify the editor via Editor.SetDirty(...) call or via Undo.RecordObject(...).

    Working with arrays as SerializedProperty can be tricky. There's plenty of info in the net about it though. Here's an example:
    https://forum.unity.com/threads/wor...erializedproperty-ies-in-editor-script.97356/

    You can get the size of the array, and then iterate over it, obtaining SerializedProperties like so:
    Code (CSharp):
    1. ArrayProperty.GetArrayElementAtIndex(i);