com.cupkekgames.gamesave — generic save-manager pattern (GameSaveManager<TData,TMeta>) built on the Data package. Serialization-agnostic: pair with com.cupkekgames.newtonsoft for JSON-backed persistence, or implement your own IDataSerializer. UI components (MainMenuView<,>, GameSaveViewList<,>, etc.) live in the sibling bridge package com.cupkekgames.gamesave.luna. Install via the CupkekGames UPM scoped registry through the Package Manager window.
The save-system framework provides UI components that work with any save implementation through a clean abstraction layer. Your UI remains unchanged whether you use JSON files, binary data, cloud storage, or encrypted saves.
Key Benefits: UI-agnostic design, pluggable save implementations, fast metadata loading, and seamless integration with Luna's UI components.
Luna provides pre-built UI components like MainMenuView and GameSaveView that handle save/load operations automatically.
Abstract interface that connects your save implementation to Luna's UI. You provide the concrete implementation.
Your game defines the save data structure (TSaveData) and metadata (TSaveMetadata) that the UI will work with.
The beauty of Luna's save system is that your UI code never changes. Whether you implement JSON files, binary data, cloud storage, or encrypted saves, the UI components work identically through the manager interface.
Create your save data class that implements IGameSaveData and CupkekGames.Data.IData. GameSaveViewList<TSaveData, …> and DataSO<T>-style flows expect IData for validation, JSON hydration hooks, and CloneData() deep copies.
using CupkekGames.Data;
using CupkekGames.GameSave;
public class MyGameSaveData : IGameSaveData, IData
{
public GameSaveMetadata Metadata { get; set; }
public string PlayerName;
public int Gold;
public Inventory Inventory;
public MyGameSaveData()
{
PlayerName = "Player";
Gold = 0;
Inventory = new Inventory();
}
// Copy constructor (or private helper) used by CloneData
public MyGameSaveData(MyGameSaveData other)
{
if (other == null) return;
Metadata = other.Metadata != null ? new GameSaveMetadata(other.Metadata) : null;
PlayerName = other.PlayerName;
Gold = other.Gold;
Inventory = other.Inventory != null ? new Inventory(other.Inventory) : new Inventory();
}
public GameSaveMetadata CreateMetadata(string saveVersion, bool isAutosave)
{
return new MyGameSaveMetadata
{
SaveVersion = saveVersion,
SaveDate = DateTime.Now,
IsAutosave = isAutosave,
Gold = Gold,
};
}
public void LoadFrom(IGameSaveData other, int slot)
{
var o = (MyGameSaveData)other;
Metadata = o.Metadata;
PlayerName = o.PlayerName;
Gold = o.Gold;
Inventory = o.Inventory;
}
public bool Validate() => true;
public void OnAfterDeserialize() { }
public IData CloneData() => new MyGameSaveData(this);
}CloneData must be a real deep copy.
DataSO<T>relies onCloneData()to isolate the authored default template from the live actual data on every play session. If any mutable sub-object (metadata, inventories, nested data classes) is copied by reference, runtime mutations bleed into the template. Give every nested data class its own copy constructor and chain them, asGameFullSaveDatadoes.LoadFromshould also restoreMetadatafrom the loaded instance, not only the gameplay fields.
For a full example (including Newtonsoft), see the GameFull sample — GameFullSaveData + GameFullSaveManager use the Newtonsoft adapter from com.cupkekgames.newtonsoft for JSON-backed persistence (resolved through the IDataSerializer service).
Define lightweight metadata for UI display:
public class MyGameSaveMetadata : GameSaveMetadata {
public int Gold;
}Inherit from GameSaveManager<TSaveData, TSaveMetadata> and implement the required methods:
using System.Collections.Generic;
using System.IO;
using System.Linq;
public class MyGameSaveManager : GameSaveManager<MyGameSaveData, MyGameSaveMetadata> {
// Reader-supplied helper — pick whatever storage root you want.
string GetSaveDirectory() => Application.persistentDataPath;
// Enumerate all save files
protected override List<string> GetAllFileNames() {
// Your implementation here - could be files, cloud entries, etc.
return Directory.GetFiles(GetSaveDirectory(), "*.sav")
.Select(Path.GetFileName)
.ToList();
}
// Create a new save instance
protected override MyGameSaveData GetNewSave(string saveVersion) {
return new MyGameSaveData();
}
// Save data to storage
protected override void OnSaveRequest(int saveSlot, string fileName,
MyGameSaveData data, bool autosave) {
// Your save implementation - could be JSON, binary, cloud, etc.
// Example: File.WriteAllBytes(Path.Combine(GetSaveDirectory(), fileName), serializedData);
}
// Delete the save for this slot/file
protected override void OnDeleteRequest(int saveSlot, string fileName) {
string path = Path.Combine(GetSaveDirectory(), fileName);
if (File.Exists(path)) File.Delete(path);
}
// Load full save data
protected override MyGameSaveData LoadFromFile(string fileName) {
// Your load implementation
// Example: return DeserializeData(File.ReadAllBytes(...));
return null;
}
// Load only metadata (for fast UI loading)
protected override MyGameSaveMetadata LoadMetadataFromFile(string fileName) {
// Fast path to read only metadata without loading full save
return null;
}
protected override string GetFileExtension() => "sav";
protected override string GetSaveVersion() => "1.0";
}Wire your manager into Luna UI components by overriding the provider method:
public class MyMainMenuView : MainMenuView<MyGameSaveData, MyGameSaveMetadata> {
[SerializeField] private MyGameSaveManager saveManager;
protected override GameSaveManager<MyGameSaveData, MyGameSaveMetadata> GetSaveManager() {
return saveManager;
}
}Info: Once connected, UI buttons (Continue, Load, New Game, Delete) automatically work through your manager. No additional UI code needed!
The manager tracks whether a save has been explicitly chosen this play session — separate from whether CurrentSave.Data happens to be non-null:
| Member | Description |
|---|---|
bool HasSessionSave | True once a save was chosen this play session (New Game / Continue / Load / auto-load). Resets every play session, even with Domain Reload disabled. |
void SetSessionSave(TSaveData data) | Assigns CurrentSave.Data and marks the session. Use this instead of assigning CurrentSave.Data directly. |
bool TryLoadNewestIntoSession() | Loads the newest save on disk (by SaveDate) into the session. False when no save exists or it fails to load. |
TSaveData StartNewSession() | Seeds a brand-new save into the session — the same path as a menu's New Game (GetNewSave + SetSessionSave). |
// Menu buttons:
protected override void OnButtonContinueClicked()
=> GameSaveManager.SetSessionSave(GameSaveManager.GetSave(LastSaveSlot));
protected override void OnButtonNewGameClicked()
=> GameSaveManager.StartNewSession();
// Cold-start guard (run in a game scene's boot step, so Play works from any scene):
if (!manager.HasSessionSave && !manager.TryLoadNewestIntoSession())
manager.StartNewSession();⚠️ Don't use
CurrentSave.Data != nullto mean "the player picked a save". When the save data lives in aDataSO, its play-session initialize clones the default template intoData— so it is never null, and a null check can't tell a chosen save from the seeded template.HasSessionSaveexists precisely for this.
Provides Continue, New Game, and Load Game buttons. Automatically enables Continue when a recent save exists.
Non-generic UIViewComponent shell with a Return button — it owns no slot logic.
Displays save slots with metadata previews and handles save/load/delete operations. Pairs with GameSaveView on the same GameObject.
For detailed UI implementation examples, see: Main Menu and Save & Load views. Also available is the Autosave Notification utility UI.
Your save manager can implement any storage format. For a complete working example with Newtonsoft JSON, see:
Info: Refer to the GameFull sample for a complete working implementation with serializer initialization, converters, and file handling — the Newtonsoft wiring lives under
Samples~/GameFull/ScriptableObjects/Serialization/, andGameFullSaveManager.csis the reference save-manager.
Settings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction