Unity: saving & loading state for an open world game

In most games, the player is able to save their progress in the game, so that they can continue their saved game at a later time. In the different game genres, there are multiple ways games can be saved. For level-based games (e.g., Candy Crush Saga, Overcooked!), a player’s progress is automatically saved in between levels, e.g., the game remembers that the player has completed levels 1–1 to 2–10. In role-playing games, players may be able to save at almost any time (e.g., Knights of the Old Republic, Mass Effect), or at certain checkpoints (e.g., Final Fantasy VIII). In open-world crafting/survival games, saving the game can be automatic (e.g., Terraria, Forager), when the character goes to bed (e.g., My Time at Portia, Garden Paws), or possibly at any time at all.

Saving and loading the game state of an open world game can be particularly tricky, since the state of the world can vary greatly from player to player, depending on what they have done in the game. For example. one player may have cut down a forest, while another player may have dug a tunnel through a mountain, so their save games have to be generic enough to capture the player-specific differences in their games. Besides the “natural” aspects of the open world, NPCs may also have different states, especially if the player is able to save at any point in the game (whether manually triggered by the player, or automatically). For example, an NPC may be in the midst of patrolling a path, while another NPC may be in the midst of battle.

Mystery Queen allows saving and loading at any time, and we’ll describe how it’s done in this article.

Why did I write this guide? When I was designing the save-load system for Mystery Queen, I spent some time searching online for existing tutorials and articles. While many of them are informative, many typically talk about the concepts that are useful, but not always immediately actionable. The best guide I found was this one that had code snippets and was pretty close to what I wanted. However, I wasn’t a huge fan of having a single class save all the game data; I wanted something a little bit more flexible. Hence, I decided to write my own variant of a save/load system that would work for an open-world game, and share my learnings and code with others!

As always in my Unity tutorials, the Unity scene, assets, and source code for this guide are available for download as a Unity package. You can also access the files on Github. The project and code have been created to be easier to understand over efficiency, so you may want to do some optimizations and customizations for your own project.

Step 0: Setting up the scene in Unity

We’re going to set up a basic scene in Unity that has similarities of open-world games, but without too much of its complexity. The goal is that you can understand the save/load system without worrying too much about the open-world systems themselves. Similar to my previous tutorials, we’re using freely-available sprites from the Liberated Pixel Cup (LPC): flowers from the [LPC] Flower Recolor set, a tree sprite from the [LPC] Trees set, the wolf sprites from the [LPC] Wolf Animation set, and the [LPC] Sara sprites for the main character.

We’ve created a WebGL version of this scene, that you can try here! It may help with reading the text description below.

Objects in the scene and interactions:

  • Sara, the player-controllable character moves around based on keyboard arrow keys, and pressing the spacebar will remove any trees/wolves near her. This removal of trees/wolves are to represent how objects can be harvested/killed/removed in an open world game.
  • The scene starts with some trees and a wolf. Trees can be added by left-clicking during the game with the mouse, and wolves can be added by right-clicking. This addition mechanic of trees and wolves represent how new objects can be crafted or spawned in an open world game.
  • Each wolf patrols back and forth, to represent how NPCs have internal state and possibly a finite state machine.
  • Each wolf will have a reference to a tree (that is randomly selected on creation, but remains static afterwards), and the scene will draw a line from each wolf to its respective tree. These wolf-tree relationships represent how game objects can have references to one another (and/or perhaps the wolves have favorite trees).
  • The flowers are used to represent the static terrain in an open world game, (e.g., grasslands, mountains, etc) that don’t change based on the player’s actions.
Sara watches as trees and wolves spawn around her, then she proceeds to remove some.

The trees are all instantiations of a single Tree prefab, and don’t have any particular differences besides their positions in the world. The wolves are all instantiations of a single Wolf prefab, but they contain a WolfBehavior with some state information: a boolean that indicates whether the wolf is patrolling left or right, a float that indicates how long the wolf will keep patrolling in that direction, and a reference to its favorite tree.

Sara, the main character, comes with a SaraBehavior that moves Sara depending when the arrow keys on the keyboard are pressed, as well as removing any objects within a set radius when the spacebar is pressed. SaraBehavior also handles the mouse clicks and spawns new trees/wolves as necessary when the left/right mouse button is pressed.

In terms of organization of game objects, all the trees and wolves will be under an empty game object called “Dynamic objects”, with the trees in a nested game object called “Trees”, and wolves in a nested game object called “Wolves”. The flowers will be under a different empty game object called “Static objects”. Static objects refer to game objects that don’t change based on the player’s actions (e.g., terrain), while dynamic objects may be created, removed, or changed depending on what the player does. The nested game objects “Trees” and “Wolves” represent how dynamic objects can be organized structurally beyond a single game object.

Step 1: Defining ObjectState to store state info

We will first define ObjectState, a class that contains state information for all dynamic objects in the game. In particular, ObjectState contains a string, that we will initialize with a random guid on construction. ObjectState also contains the name, tag, layer, and position of the game object.

Importantly, ObjectState also contains a list of ObjectState children, which will be used to contain the hierarchy of game objects that we may have. ObjectState also contains a dictionary of strings to objects, that we can use to store any general information.

[Serializable]
public class ObjectState
{
public string guid;
public string objectName;
public string objectTag;
public int objectLayer;
public float positionX;
public float positionY;
public bool isPrefab;
public string prefabGuid;
public string[] childrenGuids; public Dictionary<string, object> genericValues; public ObjectState()
{
if (string.IsNullOrEmpty(guid))
{
guid = CreateGuid();
}
isPrefab = false;
prefabGuid = guid;
genericValues = new Dictionary<string, object>();
}
protected string CreateGuid()
{
return Guid.NewGuid().ToString();
}
}

For instantiated prefabs, we will need to set isPrefab to true, and create a unique guid for each instantiation, which we will handle in later steps. The goal is to have each instantiation have a unique guid, but their prefab guids correspond to the prefab they were instantiated from.

Step 2: Defining DynamicObject, a MonoBehaviour that contains ObjectState

Although we have defined ObjectState, we can’t add it to game objects directly, since it is a basic classes, and not a MonoBehaviour. Hence, we will define DynamicObject, a MonoBehaviour that contains an ObjectState.

public class DynamicObject : MonoBehaviour
{
public delegate void LoadObjectStateEvent(
ObjectState objectState);
public event LoadObjectStateEvent loadObjectStateDelegates;
public delegate void PrepareToSaveEvent(ObjectState objectState);
public event PrepareToSaveEvent prepareToSaveDelegates;
public ObjectState objectState;void Start()
{
if ((objectState != null) && objectState.isPrefab
&& objectState.guid.Equals(objectState.prefabGuid)))
{
// Create a unique guid for each prefab instantiation
objectState.guid = ObjectState.CreateGuid();
}
}
public void Load(ObjectState objectState)
{
this.objectState = objectState;
StartCoroutine(LoadAfterFrame(objectState));
}
private IEnumerator LoadAfterFrame(ObjectState objectState)
{
// Wait for the next frame so that all objects have been
// created from ObjectStates
yield return null;
loadObjectStateDelegates?.Invoke(objectState);
}
public void PrepareToSave()
{
prepareToSaveDelegates?.Invoke(objectState);
}
}

When a DynamicObject is started, it checks if its objectState is a prefab. If so, then it generates a new unique guid for that objectState, so that the guid is different from its prefab guid.

Besides storing an ObjectState, the DynamicObject also stores delegates for loading an object state and preparing to save to an object state. We will explain how these delegates are used in detail later. Essentially, these delegates will override a game object’s internal state from the genericValues in the ObjectState (when loading), and add the game object’s internal state into the ObjectState’s genericValues(when preparing to save).

The Load and PrepareToSave functions invoke their respective delegates, although Load does so in the next frame. The intention of waiting for the next frame is to allow all objects to be created and loaded, before invoking the load delegates to initialize the states of the game objects.

Step 3: Saving ObjectStates

Now that we have defined ObjectState and DynamicObject, we can fill in more functions inside ObjectState that allows it to be saved.

private void PrepareToSave(GameObject gameObject)
{
objectName = gameObject.name;
objectTag = gameObject.tag;
objectLayer = gameObject.layer;
position = SaveUtils
.ConvertFromVector2(gameObject.transform.position);
// Let the DynamicObject fill in more information from the
// game object
gameObject.GetComponent<DynamicObject>()?.PrepareToSave();
List<string> childrenGuidsList = new List<string>(); foreach (Transform childTransform in gameObject.transform)
{
DynamicObject dynamicObject =
childTransform.GetComponent<DynamicObject>();
if (dynamicObject == null)
{
continue;
}
// Recursively tell the child to prepare to save
dynamicObject.objectState
.PrepareToSave(childTransform.gameObject);
// Add the child's guid to the list of children
childrenGuidsList.Add(dynamicObject.objectState.guid);
}
childrenGuids = childrenGuidsList.ToArray();
}

The first function is PrepareToSave, which as its name suggests, prepares to save the ObjectState to a file. It does so by saving the game object’s name, tag, layer, and position into the ObjectState's member variables, and then asking the game object’s DynamicObject component to also prepare to save.

Finally, it recursively calls itself on all the children of the game object that has the DynamicObject component, and then adds the childrens’ guids into its list of children.

public List<ObjectState> Save(GameObject gameObject)
{
// Recursively tell gameObject and its descendants to prepare
// to save
PrepareToSave(gameObject);
// The ObjectState of gameObject and its descendants will be
// saved here
List<ObjectState> savedObjects = new List<ObjectState>();
// Loop through all the children
foreach (Transform childTransform in gameObject.transform)
{
DynamicObject dynamicObject =
childTransform.GetComponent<DynamicObject>();
if (dynamicObject == null)
{
continue;
}
// Save the descendents' object states into a flat list
savedObjects.AddRange(
dynamicObject.objectState.Save(childTransform.gameObject));
}
// Save this object state into the list as well
savedObjects.Add(this);
return savedObjects;
}

The next function is Save, which returns a list of ObjectState containing the states of the given game object as well as all its descendants. It first calls PrepareToSave on the game object (which then recursively calls its children and so forth), and then calls Save on each child, and appends the returned list of ObjectState into its own list. Finally, it add the current ObjectState into the list as well.

Due to the nature of how the list is updated, the descendants deepest in the hierarchy tree are added first (the nodes), then its parents, then its parents, and so forth until the root node is added into the list.

public static List<ObjectState> SaveObjects(GameObject rootObject)
{
List<ObjectState> objectStates = new List<ObjectState>();
foreach (Transform child in rootObject.transform)
{
DynamicObject dynamicObject =
child.GetComponent<DynamicObject>();
if (dynamicObject == null)
{
continue;
}
objectStates.AddRange(
dynamicObject.objectState.Save(child.gameObject));
}
return objectStates;
}

The final function is SaveObjects, which given a root object, iterates through all its children, and for those that contain DynamicObject, it calls Save on their ObjectState and saves the results.

In a nutshell, SaveObjects returns a list of ObjectState corresponding to all the descendants of rootObject but not the root object itself. In our scene, the Dynamic Objects game object will the the root object, as it contains all the dynamic objects in our scene.

Step 4: Loading ObjectStates

Now that we can save ObjectState, we should be able to load it as well. To do that, we shall add more functions inside ObjectState.

public static void LoadObjects(
Dictionary<string, GameObject> prefabs,
List<ObjectState> objectStates,
GameObject rootObject)
{
ClearChildren(rootObject);
Dictionary<string, GameObject> createdObjects =
new Dictionary<string, GameObject>();
foreach (ObjectState objectState in objectStates)
{
GameObject createdObject;
DynamicObject dynamicObject;
if (objectState.isPrefab)
{
// Do we have a prefab with the required guid?
if (!prefabs.ContainsKey(objectState.prefabGuid))
{
throw new InvalidOperationException(
"Prefab with guid " + objectState.prefabGuid
+ " not found.");
}
// Instantiate the prefab at the specified position
createdObject = UnityEngine.Object.Instantiate(
prefabs[objectState.prefabGuid]);
// Find the DynamicObject component and set the object state
dynamicObject = createdObject.GetComponent<DynamicObject>();
}
else
{
// Create a new object
createdObject = new GameObject();
// Add a SaveableObject component and set the object state
dynamicObject = createdObject.AddComponent<DynamicObject>();
}
dynamicObject.Load(objectState); // Find and add the children
foreach (string childGuid in objectState.childrenGuids)
{
if (!createdObjects.ContainsKey(childGuid))
{
Debug.Log("Cannot find child with guid " + childGuid);
continue;
}
createdObjects[childGuid].transform
.SetParent(createdObject.transform);
}
// Set the object's name, position, etc, and attach it to
// the root (it can get attached to a different parent later)
createdObject.name = objectState.objectName;
createdObject.tag = objectState.objectTag;
createdObject.layer = objectState.objectLayer;
Vector2 position = SaveUtils
.ConvertToVector2(objectState.position);
createdObject.transform.position =
new Vector3(position.x, position.y, 0);
createdObject.transform.SetParent(rootObject.transform); // Save the object into the dictionary
createdObjects.Add(objectState.guid, createdObject);
}
}
private static void ClearChildren(GameObject root)
{
foreach (Transform child in root.transform)
{
UnityEngine.Object.Destroy(child.gameObject);
}
}

The first thing we do when loading state, is to clear any children from the root object. It is relatively straightforward to do so — we can call Destroy on any of the children game objects. In our scene, Dynamic Objects would be the root, and ClearChildren would destroy any existing dynamic objects in our scene. That’s perfectly alright, because we will load and create new game objects shortly.

LoadObjects takes in 2 parameters — a map of prefabs, as well as a list of ObjectState. The prefabs are used to instantiate any prefab objects that were saved, and the list of ObjectState contains all the objects to be loaded and (re)-created.

Essentially, if the ObjectState has isPrefab set to true, then a prefab is instantiated. Otherwise, a new game object is created. In either case, a DynamicObject component is found/created, and then the Load function of the DynamicObject is called. Afterwards, any children of the game object are found and updated, so that their parent is this newly created game object / prefab instantiation. Since the list of saved ObjectState is from the leaf of the hierarchy up to the root, we are guaranteed that children of a game object are loaded before the game object itself. Hence, it should always be possible to find the children guids in the map createdObjects.

Finally, we update the game object’s name, tag, layer, and position with information from the ObjectState. For now, we will add the game object as a direct descendant of the root, and add the game object into a map of createdObjects . If the game object contained a different parent when it was saved, then when the parent is created, this game object’s transform will be updated to reflect the saved parent.

Step 5: Saving/loading game object states

You may be wondering what saving/loading game object states means, and how it is different from saving/loading ObjectState. Recall that the WolfBehavior has some internal state: whether the wolf is heading left, the amount of time to head in its current direction, and its favorite tree. While the ObjectState contains information about the game object’s name, tag, layer, and position, it does not yet contain information about any particular behaviors/components in the object! We still need to add saving/loading to the behaviors themselves.

To do so, we will make use of the load and prepare-to-save delegates in the DynamicObject component.

void Start()
{
// Other initializations not shown
DynamicObject dynamicObject = GetComponent<DynamicObject>();
dynamicObject.prepareToSaveDelegates += PrepareToSaveObjectState;
dynamicObject.loadObjectStateDelegates += LoadObjectState;
}

First, in the Start function, we will register two private methods as delegates of the DynamicObject component.

private void PrepareToSaveObjectState(ObjectState objectState)
{
objectState.genericValues["WolfBehavior.speed"] = speed;
objectState.genericValues["WolfBehavior.timeToWalkRemaining"] =
timeToWalkRemaining;
objectState.genericValues["WolfBehavior.walkingLeft"] =
walkingLeft;
if (favoriteTree != null)
{
objectState.genericValues["WolfBehavior.favoriteTree"] =
favoriteTree.GetComponent<DynamicObject>().objectState.guid;
} else if (objectState.genericValues
.ContainsKey("WolfBehavior.favoriteTree"))
{
objectState.genericValues.Remove("WolfBehavior.favoriteTree");
}
}

When preparing to save, the speed, time to walk, and walking direction are stored into the ObjectState's genericValues with unique keys. The wolf’s favorite tree is a reference to a game object, so we should not save that directly. Instead, we will save the guid of the game object’s ObjectState, and restore the reference to a newly created/loaded game object when the game state is loaded.

private void LoadObjectState(ObjectState objectState)
{
speed = Convert.ToSingle(
objectState.genericValues["WolfBehavior.speed"]);
timeToWalkRemaining = Convert.ToSingle(
objectState.genericValues["WolfBehavior.timeToWalkRemaining"]);
walkingLeft = Convert.ToBoolean(
objectState.genericValues["WolfBehavior.walkingLeft"]);
if (objectState.genericValues
.ContainsKey("WolfBehavior.favoriteTree"))
{
favoriteTree = SaveUtils.FindDynamicObjectByGuid(
Convert.ToString(objectState.genericValues
["WolfBehavior.favoriteTree"])).gameObject;
}
}

The logic to load an object state is the reverse of saving. The WolfBehavior's variables are reset with the values in the ObjectState. The caveat is for the wolf’s favorite tree. Since its favorite tree will also be newly instantiated, we will need to find the game object from the ObjectState's guid. To do so, we use a helper function FindDynamicObjectByGuid.

public static DynamicObject FindDynamicObjectByGuid(string guid)
{
DynamicObject[] dynamicObjects = GetRootDynamicObject()
.GetComponentsInChildren<DynamicObject>();
foreach (DynamicObject dynamicObject in dynamicObjects)
{
if (dynamicObject.objectState.guid.Equals(guid))
{
return dynamicObject;
}
}
return null;
}

Finding a DynamicObject by guid is pretty straightforward. Given the root object (the Dynamic Objects game object in this scene), all DynamicObject components in its descendants are searched until the one with the correct guid is found.

How do we ensure that the DynamicObject and its ObjectState has been created when we want to restore game object references? Recall that in DynamicObject's Load function, the delegates are not invoked until after a frame. This ensures that all the ObjectState are loaded, and relevant game objects have been created / instantiated from their prefabs. Thus, when the delegates run, all saved objects exist and can be found by their guids.

Step 6: Saving/Loading the game

Now that each wolf can be saved and loaded correctly, we need a way to save the entire game. Besides saving wolves and trees, Sara’s state needs to also be saved. We will omit the code in this article since it is very similar to saving/loading the wolf; you can refer to the code in our Github page.

To save/load the entire game, we essentially have to get the list of ObjectState for all the dynamic objects under the Dynamic Object game object, and save it to a json file (or your preferred format). We used Json.Net to write and read json files, and there’s a free Unity asset you can use for it.

using Newtonsoft.Json;private static void WriteJson<T>(string path, T obj)
{
string json = JsonConvert.SerializeObject(
obj, Formatting.Indented, jsonSerializerSettings);
File.WriteAllText(path, json);
}
private static T ReadJson<T>(string path)
{
string json = File.ReadAllText(path);
return JsonConvert.DeserializeObject<T>(
json, jsonSerializerSettings);
}

And that’s it! With some helper functions (not shown here but available on our Github page), we can save and load the entire game state!

How we did it for Mystery Queen

In Mystery Queen, we largely used the same framework to save and load the game state. The main differences were the parameters saved by the components of the NPCs and other prefabs. In addition, besides the dynamic objects and Elise (the main character), Mystery Queen also has a global game state object (that is non-dynamic) that stores the state of quests and such, so the game state needs to be saved and loaded as well.

We currently save the game state as json files for easier reading/debugging, but in the final version we will likely switch to a binary format.

I hope you enjoyed this guide on how to save and load an open world game! Stay tuned for more Unity-related guides as we discuss other systems that we developed (and are developing) for Mystery Queen. If you’re interested, you can also read my guide on creating a day-night cycle linked below:

Proud mom, roboticist, software engineer.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store