Revolutionizing Saves: The Journey From Mediocre to Magnificent
The case that we’re about to explore is none other than the extraordinary and critically celebrated The Star Named EOS. In this discussion concerning console porting, we will focus specifically on the methods we discovered to adapt the save system. Without further ado, let’s dive right in..
In The Star Named EOS, the map operations are executed directly and synchronously without any asynchronous interactions. This is quite an exhilarating challenge that presents many obstacles. For example, the PlayStation 5 operates with saves via memory mounts, which does not occur instantly. The crucial point is that saves can only be completed by unmounting the memory space. In reality, for PlayStation, one might use PlayerPrefs, which functions similarly to memory mounts, but all of this takes place behind the scenes, beyond our control. Nonetheless, this approach has a significant drawback — the available memory capacity is limited since the primary purpose of this function is to store game settings. Consequently, the constraints are pretty predictable. However, since screenshots are utilized for saves, this limitation can be insufficient, meaning the primary save capacity remains crucial.
What about Xbox? Xbox presently employs the GDK API as its primary framework, and to make use of it at the start of the project, synchronization with cloud data consistently takes place. This already impacts another aspect of the project — initialization. However, that’s not our main focus here. The key concept of managing saves on Xbox is that every time you write or read, you must initiate a container, perform the required operations, inform the GDK API of the changes (if any), and then close the container.
And what of the Switch? It’s almost identical to the PlayStation: mounting and unmounting involves delay.
How is data preserved within the current game we’re working on? Saves are created in the following manner: data is stored, a screenshot of the screen is captured, and it’s recorded in the save state. Each time a save is deleted or a new one is created, the data is re-read.
We designed a unified save system for this project, serving as a single entry point for all platforms. Each platform possesses its own SDK and methods for managing saves, so developing a unified system became essential for ensuring consistency. As a result, we created a single entry point script that utilizes the Adapter pattern and orchestrates entities for each platform, identifying which platform is in use and executing the appropriate script.
Now, we have a clear overview of our primary challenges. What are they?
-
The project code must be capable of unlocking the main thread without disrupting the core execution logic.
-
We need to minimize the number of calls to our save system, as even asynchronous calls can cause freezes.
-
Given that we have asynchronous calls, we must ensure the primary condition — only one call to the save system at a time.
A straightforward and fairly effective solution is to utilize Process. Async. Why? Because it allows you to pause the main logic and resume it when necessary. Is this the only solution in terms of project performance? No. Will it deliver the fastest and most expected result? Yes.
Certainly, this method produces considerably more code post-compilation, but it delivers exactly the expected results. Now, we need to eliminate all direct calls to the save system and replace them with new calls to the “new save system implementation” that we developed using Process. Async.
Afterward, we modify all methods that call our save system to be async, allowing them to pause their execution until all save or load operations are complete. We also partially revise higher-level functions within the call hierarchy if necessary. Thus, the primary critical issue has been resolved.
A screenshot from The Star Named EOS. Credit to Pingle Studio.
What’s next?
At this point, we faced an issue with Unity’s “Player Prefs.” To resolve this, we developed a custom alternative that functions similarly but saves data to a file, making it usable on any platform, including Switch. This solution was necessary because Nintendo Switch does not support Unity’s “Player Prefs” and can only save using the native Nintendo SDK.
Next, we needed to reduce calls to the save system. The original project was implemented as follows: We have an alternative to PlayerPrefs that is written to a file—a dictionary with save names which are used to access screenshots and save data. Each time reading, writing, or deleting files happens, they are processed accordingly.