Skip to content

Instantly share code, notes, and snippets.

@ScottJDaley
Created July 8, 2021 16:21
Show Gist options
  • Save ScottJDaley/52f937c266247f221341d3fd4244e737 to your computer and use it in GitHub Desktop.
Save ScottJDaley/52f937c266247f221341d3fd4244e737 to your computer and use it in GitHub Desktop.
Save and Load for Unity ECS using SerializeUtility
[Serializable]
public struct SaveTag : IComponentData { }
[Serializable]
public struct MissingRenderMeshTag : IComponentData { }
[Serializable]
public struct MissingPhysicsColliderTag : IComponentData { }
[Serializable]
public struct MyBlobComponent : IComponentData
{
public struct MyBlobData
{
public int Data;
}
public BlobAssetReference<MyBlobData> Value;
}
[Serializable]
public struct MissingMyBlobComponentTag : IComponentData { }
// Each prefab entity will have a unique ID stored in a PrefabID component. All entities instantiated from a given
// prefab entity will have the same PrefabID component, but will be missing the Prefab tag component.
[Serializable]
public struct PrefabID : IComponentData
{
public FixedString32 ID;
}
/*
* Stores all prefabs that contain a PrefabID in a hash map keyed by ID that is accessible by other systems.
*/
[UpdateAfter(typeof(ConvertToEntitySystem))]
[UpdateInGroup(typeof(InitializationSystemGroup))]
public class PrefabSystem : SystemBase
{
public NativeHashMap<FixedString32, Entity> PrefabsByID;
private EntityCommandBufferSystem _commandBufferSystem;
private struct PrefabSystemState : ISystemStateComponentData { }
protected override void OnCreate()
{
PrefabsByID = new NativeHashMap<FixedString32, Entity>(2, Allocator.Persistent);
_commandBufferSystem = World.GetOrCreateSystem<EndInitializationEntityCommandBufferSystem>();
}
protected override void OnDestroy()
{
PrefabsByID.Dispose();
}
protected override void OnUpdate()
{
NativeHashMap<FixedString32, Entity> prefabs = PrefabsByID;
EntityCommandBuffer commandBuffer = _commandBufferSystem.CreateCommandBuffer();
// Add all new prefabs to the prefab hash map.
Entities
.WithAll<Prefab>()
.WithNone<PrefabSystemState>()
.ForEach(
(Entity entity, in PrefabID prefabID) =>
{
prefabs.Add(prefabID.ID, entity);
commandBuffer.AddComponent<PrefabSystemState>(entity);
}).Schedule();
_commandBufferSystem.AddJobHandleForProducer(Dependency);
}
}
/*
* Looks for any entities that contain the "Missing" component tags and replaces them with the actual components from
* the associated prefab for that entity.
*/
[UpdateInGroup(typeof(InitializationSystemGroup))]
public class RestoreAfterLoadSystem : SystemBase
{
private EntityCommandBufferSystem _commandBufferSystem;
private PrefabSystem _prefabSystem;
protected override void OnCreate()
{
_commandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
_prefabSystem = World.GetOrCreateSystem<PrefabSystem>();
}
protected override void OnUpdate()
{
EntityCommandBuffer commandBuffer = _commandBufferSystem.CreateCommandBuffer();
NativeHashMap<FixedString32, Entity> prefabs = _prefabSystem.PrefabsByID;
Entities
.WithAll<MissingRenderMeshTag>()
.WithoutBurst()
.ForEach(
(Entity entity, in PrefabID prefabID) =>
{
if (!prefabs.TryGetValue(prefabID.ID, out Entity prefab))
{
Debug.LogError($"Could not find prefab with id {prefabID.ID}");
return;
}
var renderMesh = EntityManager.GetSharedComponentData<RenderMesh>(prefab);
var description = new RenderMeshDescription(renderMesh.mesh, renderMesh.material);
RenderMeshUtility.AddComponents(entity, commandBuffer, description);
commandBuffer.RemoveComponent<MissingRenderMeshTag>(entity);
}
).Run();
Entities
.WithAll<MissingPhysicsColliderTag>()
.ForEach(
(Entity entity, in PrefabID prefabID) =>
{
if (!prefabs.TryGetValue(prefabID.ID, out Entity prefab))
{
Debug.LogError($"Could not find prefab with id {prefabID.ID}");
return;
}
commandBuffer.AddComponent(entity, GetComponent<PhysicsCollider>(prefab));
commandBuffer.RemoveComponent<MissingPhysicsColliderTag>(entity);
}
).Schedule();
Entities
.WithAll<MissingMyBlobComponentTag>()
.ForEach(
(Entity entity, in PrefabID prefabID) =>
{
if (!prefabs.TryGetValue(prefabID.ID, out Entity prefab))
{
Debug.LogError($"Could not find prefab with id {prefabID.ID}");
return;
}
commandBuffer.AddComponent(entity, GetComponent<MyBlobComponent>(prefab));
commandBuffer.RemoveComponent<MissingMyBlobComponentTag>(entity);
}
).Schedule();
_commandBufferSystem.AddJobHandleForProducer(Dependency);
}
}
public class SaveManager : MonoBehaviour
{
private EntityQuery _entitiesToSave;
private void Start()
{
// Cache a query that gathers all of the entities that should be saved.
// NOTE: You don't have to use a special tag component for all entities you want to save. You could instead just
// save, for example, anything with a Translation component which would exclude things like Singletons entities.
// It is important to note that prefabs (anything with a Prefab tag component) are automatically excluded from
// an EntityQuery unless EntityQueryOptions.IncludePrefab is set.
var savableEntities = new EntityQueryDesc
{
Any = new ComponentType[]
{
typeof(SaveTag),
},
Options = EntityQueryOptions.Default
};
EntityManager entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
_entitiesToSave = entityManager.CreateEntityQuery(savableEntities);
}
// Looks for and removes a set of components and then adds a different set of components to the same set
// of entities.
private void ReplaceComponents(
ComponentType[] typesToRemove,
ComponentType[] typesToAdd,
EntityManager entityManager)
{
EntityQuery query = entityManager.CreateEntityQuery(
new EntityQueryDesc { Any = typesToRemove, Options = EntityQueryOptions.Default }
);
NativeArray<Entity> entities = query.ToEntityArray(Allocator.Temp);
foreach (ComponentType removeType in typesToRemove)
{
entityManager.RemoveComponent(entities, removeType);
}
foreach (ComponentType addType in typesToAdd)
{
entityManager.AddComponent(entities, addType);
}
}
public void Save(string filepath)
{
/*
* 1. Create a new world.
* 2. Copy over the entities we want to serialize to the new world.
* 3. Remove all shared components, components containing blob asset references, and components containing
* external entity references.
* 4. Serialize the new world to a save file.
*/
EntityManager entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
using (var serializeWorld = new World("Serialization World"))
{
EntityManager serializeEntityManager = serializeWorld.EntityManager;
serializeEntityManager.CopyEntitiesFrom(entityManager, _entitiesToSave.ToEntityArray(Allocator.Temp));
// Remove RenderMesh and related components
ReplaceComponents(
new ComponentType[]
{
typeof(RenderMesh),
typeof(EditorRenderData),
typeof(WorldRenderBounds),
typeof(ChunkWorldRenderBounds),
typeof(HybridChunkInfo),
typeof(RenderBounds)
},
new ComponentType[] { typeof(MissingRenderMeshTag) },
serializeEntityManager
);
// Remove physics colliders
ReplaceComponents(
new ComponentType[]
{
typeof(PhysicsCollider),
},
new ComponentType[] { typeof(MissingPhysicsColliderTag) },
serializeEntityManager
);
// Remove blob assets.
ReplaceComponents(
new ComponentType[]
{
typeof(MyBlobComponent),
},
new ComponentType[] { typeof(MissingMyBlobComponentTag) },
serializeEntityManager
);
// Need to remove the SceneTag shared component from all entities because it contains an entity reference
// that exists outside the subscene which isn't allowed for SerializeUtility. This breaks the link from the
// entity to the subscene, but otherwise doesn't seem to cause any problems.
serializeEntityManager.RemoveComponent<SceneTag>(serializeEntityManager.UniversalQuery);
serializeEntityManager.RemoveComponent<SceneSection>(serializeEntityManager.UniversalQuery);
// Save
using (var writer = new StreamBinaryWriter(filepath))
{
SerializeUtility.SerializeWorld(serializeEntityManager, writer);
}
}
}
public void Load(string filepath)
{
EntityManager entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
entityManager.DestroyEntity(_entitiesToSave);
using (var deserializeWorld = new World("Deserialization World"))
{
ExclusiveEntityTransaction transaction = deserializeWorld.EntityManager.BeginExclusiveEntityTransaction();
using (var reader = new StreamBinaryReader(filepath))
{
SerializeUtility.DeserializeWorld(transaction, reader);
}
deserializeWorld.EntityManager.EndExclusiveEntityTransaction();
entityManager.MoveEntitiesFrom(deserializeWorld.EntityManager);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment