Save serialization and modding support

Hello everyone!

Save/load a game state to/from a file is pretty common for every games and there are already a tons of topics about the best way to perform game state serialization, but even after reading multiple articles, I'm not sure if my approach is correct and as I want to support modding (so "not existing yet" entities save), I'm feeling like I took the wrong path.

To begin, I'm currently working on a strategy game (tycoon actually) and I would like that when I save the game, I write in a file all needed data to get back to the same game state, with the same people/items at the same place doing the exact same thing when I load this file.

To do that I had to make a clear separation between the data of each classes in 2 categories: immutable data and mutable data.

Immutable data come from databases populated parsing JSON files when the game is launched. It describes the content of our game like the price of an object for example and it's used to initialize game objects data.

Mutable data are data modified by the logic during the gameplay according to the player inputs (or not as people can make their lives without the player).

To summarize, immutable data don't change and are easy to retrieve whereas mutable data is regularly updated and represent the game state.

So with a clear separation between immutable and mutable data, the only thing we should do is to save the mutable data for all objects. I've started to create a class Save to store the structure of the save file (that I've simplified to avoid confusion):

[Serializable]
public class EntitySave
{
    public string Id;
    public string InstanceId;
    public Vector3 Position;
    public float Rotation;
    public bool IsMoving;
    public string RelatedEntityInstanceId;
}

[Serializable]
public class Save
{
    public List<EntitySave> Entities;
}

Then in my Entity class I've implemented Save and Load methods:

[System.Serializable]
public class EntityData
{
    public string Id;
    public string PrefabFile;
    public int Price;
}

public class Entity : MonoBehaviour
{
    // Immutable data
    private EntityData _data;

    // Mutable data
    private string _instanceId;
    private bool _isMoving;
    private Entity _relatedEntity;

    // Properties
    public string InstanceId => _instanceId;

    public void Initialize(EntityData _data)
    {
        _instanceId = GenerateUniqueId();
    }

    public void Save(Save save)
    {
        EntitySave entitySave = new EntitySave()
        {
            Id = _data.Id,
            InstanceId = _instanceId,
            IsMoving = _isMoving,
            RelatedEntityInstanceId = _relatedEntity.InstanceId,
            Position = transform.position,
            Rotation = transform.rotation.eulerAngles.y
        };

        save.Entities.Add(entitySave);
    }

    public void Load(EntitySave entitySave)
    {
        _data = GameManager.Instance.GameDatabase.GetEntity(entitySave.Id);
        _instanceId = entitySave.InstanceId;
        _isMoving = entitySave.IsMoving;
        _relatedEntity = GameManager.Instance.FindEntity(entitySave.RelatedEntityInstanceId);

        transform.position = entitySave.Position;
        transform.rotation = UnityEngine.Quaternion.Euler(0f, entitySave.Rotation, 0f);
    }
}

And in my GameManager class, I can easily save/load a game state like that (I'm using JSON.Net for save serialization):

public class GameManager
{
    // Entity database
    private List<EntityData> _entitiesDatabase = new List<EntityData>();

    // All in game entities
    private List<Entity> _entities = new List<Entity>();

    public void LoadDatabases()
    {
        string json = File.ReadAllText(Path.Combine(Application.streamingAssetsPath, "entities.json"));
        _entitiesDatabase = JsonConvert.DeserializeObject<List<EntityData>>(json);
    }

    public EntityData GetEntityData(string entityId)
    {
        return _entitiesDatabase.FirstOrDefault(entity => entity.Id == entityId);
    }

    public void Save(string filename)
    {
        Save save = new Save();

        foreach (var entity in _entities)
        {
            entity.Save(save);
        }

        File.WriteAllText(filename, JsonConvert.SerializeObject(save));
    }

    public void Load(string filename)
    {
        string json = File.ReadAllText(Path.Combine(Application.streamingAssetsPath, filename));
        Save save = JsonConvert.DeserializeObject<Save>(json);

        foreach (var entitySave in save.Entities)
        {
            EntityData entityData = GetEntityData(entitySave.Id);

            Entity entityPrefab = Resources.Load<Entity>(entityData.PrefabFile);
            Entity entity = Instantiate(entityPrefab, entitySave.Position, Quaternion.Euler(0f, entitySave.Rotation, 0f));

            // Initialize the entity giving it its immutable data
            entity.Initialize(entityData);

            // Load the entity mutable data from the save
            entity.Load(entitySave);

            _entities.Add(entity);
        }
    }
}

And to complete, here is an example of entities.json file:

[
    {
        "Id": "CHAIR",
        "PrefabFile": "chair.prefab",
        "Price": 100
    },
    {
        "Id": "TOURIST",
        "PrefabFile": "tourist.prefab",
        "Price": 1000
    },
]

First question: Is it a good way to do that?

Second question: How can I extend it to support MOD?

Because for now, if someone want to add another kind of entity with a new logic from outside of the project, the new mutable data added for this new entity could be properly saved if this entity inherits from my Entity class and that I make the Save method virtual but how am I suppose to know what kind of entity it is when I load it?

1 Like

Your logic looks solid.

How will people be modding your game? If they are changing code then they could also change the save class for the new logic they create.

Thank you for your answer @brigas .

To mod the game, I would like to use ModTool. Basically, the mod project uses the DLL of your game to create its own scripts compiled in a new assembly that will be loaded at runtime in your game, and there are some limitations.

Changing the save class is not possible for me for instance. I thought using partial keyword for the Save class would allow me to add new fields to the class, but actually when your game is compiled, all partial classes are merged together and you can't do that. :(

Ok, I finally found a solution for my problem.

Actually, I had two main blocking issues:

  • How to create a new object instance from a single class name string?
  • How to save/load custom fields added (and used) by a new MOD class?

For the 1st point, we can get a Type (System.Type) from a string specifying the Assembly name like that:

Type myCustomType = Type.GetType("MyCustomType, MyAssembly");

But if I imagine we have a generic method to load an object of our custom type with a signature like that:

private T LoadEntity<T>(EntitySave entitySave)

How am I supposed to call LoadEntity method with my myCustomType as “generic” argument?

Because, unfortunatly, you can’t do that:

LoadEntity<myCustomType>(...)

In fact, it’s possible using MakeGenericMethod (and maybe dynamic keyword from C# 4.0, which would be better than reflection, but I didn’t succeed to do it yet). Here is the topic where I found the answer if you want more info.

So if we update the GameManager class from my previous post combining these ideas, we get something like that for the Load() method:

public class GameManager
{
    // ...
    public void Load(string filename)
    {
        string json = File.ReadAllText(Path.Combine(Application.streamingAssetsPath, filename));
        Save save = JsonConvert.DeserializeObject<Save>(json);
        foreach (var entitySave in save.Entities)
        {
            string completeType = $"{entitySave.Type}";
            completeType += $", {entitySave.Assembly ?? "MyProjectAssembly"}";
            Type entityType = Type.GetType(completeType);

            if (entityType.IsSubclassOf(typeof(Entity)))
            {
                MethodInfo method = typeof(GameManager).GetMethod(
                    nameof(GameManager.LoadEntity),
                    BindingFlags.NonPublic | BindingFlags.Instance
                );
                MethodInfo generic = method.MakeGenericMethod(entityType);
                var entity = generic.Invoke(this, new object[] { entitySave });
             
                _entities.Add(entity);
            }
        }
    }

    private T LoadEntity<T>(EntitySave entitySave) where T : Entity
    {
        EntityData entityData = GetEntityData(entitySave.Id);

        T entityPrefab = Resources.Load<T>(entityData.PrefabFile);
        T entity = Instantiate(entityPrefab, entitySave.Position, Quaternion.Euler(0f, entitySave.Rotation, 0f));

        // Initialize the entity giving it its immutable data
        entity.Initialize(entityData);

        // Load the entity mutable data from the save
        entity.Load(entitySave);

        return entity;
    }
}

This obviously supposes that the EntitySave class has a new field to specify the Type and the Assembly:

[Serializable]
public class EntitySave
{
    public string Type;
    public string Assembly;
    public string Id;
    public string InstanceId;
    public Vector3 Position;
    public float Rotation;
    public bool IsMoving;
    public string RelatedEntityInstanceId;
}

For the 2nd point, I’ve decided to use a Dictionary<string, object> to store all data fields:

[Serializable]
public class EntitySave
{
    public string Type;
    public string Assembly;
    public string Id;
    public string InstanceId;
    public Vector3 Position;
    public float Rotation;
    public Dictionary<string, object> Data;
}

And I’ve updated the Entity’s Save/Load accordingly:

public class Entity : MonoBehaviour
{
    // Immutable data
    private EntityData _data;

    // Mutable data
    private string _instanceId;
    private bool _isMoving;
    private Entity _relatedEntity;

    public void Save(Save save)
    {
        EntitySave entitySave = new EntitySave()
        {
            Id = _data.Id,
            InstanceId = _instanceId,
            Position = transform.position,
            Rotation = transform.rotation.eulerAngles.y,
            Data = new Dictionary<string, object>
            {
                { "IsMoving", _isMoving },
                { "RelatedEntityInstanceId", _relatedEntity.InstanceId }
            }
        };

        save.Entities.Add(entitySave);
    }

    public void Load(EntitySave entitySave)
    {
        _data = GameManager.Instance.GameDatabase.GetEntity(entitySave.Id);
        _instanceId = entitySave.InstanceId;

        if (entitySave.Data.ContainsKey("IsMoving"))
        {
            _isMoving = (bool)entitySave.Data["IsMoving"];
        }

        if (entitySave.Data.ContainsKey("RelatedEntityInstanceId"))
        {
            _relatedEntity = GameManager.Instance.FindEntity(
                entitySave.Data["RelatedEntityInstanceId"].ToString()
            );
        }

        transform.position = entitySave.Position;
        transform.rotation = UnityEngine.Quaternion.Euler(0f, entitySave.Rotation, 0f);
    }
}

Warning: object type is a little bit restrictive, you can’t easily cast an object in a float like that for example:

object test = 0.42f;
float test2 = (float)test;

You need to use Convert for most type (including integers):

object test = 0.42f;
float test2 = Convert.ToSingle(test);