UNPKG

com.wallstop-studios.unity-helpers

Version:

Treasure chest of Unity developer tools

812 lines (604 loc) 27.6 kB
# Serialization Guide ## TL;DRWhat Problem This Solves - Save/load data and configs reliably with JSON or Protobuf using one unified API. - Unityaware converters handle common engine types; pooled buffers keep GC low. - Pick Pretty/Normal for humanreadable; Fast/FastPOCO for hot paths. Visuals ![Serialization Flow](../../images/serialization/serialization-flow.svg) This package provides fast, compact serialization for save systems, configuration, and networking with a unified API. - JsonSystem.Text.Json with Unity-aware converters - Protobufprotobuf-net for compact, schema-evolvable binary - SystemBinary.NET BinaryFormatter for legacy/trusted-only scenarios All formats are exposed via `WallstopStudios.UnityHelpers.Core.Serialization.Serializer` and selected with `SerializationType`. ## Formats Provided ### Json Human-readable; ideal for settings, debug, modding, and Git diffs. - Includes converters for Unity types (ignores cycles, includes fields by default, case-insensitive by default; enums as strings in Normal/Pretty): - Vector2, Vector3, Vector4, Vector2Int, Vector3Int - Color, Color32, ColorBlock - Quaternion, Matrix4x4, Pose, Plane, SphericalHarmonicsL2 - Bounds, BoundsInt, Rect, RectInt, RectOffset, RangeInt - Ray, Ray2D, RaycastHit, BoundingSphere - Resolution, RenderTextureDescriptor, LayerMask, Hash128, Scene - AnimationCurve, Gradient, Touch, GameObject - ParticleSystem.MinMaxCurve, ParticleSystem.MinMaxGradient - System.Type (type metadata) - Profiles: Normal, Pretty, Fast, FastPOCO (see below) ### Protobuf (protobuf-net) **⭐ Killer Feature: Schema Evolution** — Players can load saves from older game versions without breaking! Add new fields, remove old ones, rename typesall while maintaining compatibility. - Small and fast; best for networking and large save payloads. - Forward/backward compatible message evolution (see the Schema Evolution guide below). ### SystemBinary (BinaryFormatter) Only for legacy or trusted, same-version, local data. Avoid for long-term persistence or untrusted input. - ⚠️ **Cannot handle version changes** - a single field addition breaks all existing saves. ## When To Use What Use this decision flowchart to pick the right serialization format: ```text START: What are you serializing? │ ├─ Game settings / Config files │ │ │ ├─ Need human-readable / Git-friendly? │ │ → JSON (Normal or Pretty) ✓ │ │ │ └─ Performance critical (large files)? │ → JSON (Fast or FastPOCO) ✓ │ ├─ Save game data │ │ │ ├─ First save system / Need debugging? │ │ → JSON (Pretty) ✓ │ │ │ ├─ Mobile / Size matters? │ │ → Protobuf ✓ │ │ │ └─ Need cross-version compatibility? │ → Protobuf ✓ │ ├─ Network messages (multiplayer) │ │ │ └─ Bandwidth is critical │ → Protobuf ✓ │ ├─ Editor-only / Temporary cache (trusted environment) │ │ │ └─ Same Unity version, local only │ → SystemBinary (⚠️ legacy, consider JSON Fast) │ └─ Hot path / Per-frame serialization │ ├─ Pure C# objects (no Unity types)? │ → JSON (FastPOCO) ✓ │ └─ Mixed with Unity types? → JSON (Fast) ✓ ``` ### Quick Reference - **Use JSON for:** - Player/tool settings, human-readable saves, serverless workflows, text diffs - Quick iteration and debugging - First-time save system implementation - **Use Protobuf for:** - Network payloads and large, bandwidth-sensitive saves - Cases where schema evolves across versions - Mobile games where save file size matters - **Use SystemBinary only for:** - Transient caches in trusted environments with exact version match - ⚠️ Consider JSON Fast instead - SystemBinary is legacy ## JSON Examples (Unity-aware) - Serialize/deserialize and write/read files ```csharp using System.Collections.Generic; using UnityEngine; using WallstopStudios.UnityHelpers.Core.Serialization; public class SaveData { public Vector3 position; public Color playerColor; public List<GameObject> inventory; } var data = new SaveData { position = new Vector3(1, 2, 3), playerColor = Color.cyan, inventory = new List<GameObject>() }; // Serialize to UTF-8 JSON bytes (Unity types supported) byte[] jsonBytes = Serializer.JsonSerialize(data); // Pretty stringify for human readability string jsonText = Serializer.JsonStringify(data, pretty: true); // Parse from string (convert to bytes first) byte[] textBytes = System.Text.Encoding.UTF8.GetBytes(jsonText); SaveData fromText = Serializer.JsonDeserialize<SaveData>(textBytes); // File helpers Serializer.WriteToJsonFile(data, path: "save.json", pretty: true); SaveData fromFile = Serializer.ReadFromJsonFile<SaveData>("save.json"); // Generic entry points (choose format at runtime) byte[] bytes = Serializer.Serialize(data, SerializationType.Json); SaveData loaded = Serializer.Deserialize<SaveData>(bytes, SerializationType.Json); ``` ## Advanced JSON APIs Unity Helpers provides several advanced APIs for high-performance and robust file operations. ### Async File Operations For non-blocking file I/O (useful in loading screens or background saves): ```csharp using WallstopStudios.UnityHelpers.Core.Serialization; // Async read from file SaveData data = await Serializer.ReadFromJsonFileAsync<SaveData>("save.json"); // Async write to file await Serializer.WriteToJsonFileAsync(data, "save.json", pretty: true); // With cancellation token (for interruptible operations) var cts = new CancellationTokenSource(); SaveData data = await Serializer.ReadFromJsonFileAsync<SaveData>("save.json", cts.Token); await Serializer.WriteToJsonFileAsync(data, "save.json", pretty: true, cts.Token); ``` **When to use async:** - Loading screens where you don't want to block the main thread - Auto-save systems running in the background - Large save files that may take noticeable time ### Safe Try-Pattern APIs For graceful error handling without try-catch blocks: ```csharp using WallstopStudios.UnityHelpers.Core.Serialization; // TryRead - returns false if file missing or invalid JSON if (Serializer.TryReadFromJsonFile<SaveData>("save.json", out SaveData data)) { // File exists and parsed successfully LoadGame(data); } else { // File missing or corrupted - start new game StartNewGame(); } // TryWrite - returns false if write failed if (!Serializer.TryWriteToJsonFile(data, "save.json")) { Debug.LogError("Failed to save game!"); ShowSaveErrorDialog(); } ``` **When to use Try-pattern:** - Loading saves that may not exist (new players) - Handling corrupted save files gracefully - Writing to paths that may not be writable ### Fast Serialization (Hot Paths) For performance-critical scenarios where you serialize/deserialize frequently: ```csharp using WallstopStudios.UnityHelpers.Core.Serialization; // Fast serialize - stricter options, Unity converters, minimal validation byte[] fastBytes = Serializer.JsonSerializeFast(networkMessage); // Fast deserialize NetworkMessage msg = Serializer.JsonDeserializeFast<NetworkMessage>(fastBytes); // Fast serialize with buffer reuse (zero-allocation after warmup) byte[] buffer = null; int length = Serializer.JsonSerializeFast(networkMessage, ref buffer); // Use buffer[0..length], buffer is reused on subsequent calls ``` **Fast options differences:** | Setting | Normal/Pretty | Fast | | --------------------- | ------------- | -------- | | Case-insensitive | ✅ | ❌ | | Comments allowed | ✅ | ❌ | | Trailing commas | ✅ | ❌ | | Include fields | ✅ | ❌ | | Reference handling | Safe | Disabled | | Unity type converters | ✅ | ✅ | ### Creating Custom Options Create your own options based on the Fast presets: ```csharp using WallstopStudios.UnityHelpers.Core.Serialization; using System.Text.Json; // Get a copy of Fast options to customize JsonSerializerOptions myOptions = Serializer.CreateFastJsonOptions(); myOptions.WriteIndented = true; // Add pretty-printing // FastPOCO - for pure C# objects with NO Unity types (fastest) JsonSerializerOptions pocoOptions = Serializer.CreateFastPocoJsonOptions(); // Use with any serialize method byte[] bytes = Serializer.JsonSerialize(data, myOptions); Serializer.WriteToJsonFile(data, "file.json", myOptions); ``` **Option profiles:** - `CreateFastJsonOptions()` — Fast parsing + Unity type converters (Vector3, Color, etc.) - `CreateFastPocoJsonOptions()` — Fastest, no converters, pure C# objects only ### Performance Comparison ```csharp // 🐌 Normal (most compatible, slightly slower) byte[] normal = Serializer.JsonSerialize(data); // 🚀 Fast (stricter, faster parsing/writing) byte[] fast = Serializer.JsonSerializeFast(data); // 🚀🚀 Fast + buffer reuse (zero-allocation after first call) byte[] buffer = null; int len = Serializer.JsonSerializeFast(data, ref buffer); // 🚀🚀🚀 Fast POCO (pure C# objects, no Unity types) JsonSerializerOptions pocoOpts = Serializer.CreateFastPocoJsonOptions(); byte[] fastest = Serializer.JsonSerialize(pureCSharpData, pocoOpts); ``` ## Protobuf Examples (Compact + Evolvable) - Basic usage ```csharp using ProtoBuf; // protobuf-net using WallstopStudios.UnityHelpers.Core.Serialization; [ProtoContract] public class PlayerInfo { [ProtoMember(1)] public int id; [ProtoMember(2)] public string name; } var info = new PlayerInfo { id = 1, name = "Hero" }; byte[] buf = Serializer.ProtoSerialize(info); PlayerInfo again = Serializer.ProtoDeserialize<PlayerInfo>(buf); // Generic entry points byte[] buf2 = Serializer.Serialize(info, SerializationType.Protobuf); PlayerInfo again2 = Serializer.Deserialize<PlayerInfo>(buf2, SerializationType.Protobuf); // Buffer reuse (reduce GC in hot paths) byte[] buffer = null; int len = Serializer.Serialize(info, SerializationType.Protobuf, ref buffer); PlayerInfo sliced = Serializer.Deserialize<PlayerInfo>(buffer.AsSpan(0, len).ToArray(), SerializationType.Protobuf); ``` - Unity types with Protobuf: built-in surrogates ```csharp // This package registers protobuf-net surrogates at startup so Unity structs just work in protobuf models. // The following Unity types are protobuf-compatible out of the box: // - Vector2, Vector3, Vector2Int, Vector3Int // - Quaternion // - Color, Color32 // - Rect, RectInt // - Bounds, BoundsInt // - Resolution // Example: use Vector3 directly in a protobuf-annotated model using ProtoBuf; // protobuf-net using UnityEngine; // Unity types using WallstopStudios.UnityHelpers.Core.Serialization; [ProtoContract] public class NetworkMessage { [ProtoMember(1)] public int playerId; [ProtoMember(2)] public Vector3 position; // Works via registered surrogates [ProtoMember(3)] public Quaternion facing; // Works via registered surrogates } // Serialize/deserialize as usual var msg = new NetworkMessage { playerId = 7, position = new Vector3(1,2,3), facing = Quaternion.identity }; byte[] bytes = Serializer.ProtoSerialize(msg); NetworkMessage again = Serializer.ProtoDeserialize<NetworkMessage>(bytes); ``` Notes - Surrogates are registered in the Serializer static initializer; you don't need to call anything. - If you define your own DTOs, they will continue to work; surrogates simply make Unity structs first-class. - Keep using [ProtoContract]/[ProtoMember] and stable field numbers for your own types. ### ⚠️ IL2CPP and Code Stripping Warning **Critical for IL2CPP builds (WebGL, iOS, Android, Consoles):** Protobuf uses reflection internally to serialize/deserialize types. Unity's IL2CPP managed code stripping may remove types or fields that are only accessed via reflection, causing **silent data loss or runtime crashes** in release builds. **Common symptoms:** - `NullReferenceException` or `TypeLoadException` during Protobuf deserialization - Fields mysteriously have default values after loading (data appears to be lost) - Works perfectly in Editor/Development builds, fails in Release/IL2CPP builds - "Type not found" or "Method not found" errors at runtime ### Solution: Create a link.xml file In your `Assets` folder (or any subfolder), create `link.xml` to preserve your Protobuf types: ```xml <linker> <!-- Preserve all your Protobuf-serialized types --> <assembly fullname="Assembly-CSharp"> <!-- Preserve specific types --> <type fullname="MyGame.PlayerSave" preserve="all"/> <type fullname="MyGame.InventoryData" preserve="all"/> <type fullname="MyGame.NetworkMessage" preserve="all"/> <!-- Or preserve entire namespace --> <namespace fullname="MyGame.SaveData" preserve="all"/> </assembly> <!-- If using Protobuf types across assemblies --> <assembly fullname="MyGame.Shared"> <namespace fullname="MyGame.Shared.Protocol" preserve="all"/> </assembly> <!-- Preserve Unity Helpers if needed --> <assembly fullname="WallstopStudios.UnityHelpers.Runtime"> <!-- Usually not needed, but if you see errors: --> <type fullname="WallstopStudios.UnityHelpers.Core.Serialization.Serializer" preserve="all"/> </assembly> </linker> ``` **Testing checklist (CRITICAL):** - ✅ **Test every IL2CPP build** - Development builds don't strip code, so issues only appear in Release - ✅ **Test on actual devices** - WebGL/Mobile stripping can differ from standalone builds - ✅ **Test full save/load cycle** - Save in one session, load in another to verify persistence - ✅ **Update link.xml when adding new types** - Every `[ProtoContract]` type needs preservation - ✅ **Check build logs for stripping warnings** - Unity logs which types/methods are stripped - ✅ **Test after Unity upgrades** - Stripping behavior can change between Unity versions **When you might not need link.xml:** - Only using JSON serialization (source-generated, no reflection) - Already preserving entire assembly with `preserve="all"` - Using a custom IL2CPP link file that preserves everything ### Advanced: Preserve only what's needed Instead of `preserve="all"`, you can be more selective: ```xml <type fullname="MyGame.PlayerSave"> <method signature="System.Void .ctor()" preserve="all"/> <field name="playerId" /> <field name="level" /> <field name="inventory" /> </type> ``` However, this is error-prone. **Start with `preserve="all"` and optimize later if build size is critical.** **Related documentation:** - [Unity Manual: Managed Code Stripping](https://docs.unity3d.com/Manual/managed-code-stripping.html) - [protobuf-net documentation](https://protobuf-net.github.io/protobuf-net/) - [Unity Discussions: link.xml best practices](https://discussions.unity.com/) ````text <a id="protobuf-schema-evolution-the-killer-feature"></a> ## Protobuf Schema Evolution: The Killer Feature **The Problem Protobuf Solves:** You ship your game with this save format: ```csharp [ProtoContract] public class PlayerSave { [ProtoMember(1)] public int level; [ProtoMember(2)] public string name; } ```` A month later, you want to add a new feature and change the format: ```csharp [ProtoContract] public class PlayerSave { [ProtoMember(1)] public int level; [ProtoMember(2)] public string name; [ProtoMember(3)] public int gold; // NEW FIELD [ProtoMember(4)] public bool isPremium; // NEW FIELD } ``` **With JSON or BinaryFormatter:** Players' existing saves break. You must write migration code or wipe their progress. **With Protobuf:** It just works! Old saves load perfectly with `gold = 0` and `isPremium = false` defaults. ### Real-World Save Game Evolution Example 🟡 Intermediate **Version 1.0 (Launch):** ```csharp [ProtoContract] public class PlayerSave { [ProtoMember(1)] public string playerId; [ProtoMember(2)] public int level; [ProtoMember(3)] public Vector3DTO position; } ``` **Version 1.5 (Inventory System Added):** ```csharp [ProtoContract] public class PlayerSave { [ProtoMember(1)] public string playerId; [ProtoMember(2)] public int level; [ProtoMember(3)] public Vector3DTO position; [ProtoMember(4)] public List<string> inventory = new(); // NEW: defaults to empty } ``` **Version 2.0 (Stats Overhaul - level renamed to xp):** ```csharp [ProtoContract] public class PlayerSave { [ProtoMember(1)] public string playerId; // [ProtoMember(2)] int level - REMOVED, but tag 2 is NEVER reused [ProtoMember(3)] public Vector3DTO position; [ProtoMember(4)] public List<string> inventory = new(); [ProtoMember(5)] public int xp; // NEW: experience points [ProtoMember(6)] public int skillPoints; // NEW: unspent skill points } ``` **Result:** Players who saved in v1.0 can load their save in v2.0: - Old `level` value (tag 2) is ignored - New `xp` and `skillPoints` default to 0 - All existing data (`playerId`, `position`, `inventory`) loads correctly - **Zero migration code required!** ### Schema Evolution Rules **✅ Safe Changes (Always Compatible):** - Add new fields with new tag numbers - Remove fields (but never reuse their tag numbers) - Change field names (tags are what matter, not names) - Add new message types - Change default values (only affects new saves) **⚠️ Requires Care:** - Changing field types (e.g., `int` → `long` works, `int` → `string` doesn't) - Changing `repeated` to singular or vice versa (usually breaks) - Renumbering existing tags (breaks everything!) **❌ Never Do This:** - Reuse deleted field tag numbers - Change the meaning of an existing tag - Remove required fields (avoid `required` entirely - use validation instead) ### Multi-Version Compatibility Pattern 🔴 Advanced Handle breaking changes across major versions gracefully: ```csharp [ProtoContract] public class SaveFile { [ProtoMember(1)] public int version = 3; // Track your save version // Version 1-3 fields [ProtoMember(2)] public string playerId; [ProtoMember(3)] public Vector3DTO position; // Version 2+ fields [ProtoMember(10)] public List<string> inventory; // Version 3+ fields [ProtoMember(20)] public PlayerStats stats; public void PostDeserialize() { if (version < 2) { // Migrate v1 saves: initialize empty inventory inventory ??= new List<string>(); } if (version < 3) { // Migrate v2 saves: create default stats stats ??= new PlayerStats { xp = 0, level = 1 }; } version = 3; // Update to current version } } ``` > ⚠️ **Common Mistake:** Don't put migration logic in the constructor. Use `PostDeserialize()` > or a dedicated method called after loading. Constructors don't run during deserialization. ### Testing Schema Evolution 🟢 Beginner **Recommended Testing Pattern:** ```csharp // 1. Save a file with version N: var oldSave = new PlayerSave { level = 10, name = "Hero" }; byte[] bytes = Serializer.ProtoSerialize(oldSave); File.WriteAllBytes("test_v1.save", bytes); // 2. Update your schema (add new fields) // 3. Load the old file with new schema: byte[] oldBytes = File.ReadAllBytes("test_v1.save"); var loaded = Serializer.ProtoDeserialize<PlayerSave>(oldBytes); // New fields have defaults, old fields are preserved Assert.AreEqual(10, loaded.level); Assert.AreEqual("Hero", loaded.name); Assert.AreEqual(0, loaded.gold); // New field defaults to 0 ``` **Best Practice:** Keep regression test files — Store save files from each version in your test suite. ### Common Save System Patterns **Pattern 1: Version-Aware Loading** 🟡 Intermediate ```csharp public SaveFile LoadSave(string path) { byte[] bytes = File.ReadAllBytes(path); SaveFile save = Serializer.ProtoDeserialize<SaveFile>(bytes); // Perform any version-specific migrations save.PostDeserialize(); return save; } ``` **Pattern 2: Gradual Migration (preserve old format for rollback)** 🔴 Advanced ```csharp public class SaveManager { public void SaveGame(PlayerData data) { var protobuf = ConvertToProtobuf(data); byte[] bytes = Serializer.ProtoSerialize(protobuf); // Write both formats during transition period File.WriteAllBytes("save.dat", bytes); Serializer.WriteToJsonFile(data, "save.json.backup"); } } ``` **Pattern 3: Automatic Backup Before Save** 🟡 Intermediate ```csharp public void SaveGame(SaveFile save) { string path = "player.save"; string backup = $"player.save.backup_{DateTime.Now:yyyyMMdd_HHmmss}"; // Backup existing save before overwriting if (File.Exists(path)) { File.Copy(path, backup); } byte[] bytes = Serializer.ProtoSerialize(save); File.WriteAllBytes(path, bytes); // Keep only last 3 backups CleanupOldBackups("player.save.backup_*", keepCount: 3); } ``` ### Why This Matters for Live Games **Without schema evolution (JSON/BinaryFormatter):** - ❌ Every update risks breaking player saves - ❌ Must write complex migration code for every version - ❌ Players lose progress if migration fails - ❌ Can't roll back broken updates (saves are corrupted) - ❌ Hotfixes that change save format are terrifying **With Protobuf schema evolution:** - ✅ Add features freely without breaking existing saves - ✅ Graceful degradation (old clients ignore new fields) - ✅ Can roll back game versions without data loss - ✅ Hotfixes are safe (just add new optional fields) - ✅ Reduces QA burden (less migration testing needed) ### Protobuf Compatibility Tips - Add fields with new numbers; old clients ignore unknown fields; new clients default missing fields. - Never reuse or renumber existing field tags; reserve removed numbers if needed. - Avoid changing scalar types on the same number. - Prefer optional/repeated instead of required. - Use sensible defaults to minimize payloads. - **Group field numbers by version** (e.g., v1: 1-10, v2: 11-20, v3: 21-30) for clarity. --- ## Protobuf Polymorphism (Inheritance + Interfaces) - Abstract base with [ProtoInclude] (recommended) - Protobuf-net does not infer subtype graphs unless you tell it. The recommended pattern is to put `[ProtoContract]` on an abstract base and list all concrete subtypes with `[ProtoInclude(tag, typeof(Subtype))]`. - Declare your fields/properties as the abstract base so protobuf can deserialize to the correct subtype. ```csharp using ProtoBuf; [ProtoContract] public abstract class Message { } [ProtoContract] public sealed class Ping : Message { [ProtoMember(1)] public int id; } [ProtoContract] [ProtoInclude(100, typeof(Ping))] public abstract class MessageBase : Message { } [ProtoContract] public sealed class Envelope { [ProtoMember(1)] public MessageBase payload; } // round-trip works: Envelope.payload will be Ping at runtime byte[] bytes = Serializer.ProtoSerialize(new Envelope { payload = new Ping { id = 7 } }); Envelope again = Serializer.ProtoDeserialize<Envelope>(bytes); ``` - **Interfaces require a root mapping** — Protobuf cannot deserialize directly to an interface because it needs a concrete root. You have three options: 1. Use an abstract base with `[ProtoInclude]` and declare fields as that base (preferred). 2. Register a mapping from the interface to a concrete root type at startup: ```csharp Serializer.RegisterProtobufRoot<IMsg, Ping>(); IMsg msg = Serializer.ProtoDeserialize<IMsg>(bytes); ``` 3. Specify the concrete type with the overload: ```csharp IMsg msg = Serializer.ProtoDeserialize<IMsg>(bytes, typeof(Ping)); ``` ### Random System Example All PRNGs derive from `AbstractRandom`, which is `[ProtoContract]` and declares each implementation via `[ProtoInclude]`. Use this pattern in your models: ```csharp [ProtoContract] public class RNGHolder { [ProtoMember(1)] public AbstractRandom rng; } // Serialize any implementation without surprises RNGHolder holder = new RNGHolder { rng = new PcgRandom(seed: 123) }; byte[] buf = Serializer.ProtoSerialize(holder); RNGHolder rt = Serializer.ProtoDeserialize<RNGHolder>(buf); ``` - If you truly need an `IRandom` field, register a root or pass the concrete type when deserializing: ```csharp Serializer.RegisterProtobufRoot<IRandom, PcgRandom>(); IRandom r = Serializer.ProtoDeserialize<IRandom>(bytes); // or IRandom r2 = Serializer.ProtoDeserialize<IRandom>(bytes, typeof(PcgRandom)); ``` ### Tag Numbers Are API Surface Tags in `[ProtoInclude(tag, ...)]` and `[ProtoMember(tag)]` are part of your schema. Add new numbers for new types/fields; never reuse or renumber existing tags once shipped. ## SystemBinary Examples (Legacy/Trusted Only) ```csharp using WallstopStudios.UnityHelpers.Core.Serialization; var obj = new SomeSerializableType(); byte[] bin = Serializer.BinarySerialize(obj); SomeSerializableType roundtrip = Serializer.BinaryDeserialize<SomeSerializableType>(bin); // Generic byte[] bin2 = Serializer.Serialize(obj, SerializationType.SystemBinary); var round2 = Serializer.Deserialize<SomeSerializableType>(bin2, SerializationType.SystemBinary); ``` Watch-outs - BinaryFormatter is obsolete for modern .NET and unsafe for untrusted input. - Version changes often break BinaryFormatter payloads; restrict to same-version caches. Features - Unity converters for JSON: Vector2/3/4, Color, Matrix4x4, GameObject, Type - Protobuf (protobuf-net) integration - LZMA compression utilities (`Runtime/Utils/LZMA.cs`) - Pooled buffers/writers to reduce allocations References - API: `Runtime/Core/Serialization/Serializer.cs:1` - LZMA: `Runtime/Utils/LZMA.cs:1` ## Migration - Replace direct `System.Text.Json.JsonSerializer` calls in app code with `Serializer.JsonSerialize/JsonDeserialize/JsonStringify`, or with `Serializer.Serialize/Deserialize` + `SerializationType.Json` to centralize options and Unity converters. - Replace any custom protobuf helpers with `Serializer.ProtoSerialize/ProtoDeserialize` or the generic `Serializer.Serialize/Deserialize` APIs. Ensure models are annotated with `[ProtoContract]` and stable `[ProtoMember(n)]` tags. - For existing binary saves using BinaryFormatter, prefer migrating to Json or Protobuf. If you must keep BinaryFormatter, scope it to trusted, same-version caches only. ## 2.0 changes - BinaryFormatter (`SerializationType.SystemBinary`) is deprecated but remains functional for trusted/legacy scenarios. Prefer: - `SerializationType.Json` (System.Text.Json with Unity-aware converters) for readable, diffable content. - `SerializationType.Protobuf` (protobuf-net) for compact, high-performance binary payloads. ## IL2CPP / AOT guidance System.Text.Json can require extra care under AOT (e.g., IL2CPP): - Prefer explicit `JsonSerializerOptions` and concrete generic APIs over `object`-based serialization to reduce reflection. - For hot POCO models, consider adding a source-generated context (JsonSerializerContext) in your game assembly and pass it to `JsonSerializer` calls. - If you rely on many custom converters, ensure they are referenced by code so the linker doesn't strip them. The UnityHelpers converters are referenced via options by default. - Avoid deserializing `System.Type` from untrusted input (see `TypeConverter`); this is intended for trusted configs/tools.