Search code examples
unity-game-enginescriptable-object

How to use a ScriptableObject for a level's Prefab transforms?


I have 3 types of Prefab: A, B & C

Whilst designing the levels, in Play Mode, I add and position them within the level.

How do I create a ScriptableObject that holds references to all of the instances of these prefabs and their transforms?

Specifically, whilst in PlayMode, the Scriptable object should dynamically respond to position and rotation changes of the prefabs in PlayMode.

I can't conceive of how to do this, despite this seemingly being a good use of Scriptable Objects.


Solution

  • You could store the information you want in a dedicated class like e.g.

    [Serializable]
    public class InstanceInformation
    {
        public GameObject UsedPrefab;
        public Vector3 Position;
        public Quaternion Rotation;
        public Vector3 Scale;
    
        public InstanceInformation(GameObject usedPrefab, Transform transform)
        {
            UsedPrefab = usedPrefab;
            Rotation = transform.rotation;
            Position = transform.position;
            Scale = transform.localScale;
        }
    
        public void UpdateValues(Transform transform)
        {
            Rotation = transform.rotation;
            Position = transform.position;
            Scale = transform.localScale;
        }
    }
    

    and in your ScriptableObject have a

    [CreateAssetMenu]
    public class LevelData : ScriptableObject
    {
        public List<InstanceInformation> instances = new List<InstanceInformation>();
    }
    

    Then everytime you Instantiate a prefab you also create an according entry in that instances.

    So later in your manager script where you Instantiate the stuff you do e.g.

    // Reference this via the Inspector
    public LevelData scriptable;
    
    // For keeping a link for the currently Instantiated stuff
    // so everytime you manipulate them later on you can update the according data entry
    private Dictionary<GameObject, InstanceInformation> objectToInstanceInformation = new Dictionary<GameObject, InstanceInformation>();
    
    ...
    
    var obj = Instantiate(aPrefab, aPosition, aRotation);
    
    var instanceInfo = new InstanceInfo(aPrefab, obj.transform);
    
    // Add the reference to the ScriptableObject list
    scriptable.instances.Add(instanceInfo);
    // And also keep track of the reference linked to the actual instance
    objectToInstanceInformation.Add(obj, instanceInfo);
    

    Now you could either repeatedly or at a certain moment call

    public void SaveInstanceInformations()
    {
        foreach(var kvp in objectToInstanceInformation)
        {
            var obj = kvp.key;
            var instanceInfo = kvp.value;
    
            instanceInfo.UpdateValues(obj.transform);
        }
    }
    

    Since InstanceInformation is a class and thereby a reference-type changing the values here automatically also changes the according entry in the instances in the ScriptableObject!


    So later when you want to load the state you can simply do

    foreach (var instance in scriptable.instances)
    {
        var obj = Instantiate(instance.UsedPrefab, instance.Position, instance.Rotation);
        obj.transform.localScale = instance.Scale;
        objectToInstanceInformation.Add(obj, instance);
    }
    

    Finally in order to be able to store this data persistent you could e.g. use a BinaryFormatter like

    [CreateAssetMenu]
    public class LevelData : ScriptableObject
    {
        // here you can set a filename the data shall be stored to
        [SerializeField] private string fileName = "level.dat";
    
        public List<InstanceInformation> instances = new List<InstanceInformation>();
    
        // called when the object is loaded
        private void OnEnable()
        {
            // try to load the data from drive
            // in the editor use the Application.streamingAssetsPath
            // in a build use Application.persistentDataPath
            var folder = Application.isEditor ? Application.streamingAssetsPath : Application.persistentDataPath;
            if(!File.Exists(Path.Combine(folder, fileName)))
            {
                // on the first run the folder and file will not exist
                // in the editor do nothing (the file simply doesn't exist yet)
                // in a build copy the content from the streaming assets folder to the persistent data path (if exists)
                var fallbackFolder = Application.streamingAssetsPath;
                if(!Driectoy.Exists(fallbackFolder)) return;
    
                if(!File.Exists(Path.Combine(fallbackFolder, fileName))) return;
    
                // copy fallback file to persistent file
                File.Copy(Path.Combine(fallbackFolder, FileName), Path.Combine(folder, fileName));
            } 
    
            // load the list
            using(var file = File.Open(Path.Combine(folder, fileName), FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                var binaryFormatter = new BinaryFormatter();
                instances = (List<InstanceInformation>)binaryFormatter.Deserialize(file);
            }
        }
    
        // called when the object is destroyed (you app ended)
        private void OnDestroy()
        {
            // save the data to drive
            // in the editor use the Application.streamingAssets
            // in a build use Application.persistentDataPath
            var folder = Application.isEditor ? Application.streamingAssets : Application.persistentDataPath;
            if(!Directoy.Exists(folder)) Directory.CreateDirectory(folder);
    
            using(var file = File.Open(Path.Combine(folder, fileName), FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write))
            {
                var binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(file, instances);
            }
        }
    }