Getting Started
This guide walks you through creating your first save system with TinySave. You’ll learn the core concepts and build working examples.
The Three-Step Pattern
Every TinySave implementation follows this simple pattern:
- Mark fields with
[SaveField] - Add SaveID component to GameObjects
- Call SaveManager methods to save/load
Let’s build a complete example.
Example: Player Stats
Step 1: Mark Your Fields
using UnityEngine;
using TinySave.Runtime;
public class PlayerStats : MonoBehaviour
{
[SaveField]
public int health = 100;
[SaveField]
public int maxHealth = 100;
[SaveField]
public int level = 1;
[SaveField]
public float experience = 0f;
[SaveField]
public List<string> inventory = new List<string>();
}The [SaveField] attribute tells TinySave to save and restore these fields automatically.
Step 2: Add SaveID Component
In the Unity Inspector:
- Select your GameObject with the PlayerStats component
- Click “Add Component”
- Search for “SaveID”
- Add the SaveID component
SaveID gives your GameObject a unique identifier that persists across save/load operations.
Step 3: Save and Load
Create a simple UI to test saving and loading:
using UnityEngine;
using TinySave.Runtime;
using System.Threading.Tasks;
public class SaveLoadUI : MonoBehaviour
{
public void OnSaveButton()
{
_ = SaveGame();
}
public void OnLoadButton()
{
_ = LoadGame();
}
private async Task SaveGame()
{
await SaveManager.SaveAsync("quicksave");
Debug.Log("Game saved!");
}
private async Task LoadGame()
{
bool success = await SaveManager.LoadAsync("quicksave");
if (success)
Debug.Log("Game loaded!");
else
Debug.Log("No save file found.");
}
}Working with Multiple Slots
TinySave supports multiple save slots:
// Save to different slots
await SaveManager.SaveAsync("slot1");
await SaveManager.SaveAsync("slot2");
await SaveManager.SaveAsync("autosave");
// Load from a specific slot
await SaveManager.LoadAsync("slot1");
// Check if a slot exists before loading
if (SaveManager.HasSlot("slot2"))
{
await SaveManager.LoadAsync("slot2");
}
// Delete a slot
SaveManager.DeleteSlot("autosave");Example: Saving Transform
Transform components require a bit more work since you can’t mark Transform fields directly:
using UnityEngine;
using TinySave.Runtime;
public class SaveTransform : MonoBehaviour, ISaveCallbacks
{
[SaveField]
private Vector3 savedPosition;
[SaveField]
private Quaternion savedRotation;
public void OnBeforeSave()
{
// Called before save - capture current transform
savedPosition = transform.position;
savedRotation = transform.rotation;
}
public void OnAfterLoad()
{
// Called after load - restore transform
transform.position = savedPosition;
transform.rotation = savedRotation;
}
}The ISaveCallbacks interface gives you hooks into the save/load process.
Example: Inventory System
using UnityEngine;
using TinySave.Runtime;
using System.Collections.Generic;
public class Inventory : MonoBehaviour
{
[SaveField]
public List<ItemData> items = new List<ItemData>();
[SaveField]
public int maxSlots = 20;
[System.Serializable]
public struct ItemData
{
public string itemId;
public int quantity;
public int durability;
}
public void AddItem(string itemId, int quantity)
{
// Check if item already exists
ItemData existing = items.Find(i => i.itemId == itemId);
if (existing.itemId != null)
{
existing.quantity += quantity;
return;
}
// Add new item
if (items.Count < maxSlots)
{
items.Add(new ItemData
{
itemId = itemId,
quantity = quantity,
durability = 100
});
}
}
}Example: ScriptableObject Settings
ScriptableObjects work seamlessly with TinySave:
using UnityEngine;
using TinySave.Runtime;
[CreateAssetMenu(fileName = "GameSettings", menuName = "Game/Settings")]
public class GameSettings : ScriptableObject
{
[SaveField]
public float masterVolume = 1f;
[SaveField]
public float musicVolume = 0.8f;
[SaveField]
public float sfxVolume = 1f;
[SaveField]
public int graphicsQuality = 2;
[SaveField]
public bool fullscreen = true;
}TinySave will automatically save and restore ScriptableObject instances referenced in your scene.
Save Events
Use events to perform actions before saves and after loads:
using UnityEngine;
using TinySave.Runtime;
public class GameManager : MonoBehaviour
{
void OnEnable()
{
SaveManager.BeforeSave += OnBeforeSave;
SaveManager.AfterLoad += OnAfterLoad;
}
void OnDisable()
{
SaveManager.BeforeSave -= OnBeforeSave;
SaveManager.AfterLoad -= OnAfterLoad;
}
void OnBeforeSave()
{
Debug.Log("Saving game...");
// Update timestamps, validate data, etc.
}
void OnAfterLoad()
{
Debug.Log("Game loaded!");
// Refresh UI, restart systems, etc.
}
}Checking Save Metadata
You can peek at save file metadata without loading the entire file:
SaveMetadata? meta = SaveManager.PeekMeta("slot1");
if (meta.HasValue)
{
Debug.Log($"Save version: {meta.Value.version}");
Debug.Log($"Timestamp: {meta.Value.timestamp}");
Debug.Log($"Scenes: {string.Join(", ", meta.Value.scenes)}");
}Synchronous Saving
For special cases like OnApplicationQuit, use synchronous saving:
void OnApplicationQuit()
{
// Use SaveSync to avoid deadlocks during shutdown
SaveManager.SaveSync("autosave");
}Best Practices
- Add SaveID early - Add SaveID components during development, not at runtime
- Use descriptive slot names - “quicksave”, “autosave”, “checkpoint_boss1”
- Check HasSlot before loading - Prevents errors when no save exists
- Handle load failures gracefully - LoadAsync returns false if file doesn’t exist
- Use async/await - Prevents frame drops during save/load
- Subscribe to events - Use BeforeSave/AfterLoad for initialization logic
- Test early and often - Save/load frequently during development
Common Patterns
Continue Game Flow
public async Task ContinueGame()
{
if (SaveManager.HasSlot("autosave"))
{
bool success = await SaveManager.LoadAsync("autosave");
if (success)
{
SceneManager.LoadScene("GameScene");
return;
}
}
// No save found - start new game
StartNewGame();
}Checkpoint System
public class Checkpoint : MonoBehaviour
{
[SerializeField] private string checkpointName;
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
_ = SaveCheckpoint();
}
}
private async Task SaveCheckpoint()
{
await SaveManager.SaveAsync($"checkpoint_{checkpointName}");
Debug.Log($"Checkpoint saved: {checkpointName}");
}
}Save Slot Management
public class SaveSlotManager
{
public SaveSlotInfo[] GetAllSlots()
{
string[] slotNames = { "slot1", "slot2", "slot3" };
var slots = new SaveSlotInfo[slotNames.Length];
for (int i = 0; i < slotNames.Length; i++)
{
string slotName = slotNames[i];
bool exists = SaveManager.HasSlot(slotName);
SaveMetadata? meta = SaveManager.PeekMeta(slotName);
slots[i] = new SaveSlotInfo
{
slotName = slotName,
exists = exists,
timestamp = meta?.timestamp ?? 0,
playerName = meta?.build ?? "Empty"
};
}
return slots;
}
}
public struct SaveSlotInfo
{
public string slotName;
public bool exists;
public long timestamp;
public string playerName;
}