Save/Load without Blueprints
I have asked about this problem before, but now that I'm actually implementing it there're several issues. I'm planning to keep this issue opened until I can update a working solution for others who might be searching.
What's not working now
- [x] Serialize/Hydrate components: I'm using NewtonSoft's JSON.Net together with custom Converter from this SO question
- [x] Repopulate entities: AddComponent() based on component type
- [x] ~~Not sure how to save/hydrate ScriptableObject's reference (getting warning for serializing the ScriptableObject itself)~~ actually it's a terrible idea to bind config scriptable object to entity (game update will easily corrupt save data)
- [ ] Not sure if this way of getting component index is correct:
entity.AddComponent(
GameComponentsLookup.componentTypes.ToList().IndexOf(component.GetType()),
component
);
Help is much appreciated!
Here's what I'm doing (an autosave feature) following the idea from this issue. I'm stripping entities from contexts directly because it's a "Save and Exit Game" feature (like Diablo II). For "Save and Continue" I would get entities list from contexts similarly.
EntityData.cs
[System.Serializable]
public class EntityData
{
public string contextInfo;
public string components;
public EntityData(string contextInfo, IEnumerable<IComponent> components)
{
this.contextInfo = contextInfo;
this.components = AltiJsonSerializer.SerializeJson(components);
}
}
IAutoSaveComponent.cs
public interface IAutoSaveComponent : IComponent { }
GameController.cs
private static IEnumerable<Type> autoSaveTypes;
void Awake()
{
if (autoSaveTypes == null)
{
var type = typeof(IAutoSaveComponent);
autoSaveTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p));
}
}
void AutoSave()
{
StripEntitiesForAutoSave(_contexts);
SaveContexts(_contexts);
_contexts.Reset();
}
void SaveContexts(Contexts contexts)
{
List<EntityData> savedEntities = new List<EntityData>();
foreach (var context in contexts.allContexts)
{
Entity[] entities = (Entity[])context.GetType().GetMethod("GetEntities").Invoke(context, null);
foreach (GameEntity entity in entities)
{
savedEntities.Add(
new EntityData(
entity.contextInfo.name,
entity.GetComponents().Where(component => component is IAutoSaveComponent)
)
);
}
}
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Open(Application.persistentDataPath + "/autosave.dat", FileMode.OpenOrCreate);
bf.Serialize(file, savedEntities);
file.Close();
}
void LoadContexts(Contexts contexts)
{
if (File.Exists(Application.persistentDataPath + "/autosave.dat"))
{
BinaryFormatter bf = new BinaryFormatter();
FileStream file = File.Open(Application.persistentDataPath + "/autosave.dat", FileMode.Open);
List<EntityData> savedEntities = (List<EntityData>)bf.Deserialize(file);
file.Close();
Dictionary<string, object> contextMap = new Dictionary<string, object>();
contextMap.Add(contexts.game.contextInfo.name, contexts.game);
foreach (EntityData entityData in savedEntities)
{
var context = contextMap[entityData.contextInfo];
Entity entity = (Entity)context.GetType().GetMethod("CreateEntity").Invoke(context, null);
List<IComponent> components = AltiJsonSerializer.DeserializeJson<List<IComponent>>(
entityData.components,
autoSaveTypes
);
foreach (IComponent component in components)
{
entity.AddComponent(
GameComponentsLookup.componentTypes.ToList().IndexOf(component.GetType()),
component
);
}
}
}
}
JsonKnownTypeConverter.cs from SO need some change
return typeName.Contains(x.Name + ","); // Instead of "." + x.Name + ","
Posting just to show my support right now. I had a working system using Blueprints for a while, and it worked fine, but with the new Event system it has gotten tied to MonoBehaviours in some cases and I haven't gotten deserialization for those working yet.