OK here it is. Big olā chunk of text. Skip to 3.1 if you donāt care about a quick bit of background. Iāve used 2 very different save systems at work and Iāve just started writing my own for my personal projects based off experience gained from the systems Iāve used.
1. Serialize the entire world
This our first iteration, which I did not write personally. It was the most simple approach just using SerializeWorld and the obvious benefit is extremely easy to use. Itās mostly done for you! Or is it⦠We stuck with this for nearly a year, had massive tooling built to manage the issues however
The first obvious downsides is any change to a StableTypeHash on a component breaks it. Change a namespace, the name of the component, any field on the component, add/remove any field. Your save file is dead. We replaced old components with exact same memory mapped stubs then migrated to the new versions .We had a huge range of tool in place available to developers to help migrate this including detecting and auto generating the replacements. Problems really start creeping in though when you do a huge refactor that just canāt be easily migrated.
The next big issue is inflexibility. You save the entire world, you load the entire world. But what if between this time designers have come in and changed how things should behave. Maybe the giant lobster no longer has a sing ability or youāve decreased the max. Itās a huge pain and limitation on your designers to have to manually migrate the world to the new base prefabs. (NOTE: not all games want to apply changes to existing saves and this would not apply.)
Youāre also saving a lot of data you donāt need. 90%+ of components/data you do not need to save. The worst thing is, because any component change you make requires a migration even if you you donāt care what data is on a component you still need to migrate it.
But the final nail in the coffin, at least for me is - most Unity updates break your saves and there is little you can do and there is no guarantee that some point in the future Unity wonāt make a change to the entities package that will simply make this unavoidable. This is not an acceptable risk on a launched title.
2. Serialize each āarchetypeā onto their own container
So yeah, a few months out from launch we just kept randomly breaking saves. Decided it was unacceptable and we need a new approach so I āvolunteeredā and got to work writing something else. I kind of based it off what I had done and seen done in more traditional GO type games.
Built containers for each archetype, e.g.
struct BuildingSave { int Type; float3 Position; quaterion Rotation; }
To serialize we just have a large IJobEntityBatch that reads all data we want to save on this archetype and write each entity to itās own container. Pretty quick.
To deserialize, we first create a default types of each archetype saved, like you would spawn if you were create it in a game fresh. Then we just apply the saved settings to it if it still has the component. This has the huge benefit of any changes design has made to the components on the prefab are updated. Added new components? Theyāll be there. Updated a creatures max health updated, itās there (you only need to save Current health.)
Migration isnāt too bad as we control the containers however any component change might require migrating multiple containers which is a bit annoying. Also we have to migrate the entire container instead of just a single component.
However we do save the bare minimum and only need to migrate rarely, probably only have one dev update one minor thing once a month. We shipped with this 6 months ago and have not had a major issue since. Itās not perfect but itās been good enough that we havenāt considered changing since it was up and runningā¦
This brings us to now what Iām looking at doing.
First question is why? It seems like we have a proven working solution and thatās true. However this is for my own project and I canāt exactly copy code I did at work and doing it a completely different way does help me avoid any issues (not that I think Iād actually have an issue with my employer.)
But the main reason is itās still not completely without fault. While it is reasonably easy to maintain, it still takes a lot of code to setup initially and there are some ugly things about it (keeping old systems around for each migration.) I always wanted to codegen this bu doing it this way makes codegen quite a bit of work and I ran out of time.
3.1 Serialize each Component
Disclaimer: this is a 1 weekend test and has not been proven to be production worthy yet.
The approach Iām taking now is to serialize each component separately. The process is simple.
Give each entity you want saving a ātypeā component (I call it Savable). This has a reference to itās prefab (either an int, weak asset reference, whatever you want.)
Then you can can save each component my simply give a [Saved] attribute (also very easy to manually register types for those in 3rd party libraries.)
foreach (var type in TypeManager.AllTypes)
{
if (type.Category == TypeManager.TypeCategory.ComponentData)
{
if (type.Type.GetCustomAttribute(typeof(SaveAttribute)) != null)
{
var saver = new ComponentSave(this, typeIndex);
}
From the TypeIndex you can get ComponentType and get a DynamicTypeHandle
this.System.GetDynamicComponentTypeHandle(this.componentTypeRead)
using that you can serialize the component from a chunk
foreach (var chunk in this.Chunks)
{
var components = chunk.GetDynamicComponentDataArrayReinterpret<byte>(this.ComponentType, this.ElementSize);
this.Serializer.AddBufferNoResize(components);
Very simple serialization process. No magic required except grabbing an attribute in OnCreate.
Benefits:
- Just need to attach [Saved] to any IComponent or IBufferElement and it will start working
- Changes to prefab are reflected in saved data.
- Fast serialization
- Very easy migration. Just done in 1 giant block per component.
Downsides
- I have to store an int for each component I save to match to the saved entity so file size is larger.
- Not so fast deserialization (though itās actually doing much better than I expected, simply creating the entities is still the highest cost but I havenāt stress tested really high component counts yet so I am expecting not as good performance.)
Deserialize steps are basically
- Check each serialized component for current matching type, if it doesnāt exist look for migration. If not found discard data [work in progress for this weekend]
- Create all entities.
- Apply components back 1 at a time. I thought this would be slow but surprisingly itās not nearly as bad as I expected. Biggest cost is just Instantiating entities. Overall itās not a huge deal though as my goal is fast serialization as this often has while you are playing but deserialization usually happens in a load screen.
1 more note, Entity references are really easy to handle. Thanks to Unity and Entity info saved in the TypeManager you can remap entities in components with something like
public static unsafe void RemapEntityFields([ReadOnly] byte* ptr, TypeManager.EntityOffsetInfo* offsets, int offsetCount, NativeHashMap<Entity, Entity> remap)
{
for (var i = 0; i < offsetCount; i++)
{
var entity = (Entity*)(ptr + offsets[i].Offset);
*entity = remap.TryGetValue(*entity, out var newEntity) ? newEntity : Entity.Null;
}
}
Final thoughts on 3.1.
Probably wonāt get around to it this weekend as itās going to be focused on ensuring migration workflow feels good but Iām looking at implementing, what that I think will be reasonably easy, is apply save data to entities in subscenes.
Also currently I only support full component serialization. Iāve considered this a lot and partial component serialization isnāt that big a deal to implement but does make serialization a bit slower (instead of a memcpy on the entire array, each field needs to be individually MemCpyStride) and migration a bit more of a pain. I havenāt decided if Iām going to support it yet as I donāt think separating components with save data from those without is that bad a plan. That said, I will probably add this option just minimize itās use but will be on the tail end of my feature implementation.
3.2 Serialize manually per chunk
Wait what, 3.2? Yep, after I wrote 3.1 I decided to see if I could do a version that was faster didnāt need to store references ints per component to decrease file size. This is basically the same as 3.1 but instead each possible component that can be saved is stored per chunk.
This is definitely even faster to serialize and a bit faster to load and makes a measurably smaller file size.
However, when I was planning out migration I realized it was going to be a lot more rigid and painful to manage and decided to go back o my original plan. Itās still fast and file size really isnāt that big of an issue, we are only talking 3MB compressed for 8 components on 100k entities (which is a lot more than Iāll probably ever need to actually save, though will need more components.).
Iām not ruling out switching back to 3.2 at one point if I can figure out migration but for now sticking with 3.1 and fleshing out the migration for that.
Final Thoughts
Iāve loosely heard of a few more alternatives to saving. Storing components in blobs etc. I have no experience on this. Others might have completely different solutions or found ways around the downsides of SerializeWorld in which case, great, please share!