CHECKPOINTZEROIndie Dev· Unity Project Guide: Save and Load a Checkpoint With JSON - Game Development Guide | Checkpoint ZeroSavingIntermediateUnity
Unity Project Guide: Save and Load a Checkpoint With JSON
Build a Unity save system that stores player position, health, inventory ids, and checkpoint state using JSON files.
Unity Project Guide: Save and Load a Checkpoint With JSON
Saving is one of the first systems that makes a prototype feel like a real game. This guide walks through a practical Unity save system using JSON.
You will save:
- Player position
- Player health
- Last checkpoint id
- Simple inventory item ids
This is not the final save architecture for every game, but it is a strong beginner-friendly foundation.
Step 1: Decide What Should Be Saved
Beginners often try to save entire GameObjects. Avoid that.
Save small, stable data:
TEXT
1Good to save:2- player position3- health number4- item ids5- quest ids6- checkpoint ids7 8Bad to save:9- GameObject references10- Transform components11- Sprite objects12- MonoBehaviour instances
GameObjects are runtime objects. Save data should be plain information that can be recreated.
Step 2: Create Save Data Classes
Create SaveData.cs.
CSHARP
1using System;2using System.Collections.Generic;3 4[Serializable]5public class SaveData6{7 public int version
These classes are intentionally boring. Unity's JsonUtility works best with simple serializable fields.
Step 3: Create the Save Manager
Create SaveManager.cs.
CSHARP
1using System.IO;2using UnityEngine;3 4public class SaveManager : MonoBehaviour5
Create an empty GameObject named SaveManager and attach this script.
Step 4: Capture Player Data
Create PlayerSaveAdapter.cs.
CSHARP
1using UnityEngine;2 3public class PlayerSaveAdapter : MonoBehaviour4{5 [SerializeField] private Health health
Attach it to the Player and assign the Health component.
If your Health component does not support setting health yet, add a controlled method:
CSHARP
1public void SetHealth(int value)2{3 CurrentHealth = Mathf.Clamp(value, 0, maxHealth);4 HealthChanged?.Invoke(CurrentHealth, maxHealth);5}
Then call health.SetHealth(data.health) inside Restore.
Step 5: Create Checkpoints
Create Checkpoint.cs.
CSHARP
1using UnityEngine;2 3public class Checkpoint : MonoBehaviour4{5 [SerializeField] private string checkpointId;6 7
Create a trigger object in your scene and attach this script.
Set:
- Checkpoint Id:
forest_gate
- Collider2D: trigger enabled
When the player touches it, the checkpoint id changes.
Step 6: Save From a Game Controller
Create GameSaveController.cs.
CSHARP
1using UnityEngine;2 3public class GameSaveController : MonoBehaviour4{5 [SerializeField private PlayerSaveAdapter player
This references an inventory adapter. If you do not have inventory yet, remove those lines for now.
Step 7: Save a Simple Inventory
If your inventory stores item definitions, save item ids instead of objects.
Create SimpleInventorySaveAdapter.cs.
CSHARP
1using System.Collections.Generic;2using UnityEngine;3 4public class SimpleInventorySaveAdapter : MonoBehaviour5
Create ItemDatabase.cs.
CSHARP
1using System.Collections.Generic;2using UnityEngine;3 4[CreateAssetMenu(menuName = "Game/Item Database")]5public class ItemDatabase : ScriptableObject6
This is simple, but it teaches the correct habit: save ids, then look up definitions when loading.
Step 8: Test the Save File
Press Play and test:
- Move the player somewhere.
- Pick up an item.
- Touch a checkpoint.
- Press
F5.
- Move somewhere else.
- Press
F9.
The player should return to the saved position.
On your computer, the save file is stored in:
CSHARP
1Application.persistentDataPath
Log this path if you want to inspect the JSON.
Step 9: Add Versioning Early
Your save format will change. Add a version now, even if it feels unnecessary.
Later, you can migrate old saves:
CSHARP
1if (data.version == 1)2{3 // Convert old fields into the new shape.4 data.version = 2;5}
Step 10: Common Mistakes
- Do not save passwords, tokens, or private user data in plain JSON.
- Do not assume save files are valid. Players can edit them.
- Do not save scene object references.
- Do not load before the scene has created the player.
- Do not forget to test loading an old save after changing your data classes.
Where To Go Next
Useful upgrades:
- Add autosave at checkpoints.
- Add multiple save slots.
- Add screenshot thumbnails for save slots.
- Add save migrations.
- Add encryption only if your game truly needs it.
The main lesson: saving is not magic. Capture plain data, write it to disk, then restore the game from that data in a controlled way.
CHECKPOINTZERO
Indie Dev· =
1
;
8 public PlayerSaveData player = new PlayerSaveData();
9 public List<string> inventoryItemIds = new List<string>();
10}
11
12[Serializable]
13public class PlayerSaveData
14{
15 public float x;
16 public float y;
17 public int health;
18 public string checkpointId;
19}
{
6 public static SaveManager Instance { get; private set; }
7
8 private string SavePath => Path.Combine(Application.persistentDataPath, "save.json");
9
10 private void Awake()
11 {
12 if (Instance != null && Instance != this)
13 {
14 Destroy(gameObject);
15 return;
16 }
17
18 Instance = this;
19 DontDestroyOnLoad(gameObject);
20 }
21
22 public void Save(SaveData data)
23 {
24 string json = JsonUtility.ToJson(data, true);
25 File.WriteAllText(SavePath, json);
26 Debug.Log($"Saved to {SavePath}");
27 }
28
29 public bool TryLoad(out SaveData data)
30 {
31 data = null;
32
33 if (!File.Exists(SavePath))
34 {
35 return false;
36 }
37
38 string json = File.ReadAllText(SavePath);
39 data = JsonUtility.FromJson<SaveData>(json);
40 return data != null;
41 }
42
43 public void DeleteSave()
44 {
45 if (File.Exists(SavePath))
46 {
47 File.Delete(SavePath);
48 }
49 }
50}
;
6
7 private string currentCheckpointId = "start";
8
9 public PlayerSaveData Capture()
10 {
11 return new PlayerSaveData
12 {
13 x = transform.position.x,
14 y = transform.position.y,
15 health = health.CurrentHealth,
16 checkpointId = currentCheckpointId
17 };
18 }
19
20 public void Restore(PlayerSaveData data)
21 {
22 transform.position = new Vector3(data.x, data.y, transform.position.z);
23 currentCheckpointId = data.checkpointId;
24
25 // Add SetCurrentHealth to your Health component or restore through a proper method.
26 Debug.Log($"Restore health to {data.health}");
27 }
28
29 public void SetCheckpoint(string checkpointId)
30 {
31 currentCheckpointId = checkpointId;
32 }
33}
private void OnTriggerEnter2D(Collider2D other)
8 {
9 PlayerSaveAdapter saveAdapter = other.GetComponent<PlayerSaveAdapter>();
10 if (saveAdapter == null) return;
11
12 saveAdapter.SetCheckpoint(checkpointId);
13 Debug.Log($"Checkpoint reached: {checkpointId}");
14 }
15}
]
;
6 [SerializeField] private SimpleInventorySaveAdapter inventory;
7
8 private void Update()
9 {
10 if (Input.GetKeyDown(KeyCode.F5))
11 {
12 SaveGame();
13 }
14
15 if (Input.GetKeyDown(KeyCode.F9))
16 {
17 LoadGame();
18 }
19 }
20
21 public void SaveGame()
22 {
23 SaveData data = new SaveData
24 {
25 player = player.Capture(),
26 inventoryItemIds = inventory.CaptureItemIds()
27 };
28
29 SaveManager.Instance.Save(data);
30 }
31
32 public void LoadGame()
33 {
34 if (!SaveManager.Instance.TryLoad(out SaveData data))
35 {
36 Debug.Log("No save file found.");
37 return;
38 }
39
40 player.Restore(data.player);
41 inventory.RestoreItemIds(data.inventoryItemIds);
42 }
43}
{
6 [SerializeField] private PlayerInventorySlots inventory;
7 [SerializeField] private ItemDatabase itemDatabase;
8
9 public List<string> CaptureItemIds()
10 {
11 List<string> ids = new List<string>();
12
13 foreach (InventorySlot slot in inventory.Slots)
14 {
15 if (slot.IsEmpty) continue;
16
17 for (int i = 0; i < slot.quantity; i++)
18 {
19 ids.Add(slot.item.itemId);
20 }
21 }
22
23 return ids;
24 }
25
26 public void RestoreItemIds(List<string> ids)
27 {
28 // Clear inventory first. Add a ClearAll method to your inventory.
29 foreach (string id in ids)
30 {
31 ItemDefinition item = itemDatabase.GetById(id);
32 inventory.TryAdd(item, 1);
33 }
34 }
35}
{
7 [SerializeField] private List<ItemDefinition> items;
8
9 public ItemDefinition GetById(string id)
10 {
11 return items.Find(item => item.itemId == id);
12 }
13}