Hi, I want to know if it is possible to serialize an entire entity with all its data, store it in file and then deserialize it and instantiate it. I know there is something with serializing entire world, but that is not what I want. If it isnt possible, will it be in the future and is there other way? Thank you for answers.
No answer yet…
Here is an example, for a good start. But you may want to replace that json serialization with binary one at one point (for faster read/write, smaller file sizes, minimum data security)
Basic idea here is to know all component types you want to serialize at compile time - not runtime. Because runtime component type deserialization is much more trickier that this.
#if UNITY_EDITOR
using System.Collections.Generic;
using Unity.Mathematics;
using Unity.Entities;
using Unity.Transforms;
using Unity.Collections;
using Unity.Rendering;
using IO = System.IO;
using Debug = UnityEngine.Debug;
using JsonUtility = UnityEngine.JsonUtility;
using Application = UnityEngine.Application;
using Random = Unity.Mathematics.Random;
using MenuItem = UnityEditor.MenuItem;
public static class SerializationTester
{
const string k_entitySerializationTestWorldName = nameof(SerializationTester);
static readonly ComponentType[] _testArchetypeComponents = new ComponentType[]{ typeof(Translation) , typeof(Rotation) , typeof(Scale) };
[MenuItem("Test/Entity Serialization/1. Create World",false,1)]
[MenuItem("Test/Entity Serialization/5. Create World",false,5)]
static void CreateEntitySerializationTestWorld () => new World(k_entitySerializationTestWorldName );
[MenuItem("Test/Entity Serialization/2. Create Entities",false,2)]
static void TestEntitySerializationCreateEntities ()
{
var world = FindWorld(k_entitySerializationTestWorldName);
if( world==null ){ Debug.Log($"world '{k_entitySerializationTestWorldName}' doesn't exist, aborted"); return; }
var command = world.EntityManager;
var archetype = command.CreateArchetype( _testArchetypeComponents );
var random = new Random( (uint) System.DateTime.Now.GetHashCode() );
for( int i=0 ; i<10000 ; i++ )
{
var entity = command.CreateEntity( archetype );
command.SetComponentData( entity , new Translation{
Value = random.NextFloat3()
} );
command.SetComponentData( entity , new Rotation{
Value = random.NextQuaternionRotation()
} );
command.SetComponentData( entity , new Scale{
Value = random.NextFloat()
} );
}
}
[MenuItem("Test/Entity Serialization/3. Serialize World",false,3)]
static void TestEntitySerialization ()
{
var world = FindWorld(k_entitySerializationTestWorldName);
if( world==null ){ Debug.Log($"world '{k_entitySerializationTestWorldName}' doesn't exist, aborted"); return; }
var command = world.EntityManager;
var archetype = command.CreateArchetype( _testArchetypeComponents );
var query = command.CreateEntityQuery( _testArchetypeComponents );
if( query.CalculateEntityCount()==0 ){ Debug.Log($"world '{k_entitySerializationTestWorldName}' contains no matching entities, aborted"); query.Dispose(); return; }
{
var entities = query.ToEntityArray( Allocator.TempJob );
string dir = Application.temporaryCachePath;
string saveID = $"{world.Name}-save-#{1}";
CreateSaveHeaderFile( directoryPath:dir , saveID:saveID , numEntities:entities.Length );
SaveComponentDataToJson<Translation>( entities , command , directoryPath:dir , saveID:saveID );
SaveComponentDataToJson<Rotation>( entities , command , directoryPath:dir , saveID:saveID );
SaveComponentDataToJson<Scale>( entities , command , directoryPath:dir , saveID:saveID );
entities.Dispose();
}
query.Dispose();
}
[MenuItem("Test/Entity Serialization/4. Dispose World",false,4)]
static void DisposeEntitySerializationTestWorld () => FindWorld(k_entitySerializationTestWorldName)?.Dispose();
[MenuItem("Test/Entity Serialization/6. Deserialize",false,6)]
static void TestEntityDeserialization ()
{
var world = FindWorld(k_entitySerializationTestWorldName);
if( world==null ) world = new World(k_entitySerializationTestWorldName);
var command = world.EntityManager;
var archetype = command.CreateArchetype( _testArchetypeComponents );
string dir = Application.temporaryCachePath;
string saveID = $"{world.Name}-save-#{1}";
if( ReadSaveHeaderFile( directoryPath:dir , saveID:saveID , out var header ) )
{
var entities = command.CreateEntity( archetype , header.numEntities , Allocator.TempJob );
{
LoadComponentDataFromJson<Translation>( directoryPath:dir , saveID:saveID , command , entities );
LoadComponentDataFromJson<Rotation>( directoryPath:dir , saveID:saveID , command , entities );
LoadComponentDataFromJson<Scale>( directoryPath:dir , saveID:saveID , command , entities );
}
entities.Dispose();
}
else Debug.LogWarning($"no header fle found {GetHeaderFileName(saveID)}");
}
static World FindWorld ( string name )
{
var worlds = World.All;
for( int i=0 ; i<worlds.Count ; i++ )
if( worlds_.Name==name ) return worlds*;
return null;
}
static void CreateSaveHeaderFile ( string directoryPath , string saveID , int numEntities )
{
string fileDir = IO.Path.Combine( directoryPath , saveID );
string filePath = IO.Path.Combine( fileDir , GetHeaderFileName(saveID) );
if( IO.Directory.Exists(fileDir) )
IO.Directory.Delete( fileDir , true );
if( !IO.Directory.Exists(fileDir) )
IO.Directory.CreateDirectory( fileDir );
string json = JsonUtility.ToJson( new SaveHeaderFile{ numEntities = numEntities } );
IO.File.WriteAllText( filePath , json );
}
static bool ReadSaveHeaderFile ( string directoryPath , string saveID , out SaveHeaderFile header )
{
string fileDir = IO.Path.Combine( directoryPath , saveID );
string filePath = IO.Path.Combine( fileDir , GetHeaderFileName(saveID) );
if( IO.File.Exists(filePath) )
{
string json = IO.File.ReadAllText( filePath );
header = JsonUtility.FromJson<SaveHeaderFile>( json );
return true;
}
else
{
header = null;
return false;
}
}
static void SaveComponentDataToJson <T> ( NativeArray<Entity> entities , EntityManager entityManager , string directoryPath , string saveID )
where T : unmanaged, IComponentData
{
var list = new List<T>( capacity:entities.Length );
for( int i=0 ; i<entities.Length ; i++ )
list.Add( entityManager.GetComponentData<T>(entities*) );
var collection = new ArrayContainer<T>{ array = list.ToArray() };
string fileName = GetComponentFileName<T>( saveID );
string fileDir = IO.Path.Combine( directoryPath , saveID );
string filePath = IO.Path.Combine( fileDir , fileName );
string json = JsonUtility.ToJson( collection );
if( !IO.Directory.Exists(filePath) ) IO.Directory.CreateDirectory( IO.Path.GetDirectoryName(filePath) );
IO.File.WriteAllText( filePath , json );
Debug.Log($"entity data saved: {filePath}
DATA:
{json}");
}
static void LoadComponentDataFromJson <T> ( string directoryPath , string saveID , EntityManager entityManager , NativeArray<Entity> entities )
where T : unmanaged, IComponentData
{
string fileName = GetComponentFileName<T>( saveID );
string fileDir = IO.Path.Combine( directoryPath , saveID );
string filePath = IO.Path.Combine( fileDir , fileName );
if( !IO.File.Exists(filePath) )
{
Debug.LogError($"save file not found: {filePath}");
return;
}
string json = IO.File.ReadAllText( filePath );
ArrayContainer<T> dataContainer = JsonUtility.FromJson<ArrayContainer<T>>( json );
T[] components = dataContainer.array;
for( int i=0 ; i<components.Length ; i++ )
entityManager.SetComponentData<T>( entities _, components *);
Debug.Log($"{components.Length} component data loaded: {filePath}");
}
static string GetHeaderFileName ( string saveID ) => $"{saveID}.json";
static string GetComponentFileName <T> ( string saveID ) => $"{saveID}@{typeof(T).Name}.json";
[System.Serializable] public class SaveHeaderFile { public int numEntities; }
[System.Serializable] public class ArrayContainer <T> { public T[] array; }
}
#endif