Once again, it’s been a long time since the last post. But I’ve been working on many things lately, including (but not limited to):
- the integration of the new UI of unity for the HUD (= Head Up Display = the information that is displayed in Play Mode), the Link Menu and the World Map. Graphically, this is not very different from what was already there but it should allow much more visual animations and fancy stuff in the future.
- the possibility to control the UI with the joypad and the keyboard/mouse combo (including the mouse wheel). Not perfect yet, but quite OK.
- some polish, on Zephyr Island mainly
- and a new saving system… which is what I’m going to discuss today !
Beware, this post is quite long and a bit technical!
I – A new saving system? Why? What’s the point?
The goal of setting up a new save system is to get rid of the old-school “save points” where the player must save at specific places and times. This breaks the flow of the game, and furthermore, the player might forget to save and then might lose its progression within the game, which could be very annoying.
Until now, it was this kind of system that was set up, mainly because it was easier to design and I didn’t really have the time to propose something else.
But, nowadays, in recent videogames, saves are really almost automatic, and the player might not even notice that the game is saving (very often a little icon appears in a corner of the screen to tell you that the game is being saved). This is what I want for my game, but it rises many questions about design, software architecture and real-time performances.
The little gear icon on the lower left corner indicates that the game is being saved.
II – How should it work?
Usually, before coding stuff, I try to think about how it should work in different cases (this is more or less a “use-case” approach). Here are a few important points:
- the player starts a new game. What should happen? Ideally, I would like to ask him for a pseudo/nickname/id, and then save the game using this information. For now, I don’t ask him anything, I just create a new slot (based on day and time) and use it to save the game (because I don’t have time to make something better than this, but it should be corrected in the future)
- the player is in the game. The game should be regularly saved on the current slot. The thing is, there is not necessarily a current slot. “WHAT?” might you ask. Indeed, a new slot is created for each “new game” launched. But when *I* test the game, I don’t test it from the start EVERY TIME. Generally, I run a level with some modified variables about “where and when” I am in the world and in the story and I test it. This means I don’t have a specific slot associated with my test. Then where should I save? Should I save? Can I save? What if I want to test the save system?
- the player wants to load a game. I should list all the previously saved games with various info about each slot: what level was it? how many items did the player have? How many quests were finished? Maybe a screenshot to remind the player of the place? Surely the pseudo/id of the player etc…
Of course, every time I’m saving the game, I don’t want the player to notice any lag in the game. But, sadly, when working with files on a hard drive, things happen quite slowly (in comparison, say, to how many frames a graphic card can handle per seconds). Therefore, I must use threads so that the saved game can be written on the disk “in the background” while the game still runs smoothly (hopefully).
III – How to make it work?
1 – the basic class
Until now, I had a simple class/structure mainly based on .NET dictionaries to save every important aspects of the game:
- the player info: life, items, ammo, location, state, …
- the collectibles and quest info: what has been done or asked, what has been collected, how many times, where…
- the gameplay mechanics info: which door is open, which switch has been connected to what, location of moving platforms, …
- the enemies info: location, state, …
- npc and dialog info: what has been said to who, basically
- world info: current level, day time, played time, …
The idea was to NOT save the whole state of the game, but rather what is TRULY necessary to reload the game, which should result in light-weight saves, and fastest loading times.
Of course, all this info must be accessible from anywhere at anytime, that’s why I use some kind of Singleton class to store them (well it’s not that simple, but that’s the basic idea).
2 – The higher level structure
So, now that that we know WHAT I should save, let’s see HOW I can save it. To answer all the problems and objectives listed in II, I chose to have:
- LEVEL 0 saves, that is to say, saved files on the HARD drive
- LEVEL 1 saves, that are the cloned versions of the HARD drive, but in RAM (ie variables in the code)
- a unique LEVEL 2 save, which is the save currently modified by all the objects in the game, in RAM of course.
The idea is to have many layers of saved games, each one communicating ONLY with the layers immediately above or below, depending on their main purpose and on their speed.
The real-time game engine always works with the LEVEL 2 save. This one is unique. From time to time, and when conditions are OK (the player is not fighting, he’s in a safe zone, he’s not in a “save forbidden zone”, …), I make a full copy into one of the current LEVEL 1 slots. And then, if the game is not already writing on the disk, and conditions are OK (last save has been done more than X minutes ago), I copy the LEVEL 1 save data on a file on the disk (LEVEL 0). The data is saved on the disk via basic .NET binary serializers.
Of course, to copy data into LEVEL 1 and LEVEL 0 slots, I must know which slot to copy to; By default, there are no current slots. In this case, no copy is done. If the player has started a new game, a new slot is created and it will be considered as the current one. If the player has loaded a game, this slot is now the current one. And if I’m in test mode, I can create a new slot at anytime with a magic hidden key, and from then, this slot will be the current one. This seems to answer all the cases I previously thought of in II.
A few additional info:
- The LEVEL 2 save is the one that is constantly read and written from all the objects during the game; so that any object can know if a dialog has been said, if a quest has been completed, or if an item has been unlocked. It’s fast to read and to write on it: it’s a (not that) simple variable in the RAM. It’s the up-to-date information.
- LEVEL 1 saves are some kind of stencil saves between the hard drive ones (LEVEL 0) and the living one (LEVEL 2). They are a copy of the LEVEL 2 save that I make from time to time. It’s mainly useful because saving on the hard drive is SLOW. This means that I might be saving on the hard drive WHILE the game (that is still running) is modifying the saved data which creates a concurrent access. This is absolutely forbidden if I want to keep the data integrity! If I don’t have this “middle ground” save data, I might totally break the game state in the game and also on the drive… thus… LEVEL 1 saves.
- LEVEL 0 saves are files on the disk. They are written from or read into the LEVEL 1 save buffer, and it might last many frames.
Of course, with this system, it’s also possible to save at specific points (when the player quits, finishes a quest, opens a menu, etc..). It’s quite permissive actually.
3 – What about loading?
For now, I’ve only talked about the saving process here, because it’s the most complicated one. The loading process is quite straight-forward; it consists in reading all the LEVEL 0 slots on the hard drive and populating the LEVEL 1 slots with the data during the game launch. When the player loads a slot, I copy the LEVEL 1 data to the LEVEL 2 data and load the corresponding level in Unity.
You might wonder if this is not too heavy in memory, but each save slot should be only a few 10 000 entries of string data. With a quick computation, this results in a few 100 KB in memory for each save. My biggest tests with hard drive data size led me to maximum 2 MB, which is quite OK I think.
The new loading screen. Basic, but functional.
4 – Performances
You might have already spotted it, the writing on the hard drive is slow. So copying LEVEL 1 saves to LEVEL 0 is slow. But that’s OK, because I designed it as a threaded process and it doesn’t freeze the game. During this process, I display the little “gears” icon in the lower left corner on the screen to tell the player that the game is being saved. At the beginning of the game you will barely notice it, because it’s really fast (and there’s almost nothing to save). However, Unity is not thread safe, which means I can’t access any Transform or MonoBehavior info. So I had to carefully design saved data to avoid using unity types and functions.
But another problem is copying data from LEVEL 2 to LEVEL 1: this MUST be a DEEP copy of the data, because otherwise, we could still have the same problem of concurrent access (due to the class type of many C# variables; this is pretty not intuitive to grasp so don’t hesitate to google it if you want to learn more about it). Sadly, deep copies are slow, and these ones can’t be threaded (well at least not very easily) because I copy from the LEVEL 2 data which is “alive”! It starts to get noticeable around 20 000 entries for my dictionaries, which might be OK for the whole game, but I’m not sure yet. I have to do additional tests.
However, if the future tests reveal that this LEVEL 2 to LEVEL 1 copy is too slow, I might have another workaround in mind, which I won’t detail here because this article is already too long.
IV – So what now?
So now, I hope you have a good overview of the saving system in my game. I know it’s not perfect, and I know it will break the game during the near future because it hasn’t been tested enough, but it’s really nice to be able NOT to think about it when you play. The experience is much more immersive.
A few potential problems are still there:
- the saves are not clearly identified, so it is difficult to find yours if there are many ones, but I hope I’ll fix this soon (even if this means coding a full virtual keyboard for people playing with a joypad…)
- I don’t have screenshots of the saved games (whereas I had them in the previous versions) because I changed the save system… Previously, I took a screenshot one frame before opening the save menu which caused a micro lag that was NOT visible because the menu appeared right after it. But now I constantly save the game and a micro freeze is awfully noticeable when you are freely running in the game. This is quite annoying, and the solution is not that easy; I might have to redesign the whole rendering pipeline to render the final image into readable texture so that I can access it at anytime, but even there, there might be some lagging, due to the copying of the image data. Plus the encoding itself might be slow.
- I would like to add some LEVEL -1 saves, which would be some hard drive backup of the previous save. Why? Because this save system is quite new, and I’m pretty sure some player will meet cases where the system breaks the saves and they will lose their last game. A workaround is to make backup copies regularly, so that they won’t lose the whole game.
So finally, I have quite an operational new saving system for the upcoming Toulouse Game Show this week end, which was my main objective. By the way, don’t hesitate to come and meet me there if you want!
I hope this article was not too heavy or technical or boring, but … I doubt it Hopefully it might help somebody out there.