11.5 - Understanding Unity's .NET Profile and Libraries
Unity uses the .NET framework as the foundation for its C# scripting environment. Understanding how Unity implements .NET and which libraries are available is crucial for effective game development. In this section, we'll explore Unity's .NET profile, available libraries, and how to leverage them in your projects.
Unity's .NET Implementation
Evolution of .NET in Unity
Unity's .NET support has evolved significantly over the years:
Unity Version | .NET Support | C# Version | Key Features |
---|---|---|---|
Unity 5.x | .NET 3.5 Equivalent | C# 4 | Limited generics, no async/await |
Unity 2017.x | .NET 4.6 Profile | C# 6 | Modern C# features, async/await |
Unity 2018.x | .NET Standard 2.0 | C# 7 | Better library compatibility |
Unity 2019.x+ | .NET Standard 2.0 | C# 7.3 | Improved performance, more language features |
Unity 2021.x+ | .NET Standard 2.1 | C# 9 | Latest language features, better performance |
Unity 6.x | .NET Standard 2.1 | C# 9 | Comprehensive modern C# support |
This evolution has gradually brought Unity's C# environment closer to the mainstream .NET ecosystem, making it easier to use modern C# features and third-party libraries.
.NET Profiles in Unity
Unity offers different .NET API compatibility levels that you can choose for your project:
-
.NET Standard 2.1: The recommended profile for new projects, supporting modern C# features and providing good compatibility with external libraries.
-
.NET Framework: An older profile that maintains compatibility with legacy code but lacks some modern features.
You can select the API compatibility level in the Player Settings:
Edit > Project Settings > Player > Other Settings > Configuration > API Compatibility Level
Core .NET Libraries in Unity
Unity includes many standard .NET libraries that you can use in your projects:
System Namespace
The System
namespace provides fundamental classes and base classes that define commonly-used types:
using System;
public class ExampleScript : MonoBehaviour
{
private void Start()
{
// Using DateTime from System namespace
DateTime now = DateTime.Now;
Debug.Log($"Current time: {now}");
// Using Math from System namespace
float angle = 45f;
float radians = angle * Mathf.Deg2Rad;
float sine = (float)Math.Sin(radians);
Debug.Log($"Sine of {angle} degrees: {sine}");
// Using string manipulation
string message = "Hello, Unity!";
string[] words = message.Split(',');
Debug.Log($"First word: {words[0].Trim()}");
}
}
System.Collections and System.Collections.Generic
These namespaces provide classes for managing collections of objects:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CollectionsExample : MonoBehaviour
{
private void Start()
{
// List<T> - Dynamic array
List<string> playerNames = new List<string>
{
"Alice", "Bob", "Charlie", "Diana"
};
playerNames.Add("Ethan");
playerNames.Remove("Bob");
Debug.Log($"Player count: {playerNames.Count}");
Debug.Log($"First player: {playerNames[0]}");
// Dictionary<TKey, TValue> - Key-value pairs
Dictionary<string, int> playerScores = new Dictionary<string, int>
{
{ "Alice", 100 },
{ "Charlie", 85 },
{ "Diana", 95 },
{ "Ethan", 75 }
};
playerScores["Alice"] += 20;
foreach (var pair in playerScores)
{
Debug.Log($"{pair.Key}: {pair.Value} points");
}
// Queue<T> - First-in, first-out collection
Queue<string> messageQueue = new Queue<string>();
messageQueue.Enqueue("First message");
messageQueue.Enqueue("Second message");
messageQueue.Enqueue("Third message");
while (messageQueue.Count > 0)
{
string message = messageQueue.Dequeue();
Debug.Log($"Processing: {message}");
}
// HashSet<T> - Collection of unique elements
HashSet<string> uniqueTags = new HashSet<string>();
uniqueTags.Add("Player");
uniqueTags.Add("Enemy");
uniqueTags.Add("Item");
uniqueTags.Add("Player"); // Duplicate, won't be added
Debug.Log($"Unique tags: {uniqueTags.Count}");
Debug.Log($"Contains 'Enemy': {uniqueTags.Contains("Enemy")}");
}
}
System.Linq
LINQ (Language Integrated Query) provides powerful query capabilities:
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class LinqExample : MonoBehaviour
{
[System.Serializable]
public class GameItem
{
public string name;
public int value;
public string type;
public int level;
}
[SerializeField] private List<GameItem> _items = new List<GameItem>();
private void Start()
{
// Filter items
var valuableItems = _items.Where(item => item.value > 100).ToList();
Debug.Log($"Valuable items: {valuableItems.Count}");
// Sort items by value (descending)
var sortedItems = _items.OrderByDescending(item => item.value).ToList();
Debug.Log($"Most valuable item: {sortedItems[0].name}");
// Group items by type
var itemsByType = _items.GroupBy(item => item.type);
foreach (var group in itemsByType)
{
Debug.Log($"{group.Key}: {group.Count()} items");
}
// Calculate average value by type
var averageValueByType = _items
.GroupBy(item => item.type)
.Select(group => new
{
Type = group.Key,
AverageValue = group.Average(item => item.value)
});
foreach (var avg in averageValueByType)
{
Debug.Log($"{avg.Type} average value: {avg.AverageValue:F1}");
}
// Find items matching criteria
var highLevelWeapons = _items
.Where(item => item.type == "Weapon" && item.level >= 5)
.ToList();
Debug.Log($"High-level weapons: {highLevelWeapons.Count}");
// Check if any items match a condition
bool hasLegendaryItems = _items.Any(item => item.level >= 10);
Debug.Log($"Has legendary items: {hasLegendaryItems}");
// Transform data
var itemNames = _items.Select(item => item.name).ToList();
Debug.Log($"Item names: {string.Join(", ", itemNames)}");
}
}
System.IO
The System.IO
namespace provides classes for file and directory operations:
using System.IO;
using UnityEngine;
public class FileSystemExample : MonoBehaviour
{
private void Start()
{
// Get the persistent data path (works across platforms)
string savePath = Path.Combine(Application.persistentDataPath, "saves");
// Create directory if it doesn't exist
if (!Directory.Exists(savePath))
{
Directory.CreateDirectory(savePath);
Debug.Log($"Created directory: {savePath}");
}
// Write to a file
string filePath = Path.Combine(savePath, "player_data.json");
string jsonData = "{\"name\":\"Player1\",\"level\":5,\"score\":1000}";
File.WriteAllText(filePath, jsonData);
Debug.Log($"Wrote data to: {filePath}");
// Read from a file
if (File.Exists(filePath))
{
string loadedData = File.ReadAllText(filePath);
Debug.Log($"Loaded data: {loadedData}");
}
// List files in directory
string[] saveFiles = Directory.GetFiles(savePath, "*.json");
Debug.Log($"Found {saveFiles.Length} save files:");
foreach (string file in saveFiles)
{
FileInfo info = new FileInfo(file);
Debug.Log($"- {info.Name} ({info.Length} bytes)");
}
}
}
When using System.IO
in Unity, always use Application.persistentDataPath
or Application.dataPath
as the base directory for your file operations to ensure compatibility across platforms.
System.Threading and System.Threading.Tasks
These namespaces provide classes for multi-threading and asynchronous operations:
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class ThreadingExample : MonoBehaviour
{
private void Start()
{
Debug.Log("Starting async operations...");
// Run a task asynchronously
ProcessDataAsync();
Debug.Log("Main thread continues execution...");
}
private async void ProcessDataAsync()
{
Debug.Log("Starting data processing...");
// Simulate heavy computation on a background thread
int result = await Task.Run(() =>
{
Debug.Log($"Background thread ID: {Thread.CurrentThread.ManagedThreadId}");
// Simulate work
int sum = 0;
for (int i = 0; i < 1000000; i++)
{
sum += i;
// Don't call Unity API from background threads!
// Debug.Log() is not thread-safe
}
// Simulate delay
Thread.Sleep(2000);
return sum;
});
// Back on the main thread, can use Unity API
Debug.Log($"Processing complete. Result: {result}");
// Update UI or game state with the result
UpdateGameState(result);
}
private void UpdateGameState(int result)
{
// Safe to call Unity API here (on main thread)
Debug.Log($"Updating game state with result: {result}");
}
}
Unity's main functionality is not thread-safe. You should not call most Unity APIs (like GameObject
, Transform
, Component
methods, etc.) from background threads. Use background threads only for CPU-intensive calculations that don't interact with Unity objects directly.
Unity-Specific Libraries and APIs
In addition to standard .NET libraries, Unity provides its own APIs for game development:
UnityEngine Namespace
The core Unity functionality is in the UnityEngine
namespace:
using UnityEngine;
public class UnityEngineExample : MonoBehaviour
{
private void Start()
{
// Vector3 - 3D vector struct
Vector3 position = new Vector3(1f, 2f, 3f);
Vector3 direction = Vector3.forward;
float distance = Vector3.Distance(position, Vector3.zero);
// Quaternion - Rotation representation
Quaternion rotation = Quaternion.Euler(0f, 90f, 0f);
// Time - Time-related utilities
float deltaTime = Time.deltaTime;
float gameTime = Time.time;
// Input - User input handling
bool isJumping = Input.GetButtonDown("Jump");
Vector2 movement = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
// Physics - Raycasting and physics queries
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
{
Debug.Log($"Hit object: {hit.collider.gameObject.name}");
}
// Random - Random number generation
float randomValue = Random.value;
int randomInt = Random.Range(1, 100);
Vector3 randomDirection = Random.insideUnitSphere;
// Debug - Debugging utilities
Debug.Log("Regular log message");
Debug.LogWarning("Warning message");
Debug.LogError("Error message");
Debug.DrawRay(transform.position, Vector3.up * 2f, Color.red, 1f);
}
}
Unity UI System
Unity's UI system is in the UnityEngine.UI
namespace:
using UnityEngine;
using UnityEngine.UI;
using TMPro; // TextMeshPro
public class UIExample : MonoBehaviour
{
[SerializeField] private Button _startButton;
[SerializeField] private Slider _volumeSlider;
[SerializeField] private Toggle _fullscreenToggle;
[SerializeField] private TMP_Text _scoreText;
[SerializeField] private Image _healthBar;
private int _score = 0;
private float _health = 1.0f;
private void Start()
{
// Set up button click event
_startButton.onClick.AddListener(OnStartButtonClicked);
// Set up slider value change event
_volumeSlider.onValueChanged.AddListener(OnVolumeChanged);
// Set up toggle value change event
_fullscreenToggle.onValueChanged.AddListener(OnFullscreenToggled);
// Initialize UI elements
UpdateScoreText();
UpdateHealthBar();
}
private void OnStartButtonClicked()
{
Debug.Log("Start button clicked!");
// Increment score for demonstration
_score += 100;
UpdateScoreText();
}
private void OnVolumeChanged(float volume)
{
Debug.Log($"Volume changed to: {volume}");
// Set audio volume (example)
AudioListener.volume = volume;
}
private void OnFullscreenToggled(bool isFullscreen)
{
Debug.Log($"Fullscreen toggled: {isFullscreen}");
// Set fullscreen mode
Screen.fullScreen = isFullscreen;
}
private void Update()
{
// Example: decrease health over time
if (_health > 0)
{
_health -= 0.1f * Time.deltaTime;
_health = Mathf.Max(0, _health);
UpdateHealthBar();
}
}
private void UpdateScoreText()
{
_scoreText.text = $"Score: {_score}";
}
private void UpdateHealthBar()
{
// Update fill amount of the health bar image
_healthBar.fillAmount = _health;
// Change color based on health level
if (_health > 0.6f)
{
_healthBar.color = Color.green;
}
else if (_health > 0.3f)
{
_healthBar.color = Color.yellow;
}
else
{
_healthBar.color = Color.red;
}
}
}
Using Third-Party .NET Libraries in Unity
Unity's .NET compatibility allows you to use many third-party libraries in your projects. Here's how to incorporate them:
Adding Libraries to Your Project
There are several ways to add external libraries to your Unity project:
-
Direct DLL Reference:
- Add the library's DLL file to your project's
Assets/Plugins
folder - Unity will automatically detect and reference the library
- Add the library's DLL file to your project's
-
NuGet Packages:
- Unity doesn't have built-in NuGet support, but you can use third-party tools like NuGetForUnity
- This allows you to install and manage NuGet packages directly in Unity
-
Unity Package Manager (UPM):
- Some .NET libraries are available as Unity packages
- Add them through the Package Manager window or by editing the
manifest.json
file
Compatibility Considerations
When using third-party libraries, consider these compatibility factors:
-
.NET API Compatibility Level:
- Ensure your Unity project's API Compatibility Level supports the library
- Most modern libraries require at least .NET Standard 2.0
-
Platform Compatibility:
- Some libraries may not work on all platforms Unity supports
- Test thoroughly on your target platforms
-
Assembly Definition Files:
- Consider using Assembly Definition Files to organize your project and control references
- This can improve compilation times and manage dependencies
Example: Using Newtonsoft.Json
Newtonsoft.Json (Json.NET) is a popular JSON framework for .NET. Here's how to use it in Unity:
using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEngine;
public class JsonNetExample : MonoBehaviour
{
[System.Serializable]
public class PlayerData
{
public string name;
public int level;
public float health;
public Vector3Data position;
public List<string> inventory;
public Dictionary<string, int> stats;
}
[System.Serializable]
public class Vector3Data
{
public float x;
public float y;
public float z;
public Vector3Data(Vector3 vector)
{
x = vector.x;
y = vector.y;
z = vector.z;
}
public Vector3 ToVector3()
{
return new Vector3(x, y, z);
}
}
private void Start()
{
// Create sample player data
PlayerData playerData = new PlayerData
{
name = "Adventurer",
level = 5,
health = 85.5f,
position = new Vector3Data(transform.position),
inventory = new List<string> { "Sword", "Health Potion", "Map" },
stats = new Dictionary<string, int>
{
{ "Strength", 12 },
{ "Dexterity", 8 },
{ "Intelligence", 10 }
}
};
// Serialize to JSON
string json = JsonConvert.SerializeObject(playerData, Formatting.Indented);
Debug.Log("Serialized JSON:");
Debug.Log(json);
// Deserialize from JSON
PlayerData loadedData = JsonConvert.DeserializeObject<PlayerData>(json);
Debug.Log($"Deserialized data: {loadedData.name}, Level {loadedData.level}");
// Access complex properties
Vector3 position = loadedData.position.ToVector3();
Debug.Log($"Position: {position}");
foreach (var item in loadedData.inventory)
{
Debug.Log($"Inventory item: {item}");
}
foreach (var stat in loadedData.stats)
{
Debug.Log($"Stat - {stat.Key}: {stat.Value}");
}
}
}
Performance Considerations
When using .NET libraries in Unity, consider these performance implications:
Garbage Collection
.NET uses automatic memory management through garbage collection (GC), which can cause performance hitches if not managed properly:
using System.Collections.Generic;
using System.Text;
using UnityEngine;
public class GCExample : MonoBehaviour
{
private void Update()
{
// BAD: Creates garbage every frame
string message = "Player position: " + transform.position.x + ", " +
transform.position.y + ", " + transform.position.z;
Debug.Log(message);
// BETTER: Use string interpolation (still creates garbage, but less)
string betterMessage = $"Player position: {transform.position.x}, {transform.position.y}, {transform.position.z}";
Debug.Log(betterMessage);
// BEST: Reuse a StringBuilder (minimal garbage)
StringBuilder sb = new StringBuilder();
sb.Clear();
sb.Append("Player position: ");
sb.Append(transform.position.x);
sb.Append(", ");
sb.Append(transform.position.y);
sb.Append(", ");
sb.Append(transform.position.z);
Debug.Log(sb.ToString());
// BAD: Creating new collections in frequently called methods
List<GameObject> enemies = new List<GameObject>();
GameObject[] enemyArray = GameObject.FindGameObjectsWithTag("Enemy");
foreach (GameObject enemy in enemyArray)
{
enemies.Add(enemy);
}
// BETTER: Reuse collections
// _enemies is a class member initialized once
// _enemies.Clear();
// foreach (GameObject enemy in GameObject.FindGameObjectsWithTag("Enemy"))
// {
// _enemies.Add(enemy);
// }
}
}
Boxing and Unboxing
Boxing (converting value types to reference types) and unboxing can impact performance:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BoxingExample : MonoBehaviour
{
private void Start()
{
// BAD: Causes boxing of int value
ArrayList numbers = new ArrayList();
for (int i = 0; i < 1000; i++)
{
numbers.Add(i); // Boxes int to object
}
int sum = 0;
foreach (object number in numbers)
{
sum += (int)number; // Unboxing
}
// BETTER: Generic collections avoid boxing
List<int> betterNumbers = new List<int>();
for (int i = 0; i < 1000; i++)
{
betterNumbers.Add(i); // No boxing
}
int betterSum = 0;
foreach (int number in betterNumbers)
{
betterSum += number; // No unboxing
}
}
}
Reflection
Reflection is powerful but can be slow if used excessively:
using System;
using System.Reflection;
using UnityEngine;
public class ReflectionExample : MonoBehaviour
{
private void Start()
{
// BAD: Using reflection in frequently called code
GameObject player = GameObject.FindGameObjectWithTag("Player");
Type playerType = player.GetComponent<PlayerController>().GetType();
MethodInfo jumpMethod = playerType.GetMethod("Jump", BindingFlags.Instance | BindingFlags.Public);
jumpMethod.Invoke(player.GetComponent<PlayerController>(), null);
// BETTER: Cache reflection results
// _jumpMethod is cached during initialization
// _jumpMethod.Invoke(player.GetComponent<PlayerController>(), null);
// BEST: Avoid reflection when possible
// player.GetComponent<PlayerController>().Jump();
// Alternative: Use interfaces or delegates instead of reflection
// IPlayerActions playerActions = player.GetComponent<IPlayerActions>();
// if (playerActions != null)
// {
// playerActions.Jump();
// }
}
}
Practical Example: Save System with .NET Libraries
Let's create a comprehensive save system that demonstrates effective use of .NET libraries in Unity:
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using UnityEngine;
public class SaveSystem : MonoBehaviour
{
[Serializable]
public class SaveData
{
public string playerName;
public int playerLevel;
public float[] playerPosition;
public Dictionary<string, int> inventory;
public List<QuestData> activeQuests;
public GameSettings settings;
public DateTime saveTime;
}
[Serializable]
public class QuestData
{
public string questId;
public string questTitle;
public int progress;
public bool isCompleted;
}
[Serializable]
public class GameSettings
{
public float musicVolume;
public float sfxVolume;
public bool fullscreen;
public int qualityLevel;
}
private const string SaveFileName = "save.dat";
private const string BackupExtension = ".bak";
private readonly byte[] _encryptionKey = Encoding.UTF8.GetBytes("YourSecretKey12"); // 16 bytes for AES-128
private string SaveFilePath => Path.Combine(Application.persistentDataPath, SaveFileName);
private string BackupFilePath => SaveFilePath + BackupExtension;
// Save game data
public async Task<bool> SaveGameAsync(SaveData data)
{
try
{
// Backup existing save if it exists
if (File.Exists(SaveFilePath))
{
File.Copy(SaveFilePath, BackupFilePath, true);
}
// Serialize data to JSON
string json = JsonConvert.SerializeObject(data, Formatting.None);
// Encrypt and compress data
byte[] saveData = await Task.Run(() => EncryptAndCompressData(json));
// Write to file
await File.WriteAllBytesAsync(SaveFilePath, saveData);
Debug.Log($"Game saved successfully at {DateTime.Now}");
return true;
}
catch (Exception ex)
{
Debug.LogError($"Error saving game: {ex.Message}");
return false;
}
}
// Load game data
public async Task<SaveData> LoadGameAsync()
{
try
{
string filePath = SaveFilePath;
// If main save is corrupted, try backup
if (!File.Exists(filePath) || new FileInfo(filePath).Length == 0)
{
Debug.LogWarning("Main save file not found or empty, trying backup...");
filePath = BackupFilePath;
if (!File.Exists(filePath))
{
Debug.LogWarning("No save files found.");
return null;
}
}
// Read file
byte[] saveData = await File.ReadAllBytesAsync(filePath);
// Decompress and decrypt data
string json = await Task.Run(() => DecompressAndDecryptData(saveData));
// Deserialize JSON
SaveData data = JsonConvert.DeserializeObject<SaveData>(json);
Debug.Log($"Game loaded successfully. Save from: {data.saveTime}");
return data;
}
catch (Exception ex)
{
Debug.LogError($"Error loading game: {ex.Message}");
// If main save failed, try backup
if (filePath == SaveFilePath && File.Exists(BackupFilePath))
{
Debug.Log("Attempting to load from backup...");
filePath = BackupFilePath;
return await LoadGameAsync();
}
return null;
}
}
// Delete save data
public bool DeleteSaveData()
{
try
{
if (File.Exists(SaveFilePath))
{
File.Delete(SaveFilePath);
}
if (File.Exists(BackupFilePath))
{
File.Delete(BackupFilePath);
}
Debug.Log("Save data deleted successfully");
return true;
}
catch (Exception ex)
{
Debug.LogError($"Error deleting save data: {ex.Message}");
return false;
}
}
// Check if save data exists
public bool SaveDataExists()
{
return File.Exists(SaveFilePath) || File.Exists(BackupFilePath);
}
// Get save info without loading full data
public async Task<DateTime?> GetSaveTimeAsync()
{
try
{
if (!SaveDataExists())
{
return null;
}
string filePath = File.Exists(SaveFilePath) ? SaveFilePath : BackupFilePath;
byte[] saveData = await File.ReadAllBytesAsync(filePath);
string json = await Task.Run(() => DecompressAndDecryptData(saveData));
SaveData data = JsonConvert.DeserializeObject<SaveData>(json);
return data.saveTime;
}
catch
{
return null;
}
}
// Create a save data object from current game state
public SaveData CreateSaveData()
{
// Get player
GameObject player = GameObject.FindGameObjectWithTag("Player");
PlayerController playerController = player.GetComponent<PlayerController>();
InventorySystem inventorySystem = player.GetComponent<InventorySystem>();
QuestManager questManager = FindObjectOfType<QuestManager>();
// Create save data
SaveData data = new SaveData
{
playerName = playerController.PlayerName,
playerLevel = playerController.PlayerLevel,
playerPosition = new float[]
{
player.transform.position.x,
player.transform.position.y,
player.transform.position.z
},
inventory = new Dictionary<string, int>(),
activeQuests = new List<QuestData>(),
settings = new GameSettings
{
musicVolume = AudioManager.Instance.MusicVolume,
sfxVolume = AudioManager.Instance.SfxVolume,
fullscreen = Screen.fullScreen,
qualityLevel = QualitySettings.GetQualityLevel()
},
saveTime = DateTime.Now
};
// Add inventory items
foreach (var item in inventorySystem.GetAllItems())
{
data.inventory[item.ItemId] = item.Quantity;
}
// Add active quests
foreach (var quest in questManager.GetActiveQuests())
{
data.activeQuests.Add(new QuestData
{
questId = quest.Id,
questTitle = quest.Title,
progress = quest.Progress,
isCompleted = quest.IsCompleted
});
}
return data;
}
// Apply loaded save data to the game
public void ApplySaveData(SaveData data)
{
if (data == null)
{
Debug.LogError("Cannot apply null save data");
return;
}
// Get game systems
GameObject player = GameObject.FindGameObjectWithTag("Player");
PlayerController playerController = player.GetComponent<PlayerController>();
InventorySystem inventorySystem = player.GetComponent<InventorySystem>();
QuestManager questManager = FindObjectOfType<QuestManager>();
// Apply player data
playerController.PlayerName = data.playerName;
playerController.PlayerLevel = data.playerLevel;
// Apply position
if (data.playerPosition != null && data.playerPosition.Length == 3)
{
player.transform.position = new Vector3(
data.playerPosition[0],
data.playerPosition[1],
data.playerPosition[2]
);
}
// Apply inventory
inventorySystem.ClearInventory();
foreach (var item in data.inventory)
{
inventorySystem.AddItem(item.Key, item.Value);
}
// Apply quests
questManager.ClearQuests();
foreach (var quest in data.activeQuests)
{
questManager.LoadQuest(quest.questId, quest.progress, quest.isCompleted);
}
// Apply settings
AudioManager.Instance.SetMusicVolume(data.settings.musicVolume);
AudioManager.Instance.SetSfxVolume(data.settings.sfxVolume);
Screen.fullScreen = data.settings.fullscreen;
QualitySettings.SetQualityLevel(data.settings.qualityLevel);
Debug.Log("Save data applied successfully");
}
// Encrypt and compress data
private byte[] EncryptAndCompressData(string data)
{
try
{
// Convert string to bytes
byte[] bytes = Encoding.UTF8.GetBytes(data);
// Compress data
using (MemoryStream compressedStream = new MemoryStream())
{
using (GZipStream gzip = new GZipStream(compressedStream, CompressionMode.Compress))
{
gzip.Write(bytes, 0, bytes.Length);
}
byte[] compressedBytes = compressedStream.ToArray();
// Encrypt data
using (Aes aes = Aes.Create())
{
aes.Key = _encryptionKey;
aes.GenerateIV(); // Generate random IV
byte[] iv = aes.IV;
using (MemoryStream encryptedStream = new MemoryStream())
{
// Write IV to the beginning of the file
encryptedStream.Write(iv, 0, iv.Length);
using (CryptoStream crypto = new CryptoStream(
encryptedStream,
aes.CreateEncryptor(),
CryptoStreamMode.Write))
{
crypto.Write(compressedBytes, 0, compressedBytes.Length);
}
return encryptedStream.ToArray();
}
}
}
}
catch (Exception ex)
{
Debug.LogError($"Error encrypting/compressing data: {ex.Message}");
throw;
}
}
// Decrypt and decompress data
private string DecompressAndDecryptData(byte[] data)
{
try
{
using (MemoryStream encryptedStream = new MemoryStream(data))
{
// Read IV from the beginning of the file
byte[] iv = new byte[16]; // AES IV size is 16 bytes
encryptedStream.Read(iv, 0, iv.Length);
using (Aes aes = Aes.Create())
{
aes.Key = _encryptionKey;
aes.IV = iv;
using (MemoryStream decryptedStream = new MemoryStream())
{
using (CryptoStream crypto = new CryptoStream(
encryptedStream,
aes.CreateDecryptor(),
CryptoStreamMode.Read))
{
crypto.CopyTo(decryptedStream);
}
byte[] compressedBytes = decryptedStream.ToArray();
// Decompress data
using (MemoryStream decompressedStream = new MemoryStream())
{
using (MemoryStream compressedStream = new MemoryStream(compressedBytes))
using (GZipStream gzip = new GZipStream(compressedStream, CompressionMode.Decompress))
{
gzip.CopyTo(decompressedStream);
}
byte[] decompressedBytes = decompressedStream.ToArray();
return Encoding.UTF8.GetString(decompressedBytes);
}
}
}
}
}
catch (Exception ex)
{
Debug.LogError($"Error decrypting/decompressing data: {ex.Message}");
throw;
}
}
}
// Usage example
public class GameManager : MonoBehaviour
{
[SerializeField] private SaveSystem _saveSystem;
public async void SaveGame()
{
SaveSystem.SaveData saveData = _saveSystem.CreateSaveData();
bool success = await _saveSystem.SaveGameAsync(saveData);
if (success)
{
Debug.Log("Game saved successfully!");
}
else
{
Debug.LogError("Failed to save game!");
}
}
public async void LoadGame()
{
SaveSystem.SaveData saveData = await _saveSystem.LoadGameAsync();
if (saveData != null)
{
_saveSystem.ApplySaveData(saveData);
Debug.Log("Game loaded successfully!");
}
else
{
Debug.LogError("Failed to load game!");
}
}
public void NewGame()
{
// Start a new game
Debug.Log("Starting new game...");
}
public async void ShowSaveInfo()
{
if (_saveSystem.SaveDataExists())
{
DateTime? saveTime = await _saveSystem.GetSaveTimeAsync();
if (saveTime.HasValue)
{
TimeSpan age = DateTime.Now - saveTime.Value;
Debug.Log($"Save file exists from {saveTime.Value}");
Debug.Log($"Save age: {(age.TotalHours > 24 ? $"{age.TotalDays:F1} days" : $"{age.TotalHours:F1} hours")}");
}
else
{
Debug.Log("Save file exists but couldn't read save time");
}
}
else
{
Debug.Log("No save file exists");
}
}
}
This save system demonstrates several .NET features:
- Asynchronous file operations with
Task
andasync
/await
- Data compression with
GZipStream
- Encryption with
AES
- JSON serialization with Newtonsoft.Json
- Error handling and backup management
- Memory management considerations
Conclusion
Unity's .NET implementation provides a powerful foundation for game development. By understanding the available libraries and how to use them effectively, you can leverage the full power of C# in your Unity projects.
Remember these key points:
- Unity supports most standard .NET libraries, especially with .NET Standard 2.0/2.1 profiles
- Consider performance implications, especially regarding garbage collection
- Use Unity-specific APIs for game-related functionality
- Third-party libraries can extend your capabilities but require compatibility checks
- Asynchronous programming can improve performance but requires careful implementation in Unity
As you continue your journey into Unity development, the C# and .NET knowledge you've gained will serve as a strong foundation for building complex and efficient games.
Unity's .NET implementation continues to evolve with each release. Unity 6.x brings improved .NET support, better performance, and more modern C# features. Stay updated with Unity's documentation to take advantage of the latest improvements in the .NET integration.
For performance-critical game code, consider Unity's specialized systems like the Job System and Burst Compiler, which provide additional optimization opportunities beyond standard .NET code.
In the next section, we'll explore version control with Git, an essential tool for managing your Unity projects and collaborating with others.