Skip to content

Instantly share code, notes, and snippets.

@Denchyaknow
Forked from OscarAbraham/PlainDataInstancer.cs
Created December 3, 2021 15:08
Show Gist options
  • Save Denchyaknow/cf7e59f8645c5f45d3b0c7f00c923961 to your computer and use it in GitHub Desktop.
Save Denchyaknow/cf7e59f8645c5f45d3b0c7f00c923961 to your computer and use it in GitHub Desktop.
PlainDataInstancer ScriptableObject for Unity, so you can create deep clones of data stored in SO assets at runtime without the overhead of ScriptableObject copies.

It can be useful make ScriptableObject assets that are meant to be cloned at runtime with Instantiate(object). This prevents modification of the original asset in the Editor, and allows to create multiple copies. The problem is that ScriptableObjects can have considerable overhead when they are instantiated more than a few times. They also need to be destroyed to avoid potential leaks and performance problems.

PlainDataInstancer is a ScriptableObject that instantiates plain serializable objects or structs. We get the convenience of SO assets without the overhead. The instances are deep clones, which is good in these cases, and creation is surprisignly fast when compared to other solutions.

Here's an example of how to define a data instancer that can be edited in the inspector, just like normal SO assets:

using System.Collections.Generic;
using UnityEngine;

// A class to be instantiated. It must be a plain class or struct with [System.Serializable].
[System.Serializable]
public class ExampleData
{
    // Only supported fields that are public or marked with [SerializeField] can be copied to new instances.
    public List<string> someTexts;
    [SerializeField]private Vector3 aVector;
    // References to Unity Objects also work out of the box.
    public GameObject obj;
}

// This is a ScriptableObject that creates instances of type ExampleData. Make sure to put it on a file with the same name. 
[CreateAssetMenu]
public class DataInstancerExample : PlainDataInstancer<ExampleData> { }

Here's how you use a PlainDataInstancer asset:

using UnityEngine;

public class DataInstancerUsage : MonoBehaviour
{
    // Create a DataInstancerExample asset in the project window, then drag it here in the inspector.
    public DataInstancerExample instancer;

    void Start()
    {
        if (instancer == null) return;

        // CreateDataInstance returns a new object that's a copy of the instancer's PrototypeData.
        // You can edit the PrototypeData in the instancer's inspector.
        ExampleData instance = instancer.CreateDataInstance();
    }
}
using System.Collections.Generic;
using UnityEngine;
// TData should be a plain class or struct with the [System.Serializable] attribute.
// TData can implement ISerializationCallbackReceiver if needed for more advanced uses.
public class PlainDataInstancer<TData> : ScriptableObject
{
private static bool s_DataTypeValidated;
[SerializeField] private TData m_PrototypeData;
[System.NonSerialized] private string m_CachedJson;
public TData CreateDataInstance()
{
// Caching the JSON helps a lot with performance in tight cases.
if (string.IsNullOrEmpty(m_CachedJson))
m_CachedJson = JsonUtility.ToJson(m_PrototypeData);
return JsonUtility.FromJson<TData>(m_CachedJson);
}
// An utility method to set the PrototypeData from code. It shouldn't be needed for most use cases.
public void SetPrototypeDataValues(TData data)
{
if (data == null)
{
Debug.LogError("Cannot set prototype to null.", this);
return;
}
// We copy data for consistency; in case it changes after being assigned. Otherwise, those
// changes would only be ignored if they happen after CreatePocoInstance assigns m_CachedJson.
m_CachedJson = JsonUtility.ToJson(data);
m_PrototypeData = JsonUtility.FromJson<TData>(m_CachedJson);
}
protected virtual void OnEnable()
{
#if (UNITY_EDITOR || DEVELOPMENT_BUILD)
if (!s_DataTypeValidated)
{
s_DataTypeValidated = true;
var type = typeof(TData);
if (type.IsPrimitive
|| type.IsEnum
|| type.IsArray
|| (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)))
{
Debug.LogError($"{type} is not compatible with PlainDataInstancer. Instead of primitive, list or array types, use a custom serializable container object or struct.", this);
}
else if (type.IsAbstract || type.IsSubclassOf(typeof(UnityEngine.Object)))
{
Debug.LogError($"{type} is not supported by PlainDataInstancer. Make sure it's neither abstract nor a UnityEngine.Object.", this);
}
}
#endif
}
protected virtual void OnValidate()
{
m_CachedJson = null;
}
}

Copyright (c) 2021 Oscar Abraham

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment