This article will be free until we release the video for this part.
In the next part of our tutorial series, we will be implementing a save functionality for our coins and coin pickups. We’ll be introducing a coin system that persists across play sessions, allowing players to track their progression. Alongside this, we’ll add new pickups that reward players with coins during gameplay.
Currently, there are no persistent rewards tied to completing levels. By introducing collectible and usable coins, we create a more engaging experience where players feel rewarded for their efforts and have meaningful goals to work toward.
1. Adding Coin Display in the title screen
We will first add a coin display to the title screen, similar to what’s shown in Vampire Survivors.

a. Adding the Coin Display UI
Let’s start by going to our title screen. Then in the Hierarchy go to the Canvas. This is where we will add the CoinDisplay UI, right click the Canvas > UI > Image.

Next rename the UI element to CoinDisplay
. Then, in the Rect Transform component, move your object to wherever on the screen you want to place it. I put it at (0, 100).
Next, in the Image component, set the Source Image to your preferred coin icon or sprite for the coin display.

Next set the Anchor of the text element. Split the anchor so it aligns properly with the coin icon—this ensures the text stays in the correct position across different screen sizes. You can reference this video to learn more about the technicalities of splitting anchors to position UI elements correctly and adaptively (i.e. making them look good on multiple different screen resolutions).

Your Coin UI Display should look like this once it’s set up correctly:

If you’d like to use the same sprite as shown in our example, you can save the image below:

💡 Reminder: When creating the sprite for the Coin UI Display, do not include the coin number in the sprite itself—the number will be displayed using a separate UI element, allowing it to update dynamically based on the player’s coin count.
b. Adding Coin Display Text
Now it’s time to add the text that will display the coin count. Go to the CoinDisplay
UI element we added earlier, then right-click on it in the Hierarchy and select UI > Text – TextMeshPro to create a new text element as its child. This text will be used to dynamically show the player’s current coin total.

Next rename the Text (TMP) to CoinDisplay Text. Then, in the Rect Transform component:
- Set Pos Y to
70
- Set Pos X to
5
- Set Width to
200
- Set Height to
50
Next, select the TextMeshPro – Text (UI) component, go to the Alignment section, and set the alignment to Right so that the coin count lines up neatly next to the coin icon.

Remember to split the anchor for the Coin Text. Make it something close to a 20/80 split. This is so that it will work with any screen size. Afterwards set it to auto size.

Your Coin UI Display should now look like this once the text is set up:

2. Adding Coin Display in levels
Now we’ll be adding the CoinDisplay UI into the levels. This will allow players to see how many coins they’ve collected during the current run while playing the level. For this example, we’ll use the Level 1 – Mad Forest scene.
In the Hierarchy, right-click on the Canvas
, then go to UI > Text – TextMeshPro to create a new text UI element. This text will be used to display the number of coins collected during the current run.

After creating the text name rename it to CoinDisplay Text. Then, in the Rect Transform component:
- Set Pos Y to
835
- Set Pos X to
490
- Set Width to
220
- Set height to
50
Next, select the TextMeshPro – Text (UI) component, go to the Alignment section, and set the alignment to Right so that the coin count lines up neatly next to the coin icon.

Remember to split the anchor like this:

a. Setting up UICoinDisplay.cs
script
Now it’s time to attach the UICoinDisplay.cs
script to the CoinDisplay Text UI element we just created. This will enable the text UI to automatically update and show the correct number of coins collected during gameplay.
Then, in the Player GameObject, locate the Collector component—this is the reference we need for the collector
variable in UICoinDisplay.cs
.
Next, select the CoinDisplay text object with the UICoinDisplay.cs
script attached. In the Inspector, you’ll see a field named Collector. Simply drag the Collector GameObject into this field to assign the reference.

The PlayerCollector.cs
is where we store the number of coins collected during the current game session. That’s why we assign it as a reference in the UICoinDisplay.cs
script—so the script can access that coin data and display it accurately on the UI during gameplay.
3. Adding a basic save functionality
Now it’s time to add the script that will handle saving and loading the total coins collected across play sessions.
a. Creating the SaveManager
.cs script
First, create a new C# script in your project and name it SaveManager.cs
. This script will manage reading from and writing to a save file, ensuring the player’s coin progress is preserved between game sessions.
SaveManager.cs
using System.Collections; using System.Collections.Generic; using System.Drawing; using System.IO; using UnityEngine; /// <summary> /// A simple SaveManager designed to save the total number of coins the player has. /// In later parts, this will be used to store all the player's save data, but we are /// keeping it simple for now. /// </summary> public class SaveManager { public class GameData { public float coins; } const string SAVE_FILE_NAME = "SaveData.json"; static GameData lastLoadedGameData; public static GameData LastLoadedGameData { get { if (lastLoadedGameData == null) Load(); return lastLoadedGameData; } } public static string GetSavePath() { return string.Format("{0}/{1}", Application.persistentDataPath, SAVE_FILE_NAME); } // This function, when called without an argument, will save into the last loaded // game file (this is how you should be calling Save() 99% of the time. // But you can optionally also provide an argument to it to if you want to overwrite the save completely. public static void Save(GameData data = null) { // Ensures that the save always works. if (data == null) { // If there is no last loaded game, we load the game to populate // lastLoadedGameData first, then we save. if (lastLoadedGameData == null) Load(); data = lastLoadedGameData; } File.WriteAllText(GetSavePath(), JsonUtility.ToJson(data)); } public static GameData Load(bool usePreviousLoadIfAvailable = false) { // usePreviousLoadIfAvailable is meant to speed up load calls, // since we don't need to read the save file every time we want to access data. if (usePreviousLoadIfAvailable) return lastLoadedGameData; // Retrieve the load in the hard drive. string path = GetSavePath(); if (File.Exists(path)) { string json = File.ReadAllText(path); lastLoadedGameData = JsonUtility.FromJson<GameData>(json); } else { lastLoadedGameData = new GameData(); } return lastLoadedGameData; } }
The SaveManager
class is a static utility class designed to handle saving and loading game data in a Unity project. It currently focuses on storing the player’s coin count, but we can easily expand it for additional save data in the future.
b. Explaining the SaveManager
script
The most important part of the SaveManager
script is arguably the nested GameData
class within:
public class SaveManager { public class GameData { public float coins; } ...
This class acts as a container for all the data you want to save and load. Right now, it only holds a single float value for coins
, but it’s designed to be easily extendable. As your game grows, you can add more fields—like player health, inventory, or settings—directly into the GameData
class. This makes it simple to manage all your saved information in one place.
Managing the lastLoadedGameData
const string SAVE_FILE_NAME = "SaveData.json"; static GameData lastLoadedGameData;
SAVE_FILE_NAME
: This constant defines the name of the file where your game’s save data will be stored. In this case, it’s"SaveData.json"
, which will be created in Unity’s persistent data path.lastLoadedGameData
: This static variable holds the most recently loaded save data in memory. By caching it, you avoid repeatedly reading from disk, which improves performance and simplifies data access during gameplay.
We also declare a getter for our lastLoadedGameData
, to make it easier to access. If the variable is not set, we will automatically call the Load()
function on it to retrieve it. This guarantees that any time you access this property, you’re working with valid, initialized save data—no need to manually call Load()
elsewhere in your code.
public static GameData LastLoadedGameData { get { if (lastLoadedGameData == null) Load(); return lastLoadedGameData; } }
The GetSavePath()
function
A short function that assembles the path (i.e. folder) where the save file will be stored on the user’s computer. This function saves us the work of creating the save path string whenever we need to use it.
public static string GetSavePath() { return string.Format("{0}/{1}", Application.persistentDataPath, SAVE_FILE_NAME); }
Application.persistentDataPath
: A Unity-provided directory that safely stores data between sessions, and works across all platforms.- Benefit: Ensures your save file is placed in a reliable, platform-independent location—whether on Windows, macOS, Android, iOS, etc.
The Save()
function
Very simply, the Save()
function writes the current game data into the location provided by GetSavePath()
, so that our game is saved.
public static void Save(GameData data = null) { if (data == null) { if (lastLoadedGameData == null) Load(); data = lastLoadedGameData; } File.WriteAllText(GetSavePath(), JsonUtility.ToJson(data)); }
Here’s a breakdown of how it works:
(GameData data = null
)
- This means you can call
Save()
without passing anything in. - If you do pass a
GameData
object, the function will use that data to overwrite the current save. - If you don’t pass anything, it defaults to saving the game’s most recently loaded data (called
lastLoadedGameData
).
if (data == null) { if (lastLoadedGameData == null) Load(); data = lastLoadedGameData; }
- If no data is provided and nothing has been loaded yet, it tries to load the existing save file first.
- This ensures that there’s always some data to save, so the function won’t fail.
Unity’s JsonUtility turns the GameData object into a JSON-formatted string.
- Unity’s
JsonUtility
turns theGameData
object into a JSON-formatted string. - JSON is a text-based format that’s easy to save, load, and read.
File.WriteAllText(GetSavePath(), JsonUtility.ToJson(data));
- This writes the JSON string to the save file located at the path returned by
GetSavePath()
.
The Load()
function
The opposite of Save()
, this function retrieves the file from GetSavePath()
and converts it into a GameData
object that we can retrieve data from.
public static GameData Load(bool usePreviousLoadIfAvailable = false) { // usePreviousLoadIfAvailable is meant to speed up load calls, // since we don't need to read the save file every time we want to access data. if (usePreviousLoadIfAvailable) return lastLoadedGameData; // Retrieve the load in the hard drive. string path = GetSavePath(); if (File.Exists(path)) { string json = File.ReadAllText(path); lastLoadedGameData = JsonUtility.FromJson<GameData>(json); } else { lastLoadedGameData = new GameData(); } return lastLoadedGameData; }
Here’s a breakdown of how the function works:
public static GameData Load(bool usePreviousLoadIfAvailable = false)
This function is responsible for loading saved game data from a file, or creating new default data if no save file exists.
if (usePreviousLoadIfAvailable) return lastLoadedGameData;
- This is a performance optimization.
- If you pass in
true
and the game has already loaded data before, it just returns the cached data (lastLoadedGameData
) instead of reading the file again. - This is useful when you want to access save data often but don’t want to repeatedly hit the disk, which can be slow.
string path = GetSavePath();
This gets the file path where your save data is stored on the user’s device.
if (File.Exists(path)) { string json = File.ReadAllText(path); lastLoadedGameData = JsonUtility.FromJson(json); }
If the file exists, it:
- Reads the contents of the file (which is a JSON string).
- Uses Unity’s JsonUtility.FromJson to convert the JSON string back into a GameData object.
- Stores the result in lastLoadedGameData for future use.
else { lastLoadedGameData = new GameData(); }
If there’s no existing save file, it creates a new empty GameData object with default values (e.g., coins = 0), and caches it.
return lastLoadedGameData;
Whether it loaded from file or created a new one, it returns the data to the caller.
c. Future Expansion
This SaveManager.cs
is designed to be easily expanded in the future. To store additional player data, we can add new fields to the GameData
class, as shown below (just an example):
public class GameData
{
public float coins;
//example expansions
public int levelsCompleted;
public List<string> unlockedItems = new List<string>();
public Dictionary<string, bool> achievements = new Dictionary<string, bool>();
public PlayerSettings settings = new PlayerSettings();
}
The existing Save()
and Load()
methods will automatically handle the expanded data structure without requiring changes to their implementation.
4. Updating the PlayerCollector.cs
script
PlayerCollector.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(Collider2D))] public class PlayerCollector : MonoBehaviour { PlayerStats player; CircleCollider2D detector; public float pullSpeed = 10; public UICoinDisplay UI; float coins; void Start() { player = GetComponentInParent<PlayerStats>(); } public void SetRadius(float r) { if (!detector) detector = GetComponent<CircleCollider2D>(); detector.radius = r; } public float GetCoins() { return coins; } //Updates coins Display and information public float AddCoins(float amount) { coins += amount; if (UI) UI.UpdateDisplay(); return coins; } // Saves the collected coins to the save file. public void SaveCoinsToStash() { SaveManager.LastLoadedGameData.coins += coins; coins = 0; SaveManager.Save(); } private void OnTriggerEnter2D(Collider2D col) { if (col.TryGetComponent(out Pickup p)) { p.Collect(player, pullSpeed); } } }
a. Explanation of the new PlayerCollector
changes
public UICoinDisplay UI; float coins;
A new variable called coins
has been added. This variable is responsible for storing the number of coins collected during the current game session. It keeps track of temporary coin progress, which can later be saved to the player’s total coin stash using the SaveCoinsToStash()
method and resetting its value.
Another variable called UI
is also added in order to send coin information to the CoinDisplay UI in the scene. This allows the UI to update and reflect the current number of coins the player has collected in real time.
The GetCoins()
function
public float GetCoins() { return coins; }
The GetCoins()
method is a public getter that simply returns the current number of coins the player has collected during the current game session.
The AddCoins()
function
public float AddCoins(float amount) { coins += amount; if (UI) UI.UpdateDisplay(); return coins; }
When this function is called, it will:
- Add the given amount to the
coins
variable, updating the total coins collected in the current session. - Check if the
UICoinDisplay
is set (i.e., not null). If it is, it will update the UI display to reflect the new coin total. - Return the updated coin value, allowing other scripts to use or display the new total if needed.
The SaveCoinsToStash()
function
public void SaveCoinsToStash() { SaveManager.LastLoadedGameData.coins += coins; coins = 0; SaveManager.Save(); }
Designed to be called whenever we want to save the total number of coins the player has, when this function is called, it will:
- Add the current session’s coin total (
coins
) to the player’s overall saved coins (SaveManager.LastLoadedGameData.coins
). - Reset the session coin counter by setting
coins = 0
, since the coins have now been transferred to the saved total. - Call
SaveManager.Save()
to write the updated coin data to the save file, ensuring progress is preserved across sessions.
💡 Note: When writing scripts like this, the order in which code is written is very important, because in C# (and most programming languages), code is executed from top to bottom. For example, calling Save()
before adding the session coins would save the wrong amount. Always update values first.
Now, go back to the scene and in the Inspector, navigate to Player > Collector. There, set the UI variable to the CoinDisplay Text we created earlier. This links the PlayerCollector
to the UICoinDisplay
, ensuring the UI updates when coins are collected.

b. Assigning the SaveCoinsToStash()
function
You might have noticed that this function isn’t being called anywhere in the code yet. That’s because we only want to save the coins when the game ends.
So, we’ll call this function only when the player clicks the Quit button on the Pause Screen or the Done button on the Result Screen. This ensures coins are only saved after a full session, avoiding premature or unintended saves.
Start by going to Canvas > Pause Screen > Quit then in the inspector go to Button and on the OnClick(), Add a new function to the list.

Drag the Player > Collector object into the target variable field.

Then, set the function to SaveCoinsToStash()
by selecting:PlayerCollector
> SaveCoinsToStash()
from the dropdown menu in the Unity Inspector event settings.

This will ensure the coin-saving function is triggered when the assigned UI event (like a button click) occurs. Now do the same steps with all other buttons that changes the scene.
5. Adding new Coin pickups
We are going to add three new coin pickups to the game to give players a way to earn coins during gameplay. The pickups we’ll introduce are:
- Gold Coin – awards 1 coin
- Coin Bag – awards 10 coins
- Rich Coin Bag – awards 100 coins
These will provide varying levels of coin rewards, making gameplay more dynamic and giving players satisfying moments when collecting higher-value pickups.

a. Making the CoinPickup.cs
script
Now, to create the new coin pickups, we’ll need a script that inherits from the Pickup.cs
class.
We’re subclassing Pickup
instead of modifying it directly because only coin pickups need a reference to the PlayerCollector.cs
component, whereas other pickups does not. There’s no point making every pickup call GetComponent()
for the collector when most won’t use it. This keeps the base class clean and the coin logic isolated.
Start by creating a new script in your project and name it CoinPickup.cs
. This script will define the behavior of each coin pickup, such as how many coins it gives when collected.
CoinPickup.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class CoinPickup : Pickup { PlayerCollector collector; public int coins = 1; protected override void OnDestroy() { base.OnDestroy(); // Retrieve the PlayerCollector component from the player who picked up this object // Add coins to their total. if (target != null) { collector = target.GetComponentInChildren<PlayerCollector>(); if (collector != null) collector.AddCoins(coins); } } }
b. Explanation of the CoinPickup
script
The CoinPickup
script is a simple script that simply adds coins to the player. The reason we create a new script for it (instead of just adding a new coins
variable to the Pickup
class) is because it requires access to the player’s PlayerCollector
component (whereas the Pickup
script only has access to the PlayerStats
component).
PlayerCollector collector; public int coins = 1;
Hence, why we have the 2 variables that are declared in this script. The reference to the PlayerCollector
component allows us to call AddCoins()
to send coins to it.
The second variable, coins
, is used to store the amount of coins this pickup should give when collected. This allows different pickups (like Gold Coin, Coin Bag, or Rich Coin Bag) to award different coin values.
Initially, we actually placed the coin tracking functionality in GameManager
before later deciding to code it into PlayerCollector
. The reason for letting PlayerCollector
handle this was to make it easier for us to support multiplayer in the future, since we can have multiple players each managing their own coin total independently.
OnDestroy() function
protected override void OnDestroy() { base.OnDestroy(); // Retrieve the PlayerCollector component from the player who picked up this object // Add coins to their total. if (target != null) { collector = target.GetComponentInChildren<PlayerCollector>(); if (collector != null) collector.AddCoins(coins); } }
When this function is called, it will:
- Call the base
OnDestroy()
method from the parentPickup
class to ensure any inherited cleanup logic is executed. - Check if
target
is not null—if it isn’t, it setscollector
to thePlayerCollector
component of the player who picked up the item. - Check if
collector
is not null—if valid, it calls theAddCoins()
function on thecollector
to add the coins to that player’s session total.
c. Making Coin Pickups Prefabs
In the Project folder, go to the Prefabs folder > Pick-ups, and duplicate one of the existing pickups, such as the Blue Gem.
- In the Inspector of the duplicate, change the Sprite Renderer sprite to your desired sprite for the coin pickup.
- Then, replace the PickUp.cs script with the newly created CoinPickup.cs script.
- Finally, set the Coins variable in the Inspector to the amount of coins you want the pickup to give (e.g., 1 for Gold Coin, 10 for Coin Bag, etc.).

Now the pickups should be fully working and ready for use. Try dragging them into the scene and giving them a test run to ensure that they correctly award coins when collected. If everything is set up properly, you should see the coin amount update in the UI when the player picks them up.

If you’d like to use the same sprite as shown in our example, you can download it here:
6. Making the UICoinDisplay
script
Now, we will create a script that will help us display the player’s coin count of both the title screen and levels.
UICoinDisplay
.cs
using TMPro; using UnityEngine; /// <summary> /// Component that is attached to GameObjects to make it display the player's coins. /// Either in-game, or the total number of coins the player has, depending on whether /// the collector variable is set. /// </summary> public class UICoinDisplay : MonoBehaviour { TextMeshProUGUI displayTarget; public PlayerCollector collector; void Start() { displayTarget = GetComponentInChildren<TextMeshProUGUI>(); UpdateDisplay(); } public void UpdateDisplay() { // If a collector is assigned, we will display the number of coins the collector has. if (collector != null) { displayTarget.text = Mathf.RoundToInt(collector.GetCoins()).ToString(); } else { // If not, we will get the current number of coins that are saved. float coins = SaveManager.LastLoadedGameData.coins; displayTarget.text = Mathf.RoundToInt(coins).ToString(); } } }
Be reminded that:
- On the title screen, the coins displayed should be the total number of coins the player has collected.
- In-game, the coins displayed should be the total number of coins the player has collected in the game itself.
Hence, the script will be designed in such a way that every instance of it can be configured to display either the coins collected in the current game, or the grand total number of coins collected.


Next After making the UICoinDisplay.cs
script, attach it to the UI element CoinDisplay
made previously.

💡 Reminder: Make sure the collector
variable is not set when using this script on menus like the title screen—this will ensure it displays the total coins collected across all sessions instead of just the current game’s coins.
7. Conclusion
After following all the steps and setting the necessary variables, the Save function and Coin system should now be fully working!
You can test it by collecting coins in-game, saving the progress, and checking that the total coin count is retained between sessions. And that’s it for this part!
Silver Patrons and above can download the project files.