Skip to main content

9.4 - Data Serialization Introduction

Data serialization is the process of converting complex data structures or objects into a format that can be easily stored, transmitted, or reconstructed later. In game development, serialization is essential for saving game state, player progress, high scores, and configuration settings.

What is Serialization?

Imagine you're playing an RPG and want to save your progress. Your game needs to store information about:

  • Your character's stats, inventory, and position
  • The state of the game world (which quests are complete, which enemies are defeated)
  • Your game settings and preferences

All this information exists in memory as complex, interconnected objects. Serialization converts these objects into a linear format (like text or binary data) that can be written to a file. Later, deserialization reads this data and reconstructs the original objects.

Why Use Serialization?

Serialization offers several benefits for game developers:

  1. Persistence: Save game state between sessions
  2. Data Exchange: Share data between different parts of your game or with external services
  3. Deep Copying: Create exact copies of complex objects
  4. Networking: Send game state over a network for multiplayer games
  5. Modding Support: Allow players to modify game data in a structured way

Serialization Formats

There are several common formats for serializing data, each with its own advantages and use cases:

Text-Based Formats

JSON (JavaScript Object Notation)

JSON is a lightweight, human-readable format that's widely used for data exchange:

{
"playerName": "Hero",
"level": 5,
"health": 100,
"position": {
"x": 156.3,
"y": 10.0,
"z": -23.7
},
"inventory": [
{"id": 101, "name": "Health Potion", "count": 3},
{"id": 205, "name": "Iron Sword", "count": 1}
]
}

Advantages:

  • Human-readable and editable
  • Widely supported across languages and platforms
  • Compact compared to XML
  • Easy to debug and inspect

Disadvantages:

  • Less efficient than binary formats
  • Limited data types (no direct support for dates, binary data, etc.)
  • Cannot preserve object references or cycles

XML (eXtensible Markup Language)

XML is a more verbose but highly structured format:

<Player>
<Name>Hero</Name>
<Level>5</Level>
<Health>100</Health>
<Position X="156.3" Y="10.0" Z="-23.7" />
<Inventory>
<Item ID="101" Name="Health Potion" Count="3" />
<Item ID="205" Name="Iron Sword" Count="1" />
</Inventory>
</Player>

Advantages:

  • Human-readable and editable
  • Strong schema support for validation
  • Widely supported
  • Can represent complex hierarchical data

Disadvantages:

  • Very verbose compared to other formats
  • More complex to parse
  • Less efficient than binary formats

Binary Formats

Binary serialization converts objects into a compact binary format that's not human-readable but is efficient for storage and transmission.

Advantages:

  • Very compact and efficient
  • Can preserve object references and handle complex object graphs
  • Faster to serialize and deserialize
  • Can include custom data types

Disadvantages:

  • Not human-readable or editable
  • Often tied to specific platforms or languages
  • Can break when class definitions change
  • Security concerns when deserializing untrusted data

Serialization in C#

C# provides several built-in mechanisms for serialization:

System.Text.Json (Modern JSON Serialization)

Introduced in .NET Core 3.0, System.Text.Json is the modern, high-performance JSON library:

using System.Text.Json;

public class Player
{
public string Name { get; set; }
public int Level { get; set; }
public int Health { get; set; }
public Position Position { get; set; }
public List<Item> Inventory { get; set; }
}

public class Position
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
}

public class Item
{
public int Id { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}

// Serialization example
public void SavePlayer(Player player, string filePath)
{
try
{
// Create options for pretty printing
var options = new JsonSerializerOptions
{
WriteIndented = true
};

// Serialize the player object to a JSON string
string jsonString = JsonSerializer.Serialize(player, options);

// Write the JSON to a file
File.WriteAllText(filePath, jsonString);

Console.WriteLine($"Player saved to {filePath}");
}
catch (Exception ex)
{
Console.WriteLine($"Error saving player: {ex.Message}");
}
}

// Deserialization example
public Player LoadPlayer(string filePath)
{
try
{
if (!File.Exists(filePath))
{
Console.WriteLine("Save file not found. Creating a new player.");
return new Player
{
Name = "New Player",
Level = 1,
Health = 100,
Position = new Position { X = 0, Y = 0, Z = 0 },
Inventory = new List<Item>()
};
}

// Read the JSON from the file
string jsonString = File.ReadAllText(filePath);

// Deserialize the JSON to a Player object
Player player = JsonSerializer.Deserialize<Player>(jsonString);

Console.WriteLine($"Player loaded from {filePath}");
return player;
}
catch (Exception ex)
{
Console.WriteLine($"Error loading player: {ex.Message}");
Console.WriteLine("Creating a new player instead.");

return new Player
{
Name = "New Player",
Level = 1,
Health = 100,
Position = new Position { X = 0, Y = 0, Z = 0 },
Inventory = new List<Item>()
};
}
}

// Usage example
public void GameSaveExample()
{
// Create a player
Player player = new Player
{
Name = "Hero",
Level = 5,
Health = 100,
Position = new Position { X = 156.3f, Y = 10.0f, Z = -23.7f },
Inventory = new List<Item>
{
new Item { Id = 101, Name = "Health Potion", Count = 3 },
new Item { Id = 205, Name = "Iron Sword", Count = 1 }
}
};

// Save the player
SavePlayer(player, "playerSave.json");

// Later, load the player
Player loadedPlayer = LoadPlayer("playerSave.json");

// Display player info
Console.WriteLine($"Loaded player: {loadedPlayer.Name} (Level {loadedPlayer.Level})");
Console.WriteLine($"Health: {loadedPlayer.Health}");
Console.WriteLine($"Position: X={loadedPlayer.Position.X}, Y={loadedPlayer.Position.Y}, Z={loadedPlayer.Position.Z}");
Console.WriteLine("Inventory:");
foreach (var item in loadedPlayer.Inventory)
{
Console.WriteLine($"- {item.Name} x{item.Count}");
}
}

System.Xml.Serialization (XML Serialization)

C# also provides built-in support for XML serialization:

using System.Xml.Serialization;

// Add [Serializable] attribute to classes you want to serialize
[Serializable]
public class Player
{
public string Name { get; set; }
public int Level { get; set; }
public int Health { get; set; }
public Position Position { get; set; }
public List<Item> Inventory { get; set; }
}

[Serializable]
public class Position
{
[XmlAttribute] // This will be an attribute instead of a child element
public float X { get; set; }

[XmlAttribute]
public float Y { get; set; }

[XmlAttribute]
public float Z { get; set; }
}

[Serializable]
public class Item
{
[XmlAttribute]
public int Id { get; set; }

[XmlAttribute]
public string Name { get; set; }

[XmlAttribute]
public int Count { get; set; }
}

// Serialization example
public void SavePlayerXml(Player player, string filePath)
{
try
{
// Create an XML serializer for the Player type
XmlSerializer serializer = new XmlSerializer(typeof(Player));

// Create a file stream to write to
using (FileStream stream = new FileStream(filePath, FileMode.Create))
{
// Serialize the player object to XML
serializer.Serialize(stream, player);
}

Console.WriteLine($"Player saved to {filePath}");
}
catch (Exception ex)
{
Console.WriteLine($"Error saving player: {ex.Message}");
}
}

// Deserialization example
public Player LoadPlayerXml(string filePath)
{
try
{
if (!File.Exists(filePath))
{
Console.WriteLine("Save file not found. Creating a new player.");
return new Player
{
Name = "New Player",
Level = 1,
Health = 100,
Position = new Position { X = 0, Y = 0, Z = 0 },
Inventory = new List<Item>()
};
}

// Create an XML serializer for the Player type
XmlSerializer serializer = new XmlSerializer(typeof(Player));

// Create a file stream to read from
using (FileStream stream = new FileStream(filePath, FileMode.Open))
{
// Deserialize the XML to a Player object
Player player = (Player)serializer.Deserialize(stream);

Console.WriteLine($"Player loaded from {filePath}");
return player;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading player: {ex.Message}");
Console.WriteLine("Creating a new player instead.");

return new Player
{
Name = "New Player",
Level = 1,
Health = 100,
Position = new Position { X = 0, Y = 0, Z = 0 },
Inventory = new List<Item>()
};
}
}

Binary Serialization

C# also supports binary serialization, though it's less commonly used in modern applications due to security concerns and compatibility issues:

using System.Runtime.Serialization.Formatters.Binary;

// Note: BinaryFormatter is marked as obsolete in newer .NET versions due to security concerns
// This example is provided for educational purposes

[Serializable] // Required for binary serialization
public class Player
{
public string Name { get; set; }
public int Level { get; set; }
public int Health { get; set; }
public Position Position { get; set; }
public List<Item> Inventory { get; set; }
}

[Serializable]
public class Position
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
}

[Serializable]
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
public int Count { get; set; }
}

// Serialization example
public void SavePlayerBinary(Player player, string filePath)
{
try
{
// Create a binary formatter
BinaryFormatter formatter = new BinaryFormatter();

// Create a file stream to write to
using (FileStream stream = new FileStream(filePath, FileMode.Create))
{
// Serialize the player object to binary
formatter.Serialize(stream, player);
}

Console.WriteLine($"Player saved to {filePath}");
}
catch (Exception ex)
{
Console.WriteLine($"Error saving player: {ex.Message}");
}
}

// Deserialization example
public Player LoadPlayerBinary(string filePath)
{
try
{
if (!File.Exists(filePath))
{
Console.WriteLine("Save file not found. Creating a new player.");
return new Player
{
Name = "New Player",
Level = 1,
Health = 100,
Position = new Position { X = 0, Y = 0, Z = 0 },
Inventory = new List<Item>()
};
}

// Create a binary formatter
BinaryFormatter formatter = new BinaryFormatter();

// Create a file stream to read from
using (FileStream stream = new FileStream(filePath, FileMode.Open))
{
// Deserialize the binary to a Player object
Player player = (Player)formatter.Deserialize(stream);

Console.WriteLine($"Player loaded from {filePath}");
return player;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading player: {ex.Message}");
Console.WriteLine("Creating a new player instead.");

return new Player
{
Name = "New Player",
Level = 1,
Health = 100,
Position = new Position { X = 0, Y = 0, Z = 0 },
Inventory = new List<Item>()
};
}
}
caution

The BinaryFormatter is marked as obsolete in newer .NET versions due to security vulnerabilities. For modern applications, consider using alternative binary serialization libraries or JSON serialization.

Custom Serialization

Sometimes the default serialization behavior doesn't meet your needs. For example, you might want to:

  • Exclude certain properties from serialization
  • Handle complex object references
  • Customize the format of serialized data
  • Optimize for size or performance

C# provides several ways to customize serialization:

JSON Serialization Attributes

using System.Text.Json.Serialization;

public class Player
{
public string Name { get; set; }

public int Level { get; set; }

// Rename a property in the JSON output
[JsonPropertyName("hp")]
public int Health { get; set; }

// Ignore this property when serializing
[JsonIgnore]
public bool IsDead => Health <= 0;

public Position Position { get; set; }

public List<Item> Inventory { get; set; }

// Include this field in serialization (fields are ignored by default)
[JsonInclude]
public readonly DateTime CreatedAt = DateTime.Now;
}

XML Serialization Attributes

using System.Xml.Serialization;

[XmlRoot("PlayerData")] // Customize the root element name
public class Player
{
public string Name { get; set; }

public int Level { get; set; }

// Rename an element in the XML output
[XmlElement("HP")]
public int Health { get; set; }

// Make this an attribute instead of an element
[XmlAttribute("Created")]
public DateTime CreatedAt { get; set; } = DateTime.Now;

// Ignore this property
[XmlIgnore]
public bool IsDead => Health <= 0;

public Position Position { get; set; }

// Rename the collection element
[XmlArray("Items")]
public List<Item> Inventory { get; set; }
}

Custom Converters

For more complex customization, you can create custom converters:

using System.Text.Json;
using System.Text.Json.Serialization;

// Custom JSON converter for the Position class
public class PositionConverter : JsonConverter<Position>
{
public override Position Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Check for array format
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException("Expected start of array for Position");
}

reader.Read(); // Move to X value
float x = reader.GetSingle();

reader.Read(); // Move to Y value
float y = reader.GetSingle();

reader.Read(); // Move to Z value
float z = reader.GetSingle();

reader.Read(); // Move past end of array

return new Position { X = x, Y = y, Z = z };
}

public override void Write(Utf8JsonWriter writer, Position value, JsonSerializerOptions options)
{
// Write position as a compact array [x, y, z] instead of an object
writer.WriteStartArray();
writer.WriteNumberValue(value.X);
writer.WriteNumberValue(value.Y);
writer.WriteNumberValue(value.Z);
writer.WriteEndArray();
}
}

// Apply the converter to the Position class
[JsonConverter(typeof(PositionConverter))]
public class Position
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
}

Practical Example: Game Save System

Let's create a more comprehensive game save system that handles multiple save slots and different types of game data:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;

// Game data classes
public class GameSave
{
public string SaveName { get; set; }
public DateTime SaveDate { get; set; }
public PlayerData Player { get; set; }
public WorldData World { get; set; }
public GameSettings Settings { get; set; }
}

public class PlayerData
{
public string Name { get; set; }
public int Level { get; set; }
public int Experience { get; set; }
public int Health { get; set; }
public int MaxHealth { get; set; }
public int Mana { get; set; }
public int MaxMana { get; set; }
public Position Position { get; set; }
public List<InventoryItem> Inventory { get; set; }
public List<string> CompletedQuests { get; set; }
public Dictionary<string, int> Skills { get; set; }
}

public class Position
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }

public override string ToString()
{
return $"({X}, {Y}, {Z})";
}
}

public class InventoryItem
{
public int ItemId { get; set; }
public string Name { get; set; }
public string Type { get; set; }
public int Count { get; set; }
public bool IsEquipped { get; set; }
public Dictionary<string, int> Stats { get; set; }
}

public class WorldData
{
public string CurrentRegion { get; set; }
public int GameTime { get; set; }
public List<string> DiscoveredLocations { get; set; }
public Dictionary<string, bool> RegionStates { get; set; }
public List<NpcData> ImportantNpcs { get; set; }
}

public class NpcData
{
public int NpcId { get; set; }
public string Name { get; set; }
public bool IsAlive { get; set; }
public int Relationship { get; set; }
public Position Position { get; set; }
}

public class GameSettings
{
public int MusicVolume { get; set; }
public int SfxVolume { get; set; }
public bool FullScreen { get; set; }
public string Difficulty { get; set; }
public Dictionary<string, string> KeyBindings { get; set; }
}

// Save system class
public class GameSaveSystem
{
private readonly string saveDirectory;
private readonly JsonSerializerOptions jsonOptions;

public GameSaveSystem(string saveDir)
{
// Set up the save directory
saveDirectory = saveDir;
if (!Directory.Exists(saveDirectory))
{
Directory.CreateDirectory(saveDirectory);
}

// Configure JSON serialization options
jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}

// Save a game to a specific slot
public bool SaveGame(GameSave gameSave, int slotNumber)
{
try
{
// Update save metadata
gameSave.SaveDate = DateTime.Now;
if (string.IsNullOrEmpty(gameSave.SaveName))
{
gameSave.SaveName = $"Save {slotNumber} - {gameSave.Player.Name} Lvl {gameSave.Player.Level}";
}

// Serialize to JSON
string json = JsonSerializer.Serialize(gameSave, jsonOptions);

// Write to file
string savePath = GetSaveFilePath(slotNumber);
File.WriteAllText(savePath, json);

Console.WriteLine($"Game saved to slot {slotNumber}");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Error saving game: {ex.Message}");
return false;
}
}

// Load a game from a specific slot
public GameSave LoadGame(int slotNumber)
{
try
{
string savePath = GetSaveFilePath(slotNumber);

if (!File.Exists(savePath))
{
Console.WriteLine($"No save file found in slot {slotNumber}");
return null;
}

// Read the JSON
string json = File.ReadAllText(savePath);

// Deserialize
GameSave gameSave = JsonSerializer.Deserialize<GameSave>(json, jsonOptions);

Console.WriteLine($"Game loaded from slot {slotNumber}");
return gameSave;
}
catch (Exception ex)
{
Console.WriteLine($"Error loading game: {ex.Message}");
return null;
}
}

// Get save metadata without loading the full save
public SaveMetadata GetSaveMetadata(int slotNumber)
{
try
{
string savePath = GetSaveFilePath(slotNumber);

if (!File.Exists(savePath))
{
return null;
}

// Read the JSON
string json = File.ReadAllText(savePath);

// Use a custom class to extract just the metadata
SaveMetadata metadata = JsonSerializer.Deserialize<SaveMetadata>(json, jsonOptions);
metadata.SlotNumber = slotNumber;

return metadata;
}
catch
{
return null;
}
}

// Get metadata for all save slots
public List<SaveMetadata> GetAllSaveMetadata()
{
List<SaveMetadata> results = new List<SaveMetadata>();

// Check slots 1-10
for (int i = 1; i <= 10; i++)
{
SaveMetadata metadata = GetSaveMetadata(i);
if (metadata != null)
{
results.Add(metadata);
}
}

return results;
}

// Delete a save
public bool DeleteSave(int slotNumber)
{
try
{
string savePath = GetSaveFilePath(slotNumber);

if (File.Exists(savePath))
{
File.Delete(savePath);
Console.WriteLine($"Deleted save in slot {slotNumber}");
return true;
}

return false;
}
catch (Exception ex)
{
Console.WriteLine($"Error deleting save: {ex.Message}");
return false;
}
}

// Create a backup of a save
public bool BackupSave(int slotNumber)
{
try
{
string savePath = GetSaveFilePath(slotNumber);

if (!File.Exists(savePath))
{
return false;
}

string backupPath = $"{savePath}.bak";
File.Copy(savePath, backupPath, true);
Console.WriteLine($"Created backup of save slot {slotNumber}");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Error backing up save: {ex.Message}");
return false;
}
}

// Helper method to get the file path for a save slot
private string GetSaveFilePath(int slotNumber)
{
return Path.Combine(saveDirectory, $"save_{slotNumber}.json");
}
}

// Class for extracting just the metadata from a save file
public class SaveMetadata
{
public int SlotNumber { get; set; }
public string SaveName { get; set; }
public DateTime SaveDate { get; set; }

// Include minimal player info for the save selection screen
[JsonPropertyName("player")]
public SavePlayerInfo PlayerInfo { get; set; }

public class SavePlayerInfo
{
[JsonPropertyName("name")]
public string Name { get; set; }

[JsonPropertyName("level")]
public int Level { get; set; }

[JsonPropertyName("currentRegion")]
public string CurrentRegion { get; set; }
}
}

// Usage example
public class Program
{
public static void Main()
{
// Initialize the save system
string saveDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "MyGame", "Saves");
GameSaveSystem saveSystem = new GameSaveSystem(saveDir);

// Create a sample game save
GameSave gameSave = CreateSampleGameSave();

// Save the game to slot 1
saveSystem.SaveGame(gameSave, 1);

// List all saves
Console.WriteLine("\nAvailable Saves:");
List<SaveMetadata> saves = saveSystem.GetAllSaveMetadata();
foreach (var save in saves)
{
Console.WriteLine($"Slot {save.SlotNumber}: {save.SaveName} - {save.SaveDate}");
Console.WriteLine($" Player: {save.PlayerInfo.Name} (Level {save.PlayerInfo.Level})");
Console.WriteLine($" Region: {save.PlayerInfo.CurrentRegion}");
}

// Load the game from slot 1
GameSave loadedGame = saveSystem.LoadGame(1);
if (loadedGame != null)
{
Console.WriteLine("\nLoaded Game Data:");
Console.WriteLine($"Player: {loadedGame.Player.Name} (Level {loadedGame.Player.Level})");
Console.WriteLine($"Health: {loadedGame.Player.Health}/{loadedGame.Player.MaxHealth}");
Console.WriteLine($"Position: {loadedGame.Player.Position}");
Console.WriteLine($"Inventory: {loadedGame.Player.Inventory.Count} items");
Console.WriteLine($"Current Region: {loadedGame.World.CurrentRegion}");
Console.WriteLine($"Game Time: {loadedGame.World.GameTime} minutes");
Console.WriteLine($"Discovered Locations: {loadedGame.World.DiscoveredLocations.Count}");
}

// Create a backup of the save
saveSystem.BackupSave(1);
}

// Helper method to create a sample game save for testing
private static GameSave CreateSampleGameSave()
{
return new GameSave
{
SaveName = "Forest Adventure",
SaveDate = DateTime.Now,
Player = new PlayerData
{
Name = "Elara",
Level = 7,
Experience = 3500,
Health = 85,
MaxHealth = 100,
Mana = 50,
MaxMana = 75,
Position = new Position { X = 156.3f, Y = 10.0f, Z = -23.7f },
Inventory = new List<InventoryItem>
{
new InventoryItem
{
ItemId = 101,
Name = "Health Potion",
Type = "Consumable",
Count = 5,
IsEquipped = false,
Stats = new Dictionary<string, int> { { "HealAmount", 25 } }
},
new InventoryItem
{
ItemId = 205,
Name = "Enchanted Sword",
Type = "Weapon",
Count = 1,
IsEquipped = true,
Stats = new Dictionary<string, int>
{
{ "Damage", 15 },
{ "CritChance", 10 },
{ "FireDamage", 5 }
}
}
},
CompletedQuests = new List<string> { "intro", "find_sword", "defeat_goblin" },
Skills = new Dictionary<string, int>
{
{ "Swordsmanship", 3 },
{ "Archery", 1 },
{ "Alchemy", 2 }
}
},
World = new WorldData
{
CurrentRegion = "Whispering Forest",
GameTime = 185, // minutes
DiscoveredLocations = new List<string>
{
"Village of Oakvale",
"Whispering Forest",
"Goblin Cave"
},
RegionStates = new Dictionary<string, bool>
{
{ "village_fountain_repaired", true },
{ "forest_boss_defeated", false },
{ "bridge_collapsed", true }
},
ImportantNpcs = new List<NpcData>
{
new NpcData
{
NpcId = 1001,
Name = "Elder Thorne",
IsAlive = true,
Relationship = 75, // 0-100 scale
Position = new Position { X = 120.5f, Y = 0.0f, Z = 45.2f }
},
new NpcData
{
NpcId = 1002,
Name = "Merchant Galen",
IsAlive = true,
Relationship = 50,
Position = new Position { X = 115.0f, Y = 0.0f, Z = 40.0f }
}
}
},
Settings = new GameSettings
{
MusicVolume = 80,
SfxVolume = 100,
FullScreen = true,
Difficulty = "Normal",
KeyBindings = new Dictionary<string, string>
{
{ "MoveForward", "W" },
{ "MoveBack", "S" },
{ "MoveLeft", "A" },
{ "MoveRight", "D" },
{ "Jump", "Space" },
{ "Attack", "Mouse0" },
{ "Inventory", "I" }
}
}
};
}
}

This comprehensive save system demonstrates:

  1. Organizing game data into logical classes
  2. Handling complex nested objects and collections
  3. Saving and loading from multiple slots
  4. Extracting metadata without loading the full save
  5. Creating backups of save files
  6. Proper error handling

Serialization in Unity

Unity Relevance

Unity provides several options for serialization:

  1. Unity's Built-in Serialization: Used for Inspector fields and ScriptableObjects

    // Unity will serialize public fields and fields with [SerializeField]
    public class PlayerStats : MonoBehaviour
    {
    public string playerName;
    public int level;

    [SerializeField] private int health;
    [SerializeField] private List<string> abilities;
    }
  2. JsonUtility: Unity's built-in JSON serializer

    [Serializable] // Required for JsonUtility
    public class SaveData
    {
    public string playerName;
    public int level;
    public float[] position;
    }

    // Saving
    SaveData data = new SaveData
    {
    playerName = "Hero",
    level = 5,
    position = new float[] { transform.position.x, transform.position.y, transform.position.z }
    };

    string json = JsonUtility.ToJson(data);
    File.WriteAllText(Path.Combine(Application.persistentDataPath, "save.json"), json);

    // Loading
    string loadedJson = File.ReadAllText(Path.Combine(Application.persistentDataPath, "save.json"));
    SaveData loadedData = JsonUtility.FromJson<SaveData>(loadedJson);
  3. Binary Formatter: For more complex object graphs (with the same security caveats mentioned earlier)

  4. Third-party Libraries: Many Unity developers use libraries like Newtonsoft.Json (JSON.NET) for more advanced serialization needs

Unity's serialization system has some limitations to be aware of:

  • JsonUtility doesn't support dictionaries
  • Only public fields or those marked with [SerializeField] are serialized
  • Complex object references can be challenging to serialize correctly

For more advanced serialization needs in Unity, consider using a third-party solution or implementing custom serialization logic.

Best Practices for Serialization

  1. Choose the right format for your needs:

    • JSON for human-readable, cross-platform data
    • Binary for performance-critical or large data sets
    • XML for highly structured data with schema requirements
  2. Design with serialization in mind:

    • Use simple data structures when possible
    • Avoid circular references
    • Consider versioning your data format
  3. Handle versioning and backward compatibility:

    • Include a version number in your save format
    • Write code to migrate old save formats to new ones
    • Be cautious when changing class structures
  4. Implement error handling and validation:

    • Always use try-catch blocks when serializing/deserializing
    • Validate data after deserialization
    • Have fallback mechanisms for corrupted saves
  5. Consider security implications:

    • Don't deserialize data from untrusted sources
    • Be cautious with binary serialization
    • Consider encrypting sensitive save data
  6. Test thoroughly:

    • Test with edge cases (empty collections, null values, etc.)
    • Test backward compatibility with old save formats
    • Test with corrupted or incomplete save files

Conclusion

Data serialization is a fundamental technique for game developers, enabling save systems, configuration management, and data exchange. By understanding the different serialization formats and techniques available in C#, you can implement robust, flexible systems for persisting your game's data.

In the next section, we'll put these concepts into practice with a mini-project that combines file I/O and serialization to create a complete game save system.