This sample demonstrates a complete file-based save system using Newtonsoft JSON within Luna. It shows how to implement a concrete save manager that works seamlessly with Luna's UI components.
Refer to Save System Framework to learn about the UI-agnostic save system architecture.
Key Features: Pluggable serializers, secure type allow-lists, fast metadata loading, and complete UI integration.
GameSaveManagerExample — concrete manager handling files, folders, and JSON IOGameSaveDataExample — example player/save data modelGameSaveMetadataExample — lightweight metadata for lists and previewsInitializeSerializerExample — safe serializer initialization with converters and type allow-listAdd the serializer initializer to your bootstrap scene:
_serializationManager.Initialize(
new Type[] {
typeof(GameSaveMetadata),
typeof(GameSaveMetadataExample),
typeof(GameSaveDataExample),
typeof(Inventory),
typeof(InventoryItem),
typeof(Potion),
typeof(Equipment),
},
new JsonConverter[] {
new Vector2IntConverter(),
new GenericDictionaryConverter()
},
new PrivateSetterContractResolver()
);Warning: Keep the type allow-list tight for security when using
TypeNameHandlingfeatures.
If you only need to add project-specific types/converters/resolver behavior, do not fork or rewrite the serialization pipeline.
Use SerializationTypeProviderSO and plug it into SerializationManagerRegistrar:
[CreateAssetMenu(menuName = "MyGame/Serialization/My Serialization Types")]
public class MySerializationTypesSO : SerializationTypeProviderSO {
public override IList<Type> GetKnownTypes() {
return new Type[] {
typeof(GameSaveMetadata),
typeof(MyGameSaveMetadata),
typeof(MyGameSaveData),
typeof(MyPolymorphicType),
};
}
}In the sample, DemoSerializationTypesSO : SerializationTypeProviderSO follows this pattern. The registrar then composes providers via its _providers list, so you can keep Luna defaults and add your own types side by side.
Inspector workflow
SerializationTypeProviderSO class.SerializationManagerRegistrar asset and add the provider to _providers.Info: This approach is additive and maintainable: update provider assets as your data model evolves, instead of replacing serializer setup code.
public class MyGameSaveData : IGameSaveData {
[JsonProperty(Order = -100)]
public GameSaveMetadata Metadata { get; set; }
public string PlayerName;
public int Gold;
public Inventory Inventory;
public MyGameSaveData() {
PlayerName = "Player";
Gold = 0;
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;
}
}Define lightweight metadata for UI display:
public class MyGameSaveMetadata : GameSaveMetadata {
public int Gold;
}Info: Use
[JsonProperty(Order = -100)]onMetadataso it appears first in JSON for fast metadata reads.
Inherit from GameSaveManager<MyGameSaveData, MyGameSaveMetadata> and implement the JSON-specific methods:
public class MyGameSaveManager : GameSaveManager<MyGameSaveData, MyGameSaveMetadata> {
protected override string[] GetAllFileNames() {
// Enumerate .json files in save directory
return Directory.GetFiles(GetSaveDirectory(), "*.json")
.Select(Path.GetFileName)
.ToArray();
}
protected override MyGameSaveData GetNewSave(string saveVersion) {
return new MyGameSaveData();
}
protected override void OnSaveRequest(int saveSlot, string fileName,
MyGameSaveData data, bool autosave) {
// Serialize and write JSON file
string json = JsonConvert.SerializeObject(data, Formatting.Indented);
File.WriteAllText(Path.Combine(GetSaveDirectory(), fileName), json);
}
protected override MyGameSaveData LoadFromFile(string fileName) {
// Deserialize full save data
string json = File.ReadAllText(Path.Combine(GetSaveDirectory(), fileName));
return JsonConvert.DeserializeObject<MyGameSaveData>(json);
}
protected override MyGameSaveMetadata LoadMetadataFromFile(string fileName) {
// Fast path to read only metadata
using var reader = new JsonTextReader(new StringReader(
File.ReadAllText(Path.Combine(GetSaveDirectory(), fileName))));
while (reader.Read()) {
if (reader.TokenType == JsonToken.PropertyName &&
reader.Value?.ToString() == "Metadata") {
reader.Read();
return JsonConvert.DeserializeObject<MyGameSaveMetadata>(
reader.ReadAsString());
}
}
return null;
}
protected override void OnDeleteRequest(int saveSlot, string fileName) {
// Delete JSON file
File.Delete(Path.Combine(GetSaveDirectory(), fileName));
}
protected override string GetFileExtension() => "json";
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!
Application.persistentDataPath/saves/.json (configured by your manager)GetSaveVersion() methodAdd encryption/decryption in your manager's save/load methods:
protected override void OnSaveRequest(int saveSlot, string fileName,
MyGameSaveData data, bool autosave) {
string json = JsonConvert.SerializeObject(data, Formatting.Indented);
byte[] encrypted = Encrypt(json); // Your encryption logic
File.WriteAllBytes(Path.Combine(GetSaveDirectory(), fileName), encrypted);
}
protected override MyGameSaveData LoadFromFile(string fileName) {
byte[] encrypted = File.ReadAllBytes(Path.Combine(GetSaveDirectory(), fileName));
string json = Decrypt(encrypted); // Your decryption logic
return JsonConvert.DeserializeObject<MyGameSaveData>(json);
}Replace file IO with cloud API calls while keeping the same UI interface:
protected override void OnSaveRequest(int saveSlot, string fileName,
MyGameSaveData data, bool autosave) {
string json = JsonConvert.SerializeObject(data);
CloudStorage.Upload(fileName, json); // Your cloud API
}
protected override MyGameSaveData LoadFromFile(string fileName) {
string json = CloudStorage.Download(fileName); // Your cloud API
return JsonConvert.DeserializeObject<MyGameSaveData>(json);
}Warning: No save files found
Ensure
Application.persistentDataPath/savesexists and the platform has write permissions.
Warning: Metadata shows null
Ensure
Metadatais serialized first with[JsonProperty(Order = -100)]and created before saving.
Warning: Deserialization errors
Update the serializer allow-list and add required converters for custom types.
Warning: UI buttons disabled
Verify your manager returns valid metadata from
GetLastMetadata()and the serializer is properly initialized.
[JsonProperty(Order = -100)] on Metadata for fast metadata readsInfo: Remember: The UI remains completely unchanged when you swap between JSON, binary, cloud, or encrypted storage. Only your manager implementation changes.
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