I do love the serialization package. However, to my knowledge, it does not use the Jobs system to perform the serialization/deserialization.
The Json serializer does use Jobs when deserializing, but only to read the json string and tokenize it. The actual deserialization is then performed normally, without Jobs.
However it might be faster than copying to a managed dict. But most likely only if you use the binary serialization approach, but i dont really know.
I can provide some examples to do this, in both Binary & Json.
Json Serialization for that package is done by leaning into the Unity.Properties module. So the serializer, by default, will visit the properties of the provided instance that are visible to Unity.Properties. This is:
- public fields.
- private/protected/internal fields marked with [SerializeField] or [CreateProperty].
- any property marked with [CreateProperty].
- auto-properties marked with [field:SerializeField].
So by default it wont be able to serialize a NativeHashMap<T,V>
. However you can write and provide Adapters that will be the classes responsible for actually writing&reading your data.
int4
has public members x,y,z,w. So thats ok.
- for
TValue
, it will only serialize it without adapters if its properties follow the rules described above, if not, you can write a dedicated adapter for your type.
The advantage of the Binary serializer, is that it can directly serialize the byte representation of any unmanaged struct. And it also directly supports NativeArray<T>
's. So there is no need to follow the property / field rules, as the native map already restricts its contents to unmanaged structs.
A Binary adapter for the NativeHashMap<T,V>
could look like this:
// notice the "unsafe" keyword. You can only use "unsafe"
// when compiling within an AssemblyDefinition with the flag "allow unsafe code" checked.
// Unsafe only means that you might want to use pointers in the code.
unsafe class NativeHashmapBinaryAdapter<TKey, TValue> : IBinaryAdapter<NativeHashMap<TKey, TValue>>
where TKey : unmanaged, IEquatable<TKey>
where TValue : unmanaged
{
public void Serialize(
in BinarySerializationContext<NativeHashMap<TKey, TValue>> context,
NativeHashMap<TKey, TValue> value
)
{
// here im getting a copy to demonstrate that it can directly serialize any NativeArray<T>,
// but really there is no need, you could just avoid copying and write the Length as an integer,
// and then, write each key & value sequentally.
// On the Deserialize method, you would do the same in reverse
// (i leave this to you)
var copy = value.GetKeyValueArrays(Allocator.Temp);
try
{
// notice that "Writer" is really a pointer. To call
// its methods you should use ->Method() insteado of .Method()
context.Writer->Add(copy.Keys);
context.Writer->Add(copy.Values);
}
finally { copy.Dispose(); }
}
public NativeHashMap<TKey, TValue> Deserialize(in BinaryDeserializationContext<NativeHashMap<TKey, TValue>> context)
{
NativeArray<TKey> keys = default;
NativeArray<TValue> values = default;
try
{
context.Reader->ReadNext<TKey>(out keys, Allocator.Temp);
context.Reader->ReadNext<TValue>(out values, Allocator.Temp);
// notice that im initializing the instance directly in the serializer with Allocator.Temp
// this might be dangerous with any other allocator if the deserialization for another item
// fails and throws an exception. I guess there could be better ways to approach this
var map = new NativeHashMap<TKey, TValue>(keys.Length, Allocator.Temp);
for (int index = 0; index < keys.Length; index++)
map.Add(keys[index], values[index]);
return map;
}
finally
{
if (keys.IsCreated)
keys.Dispose();
if (values.IsCreated)
values.Dispose();
}
}
}
A JsonAdapter, could look like this:
class NativeHashmapJsonAdapter<TKey, TValue> : IJsonAdapter<NativeHashMap<TKey, TValue>>
where TKey : unmanaged, IEquatable<TKey>
where TValue : unmanaged
{
public void Serialize(
in JsonSerializationContext<NativeHashMap<TKey, TValue>> context,
NativeHashMap<TKey, TValue> value
)
{
using (context.Writer.WriteArrayScope())
foreach (var item in value)
using (context.Writer.WriteObjectScope())
{
// this wont work propperly if TKey or TValue dont have properties / fields
// visible for Unity.Properties.
context.SerializeValue("Key", item.Key);
context.SerializeValue("Value", item.Value);
}
}
public NativeHashMap<TKey, TValue> Deserialize(in JsonDeserializationContext<NativeHashMap<TKey, TValue>> context)
{
// This is valid only if we are using "FromJsonOverride",
var instance = context.GetInstance();
// if not, you should manually create the NativeHashMash using a desired allocator
// (the same danger exists than with the BinaryAdapter)
if (!instance.IsCreated)
instance = new NativeHashMap<TKey, TValue>(0, Allocator.Temp);
foreach (var item in context.SerializedValue.AsArrayView())
{
// this wont work propperly if TKey or TValue dont have properties / fields
// visible for Unity.Properties.
var key = context.DeserializeValue<TKey>(item["Key"]);
var value = context.DeserializeValue<TValue>(item["Value"]);
instance.Add(key, value);
}
return instance;
}
}
You can test this approach like this:
using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Collections.LowLevel.Unsafe.NotBurstCompatible;
using Unity.Mathematics;
using Unity.Serialization.Binary;
using Unity.Serialization.Json;
using UnityEditor;
using UnityEngine;
public class TestSerializedUnmanaged : EditorWindow
{
public struct SomeUnmanagedStruct
{
public float someFloat;
public int someInt;
}
private static readonly NativeHashmapJsonAdapter<int4, SomeUnmanagedStruct> s_jsonAdapter = new();
private static readonly NativeHashmapBinaryAdapter<int4, SomeUnmanagedStruct> s_binaryAdapter = new();
private static readonly JsonSerializationParameters s_jsonParams = new()
{
UserDefinedAdapters = new() { s_jsonAdapter }
};
private static readonly BinarySerializationParameters s_binaryParams = new()
{
UserDefinedAdapters = new() { s_binaryAdapter }
};
[SerializeField]
private byte[]? _lastSerializedBinary;
[SerializeField]
private string? _lastSerializedJson;
private Vector2 _scrollPosition;
public void OnGUI()
{
using var scroll = new EditorGUILayout.ScrollViewScope(_scrollPosition);
using var serializedSelf = new SerializedObject(this);
using (new EditorGUI.DisabledScope(true))
using (var serializedBinaryProperty = serializedSelf.FindProperty(nameof(_lastSerializedBinary)))
EditorGUILayout.PropertyField(serializedBinaryProperty);
using (var serializedJsonProperty = serializedSelf.FindProperty(nameof(_lastSerializedJson)))
EditorGUILayout.PropertyField(serializedJsonProperty);
serializedSelf.ApplyModifiedProperties();
DoJsonButtons();
DoBinaryButtons();
_scrollPosition = scroll.scrollPosition;
}
// notice again the "unsafe" keyword
private unsafe void DoBinaryButtons()
{
if (GUILayout.Button("Serialize To Binary"))
{
using var data = CreateDummyData();
var buffer = new UnsafeAppendBuffer(0, UnsafeUtility.AlignOf<byte>(), Allocator.Temp);
try
{
// here, the ToBinary method needs a pointer to an UnsafeAppendBuffer.
// We pass a pointer by preceding the variable with the & symbol.
// Directly using pointers when dealing with structs is always allowed.
BinarySerialization.ToBinary(&buffer, data, s_binaryParams);
_lastSerializedBinary = buffer.ToBytesNBC();
}
finally { buffer.Dispose(); }
}
if (_lastSerializedBinary?.Length > 0 && GUILayout.Button("Deserialize From Binary"))
{
// here, UnsafeAppendBuffer.Reader, needs a void* pointer to be initialized.
// It really wants a pointer to the start of the byte buffer that we are reading.
// However, a "byte[]" (any array) is a managed type, so we can only create a
// pointer for it if we temporarily "fix" the memory location (this is because managed
// types are free to be moved around in memory, at any time, so a pointer could fail if the
// array gets moved). The "fixed" keyword does precisely that, it just "fixes" the memory
// location of the array while inside the fixed scope. This allows to "cast" the array to a void*
// pointer without raising compiler errors.
fixed (void* buffer = _lastSerializedBinary)
{
var reader = new UnsafeAppendBuffer.Reader(buffer, _lastSerializedBinary.Length);
using var map =
BinarySerialization.FromBinary<NativeHashMap<int4, SomeUnmanagedStruct>>(&reader, s_binaryParams);
LogDeserializedMap(map);
}
}
}
private void DoJsonButtons()
{
if (GUILayout.Button("Serialize To Json"))
{
using var data = CreateDummyData();
_lastSerializedJson = JsonSerialization.ToJson(data, s_jsonParams);
}
if (!string.IsNullOrEmpty(_lastSerializedJson) && GUILayout.Button("Deserialize From Json"))
{
var dummyToOverride = new NativeHashMap<int4, SomeUnmanagedStruct>(0, Allocator.Temp);
try
{
JsonSerialization.FromJsonOverride(_lastSerializedJson, ref dummyToOverride, s_jsonParams);
LogDeserializedMap(dummyToOverride);
}
finally { dummyToOverride.Dispose(); }
}
}
private static void LogDeserializedMap(NativeHashMap<int4, SomeUnmanagedStruct> map)
{
string log = $"Hashmap with {map.Count} items deserialized.\n";
foreach (var item in map)
{
log += $"{item.Key} => SomeFloat: {item.Value.someFloat}, SomeInt: {item.Value.someInt}\n";
}
Debug.Log(log);
}
private static NativeHashMap<int4, SomeUnmanagedStruct> CreateDummyData()
{
return new NativeHashMap<int4, SomeUnmanagedStruct>(4, Allocator.Temp)
{
{ new int4(0, 0, 0, 0), new SomeUnmanagedStruct() { someFloat = 0, someInt = 0 } },
{ new int4(1, 1, 1, 1), new SomeUnmanagedStruct() { someFloat = 1, someInt = 1 } },
{ new int4(2, 2, 2, 2), new SomeUnmanagedStruct() { someFloat = 2, someInt = 2 } },
{ new int4(3, 3, 3, 3), new SomeUnmanagedStruct() { someFloat = 3, someInt = 3 } }
};
}
[MenuItem("Tools/TestSerializedUnmanaged")]
private static void ShowWindow()
{
GetWindow<TestSerializedUnmanaged>().Show();
}
}
I hope this helps to get you started!