I've come up with something that works (so far in my tests). I'm fairly certain it is not perfect. So I am sharing it here for two purposes: One, to help out others who find they have the same use case and two, to get feedback and improve it. Who knows, maybe it will even be officially included and supported in a future version (permission to use my code for such purposes granted).
My game is on Steam if you want to study the use case:
So here is what I am doing:
All my buildings are under a single parent. I wrote a saver component for that parent that loops over its children and saves them all. This is based on the SpawnedObjectManager. However, it uses the fact I have this hierarchy structure. First it clears out all children, then it rebuilds them from the save data. Note I also have Peasants living in those houses. This is a pure data class (no MonoBehaviour, no game objects associated) so I need to assign it here.
Here's the code, some more explanations at the end:
Code: Select all
public class VillageSaver : Saver {
public Game game;
public PeasantsInfo peasantInfo;
[Serializable]
public class BuildingData {
public string GUID;
public string typeName;
public string prefabName;
public Vector3 position;
public Quaternion rotation;
public bool isFinished;
public int currentHealth;
public float elapsedConstructionTime;
}
[Serializable]
public class FamilyData {
public string lastName;
public List<PeasantData> membersData = new List<PeasantData>();
public string livesAtRef;
}
[Serializable]
public class PeasantData {
public Peasant peasant;
public string taskRef;
}
[Serializable]
public class VillageData {
public List<FamilyData> familyList = new List<FamilyData>();
public List<BuildingData> buildingList = new List<BuildingData>();
}
public override string RecordData() {
Assert.IsNotNull(game);
VillageData villageData = new VillageData();
foreach (Transform child in transform) {
BuildingController BC = child.GetComponent<BuildingController>();
if (BC != null) {
BuildingData buildingData = new BuildingData();
buildingData.GUID = BC.GUID;
buildingData.typeName = BC.data.name;
buildingData.prefabName = child.name.Replace("(Clone)", string.Empty);
buildingData.position = child.position;
buildingData.rotation = child.rotation;
buildingData.isFinished = BC.isFinished;
buildingData.currentHealth = BC.currentHealth;
if (BC.CB != null) {
buildingData.elapsedConstructionTime = BC.CB.elapsedTime;
} else {
buildingData.elapsedConstructionTime = -1;
}
villageData.buildingList.Add(buildingData);
}
}
foreach (Family f in game.Families) {
FamilyData familyData = new FamilyData();
familyData.lastName = f.lastName;
if (f.livesAt == null) {
familyData.livesAtRef = null;
} else {
familyData.livesAtRef = f.livesAt.GUID;
}
foreach (Peasant p in f.members) {
PeasantData peasantData = new PeasantData();
peasantData.peasant = p;
if (p.currentTask == null) {
peasantData.taskRef = null;
} else {
peasantData.taskRef = p.currentTask.getReference();
}
familyData.membersData.Add(peasantData);
}
villageData.familyList.Add(familyData);
}
return SaveSystem.Serialize(villageData);
}
public override void ApplyData(string m_data) {
if (string.IsNullOrEmpty(m_data)) return;
var villageData = SaveSystem.Deserialize<VillageData>(m_data);
if (villageData == null) return;
// stuff we need
GameObject GCO = GameObject.FindGameObjectWithTag("GameController");
Assert.IsNotNull(GCO, "no GameController found");
GameController GC = GCO.GetComponent<GameController>();
Assert.IsNotNull(GC, "GameControler object has no GameController component");
// clear out the village, so we don't get duplicates
foreach (Transform child in transform) {
BuildingController BC = child.GetComponent<BuildingController>();
if (BC != null) {
Destroy(child.gameObject);
}
}
for (int i = 0; i < villageData.buildingList.Count; i++) {
var buildingData = villageData.buildingList[i];
if (buildingData == null) continue;
Building b = Globals.Instance.buildingsMasterList.Find(x => x != null && string.Equals(x.name, buildingData.typeName));
Assert.IsNotNull(b);
GameObject prefab = b.Prefabs.Find(x => x != null && string.Equals(x.name, buildingData.prefabName));
if (prefab == null) prefab = b.Prefab();
GameObject building = Instantiate(b.Prefab(), buildingData.position, buildingData.rotation, transform);
BuildingController BC = building.GetComponent<BuildingController>();
Assert.IsNotNull(BC);
BC.loadedFromSaveGame = true;
BC.GUID = buildingData.GUID;
BC.isFinished = buildingData.isFinished;
BC.currentHealth = buildingData.currentHealth;
if (BC.Ghost!=null) Destroy(BC.Ghost); // we never save ghosts, so we never need this
if (BC.isFinished == false) {
BC.CB.elapsedTime = buildingData.elapsedConstructionTime;
BC.CB.gameObject.SetActive(true);
}
}
// families and peasants
game.Families = new List<Family>();
game.Peasants = new List<Peasant>();
BuildingController[] homes = GetComponentsInChildren<BuildingController>();
foreach (FamilyData f in villageData.familyList) {
Debug.Log("restoring family "+f.lastName);
Family family = new Family();
family.lastName = f.lastName;
if (f.livesAtRef != null) {
foreach (BuildingController home in homes) {
if (home.GUID == f.livesAtRef) {
Debug.Log("found home of family "+f.lastName);
family.moveInto(home);
}
}
}
game.Families.Add(family);
foreach (PeasantData pd in f.membersData) {
Peasant peasant = new Peasant(family, pd.peasant.Age);
family.members.Add(peasant);
game.Peasants.Add(peasant);
GC.peasantInfo.AddPeasant(peasant);
// re-assign to current task, if any
if (!string.IsNullOrEmpty(pd.taskRef)) {
Debug.Log("looking for task "+pd.taskRef);
string[] parts = pd.taskRef.Split('/');
Assert.IsTrue(parts.Length == 3, "task has not exactly 3 parts: "+pd.taskRef);
if (parts[0] == "x") {
// GameController task
foreach (Transform t in GCO.transform) {
Debug.Log(t.name+" ? "+parts[1]);
if (t.name == parts[1]) {
Debug.Log("found gather task");
TaskController task = t.GetComponent<TaskController>();
task.ReturnToTask(peasant);
break;
}
}
} else {
// some task on a building
bool foundBuilding = false;
foreach (Transform building in transform) {
BuildingController BC = building.GetComponent<BuildingController>();
if (BC != null && BC.GUID == parts[0]) {
Debug.Log("found building "+building.name);
foundBuilding = true;
Debug.Log("finished = "+BC.isFinished+" / CT = "+BC.ConstructionTask);
if (BC.isFinished == false && BC.ConstructionTask != null) {
// building still under construction, so that's the only task we can assign someone to:
Debug.Log("assigning to construction");
BC.ConstructionTask.ReturnToTask(peasant);
} else {
foreach (TaskController task in BC.availableTasks) {
Debug.Log(task.name+" ? "+parts[1]);
if (task.name == parts[1]) {
Debug.Log("found building task");
task.ReturnToTask(peasant);
break;
}
}
}
break;
}
}
if (foundBuilding == false) {
Debug.LogError("couldn't find building for this task!");
}
}
}
}
}
if (SaveSystem.framesToWaitBeforeApplyData == 0) {
ApplyDataToBuildings();
} else {
StartCoroutine(ApplyDataToBuildingsAfterFrames(SaveSystem.framesToWaitBeforeApplyData));
}
}
protected void ApplyDataToBuildings() {
foreach (Transform building in transform) {
BuildingController BC = building.GetComponent<BuildingController>();
if (BC != null) {
foreach (var saver in building.GetComponentsInChildren<Saver>()) {
saver.ApplyData(SaveSystem.currentSavedGameData.GetData(saver.key));
}
}
}
}
protected IEnumerator ApplyDataToBuildingsAfterFrames(int numFrames) {
for (int i = 0; i < numFrames; i++) {
yield return null;
}
ApplyDataToBuildings();
}
}
As you can see, I rely on a class called BuildingController which all of my buildings have. The important part for the saver is that it has this:
Code: Select all
public string GUID;
...
void Start() {
if (string.IsNullOrEmpty(GUID)) {
GUID = System.Guid.NewGuid().ToString();
}
For additional, type-specific data, I have additional controllers on my buildings, so I need additional savers for those. For example, one building type is animal pens. They have only one value worth saving: How much food has accumulated. These savers also rely on the BuildingController's GUID so I overwrite how the key is generated and otherwise it's a fairly straightforward saver and looks like this:
Code: Select all
[RequireComponent(typeof(BuildingController))]
[RequireComponent(typeof(AnimalPenController))]
public class AnimalPenSaver : Saver {
[System.Serializable]
public class AnimalData {
public float foodGain = 0;
}
AnimalData m_data = new AnimalData();
/// <summary>
/// Save data under this key. use BC.GUID + savertype
/// </summary>
public override string key {
get {
BuildingController BC = GetComponent<BuildingController>();
Assert.IsNotNull(BC, "BuildingController not found for field saver");
m_runtimeKey = BC.GUID;
var typeName = GetType().Name;
if (typeName.EndsWith("Saver")) typeName.Remove(typeName.Length - "Saver".Length);
m_runtimeKey += typeName;
return m_runtimeKey;
}
set { } // ignore
}
public override string RecordData() {
AnimalPenController pen = GetComponent<AnimalPenController>();
Assert.IsNotNull(pen, "animal pen not found for saver");
m_data.foodGain = pen.foodGain;
return SaveSystem.Serialize(m_data);
}
public override void ApplyData(string s) {
if (string.IsNullOrEmpty(s)) return;
AnimalPenController pen = GetComponent<AnimalPenController>();
Assert.IsNotNull(pen, "animal pen not found for saver");
m_data = new AnimalData();
var data = SaveSystem.Deserialize<AnimalData>(s, m_data);
if (data == null) return;
pen.foodGain = data.foodGain;
}
}
Code: Select all
\"buildingList\": [
{
\"GUID\": \"1ce06f16-134e-4ece-a526-a5ef467d66ab\",
\"typeName\": \"Chickens\",
\"prefabName\": \"Chicken Pen\",
\"position\": {
\"x\": -18.299999237060548,
\"y\": 0.0,
\"z\": -30.3700008392334
},
\"rotation\": {
\"x\": 0.0,
\"y\": -0.6573619246482849,
\"z\": 0.0,
\"w\": 0.7535750269889832
},
\"isFinished\": true,
\"currentHealth\": 10,
\"elapsedConstructionTime\": -1.0
},
...
{
"key": "1ce06f16-134e-4ece-a526-a5ef467d66abAnimalPenSaver",
"sceneIndex": -1,
"data": "{
\"foodGain\": 7.0
}"
},
I'm pretty sure my code is clumsy and very far from elegant. I'm still not a good C# coder. So here's to hoping that with the help of a few others with similar needs, we can turn this into something cool and generic that works better.
For example, I'm not happy with having to expose foodGain as a public variable just to make saving it possible. I have other buildings that are more complex. There I'm experimenting with having an internal data class, like this:
Code: Select all
[System.Serializable]
public class FieldData {
public int growDays = 0;
public int prevDay = 0;
public float foodGain = 0;
public float water = 0;
}
public FieldData data = new FieldData(); // FIXME: I don't actually want this public, but the saver needs it (or I write get/set functions which is essentially the same