SerializedObject asset loses data when pressing play
I have an array inside a SerializedObject that loses all its data upon pressing play, however on the actual asset the data persists if I open it with a text editor, which is weird to me. I should also mention that I can see it populate and everything on the inspector without any issues.
This is the ScriptableObject I'm trying to persist (roomData is the array that doesn't get saved/serialized? properly):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
namespace Strata
{
[System.Serializable]
public class RoomData
{
public string tilemap;
public char[] chars;
}
[CreateAssetMenu(menuName = "Strata/Templates/RoomTemplate")]
[System.Serializable]
public class RoomTemplate : ScriptableObject
{
public int roomSizeX = 10;
public int roomSizeY = 10;
// Array of char arrays.
// Size of the array is the number of different tilemaps on the project.
public RoomData[] roomData = new RoomData[12];
public bool opensToNorth;
public bool opensToEast;
public bool opensToSouth;
public bool opensToWest;
void OnValidate()
{
// Populate "jagged" array so that each tilemap has its own.
for (int i = 0; i < roomData.Length; i++)
{
RoomData data = new RoomData();
data.chars = new char[100];
roomData[i] = data;
}
// Resize them if incorrect.
if (roomData[0].chars.Length != roomSizeX * roomSizeY)
{
for (int i = 0; i < roomData.Length; i++)
{
roomData[i].chars = new char[roomSizeX * roomSizeY];
}
}
}
}
}
And this the script in charge of saving and loading these assets (with unnecessary bits cut out):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
using UnityEditor;
namespace Strata
{
public class StrataContentEditorWindow : EditorWindow
{
//The currently loaded RoomTemplate which we are loading and saving from/to.
public RoomTemplate roomTemplate;
//The currently loaded BoardLibrary which we are reading from to match Tiles to Characters.
public BoardLibrary boardLibrary;
//The currently loaded BoardGenerator which we are getting available templates from.
public BoardGenerator boardGenerator;
//Store a reference to the Dictionary of BoardLibrary which we use to match TileBase objects to BoardLibraryEntry objects (which contain ASCII characterIds)
private Dictionary<TileBase, BoardLibraryEntry> libraryDictionary;
//Set up the Window
[MenuItem("Tools/Strata Content Editor")]
static void Init()
{
EditorWindow.GetWindow(typeof(StrataContentEditorWindow)).Show();
}
//Redraw the EditorWindow
void OnGUI()
{
//Set up the Serialized Objects we need to draw in the Window and apply their properties when modified
SerializedObject serializedObject = new SerializedObject(this);
SerializedProperty serializedRoomTemplateProperty = serializedObject.FindProperty("roomTemplate");
EditorGUILayout.PropertyField(serializedRoomTemplateProperty, true);
SerializedProperty serializedBoardLibraryProperty = serializedObject.FindProperty("boardLibrary");
EditorGUILayout.PropertyField(serializedBoardLibraryProperty, true);
SerializedProperty serializedBoardGeneratorProperty = serializedObject.FindProperty("boardGenerator");
EditorGUILayout.PropertyField(serializedBoardGeneratorProperty, true);
serializedObject.ApplyModifiedProperties();
//Draw the Buttons and call appropriate functions when they are pressed
if (GUILayout.Button("Load Room"))
{
LoadTileMapFromRoomTemplate();
}
if (GUILayout.Button("Save Room"))
{
SaveTilemapToRoomTemplate();
}
//Use to create a new RoomTemplate for authoring
if (GUILayout.Button("Create New RoomTemplate"))
{
CreateNewRoomTemplateAsset();
}
}
//Subscribe to the delegates for scene drawing, used for drawing the red box guides in the Scene view
void OnEnable()
{
SceneView.duringSceneGui += this.OnSceneGUI;
}
void OnDisable()
{
SceneView.duringSceneGui -= this.OnSceneGUI;
}
//Draw the red box in the shape of the loaded RoomTemplate in the Scene view
void OnSceneGUI(SceneView sceneView)
{
if (roomTemplate != null)
{
Handles.BeginGUI();
Debug.DrawLine(Vector3.zero, new Vector3(roomTemplate.roomSizeX, 0, 0), Color.red);
Debug.DrawLine(Vector3.zero, new Vector3(0, roomTemplate.roomSizeY, 0), Color.red);
Debug.DrawLine(new Vector3(roomTemplate.roomSizeX, roomTemplate.roomSizeY, 0), new Vector3(roomTemplate.roomSizeX, 0, 0), Color.red);
Debug.DrawLine(new Vector3(roomTemplate.roomSizeX, roomTemplate.roomSizeY, 0), new Vector3(0, roomTemplate.roomSizeY, 0), Color.red);
HandleUtility.Repaint();
Handles.EndGUI();
}
}
//Use this to create and load a new RoomTemplate asset for authoring
public void CreateNewRoomTemplateAsset()
{
roomTemplate = CreateAsset<RoomTemplate>("Room") as RoomTemplate;
}
//Helper function for creating all the ScriptableObject assets we'll need
public static ScriptableObject CreateAsset<T>(string assetName) where T : ScriptableObject
{
var asset = ScriptableObject.CreateInstance<T>();
ProjectWindowUtil.CreateAsset(asset, assetName + " " + typeof(T).Name + ".asset");
return asset;
}
//This method saves Tilemaps to RoomTemplate
public void SaveTilemapToRoomTemplate()
{
//Build the LibraryDictionary so that we can match Tiles to characters
libraryDictionary = boardLibrary.BuildTileKeyLibraryDictionary();
// Cycle this process for every tilemap
for (int i = 0; i < boardGenerator.tilemapArray.Length; i++)
{
//Get a reference to the Tilemap
Tilemap tilemap = boardGenerator.tilemapArray[i];
roomTemplate.roomData[i].tilemap = tilemap.gameObject.name;
//An int to store the index as we loop through the RoomTemplate
int charIndex = 0;
for (int x = 0; x < roomTemplate.roomSizeX; x++)
{
for (int y = 0; y < roomTemplate.roomSizeY; y++)
{
//Get the Tile from the Tilemap as a TileBase, this allows us to use other types of Tiles including RuleTiles, RandomTiles and other
//scripted tiles, as opposed to just Sprite based tiles.
TileBase foundTile = GetTileFromTilemap(x, y, tilemap) as TileBase;
if (foundTile)
{
//Get the BoardLibraryEntry that matches this TileBase
BoardLibraryEntry entry = boardLibrary.CheckLibraryForTile(foundTile, libraryDictionary);
if (entry == null)
{
//If we don't find a matching Entry, we need one, so let's create it in the BoardLibrary
entry = boardLibrary.AddBoardLibraryEntryIfTileNotYetEntered(foundTile);
//Set this dirty because we need to save it
EditorUtility.SetDirty(boardLibrary);
}
//Set the character into the RoomTemplate to record it
roomTemplate.roomData[i].chars[charIndex] = entry.characterId;
}
else if (foundTile == null)
{
//Debug.Log("This shouldn't happen, foundTile is: "+foundTile);
//If tilemap is blank inside grid, write in default empty space character defined in board library, usually 0
roomTemplate.roomData[i].chars[charIndex] = boardLibrary.GetDefaultEmptyChar();
}
charIndex++;
}
}
}
//Set the RoomTemplate dirty to make sure we'll save changes
EditorUtility.SetDirty(roomTemplate);
//Save the AssetDatabase to write changes back to disk
AssetDatabase.SaveAssets();
}
//This is used to convert the ASCII data stored in the RoomTemplate back into Tile data for display and editing in the Tilemap in the Scene view
public void LoadTileMapFromRoomTemplate()
{
bool loaded = false;
//Build up the Dictionary from the Library since we need it to match characters to Tiles
libraryDictionary = boardLibrary.BuildTileKeyLibraryDictionary();
// Cycle this process for every tilemap
for (int i = 0; i < boardGenerator.tilemapArray.Length; i++)
{
//Make sure the Tilemap is set
Tilemap tilemap = boardGenerator.tilemapArray[i];
tilemap.ClearAllTiles();
tilemap.ClearAllEditorPreviewTiles();
//Loop through the ASCII roomData.chars stored in the RoomTemplate and match them to entries in the BoardLibrary, load those Tiles
int charIndex = 0;
for (int x = 0; x < roomTemplate.roomSizeX; x++)
{
for (int y = 0; y < roomTemplate.roomSizeY; y++)
{
//Get the tile to match the character found from BoardLibrary
TileBase tileToSet = boardLibrary.GetTileFromChar(roomTemplate.roomData[i].chars[charIndex]) as TileBase;
if (tilemap == boardGenerator.tilemapArray[0])
{
//Debug.Log(tilemap+" has "+tileToSet+" char at "+x+","+y+" position. and is on the charIndex number "+charIndex+".");
}
if (tileToSet == null)
{
Debug.LogError("Attempting to load empty tiles, draw and save something first");
}
Vector3Int pos = new Vector3Int(x, y, 0) + tilemap.origin;
tilemap.SetTile(pos, tileToSet);
charIndex++;
}
}
}
}
}
}
I'm really confused since as you can see I'm serializing everything, I got rid of the jagged array I was using because those can't be serialized, I'm using SetDirty on the assets so they're saved (and they're, since this is what the .asset shows: as you can see roomData has data here).
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 184007c02a596bb4ab011e3ac438387b, type: 3}
m_Name: First Room
m_EditorClassIdentifier:
roomSizeX: 12
roomSizeY: 10
roomData:
- tilemap: Platforms
chars: 5300
- tilemap: OneWayPlatforms
chars: 4200
opensToNorth: 1
opensToEast: 1
opensToSouth: 0
opensToWest: 1
And this .asset data never gets lost even after pressing play on Unity. And as I said, the Inspector shows the roomData array without issues before pressing play and it all vanishes upon pressing it. Oddly enough the other parameters don't "reset", they persist, so I'm guessing it's an issue with Unity being unable to serialize this array but I thought this was a working substitute for jagged arrays? I'd appreciate any help since I'm stuck.
Thanks in advance.
Answer by adriasierra · Jul 23, 2021 at 12:24 AM
I'm dumb, OnValidate() on RoomTemplate.cs is also called when the game starts and it was resetting the array, I made these changes to OnValidate() which make it only populate the array if it's null and now it works:
void OnValidate()
{
Debug.Log("Validated.");
// Populate jagged array so that each tilemap has its own.
for (int i = 0; i < roomData.Length; i++)
{
if (roomData[0] == null)
{
RoomData data = new RoomData();
data.chars = new char[100];
roomData[i] = data;
}
}
// Resize them if incorrect.
if (roomData[0].chars.Length != roomSizeX * roomSizeY && roomData[0] == null)
{
for (int i = 0; i < roomData.Length; i++)
{
roomData[i].chars = new char[roomSizeX * roomSizeY];
}
}
}