A very common and hard to solve problem is saving Objects that were spawned at runtime.
I.e. Bullets which the player spawned.
Addressables are one way to solve that. Unity-Adressables is a package which you can download from the Package-Manager and it lets access these assets from code via an address or the guid of the asset. I won’t explain how to set up Adressables in this post because it’s not that hard and can be read in the official-documentation of the package.
This means, if we want to save 3 Bullets which the player shot, we could just create a json-Savegame that looks something like this:
{
"savedBullets": [
{
"prefabGUID": "ec2cef865e6056f498b77b5e8a1c9e14",
"PosX": -2.602089,
"PosY": 0.0,
"PosZ": -0.8487549
},
{
"prefabGUID": "ec2cef865e6056f498b77b5e8a1c9e14",
"PosX": 6.198368,
"PosY": 0.0,
"PosZ": -1.76122284
},
{
"prefabGUID": "ec2cef865e6056f498b77b5e8a1c9e14",
"PosX": 0.0,
"PosY": 5.32,
"PosZ": -0.16
}
]
}
The prefabGUID in this case refers to the GUID of the prefab and by using the addressables-Package we can then instantiate the prefab which belongs to this GUID.
In order to Address an Object we need to create an AssetReference variable and assign it to the prefab of the Bullet:
public class Bullet : MonoBehaviour
{
[SerializeField] private AssetReferenceGameObject _assetReference;
//...
}
The following Codesnippet generates the above JSON – File containing all runtime-instantiated Bullets.
void SaveBullets()
{
Bullet[] bullets = GameObject.FindObjectsOfType<Bullet>();
JObject saveGameJObj = new JObject();
//Save all Spheres into JArray
JArray spheresJArray = new JArray();
foreach(Bullet bullet in bullets)
{
JObject sphereData = new JObject();
sphereData.Add("prefabGUID", bullet._assetReference.AssetGUID);
sphereData.Add("PosX", bullet.transform.position.x);
sphereData.Add("PosY", bullet.transform.position.y);
sphereData.Add("PosZ", bullet.transform.position.z);
spheresJArray.Add(sphereData);
}
saveGameJObj.Add("savedBullets", spheresJArray);
//Write to disk
string filePath = Application.persistentDataPath + "/savegame.sav";
System.IO.File.WriteAllText(filePath, saveGameJObj.ToString());
}
To load this savegame we can use this Snippet:
IEnumerator LoadBullets()
{
//Clear scene
Bullet[] spheres = GameObject.FindObjectsOfType<Bullet>();
foreach (Bullet sphere in spheres)
{
Destroy(sphere.gameObject);
}
//Read Json-File
string filePath = Application.persistentDataPath + "/savegame.sav";
string jsonString = System.IO.File.ReadAllText(filePath);
//Instantiate prefabs
JObject saveGameJObj = JObject.Parse(jsonString);
foreach (JToken sphereData in saveGameJObj["savedBullets"])
{
string prefabAddress = sphereData["prefabGUID"].Value<string>();
AssetReferenceGameObject prefabRef = new AssetReferenceGameObject(prefabAddress);
//Wait until the Asset was loaded
yield return prefabRef.LoadAssetAsync();
Vector3 spawnPos = new Vector3(
sphereData["PosX"].Value<float>(),
sphereData["PosY"].Value<float>(),
sphereData["PosZ"].Value<float>()
);
Instantiate(prefabRef.Asset, spawnPos, Quaternion.identity);
}
}
Note that we use a Coroutine here because the reference to the prefab is loaded asynchronously.
Also you could of course improve this solution by preloading the assets at gamestart and not loading each prefab on its own. But I think this example demonstrates how one could use Addressables to create a Savegame-System.