Saving your game
I knew that one day I would have to tackle this problem. When I first started making the game I took some very bad design decisions. One of them being not separating my model end view. This easier said than done, to be honest I still struggle with the concept. But nonetheless if you're curious to see how I achieved it, read on (extremely long post).
So here is a glimpse of my main class which is the Citizens. This is a MonoBehaviour that every villager receives. It contains everything the person needs to know it's sort of the master script for each villager.
//Serializable
base values
public int age;
public float ageCounter;
public string citizenName;
public string lastName;
public string job;
public string currentStateString;
public bool isMale;
public bool isPregnant = false;
public int citizenIndex;
public float hunger = 5.0f; //Hunger level
public float hungerTimer = 200f; //round time for hunger to go up one point
public bool isWorking = false;
private bool isRunner = false;
public float speed = 150.0f;
//Variables
contained on the GameObject (References picked up on instantiate or change
mesh)
public Arrow ArrowScript;
public SkinnedMeshRenderer citizenMesh;
public Animation citizenAnimation{get; private set;}
//Non-Mono
classes most can be serialized directly(instances)
public FamilyNode familyNode;
public StateMachine FSM;
public Inventory tempInventory;
public Attributes attributes;
public WS_Positions.PositionType workPosition;
private CitizenHealth health;
public CitizenOpinion opinion;
public Flags flags;
//MonoBehaviour
attached (set in Start())
public Seeker seeker;
private CitizenHealth health;
//References
to other scripts only - Cannot be serialized
public Building buildScript;
public FieldScript fieldScript;
public HouseScript houseScriptMain;
public WorkScript workScript;
public Transform _myTransform;
Alright, so when the player decides to save the game, there is a lot of stuff I need to save. Here comes the first issue, you cannot serialize a class that derives from MonoBehaviour. I googled for some solution and found out about Data Transfer Objects. These are basically container classes that can be directly serialized. Here comes the CitizenDTO class! (constructor)
public CitizenDTO(CitizenAI cs){
this.position = new SerializableVector3(cs._myTransform.position);
this.age = cs.age;
this.ageCounter = cs.ageCounter;
this.citizenName = cs.citizenName;
this.lastName = cs.lastName;
this.job = cs.job;
this.jobDisposition = cs.jobDisposition;
this.currentStateString = cs.currentStateString;
this.isMale = cs.isMale;
this.isPregnant = cs.isPregnant;
this.citizenIndex = cs.citizenIndex;
this.isNoble = cs.isNoble;
this.hunger = cs.hunger;
this.hungerTimer = cs.hungerTimer;
this.isWorking = cs.isWorking;
this.workPosition = cs.workPosition;
this.attributes = cs.attributes;
this.tempInventory = cs.tempInventory.GetSerializable();
this.opinion = cs.opinion.GetSerializable();
this.family = cs.familyNode.GetSerializable();
this.flags = cs.flags;
this.socialClass = cs.socialClass;
this.isLeader = cs.isLeader;
//We'll come back to this one
this.sFSM = cs.FSM.GetSerializable();
}
Each villager is given a unique index that is stored in a Dictionnary when the game is loaded. So when I load first I restore all the citizens, then load their data back, and finally I restore the different buildings and reassign all the references. For example here's the code that is called when restoring a house:
public void Load(HouseDTO hDTO){
CitizenAI cs;
locationIndex = hDTO.locationIndex;
SaveManager.Instance.RegisterLocation(hDTO.locationIndex, gameObject);
for(int i = 0 ; i < beds ; i++){
int currentIndex = hDTO.residentsIndex[i];
if(currentIndex >= 0){
if(SaveManager.Instance.citizensLookUpForLoad.TryGetValue(currentIndex, out cs)){
cs.houseScriptMain = this;
resident[i] = cs.gameObject;
}
}
}
transform.position = hDTO.position.getVector();
transform.eulerAngles = hDTO.rotation.getVector();
this.inventory = hDTO.inventory;
this.stats.BuildingHealth = hDTO.buildingStats.Health;
if(SaveManager.Instance.citizensLookUpForLoad.TryGetValue(hDTO.buildingStats.ownerIndex, out cs)){
stats.SetNewOwner(cs.gameObject);
}
}
When I save a building, rather than serializing the reference to the citizen that either lives or works there, I simply save it's index. When we load, we get the citizen reference back from that index.
Once every citizen and building has been restored, I can restore the familly trees but I won't go into details of that just yet as I'm not satisfied with the current implementation. It works but I will definitly rewrite that system eventually.
Ok so far I have learned that I can use my own Data Transfer Objects to serialize anything I could possibly want! That's great! So I started testing some cases, saving at different stages and seeing how everyone would perform. It worked perfectly, ... or so I thought.
The action queue - there was one problem. The way my AI works is by iterating over a queue of actions, when an action is done move to the next one, if the queue is empty get a new action queue. I was not saving that queue, so when the game was loaded everyone would just look for something to do.
So I had this field, it was harvest time, Duncan the farmer was happily harvesting some wheat. When he was loaded (60 units of wheat!!) he made it's way to the nearest storage barn. While he was in transit, I saved and then loaded. Not surprisingly Duncan was just chilling around with 60 units of wheat in his backpack. Eventually he tried harvesting some more, realized he was full and started making it's way back to the barn.
It became quite clear that I had to save all the actions in queue to prevent stuff like this. Imagine upon loading your game someone is literally 2 meters away from the barn, then goes all the way back to work before coming back. In a game like this it can throw your entire economy down.
Let's take a look at the StateMachine class (note this is a base class but most of the functionality is kept here, the only difference with the derived class is the AssessNeeds method (Noblemen will not plow the fields for example.)
public abstract class StateMachine {
public CitizenAI cScript;
public List<AIState> Tasks;
public StateMachine(CitizenAI cs){
cScript = cs;
Tasks = new List<AIState>(15);
}
public void DoCurrent(){
if(Tasks.Count > 0){
Tasks[0].StateUpdate();
}
else{
AssessNeeds ();
}
}
public void AddAction(AIState _task){
if(Tasks.Count == 0){
Tasks.Add(_task);
Tasks[0].SetUp();
}
else{
Tasks.Add(_task);
}
}
public void AddActionPriority(AIState _task){
Tasks.Insert(0, _task);
}
public void DeQueue(){
if(Tasks.Count > 0){
Tasks.RemoveAt(0);
if(Tasks.Count > 0){
Tasks[0].SetUp();
}
else{
AssessNeeds();
}
}
else{
AssessNeeds();
}
}
public void ClearTasks(){
if(Tasks.Count > 0){
Tasks[0].Exit();
}
Tasks.Clear();
}
public virtual void AssessNeeds(){}
[System.Serializable]
public class DropItemAtInventoryLocation : AIState, ISerializable {
public int[] ItemsToDrop;
private Iinventory inventory;
private int InventoryIndex;
private CitizenAI cs;
public DropItemAtInventoryLocation(){}
public DropItemAtInventoryLocation(int[] itemsToDrop, Iinventory placeToDrop, CitizenAI _cs){
ItemsToDrop = itemsToDrop;
inventory = placeToDrop;
cs = _cs;
}
#region
AIState implementation
public void SetUp(){}
public void StateUpdate() {
for(int i = 0 ; i < ItemsToDrop.Length ; i++){
int item = ItemsToDrop[i];
int amountToDrop = cs.tempInventory.removeItem(item);
int amountNotDropped = inventory.addItem(item, amountToDrop);
cs.tempInventory.addItem(item, amountNotDropped);
}
Exit ();
}
public void Exit(){
cs.FSM.DeQueue();
}
public void ResetOwner(CitizenAI _cs){
this.cs = _cs;
this.inventory = SaveManager.Instance.GetIinventoryFromIndex(InventoryIndex);
if(this.inventory == null){
Debug.Log("Couldn't locate
inventory script");
}
}
#endregion
#region
ISerializable implementation
public void GetObjectData (SerializationInfo info, StreamingContext context)
{
info.AddValue("items", ItemsToDrop, typeof(int[]));
info.AddValue("locationIndex", inventory.getIndex(), typeof(int));
}
public DropItemAtInventoryLocation(SerializationInfo info, StreamingContext context){
ItemsToDrop = (int[])info.GetValue("items", typeof(int[]));
InventoryIndex = (int)info.GetValue("locationIndex", typeof(int));
}
#endregion
}
The next game I make I will definitely take that into account, it's been a very nice learning experience. I'm sure there are better ways to tackle the problem but I'm actually very proud with what I came up with!
If you have any questions or feedback please let me know!