Simple Level Save and Load System (Unity Editor)

One of the best things about game jams is the creativity that they require or even force. With such little time to make a game the developer has to get creative to save time. In my case with “Where’s My Lunch?” creating levels was a big bottleneck!

Creating individual scenes for each level was out of the question and frankly, I’m not convinced it would be a good plan even if I had more time. What happens if I need to change the UI? What if I want to redesign the layout? Sure I could use prefabs, but how much easier would it be to just have one scene and then load in the parts for different levels? No juggling scenes. No “scene manager.” Just simple data files with all the needed info for a given scene.

Yes! That’s the way I want to do it.

And that’s how the level save and load system for “Where’s My Lunch?” was created. Now to be SUPER CLEAR, the basic setup does NOT save data during runtime. Not even close. This can only save data in the editor. But! It can load data both in the editor and during runtime. Which for my purposes made it pretty perfect and I suspect pretty perfect for lots of projects.

Looking for a save/load system that works at run time? Stay with me. I use the asset “Easy Save” to convert my system rather painlessly to do just that!

The overarching idea is that the scene is made up of various gameObjects (duh!). The code on each object stays the same from level to level and the only real difference between levels is the positioning and the number of objects…

Which I think maybe true of an awful lot of games.

The Big Idea

Save+Level+Object.jpg

So all the system needs to do is record the position and the type of object. The data for each object then gets stored in a list. This list can be easily accessed to load the level whenever we need it. If that list then gets stored on a scriptable object we’ll get a nice project asset for each level in the game.

Pretty simple and pretty slick!

Or at least I think so.

Finding and Saving Objects

To keep things easy I created a monobehavior that can go on every scene object that needs to be saved or loaded. The class has just one field, which is an enum that identifies what type of object it is. This enum will be used by the loading function to locate the correct prefab.

Level+Manager+Collapsed.jpg

Then all that needs to happen is to search the scene for that type of object and we get a list (array) of objects to be saved. This all happens in the “Level Manager” class. Or specifically in the “Save Level” function.

Do note that this function is never called because I’m using Odin Inspector to create buttons that can be used from the editor. If you haven’t heard of Odin (full disclosure, I work for them) it’s a pretty amazing tool and in this case, is saving me a lot of time by not having to create a custom inspector script!

The save level function from the “level Manager”

The save level function from the “level Manager”

If you don’t have Odin and don’t want to create a custom inspector you can always use the old trick of using a boolean and checking the value of the boolean in the update function. Use this in combination with the “Execute In Edit Mode” attribute and you can get similar albeit clumsier functionality.

It’s also worth noting that In the “Save Level” function we clear the level before saving it. This ensures that we only save one copy of the level and don’t duplicate objects in our list.

After that, it’s just a matter of iterating through the list (array) of Save Level Objects and adding them to the list on the current game level.

Storing Objects

Once the objects have been found we need a place to keep them. And that happens in the “Game Level” class.

The Game Level class is a scriptable object which means that the Game Level objects will be project assets and that makes them easy to handle in so many ways.

Game+Level.jpg

If you aren’t familiar with SO’s check out this video I made for the Odin channel. It’s pretty good if I don’t say so myself.

The game level object is pretty simple. It has a list of “level object info” that will contain an enum of the object type and the position of that object.

There are also two important functions. The first clears the list of objects, this was used in the “Save Level” function on the level manager. The second is to add a level object. This creates a new level object, injects all the needed data to the class constructor and finally adds the level object to the list of level objects.

Level Object Info.png

The last two additional functions are specific to WML and just provide an easy way to get the position of the sandwich and the player.

If we take a look at the “Level Object Info” class, we’ll see a constructor that takes in the “Save Level Object” (which is a monobehavior) and stores the needed information. Do note that there is some special handling of the portal or teleporter - this is due to the structure of the prefab and that the portals come in pairs that are parented to a third “portal pair” object.

Also note that the Level Object Info class is NOT a monobehavior. This class exists only in the list on the scriptable object.

Loading Objects

Save Level Prefab.png

Up to this point, things aren’t too complicated. Sure there are a handful of similarly named classes and you could definitely criticize my naming, but the big idea isn’t too tough to understand. And when it comes to loading the objects, it’s still not too bad but there are a few additional complications.

The biggest issue is to link or connect the object type with a prefab. To do this I wanted a strongly typed connection, not some janky switch statement or block of if statements that are based on the wall objects being the third prefab in a list and the bomb is fifth…

No to that on so many levels! That will 100% for sure break.

It’s going to take a little effort to make this work.

I created yet another class that will contain the enum for the type of object and a reference to the corresponding prefab. This class is the “Save Level Prefab” class.

You can see a list of this type above in the Level Manager. This list will hold references to the prefab that corresponds to the level object type.

You can populate this list manually or (again) if you have Odin Inspector you can use some of their magic to populate it automatically, but I’m going to skip that part for the sake of clarity as it doesn’t really add to the overall concept.

Load Current Level Function.png

So at this point, we have a list that connects the “level object type” to a correct prefab. All we still need to do in order to load the level is to it iterate through the list of “level object info” and instantiate the prefabs.

So, in the “Load Current Level” function we double check that the current level isn’t null, clear the current level (i.e. the scene), and then iterate through the list of level object info that is saved on the scriptable object. For each level object info, we then iterate through the list of save level prefabs to get the corresponding prefab.

Now, there is certainly some inefficiency to this method. I think creating a dictionary might be a better idea - since dictionaries are faster at lookup than lists. The dictionary could use the “level object type” enum as a key and the object prefabs as the value. The hitch to this plan is that dictionaries are serialized by Unity. However! Dictionaries can be serialized with Odin Inspector which means you can add prefabs in the inspector ;) I might make this change down the road, but for now, the inefficiency isn’t a problem as a few extra milliseconds to load a level won’t be noticed and levels might at worst have a couple of hundred objects. So if it’s not broken I’m not going to break it.

Also using an object pooling solution could also improve performance, but once again this is happening once we level and not while the player is trying to do anything so a small lag spike isn’t currently an issue I need to design around. If it does become an issue, it should be easy to bolt on a system.

Load Level Function.png

Extra bits

You may have noticed that there are two other functions in the level manager. A load level function and a clear level function.

The first is a public function and can be called to change the “current level” and then load that new current level. Nothing too fancy.

Clear Level Function.png

And the second simply clears the current level by iterating through all the Save Level Objects in the scene and destroying them.

Runtime Save = Easy Save

The system as is works really well. This is if you are doing all your saving and loading in the editor - which for the game jam I was doing exactly that.

But I want to go further than that. I want to allow players to design and share levels via the Steam Workshop. This means I need a way to save data at runtime and scriptable objects can’t do that - they can appear to, but trust me they won’t work.

I purchased Easy Save from the asset store a few years back and while it might be overkill for this project - it’s really easy to use and saves all the data to an external file AND will work at runtime so it’s ticking all the boxes.

Custom Types are easily added to Easy Save which allows all of the fields of a class to be saved. Data is saved by type and with a key to look it up later. It’s possible to save an entire class, i.e. the Game Level scriptable object, which is going to make things surprisingly easy! Adding custom types is done in the Easy Save editor window.

Notice the 1 line save functionality!!

Notice the 1 line save functionality!!

With that done it’s simply a matter of creating an instance of the Game Level scriptable object and populating the fields with the correct data (hidden in the WML specific data region). Then in one line, we can save the Game Level data to a file of our choosing!

Seriously! If that’s not worth the money for Easy Save? I’m honestly not sure what is?

Easy Load

Could it be easier?

Could it be easier?

Loading the data back into the game is a slight bit tougher, but still not hard. We need to check that the files exist, if not then we stop what we’re doing.

If the file does exist we check for the “level data” key and if that exists we can clear the level, load the data into the game level object and then simply call the same “Load Level” function we did before.

Note: The last line of the function is for legacy support since I made some significant changes to the save system and there are a few levels already on the Steam Workshop ;)