I’m developing a top down 2D space sim/RPG, and I’m currently in the process of designing the over arching game architecture.
In the game, the player’s ship travels through space and can interact with a variety of NPC ships. When docking at a starbase, the game switches to a different scene that’s dedicated to that specific starbase.
The thing I having a bit of trouble figuring out is, how to keep the data specific to all of those NPC ships persistent when leaving the space scene and moving to the starbase scene?
I have a GameManager game object that is “DontDestroyOnLoad”, but I’m hearing from a variety of different sources, including the Unity devs, that keeping all of that data on the Game Manager is not a particularly effective way to do this.
So I’ve been exploring the use of Scriptable Objects as a way of temporarily storing the NPC ship data, but I’m anticipating a significant problem: NPC ships are routinely destroyed and instantiated, so how do I set things up in such a way that there is a dedicated scriptable object for each NPC ship?
I know this question demonstrates my lack of knowledge concerning scriptable objects, but I’m not sure where to look for information on this particular use of them.
Any thoughts or information regarding this would be greatly appreciated.
I’m not an expert either so I’m writing my approach and maybe someone else can comment on how good/bad it is:
I use Scriptable Objects a lot, but only use them to store dynamic data when I know the number of instances before, e.g. a scriptable object that holds the amount of money the player has.
In your case I would go with something similar to keeping it in the GameManager, but a dedicated class/object.
You could make a class called ShipData that holds all the dynamic data, and a class with Don’t destroy on load that has a List. When a ship is spawned, it will add itself to that list, and when it gets destroyed it removes itself.
The ShipData class could have a field like “public int uniqueSaveID” to find itself in the list. Just write a script that makes sure the saveID is never assigned twice.
The advantages are that you could use a factory pattern, a class that holds the prefabs and can create new ships as well as go through the ShipData list and create ships based on that when loading a scene. And if you want to save the game state you can easily serialize the class holding the list to json format.
Anyhow, just my non-expert approach. I’ve seen people create Scriptable Objects at runtime but have never done so myself.
Yeah, I was considering something along those lines, but didn’t know if that was the way to go. I will definitely have to start researching the Factory Pattern. I’m only vaguely familiar with it, having seen only one video on the subject and not fully understanding it.
This is not a game design question (Getting-Started would be a more appropriate forum), but anyway…
You do this sort of thing by storing the data in static properties (which persist for the life of your app), or in PlayerPrefs (which persist even between runs of your app), or by using objects marked DontDestroyOnLoad (which persist until you destroy them).
Nonsense. I think maybe you misunderstood what was being said. A persistent Game Manager is a perfectly cromulent way to solve this problem. Get on with making your game.
No. This is not what Scriptable Objects are for, and abusing them this way will only cause you grief. Scriptable Objects are meant to be a way for you to store configuration data in your project which you can edit in the Inspector. They have nothing to do with persisting data between scenes.
I don’t agree that they have nothing to do with persisting data between scenes, but I definitely agree that they can lead you down a rabbit hole very quickly if you don’t understand how to use them. Basically you have two ways to work with them.
The first way is likely the way most people think of and that’s by adding the [CreateAssetMenu] attribute and then creating an asset in your project with it and using it directly. While this lets you persist data across scenes it also has the side effect that it persists across runs in the editor making it very easy for you to have data in it you don’t want saved when you make a build.
The second way is to create a scriptable object in your project like above but to use a clone of them created at runtime with Instantiate(). The clone will persist data across scenes but will not be saved between runs in the editor.
Getting back to why I disagree with this statement, one major disadvantage of working with multiple scenes is that you’re unable to manually link objects directly to each other in the Inspector (at least not without it breaking spectacularly).
What you can do though is create a scriptable object that stores the data that needs to be passed between them, and then have both objects reference and read/write data to it.
Check the video below if you want to see it in action.
Unite Austin 2017 - Game Architecture with Scriptable Objects
I would say that it is, as I’m asking about how to do it as part of my overall game design.
But I don’t want the objects to persist, just their relevant data.
You may want to pass along that information to the guys at Schell Games that made an official Unity Training Day talk encouraging users to do exactly that.
Yeah, I just watched that video the other day. Some of the stuff he was going through is little above my pay grade at the moment, and I’m still trying to wrap my head around it.
I don’t know about a tutorial, but it’s easy to do:
Make a class that encapsulates the data you care about. For example:
public class NPCShip {
public string name;
public int value;
}
Now, make a static class that hangs onto this data.
public static class GameData {
public static List<NPCShip> npcShips = new List<NPCShip>();
}
And that’s it. Reference GameData.npcShips from wherever you need to, and don’t worry about it going away at scene changes (it won’t).
There are a few arguments for avoiding this sort of static data, mainly related to automated testing, but chances are good that you’re not going to be doing any such automated testing in this project anyway. So, by the KISS principle, this is the approach I would recommend.
Of course if you’re planning to give a conference talk, you’d need to present a much more complex approach, as this one would be done in five minutes.
When you use the Editor, you can save data to ScriptableObjects while editing and at run time because ScriptableObjects use the Editor namespace and Editor scripting. In a deployed build, however, you can’t use ScriptableObjects to save data, but you can use the saved data from the ScriptableObject Assets that you set up during development.
It sounds like you can’t save data into a scriptable object in a deployed build. I haven’t tried this but it’s in the doco!
I would play it safe and stick to a persistent object (dontdestroyOnLoad).
If you want to serialize the data so it can be written to file and loaded up in a different session, I would just use BinaryFormatter.
Better to rely on something that’s native to C# and unlikely to change like Unity’s implementations.
Just in case you don’t know, Serialize basically means convert the object to binary and save to a file, Deserialize loads the file and converts the binary to your object.
This is referring to the automatic persisting of data across runs of the build. With the editor when you hit the play button, modify a scriptable object, and then stop playing the editor’s serialization system will automatically retain this data. When you try to do the same thing with a build it will lose the data because the editor’s serialization system is no longer present.
Basically if you want to persist data across multiple plays of the build then you need to manually serialize it.
Edit: I’ve attached a project below that you can use to see that this does work as I described. Made with Unity 2019.2.
And before it slips my mind yet again you can hook a scriptable object directly into an event (only limitation is you can’t have a scene object in a scriptable object’s event). I know how much you love playing with events. The example project below does this with an input field and the example scriptable object.
This looks good. Just a couple of questions, though:
What would the syntax for adding it to the list look like?
Here is what I’ve tried to do so far in GameData:
public void UpdateShipList()
{
shipDatas.Clear();
ObjectID[] go = GameObject.FindObjectsOfType<ObjectID>();
foreach (ObjectID shipID in go)
{
shipDatas.Add("NOT SURE WHAT GOES HERE");
}
}
FYI, ObjectID is a small script on the NPC Ship that I’m using to hold a unique ID number to identify the ship by. My intention was to grab all the objects that have this script and create an instance of ShipData from that game object to add to the list. I’m not sure if that is the way I should be doing it, but I’ve been doing a lot of googling and am having some difficulty finding information on this particular thing.
I wouldn’t add the object to the list. I would use a class that holds all relevant (and dynamic) data concerning the ship, could look like
public class ShipData
{
public Vector3 position;
public float shieldStrength;
}
In the actual ship class you can refer to that data and change it accordingly. Assuming you use the static approach:
public static class GameData
{
public static List<ShipData> shipDatas = new List<ShipData>();
public static void AddShipData(ShipData newShipData)
{
shipDatas.Add(newShipData);
}
public static void RemoveShipData(ShipData dataToRemove)
{
shipDatas.Remove(dataToRemove);
}
}
Now when entering a scene you could have a class that goes through that list and for every entry creates a ship and of course injects the shipData.
Okay. I think I got what you’re describing to work. Another question I have is, if my GameData class is on a game object that is “DontDestroyOnLoad”, do I still need it to be static?
Also, and I realize this is venturing a little off topic, I can’t get the ShipData fields to show up in the Inspector when the list is generated, despite making both the ShipData class and the list “serializable”. I’ll post my script here. If anyone can tell me what I’m doing wrong, I would appreciate it.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameData : MonoBehaviour
{
[SerializeField]
public List<ShipData> shipDatas = new List<ShipData>();
public GameObject ship;
private void Start()
{
UpdateShipList();
RespawnAllShips();
}
public void UpdateShipList()
{
shipDatas.Clear();
ShipData[] go = GameObject.FindObjectsOfType<ShipData>();
foreach (ShipData shipID in go)
{
AddShipData(shipID);
}
}
public void RespawnAllShips()
{
for (int i = 0; i < shipDatas.Count; i++)
{
GameObject newShip = Instantiate(ship);
ShipData sData = newShip.GetComponent<ShipData>();
sData.shipName = shipDatas[i].shipName;
sData.uniqueID = shipDatas[i].uniqueID;
sData.fuel = shipDatas[i].fuel;
newShip.name = "NPC Ship " + sData.uniqueID;
}
}
public void AddShipData(ShipData newShipData)
{
shipDatas.Add(newShipData);
Debug.Log(newShipData.shipName + " added");
}
public void RemoveShipData(ShipData dataToRemove)
{
shipDatas.Remove(dataToRemove);
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class ShipData : MonoBehaviour
{
public int uniqueID;
public string shipName;
public float fuel;
}
ShipData probably isn’t showing up because it’s still a Monobehaviour, just remove it like in my example aboe (butkeep the Serializable).
Your GameData class isn’t static at all. To use it the way we were suggesting, make the class, fields and functions static, remove the Monobehaviour as well as the Start function. Now you don’t have to attach it to a game object at all, just call the functions from anywhere, e.g. GameData.UpdateShipLists.
Regarding the functions you got in there, I think you’re still not grasping the concept of separating the data and functionality. The point of the GameData class is to store the data, not finding ships, creating them etc.
Okay. So I’ve got ShipData attached to each of the actual ships, which I can’t do if it doesn’t inherit from monobehavior. So I’m guessing you didn’t mean for me to do that. So if that’s the case, what should I be doing with ShipData?
As for “not grasping the concept of separating the data and functionality”, I know. It’s an area that I’ve avoided for a long time now because I’ve always had trouble wrapping my head around it.
And just to clarify, the function that respawns the ships was just in there for testing purposes, but now I get why it can’t be in there.
ShipData can simply be a field in your ship class.
public class NPCShip : MonoBehaviour
{
private ShipData data;
public void Activate(ShipData newData)
{
data = newData;
GameData.AddShip(newData);
}
public void TakeDamage(float damageAmount)
{
data.health -= damageAmount;
}
public void DestroyShip()
{
GameData.RemoveShip(data);
gameObject.SetActive(false);
}
}
And whenever you spawn a ship, make sure to create a new ShipData and assign the reference to the ship:
public class ShipSpawner : MonoBehaviour
{
[SerializeField]
private NPCShip npcShipPrefab;
public void SpawnShip()
{
NPCShip newShip = Instantiate(npcShipPrefab);
ShipData newShipData = new ShipData();
newShip.Activate(newShipData);
}
}
When loading a scene, have the ShipSpawner go through the list GameData.Ships and create a ShipGameobject for each item, inject the data and activate it.
Keep in mind I’m not an expert, there are more elegant ways to do this, but I think overall this is a good starting point…