Do you have any other components you want to restore?

In this lecture we restored the health component. But are there any others that you have added with state you want to restore?

There are a few components already in need of restoration… one that immediately comes to mind is the AI Controllers for any enemy that has a patrol path. When you reload a scene, if an enemy was on point 3 in the path, he’ll walk back from where he started to point 1. This could, of course, be rectified by simply having the enemy iterate through the paths and select the one that is the closest, but it would be much easier to just save the current progress in the path and then restore it.

Going forward, as our enemies and players get items or things with charges (I like to have a system where as you attack you build up energy that can be used in a super attack, much like FFVII’s Limit Break), you would need to store those items…

During the interval as I was waiting for the lectures, I expanded this saving system a teeny bit… In many cases, our saving system will need to store more than a single float or integer… and it’s entirely possible that we may add things later that we realize need to be saved… making old save files quickly incompatible with new ones. (An example… as the character level’s up, his maximum hit points may change, so it might be prudent to save both the Max health and the current health… Initially, I created custom classes for this… but I quickly realized I was going to be writing custom classes for every component out there… what makes more sense is a single custom class that can A) handle any data type that might need to be saved, and B) be backwards compatable with old save files…

It turns out that Sam has already pointed us in the direction of just such a class in the outer wrappers of the save file… a Dictionary<string, object>… In this case I made a class with just this object in it, along with some helper functions to make life a little easier…

     [System.Serializable]
public class SaveBundle: object
{
    Dictionary<string, object> map = new Dictionary<string, object>();

    public SaveBundle GetSaveBundle(string key)
    {
        return map.ContainsKey(key) ? (SaveBundle)map[key] : new SaveBundle();
    }

    public void PutSaveBundle(string key, SaveBundle bundle)
    {
        map[key] = bundle;
    }
    //When you assign a value to a parameter, it becomes an optional parameter, so GetBool("x") is the same as GetBool("x", false)
    public bool GetBool(string key, bool fallback = false)
    {
        //Using a C# trick, this is equivilant to if(map.ContainsKey(key) {return (bool)map[key];} else return fallback;
        return map.ContainsKey(key) ? (bool)map[key] : fallback;
    }

    public void PutBool(string key, bool val)
    {
        map[key] = val;
    }

    public int GetInt(string key, int fallback = 0)
    {
        return map.ContainsKey(key) ? (int)map[key] : fallback;
    }

    public void PutInt(string key, int val)
    {
        map[key] = val;
    }

    public float GetFloat(string key, float fallback = 0.0f)
    {
        return map.ContainsKey(key) ? (float)map[key] : fallback;
    }

    public void PutFloat(string key, float val)
    {
        map[key] = val;
    }

    public string GetString(string key, string fallback = "")
    {
        return map.ContainsKey(key) ? (string)map[key] : fallback;
    }

    public void PutString(string key, string val)
    {
        map[key] = val;
    }

    public Vector3 GetVector3(string key)
    {
        return GetVector3(key, new Vector3(0, 0, 0));
    }

    public Vector3 GetVector3(string key, Vector3 fallback)
    {
        if (map.ContainsKey(key))
        {
            SerializableVector3 temp = (SerializableVector3)map[key];
            return temp.ToVector();
        }
        else
        {
            return fallback;
        }
    }

    public void PutVector3(string key, Vector3 val)
    {
        map[key] = new SerializableVector3(val);
    }

    public Quaternion GetQuaternion(string key)
    {
        return GetQuaternion(key, new Quaternion(0, 0, 0,0));
    }

    public Quaternion GetQuaternion(string key, Quaternion fallback)
    {
        if (map.ContainsKey(key))
        {
            SerializableQuaternion temp = (SerializableQuaternion)map[key];
            return temp.ToQuaternion();
        }
        else
        {
            return fallback;
        }
    }

    public void PutQuaternion(string key, Quaternion val)
    {
        map[key] = new SerializableQuaternion(val);
    }
}

Now my savesystem passes a SaveBundle instead of an object in the ISaveable functions (I actually put this code in the same file as the ISaveable).
So the Health class would look like this:

 public SaveBundle CaptureState()
 {
       SaveBundle bundle = new SaveBundle();
       bundle.PutFloat("MaxHealth", maxHealth);
       bundle.PutFloat("CurrentHealth", currentHealth);
  }
 public void RestoreState(SaveBundle bundle)
 {
      maxHealth=bundle.GetFloat("MaxHealth",maxHealth); //The 2nd parameter is the default if the key doesn't exist
      currentHealth=bundle.GetFloat("CurrentHealth", currentHealth);
      if(isDead) InvokeDeath();
 }

Because I included a fallback mechanism… suppose that at a future date I added another statistic to health (I know, unlikely, I’d probably factor bonuses into a different class), I can simply ask the bundle for the info with a fallback value if it doesn’t exist in the bundle.

I know this reply is long winded… the current new structure of the forums leaves me baffled on how to add this information in it’s own post that would be easily findable by somebody researching this lecture.

2 Likes

Saving multiple different properties of the same component is the main thing that I also thought was missing from the lectures (I’ve found the Saving section as a whole really good and interesting), and this is a good way to do it that I hadn’t considered. I came up with a different way of doing it, which I’ll also share here so other people can just see other options.

Rather than using a dictionary which just stores whichever keys/fields you need, I created a private struct inside any ISaveable class which needs to save multiple things. Then, for example, you get things that look like the following:

public class Character : MonoBehaviour, ISaveable
{
    //other code
    
    public object CaptureState() {
        return new SaveStateData(transform.position, transform.eulerAngles);
    }
    public void RestoreState(object state) {
        var saveState = (SaveStateData)state;
        transform.position = saveState.position.ToVector();
        transform.eulerAngles = saveState.rotation.ToVector();
    }

    [Serializable]
    private struct SaveStateData {
        public SerializableVector3 position;
        public SerializableVector3 rotation;

        public SaveStateData(Vector3 position, Vector3 rotation) {
            this.position = new SerializableVector3(position);
            this.rotation = new SerializableVector3(rotation);
        }
    }
}

The SaveStateData struct isn’t visible anywhere else, so any other class that needs one can define its own version, with different properties in the struct. You also don’t have to use a constructor for the struct like I did here, you could just create a new SaveStateData() and set each of its values individually during CaptureState().
Additionally, it keeps any classes where you only need to save one thing nice and simple - you can still just return the currentHealth float out of the Health component if that’s all you need, without needing to worry about any of this extra complexity.

1 Like

That was actually my original setup… with a HealthBundle in health, a PositionBundle, etc… and there’s no reason not to do it that way if it works for you.
I worked up the SaveBundle for myself when I realized exactly how much information I was going to be saving when I get my 5v5 battle game going… (if you look at a typical 5v5 like Marvel Strike Force or Juggernaught Wars, you’ve got character level, skill levels, collectible gear, gear tiers, experience, soul fragments, Star levels, etc… and all of these things are added together to get the actual stats like attack power, hit points, defense, magic resistance, etc…) When I started to port the save system into my existing 5v5 project I came up with 8 different data structures. That’s why I went ahead and created the SaveBundle.

Awesome work guys! Thanks for posting your solutions. I do have dictionary based saving in the prototype but I figured I could introduce that stuff when we need it.

Very true, I hadn’t considered this but it makes sense.

Be careful not to worry about this too much during development. Backwards compatibility can be a really burden. You probably only want to make sure saves stay compatible once the game is launched and you have real players who’s progress shouldn’t be lost.

One issue with that might be that if the player lured up a guard from their patrol path to another guard’s path and then managed to get out of range and save before the guard was back on their own path, you might end up with having two guards patrolling the same path where there was only one, and the other path being unoccupied.

But then it might be some situation you might want to have for the player to be able to do by luring some too strong guard away from where they’re blocking some path and sneaking out of view and then returning from another portal and having free access to whatever that guard was guarding before… :thinking:

If that was a design choice for a game mechanic one might want to have some properties on the guards to be able to set this up on purpose, though…

Proofreading my pre-TA days, I see. LOL.

I had actually meant iterate through the points and select the one that’s closest, which is, as it turns out, exactly what I do now when returning to a Patrolling state. :slight_smile:

Privacy & Terms