tools-unity-memorypack

SKILL.md

MemoryPack

Overview

MemoryPack is a high-performance zero-allocation serializer for C#. This skill covers patterns for Unity serialization, custom formatters, and performance optimization.

When to Use

  • Game save data
  • Network message serialization
  • Asset data serialization
  • Cache/persistence systems
  • High-frequency serialization needs

Basic Usage

Serializable Class

using MemoryPack;

[MemoryPackable]
public partial class PlayerSaveData
{
    public string PlayerId;
    public string PlayerName;
    public int Level;
    public float Experience;
    public Vector3 Position;
    public List<ItemData> Inventory;
    public Dictionary<string, int> Statistics;
    
    [MemoryPackConstructor]
    public PlayerSaveData() { }
}

[MemoryPackable]
public partial class ItemData
{
    public string ItemId;
    public int Quantity;
    public int Durability;
}

Serialization/Deserialization

public class SaveSystem
{
    public byte[] Serialize<T>(T data)
    {
        return MemoryPackSerializer.Serialize(data);
    }
    
    public T Deserialize<T>(byte[] bytes)
    {
        return MemoryPackSerializer.Deserialize<T>(bytes);
    }
    
    public async UniTask SaveToFileAsync(string path, PlayerSaveData data)
    {
        byte[] bytes = MemoryPackSerializer.Serialize(data);
        await File.WriteAllBytesAsync(path, bytes);
    }
    
    public async UniTask<PlayerSaveData> LoadFromFileAsync(string path)
    {
        if (!File.Exists(path))
            return null;
        
        byte[] bytes = await File.ReadAllBytesAsync(path);
        return MemoryPackSerializer.Deserialize<PlayerSaveData>(bytes);
    }
}

Unity Type Formatters

Vector3 Formatter

using MemoryPack;
using UnityEngine;

public class Vector3Formatter : MemoryPackFormatter<Vector3>
{
    public override void Serialize<TBufferWriter>(
        ref MemoryPackWriter<TBufferWriter> writer, 
        scoped ref Vector3 value)
    {
        writer.WriteUnmanaged(value.x);
        writer.WriteUnmanaged(value.y);
        writer.WriteUnmanaged(value.z);
    }

    public override void Deserialize(
        ref MemoryPackReader reader, 
        scoped ref Vector3 value)
    {
        value.x = reader.ReadUnmanaged<float>();
        value.y = reader.ReadUnmanaged<float>();
        value.z = reader.ReadUnmanaged<float>();
    }
}

Quaternion Formatter

public class QuaternionFormatter : MemoryPackFormatter<Quaternion>
{
    public override void Serialize<TBufferWriter>(
        ref MemoryPackWriter<TBufferWriter> writer, 
        scoped ref Quaternion value)
    {
        writer.WriteUnmanaged(value.x);
        writer.WriteUnmanaged(value.y);
        writer.WriteUnmanaged(value.z);
        writer.WriteUnmanaged(value.w);
    }

    public override void Deserialize(
        ref MemoryPackReader reader, 
        scoped ref Quaternion value)
    {
        value.x = reader.ReadUnmanaged<float>();
        value.y = reader.ReadUnmanaged<float>();
        value.z = reader.ReadUnmanaged<float>();
        value.w = reader.ReadUnmanaged<float>();
    }
}

Color Formatter

public class ColorFormatter : MemoryPackFormatter<Color>
{
    public override void Serialize<TBufferWriter>(
        ref MemoryPackWriter<TBufferWriter> writer, 
        scoped ref Color value)
    {
        writer.WriteUnmanaged(value.r);
        writer.WriteUnmanaged(value.g);
        writer.WriteUnmanaged(value.b);
        writer.WriteUnmanaged(value.a);
    }

    public override void Deserialize(
        ref MemoryPackReader reader, 
        scoped ref Color value)
    {
        value.r = reader.ReadUnmanaged<float>();
        value.g = reader.ReadUnmanaged<float>();
        value.b = reader.ReadUnmanaged<float>();
        value.a = reader.ReadUnmanaged<float>();
    }
}

Formatter Registration

public static class MemoryPackFormatterInitializer
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    static void Initialize()
    {
        MemoryPackFormatterProvider.Register(new Vector3Formatter());
        MemoryPackFormatterProvider.Register(new QuaternionFormatter());
        MemoryPackFormatterProvider.Register(new ColorFormatter());
        MemoryPackFormatterProvider.Register(new AnimationCurveFormatter());
    }
}

AnimationCurve Formatter

public class AnimationCurveFormatter : MemoryPackFormatter<AnimationCurve>
{
    public override void Serialize<TBufferWriter>(
        ref MemoryPackWriter<TBufferWriter> writer, 
        scoped ref AnimationCurve value)
    {
        if (value == null)
        {
            writer.WriteUnmanaged(-1);
            return;
        }
        
        var keys = value.keys;
        writer.WriteUnmanaged(keys.Length);
        
        foreach (var key in keys)
        {
            writer.WriteUnmanaged(key.time);
            writer.WriteUnmanaged(key.value);
            writer.WriteUnmanaged(key.inTangent);
            writer.WriteUnmanaged(key.outTangent);
            writer.WriteUnmanaged(key.inWeight);
            writer.WriteUnmanaged(key.outWeight);
            writer.WriteUnmanaged((int)key.weightedMode);
        }
        
        writer.WriteUnmanaged((int)value.preWrapMode);
        writer.WriteUnmanaged((int)value.postWrapMode);
    }

    public override void Deserialize(
        ref MemoryPackReader reader, 
        scoped ref AnimationCurve value)
    {
        int keyCount = reader.ReadUnmanaged<int>();
        
        if (keyCount < 0)
        {
            value = null;
            return;
        }
        
        var keys = new Keyframe[keyCount];
        
        for (int i = 0; i < keyCount; i++)
        {
            keys[i] = new Keyframe
            {
                time = reader.ReadUnmanaged<float>(),
                value = reader.ReadUnmanaged<float>(),
                inTangent = reader.ReadUnmanaged<float>(),
                outTangent = reader.ReadUnmanaged<float>(),
                inWeight = reader.ReadUnmanaged<float>(),
                outWeight = reader.ReadUnmanaged<float>(),
                weightedMode = (WeightedMode)reader.ReadUnmanaged<int>()
            };
        }
        
        value = new AnimationCurve(keys)
        {
            preWrapMode = (WrapMode)reader.ReadUnmanaged<int>(),
            postWrapMode = (WrapMode)reader.ReadUnmanaged<int>()
        };
    }
}

Polymorphic Serialization

Union Types

[MemoryPackable]
[MemoryPackUnion(0, typeof(MeleeAbilityData))]
[MemoryPackUnion(1, typeof(RangedAbilityData))]
[MemoryPackUnion(2, typeof(AreaAbilityData))]
public abstract partial class AbilityData
{
    public string AbilityId;
    public string DisplayName;
    public float Cooldown;
}

[MemoryPackable]
public partial class MeleeAbilityData : AbilityData
{
    public float Range;
    public float DamageMultiplier;
}

[MemoryPackable]
public partial class RangedAbilityData : AbilityData
{
    public float ProjectileSpeed;
    public float MaxRange;
}

[MemoryPackable]
public partial class AreaAbilityData : AbilityData
{
    public float Radius;
    public float Duration;
}

Usage

public class AbilitySerializer
{
    public byte[] SerializeAbility(AbilityData ability)
    {
        return MemoryPackSerializer.Serialize(ability);
    }
    
    public AbilityData DeserializeAbility(byte[] bytes)
    {
        // Automatically deserializes to correct type
        return MemoryPackSerializer.Deserialize<AbilityData>(bytes);
    }
}

Versioning

Version Tolerant Serialization

[MemoryPackable(GenerateType.VersionTolerant)]
public partial class GameSettings
{
    [MemoryPackOrder(0)]
    public float MasterVolume;
    
    [MemoryPackOrder(1)]
    public float MusicVolume;
    
    [MemoryPackOrder(2)]
    public float SfxVolume;
    
    // Added in version 2 - old data will have default value
    [MemoryPackOrder(3)]
    public bool VibrationEnabled;
    
    // Added in version 3
    [MemoryPackOrder(4)]
    public int GraphicsQuality;
}

Handling Missing Fields

[MemoryPackable(GenerateType.VersionTolerant)]
public partial class PlayerProgress
{
    [MemoryPackOrder(0)]
    public int Level;
    
    [MemoryPackOrder(1)]
    public long Experience;
    
    [MemoryPackOrder(2)]
    public List<string> UnlockedAbilities;
    
    // Set default for missing field
    [MemoryPackOrder(3)]
    public int Prestige = 0;
    
    [MemoryPackOnDeserializing]
    static void OnDeserializing(ref PlayerProgress value)
    {
        // Initialize collections to prevent null
        value.UnlockedAbilities ??= new List<string>();
    }
}

Performance Patterns

Buffer Reuse

public class OptimizedSerializer
{
    private readonly ArrayBufferWriter<byte> _buffer = new(1024);
    
    public ReadOnlySpan<byte> SerializeReusable<T>(T data)
    {
        _buffer.Clear();
        MemoryPackSerializer.Serialize(_buffer, data);
        return _buffer.WrittenSpan;
    }
    
    public void SerializeToStream<T>(Stream stream, T data)
    {
        _buffer.Clear();
        MemoryPackSerializer.Serialize(_buffer, data);
        stream.Write(_buffer.WrittenSpan);
    }
}

Span-Based Deserialization

public class SpanDeserializer
{
    public T DeserializeFromSpan<T>(ReadOnlySpan<byte> span)
    {
        return MemoryPackSerializer.Deserialize<T>(span);
    }
    
    public bool TryDeserialize<T>(ReadOnlySpan<byte> span, out T result)
    {
        try
        {
            result = MemoryPackSerializer.Deserialize<T>(span);
            return true;
        }
        catch
        {
            result = default;
            return false;
        }
    }
}

Network Message Serialization

Network Message Base

[MemoryPackable]
[MemoryPackUnion(0, typeof(MoveMessage))]
[MemoryPackUnion(1, typeof(AttackMessage))]
[MemoryPackUnion(2, typeof(ChatMessage))]
public abstract partial class NetworkMessage
{
    public uint MessageId;
    public long Timestamp;
}

[MemoryPackable]
public partial class MoveMessage : NetworkMessage
{
    public Vector3 Position;
    public Quaternion Rotation;
    public Vector3 Velocity;
}

[MemoryPackable]
public partial class AttackMessage : NetworkMessage
{
    public string AttackerId;
    public string TargetId;
    public string AbilityId;
}

[MemoryPackable]
public partial class ChatMessage : NetworkMessage
{
    public string SenderId;
    public string Content;
    public int Channel;
}

Message Handler

public class NetworkMessageHandler
{
    private readonly ArrayBufferWriter<byte> _sendBuffer = new(256);
    
    public byte[] PackMessage(NetworkMessage message)
    {
        _sendBuffer.Clear();
        MemoryPackSerializer.Serialize(_sendBuffer, message);
        return _sendBuffer.WrittenSpan.ToArray();
    }
    
    public NetworkMessage UnpackMessage(byte[] data)
    {
        return MemoryPackSerializer.Deserialize<NetworkMessage>(data);
    }
    
    public void HandleMessage(byte[] data)
    {
        var message = UnpackMessage(data);
        
        switch (message)
        {
            case MoveMessage move:
                HandleMove(move);
                break;
            case AttackMessage attack:
                HandleAttack(attack);
                break;
            case ChatMessage chat:
                HandleChat(chat);
                break;
        }
    }
}

Ignore and Custom Serialization

Ignoring Fields

[MemoryPackable]
public partial class EntityState
{
    public string EntityId;
    public Vector3 Position;
    public float Health;
    
    // Don't serialize - computed at runtime
    [MemoryPackIgnore]
    public bool IsDead => Health <= 0;
    
    // Don't serialize - reference to scene object
    [MemoryPackIgnore]
    public GameObject GameObjectRef;
    
    // Don't serialize - cached component
    [MemoryPackIgnore]
    public Transform TransformRef;
}

Custom Serialize Logic

[MemoryPackable]
public partial class CustomSerializable : IMemoryPackable<CustomSerializable>
{
    public int Value;
    public string Name;
    
    // Custom validation on deserialize
    static void IMemoryPackable<CustomSerializable>.Deserialize(
        ref MemoryPackReader reader, 
        scoped ref CustomSerializable value)
    {
        value ??= new CustomSerializable();
        
        value.Value = reader.ReadUnmanaged<int>();
        value.Name = reader.ReadString();
        
        // Validate after deserialize
        if (value.Value < 0)
            value.Value = 0;
            
        if (string.IsNullOrEmpty(value.Name))
            value.Name = "Default";
    }
}

Best Practices

  1. Use partial classes with [MemoryPackable]
  2. Register Unity formatters on initialization
  3. Use VersionTolerant for save data
  4. Reuse buffers for high-frequency serialization
  5. Use unions for polymorphic types
  6. Ignore runtime references - GameObjects, Components
  7. Initialize collections in OnDeserializing
  8. Test serialization roundtrip in unit tests
  9. Profile serialization performance
  10. Handle version migration gracefully

Troubleshooting

Issue Solution
Type not serializable Add [MemoryPackable] and partial
Unity type fails Register custom formatter
Null collection after load Initialize in OnDeserializing
Wrong type deserialized Check union attribute order
Old save won't load Use VersionTolerant generation
Large serialized size Check for unneeded fields
Weekly Installs
1
GitHub Stars
1
First Seen
2 days ago
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1