Save File Migration

Crystal Save includes a robust migration system to ensure your players' saved data remains compatible when updating your game's format. When you change your game data structure, Crystal Save can automatically upgrade old saves, preventing loss of progress.

This documentation provides step-by-step instructions on how to set up and manage save file migrations.


Overview

Crystal Save’s migration system consists of three main components:

  • Versioning: Tracks the game version associated with each save.

  • MigrationManager: A ScriptableObject containing migration steps.

  • MigrationAction: ScriptableObjects defining specific migration operations.

When a game is loaded, Crystal Save checks the saved version and automatically applies necessary migrations to match the current game version.


How Versioning Works

Each save slot includes version metadata:

public partial class SaveData
{
    public VersionData Version { get; set; }

    public SaveData()
        : this(SaveManager.Instance?.SaveSettings?.version.Clone() as VersionData
               ?? new VersionData(1, 0, 0))
    { /* … */ }
}

VersionData tracks Major, Minor, and Patch version numbers, supports version comparisons, and can be cloned.

Define the current version of your game within the SaveSettings asset:

[Tooltip("Your Game Version")]
public VersionData version = new VersionData(1, 0, 0);

[Header("Migration Settings")]
[Tooltip("Overwrite existing Save after migration")]
public bool autoSaveMigratedData = false;

Components of the Migration System

Versioning Service

The VersioningService initializes the version management, serializer, and migration manager during game startup:

public Task InitializeAsync()
{
    VersionManager = new VersionManager(manager.SaveSettings);
    Serializer = SaveDataSerializer.Instance;
    MigrationManager = AssetProvider.Load<MigrationManager>("MigrationManager")
        ?? throw new InvalidOperationException("MigrationManager asset not found.");

    manager.VersionManagerInternal = VersionManager;
    manager.SerializerInternal = Serializer;
    manager.MigrationManagerInternal = MigrationManager;
    return Task.CompletedTask;
}

public void Migrate(SaveData data)
{
    MigrationManager?.Migrate(data);
}

Migration Manager

The MigrationManager is a ScriptableObject containing ordered migration steps:

public void Migrate(SaveData data)
{
    foreach (var step in migrationSteps)
    {
        if (data.Version.CompareTo(step.targetVersion) < 0)
        {
            foreach (var action in step.migrationActions)
                action?.ApplyMigration(data);

            data.Version = step.targetVersion.Clone() as VersionData;
        }
    }
}

Migration Actions

MigrationAction is an abstract base for defining custom migration logic:

public abstract class MigrationAction : ScriptableObject
{
    public abstract void ApplyMigration(SaveData data);
}

Example Migration Action

Here's an example migration action that updates multiple GameObject transforms:

[CreateAssetMenu(fileName = "MigrateMultipleTransforms",
                 menuName = "Crystal Save/Create Migration Actions/Migrate Multiple Transforms")]
public class MigrateMultipleTransforms : MigrationAction
{
    [System.Serializable]
    public class TransformMigrationEntry
    {
        public string targetUniqueID;
        public string targetName;
        public Vector3 newPosition;
        public Vector3 newEulerRotation;
        public Vector3 newScale;
    }

    public List<TransformMigrationEntry> migrationEntries = new();

    public override void ApplyMigration(SaveData data)
    {
        // Implement migration logic here to rewrite component data.
    }
}

Runtime Migration Flow

When loading saved data:

  1. The system deserializes the save slot and checks the version.

  2. If the save's version is older, the migration service automatically applies necessary migration steps:

Logger.Log($"SaveData version {loadedData.Version} is older... Initiating migration.");
saveManager.VersioningService.Migrate(loadedData);

if (saveManager.SaveSettings.autoSaveMigratedData)
    migrationPerformed = true;

The system handles version mismatches gracefully, ensuring backward compatibility:

case VersionComparisonResult.Older:
    Logger.Log($"Save data version {data.Version} is older than current version {VersioningService.VersionManager.CurrentVersion}. Attempting to load with compatibility mode.", LogLevel.Warning);
    VersioningService?.Migrate(data);
    break;

If autoSaveMigratedData is enabled, migrated data overwrites the original slot automatically.


Step-by-Step Example: Migrating from Version 1.0 → 2.0

Here's how to set up a migration from save version 1.0 to version 2.0:

Step 1: Update Save Settings

  • In your Crystal Save Settings asset, set the version to 2.0.0.

Step 2: Create or Locate Migration Manager

  • The MigrationManager already exists but if not: Create a new MigrationManager asset named MigrationManager within a Resources folder (Default location: Assets/Plugins/CrystalSave/Resources/). This ensures the VersioningService can find it.

Step 3: Add a Migration Step

  • Open the Crystal Save Migration Window (Top Menu: Tools/Crystal Save/Migration Window), add a Description and a new migration step targeting version 2.0.0 and provide a clear description of the changes.

Step 4: Implement Migration Actions

  • Create one or more MigrationAction ScriptableObjects to define specific data transformations needed for your update (e.g., updating transform data, changing item states, etc.). In the Project window Right Click > Create/Crystal Save/Create Migration Action/

  • Add these actions to the migration step targeting version 2.0.0.

Step 5: Test Loading Old Saves

  • When loading a save file from version 1.0, Crystal Save will automatically apply the defined migration steps until the save’s version matches the current version (2.0.0).

  • Optionally enable autoSaveMigratedData in the Save Settings to automatically overwrite the old save slot with the migrated data.


Best Practices

  • Always back up existing save data before performing migrations during development.

  • Thoroughly test migration actions to ensure data integrity.

  • Document each migration step clearly in the MigrationManager asset for easy future reference.


By following these guidelines, you can ensure smooth and seamless transitions for players, even as your game's data structures evolve. Crystal Save's migration system minimizes complexity, allowing you to focus on game development rather than managing legacy data manually.

Last updated