Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
In this article about console porting, we’ll discuss specifically the solutions we found to translate the save system.
January 15, 2025
The case we’re about to discuss is none other than the beautiful and critically acclaimed The Star Named EOS. In this article about console porting, we’ll discuss specifically the solutions we found to translate the save system. Without further ado, let’s jump right in..
In The Star Named EOS, the system operations are performed directly and synchronously without a hint of asynchronous interaction. This is quite an exciting task that presents many challenges. For example, PlayStation 5 works with saves through memory mount, which does not happen instantly. The critical point is that the saves will only be completed by unmounting the memory area. In fact, for PlayStation, you can use PlayerPrefs, which works similarly to memory mount, but all of this happens behind the scenes, out of our control. However, this approach has a significant drawback — the available memory volume is limited since the primary purpose of this feature is to save game settings. Therefore, the limits are pretty expected. However, since screenshots are used for saves, this limit will be insufficient, so the first save option remains primary.
What about Xbox? Xbox currently uses GDK API as the main one, and to use it at the start of the project, synchronization with cloud data always takes place. This already affects another element of the project — initialization. But that’s not what we are here to discuss. The main idea of working with saves on Xbox is that each time you write or read, you need to open a container, perform the necessary operations, notify the GDK API about the changes (if any), and close the container.
What about the Switch? It is almost the same as on PlayStation: mounting and unmounting take time.
How is data saved in the original game we are working on? Saves are created as follows: data is saved, a screenshot of the screen is taken, and it is recorded in the save. Each time a save is deleted or a new one is created, the data is reread.
We created a unified save system for this project as a single entry point for any platform. Each platform has its own SDK and methods for working with saves, so creating a unified system became essential for ensuring consistency. As a result, we made a single entry point script that works using the Adapter pattern and controls entities for each platform, determining which platform is in use and running the appropriate script.
Now, we have a more or less complete picture of our main challenges. What are they?
The project code must be able to release the main thread without breaking the core execution logic.
We need to minimize the number of calls to our save system, as even asynchronous calls can cause freezes.
Since we have asynchronous calls, we must ensure the main condition — only one call to the save system at a time.
A simple and quite effective solution is to use Task. Async. Why? Because it allows you to pause the original logic and resume it when needed. Is this the best solution in terms of project performance? No. Will it provide the fastest and most expected result? Yes.
Of course, this approach generates much additional code after compilation, but it gives us precisely the expected result. Now, we need to remove all direct calls to the file system and replace them with new calls to the "new save system implementation" that we developed on Task. Async.
After that, we rework all methods that call our save system methods to async so they can pause their execution until all save or load actions are completed. We also partially rework higher-level methods in the call hierarchy if necessary. Thus, the first most critical issue has been resolved.
A screenshot of The Star Named EOS. Image via Pingle Studio.
What next?
At this stage, we encountered an issue with Unity’s “Player Prefs.” To address it, we created a custom analog that works similarly but saves data to a file, allowing it to be used on any platform, including Switch. This solution was necessary because Nintendo Switch does not support Unity “Player Prefs” and can only save using the native Nintendo SDK.
Then, we had to minimize calls to the save system. The original project was implemented as follows: We have an analog of PlayerPrefs that is written to a file—a dictionary with save names that are used to access screenshots and save data. Every time reading, writing, or deleting files occurs, they are read from scratch. On PCs, especially with SSDs, this is not a problem, so optimization can be ignored, but on consoles with more than a dozen saves, this can lead to serious problems.
There are several solutions to this problem:
Bundle a large amount of data and access the saves with a batch of operations (which requires reworking the original logic and may take a lot of time).
Create a cache for already loaded saves and use cached data for repeated access.
The second option was much more convenient to implement, so we chose it as the main one. It does not create additional interaction scenarios when there are dozens rewrites of one object or simultaneous read-and-write operations. Such situations might not be very obvious, but avoiding them from the start is better so they do not become a significant challenge later on.
So, we have one final challenge left — multiple simultaneous write, read, and delete operations. This is pretty easy to manage when using asynchronous calls. Each time we are about to work with a file, we can use an indicator, such as a semaphore or a simple variable that signals that the queue is still busy. Since we reworked the original logic for Task, async, our code awaits further calls, and a simple variable where we do an increment when the save operation starts and a decrement when it ends is sufficient. This way, we can ensure that multiple operations do not happen simultaneously, and the interaction logic eliminates competition at the entry point.
Hope you’ve had a good read and maybe learn something. Until next time...
You May Also Like