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();
}
}
@zukas3 Yes :). It is relatively faster, specially in some cases. Here is what I know:
Instantiating ScriptableObjects in the Editor is slower than in the Player. A very simple SO can take 4 times as much when instantiating in the Editor. The more complex the SO is, the smaller the difference between runtime and the Editor.
My technique is always noticeably faster in the Editor, even for complex data. It can be 14 times faster for simple types, and about twice as fast for very complex types that contain stuff like [SerializeReference]. Outside the editor, my technique is about 5 times faster for simple types, and it can get to just a little faster for very complex types.
These timings apply for instantiations done after the first one; a big part of this optimization is caching the JSON. Unity doesn't cache the serialized data when calling
Instantiate
; it must serialize and deserialize it every time. Also, this optimization seems even better when one considers the savings for GC and memory; GC is specially heavy with Unity Objects.I've found an improvement for this technique that supports any serialized type directly, even simple types like float or int. It works by deserializing a generic struct instead of deserializing the type directly. I'll try to update this code to use that later.
EDIT
Another optimization for this approach is to use
Unity.Collections.LowLevel.Unsafe.UnsafeUtility.IsUnmanaged
to detect if the type of data needs JSON deserialization. Unmanaged structs, strings, and UnityEngine.Object references can be returned directly from m_PrototypeData; Unmanaged structs are already copied when assigned, strings are immutable, and Unity Objects are expected to be references.