7.8 - Error Handling Exercise
Now that you've learned about error handling and exceptions in C#, it's time to put your knowledge into practice. In this exercise, you'll implement a robust game save system that handles various error scenarios gracefully.
Exercise: Building a Robust Save System
Scenario
You're developing a game that needs to save and load player progress. The save system needs to handle various error conditions, such as:
- File not found
- Corrupted save data
- Insufficient disk space
- Permission issues
- Version mismatches between saves and the current game
Requirements
- Create a
SaveSystem
class that can save and load game data - Implement proper exception handling for all potential error scenarios
- Create at least two custom exception types for game-specific errors
- Use
try-catch-finally
blocks appropriately - Use the
using
statement for file operations - Provide user-friendly error messages
- Implement fallback mechanisms where appropriate
Step 1: Define the Game Data Structure
First, let's define what our game data looks like:
[Serializable]
public class PlayerData
{
public string PlayerName;
public int Level;
public int Experience;
public float Health;
public Vector3 Position;
public List<string> Inventory;
public DateTime LastSaved;
public string GameVersion;
}
Step 2: Create Custom Exceptions
Create custom exceptions for game-specific error scenarios:
[Serializable]
public class SaveSystemException : Exception
{
public SaveSystemException() : base() { }
public SaveSystemException(string message) : base(message) { }
public SaveSystemException(string message, Exception innerException)
: base(message, innerException) { }
protected SaveSystemException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}
[Serializable]
public class SaveCorruptedException : SaveSystemException
{
public string SaveFileName { get; }
public SaveCorruptedException(string saveFileName)
: base($"Save file '{saveFileName}' is corrupted")
{
SaveFileName = saveFileName;
}
public SaveCorruptedException(string saveFileName, Exception innerException)
: base($"Save file '{saveFileName}' is corrupted", innerException)
{
SaveFileName = saveFileName;
}
protected SaveCorruptedException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
SaveFileName = info.GetString(nameof(SaveFileName));
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue(nameof(SaveFileName), SaveFileName);
}
}
[Serializable]
public class SaveVersionMismatchException : SaveSystemException
{
public string SaveVersion { get; }
public string CurrentVersion { get; }
public SaveVersionMismatchException(string saveVersion, string currentVersion)
: base($"Save version '{saveVersion}' does not match current game version '{currentVersion}'")
{
SaveVersion = saveVersion;
CurrentVersion = currentVersion;
}
protected SaveVersionMismatchException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
SaveVersion = info.GetString(nameof(SaveVersion));
CurrentVersion = info.GetString(nameof(CurrentVersion));
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue(nameof(SaveVersion), SaveVersion);
info.AddValue(nameof(CurrentVersion), CurrentVersion);
}
}
Step 3: Implement the Save System
Now, implement the SaveSystem
class with proper error handling:
public class SaveSystem
{
private readonly string saveFolderPath;
private readonly string currentGameVersion;
public SaveSystem(string gameVersion)
{
currentGameVersion = gameVersion;
saveFolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "MyGame", "Saves");
// Ensure the save directory exists
try
{
if (!Directory.Exists(saveFolderPath))
{
Directory.CreateDirectory(saveFolderPath);
}
}
catch (Exception e)
{
Console.WriteLine($"Failed to create save directory: {e.Message}");
// Fall back to a different location
saveFolderPath = Path.Combine(Environment.CurrentDirectory, "Saves");
Directory.CreateDirectory(saveFolderPath);
}
}
public void SaveGame(PlayerData playerData, string saveName)
{
if (playerData == null)
{
throw new ArgumentNullException(nameof(playerData), "Player data cannot be null");
}
if (string.IsNullOrEmpty(saveName))
{
throw new ArgumentException("Save name cannot be null or empty", nameof(saveName));
}
// Update metadata
playerData.LastSaved = DateTime.Now;
playerData.GameVersion = currentGameVersion;
string saveFilePath = Path.Combine(saveFolderPath, $"{saveName}.json");
string tempFilePath = Path.Combine(saveFolderPath, $"{saveName}.tmp");
try
{
// First write to a temporary file
using (FileStream fileStream = new FileStream(tempFilePath, FileMode.Create))
{
using (StreamWriter writer = new StreamWriter(fileStream))
{
string json = JsonUtility.ToJson(playerData, true);
writer.Write(json);
}
}
// Check if we have enough disk space (simplified check)
FileInfo tempFileInfo = new FileInfo(tempFilePath);
DriveInfo driveInfo = new DriveInfo(Path.GetPathRoot(tempFilePath));
if (driveInfo.AvailableFreeSpace < tempFileInfo.Length * 2)
{
throw new IOException("Insufficient disk space to save the game");
}
// Create a backup of the existing save if it exists
if (File.Exists(saveFilePath))
{
string backupFilePath = Path.Combine(saveFolderPath, $"{saveName}.bak");
File.Copy(saveFilePath, backupFilePath, true);
}
// Replace the actual save file with our temporary file
File.Copy(tempFilePath, saveFilePath, true);
File.Delete(tempFilePath);
Console.WriteLine($"Game saved successfully to {saveFilePath}");
}
catch (UnauthorizedAccessException e)
{
Console.WriteLine($"Permission denied: {e.Message}");
throw new SaveSystemException("Could not save game: permission denied", e);
}
catch (IOException e)
{
Console.WriteLine($"IO error: {e.Message}");
throw new SaveSystemException("Could not save game: I/O error", e);
}
catch (Exception e)
{
Console.WriteLine($"Unexpected error: {e.Message}");
throw new SaveSystemException("Could not save game: unexpected error", e);
}
finally
{
// Clean up the temporary file if it still exists
if (File.Exists(tempFilePath))
{
try
{
File.Delete(tempFilePath);
}
catch
{
// Ignore errors in cleanup
}
}
}
}
public PlayerData LoadGame(string saveName)
{
if (string.IsNullOrEmpty(saveName))
{
throw new ArgumentException("Save name cannot be null or empty", nameof(saveName));
}
string saveFilePath = Path.Combine(saveFolderPath, $"{saveName}.json");
string backupFilePath = Path.Combine(saveFolderPath, $"{saveName}.bak");
// Check if the save file exists
if (!File.Exists(saveFilePath))
{
if (File.Exists(backupFilePath))
{
Console.WriteLine($"Main save not found, but backup exists. Restoring from backup.");
File.Copy(backupFilePath, saveFilePath, false);
}
else
{
throw new FileNotFoundException($"Save file not found: {saveName}");
}
}
try
{
string json;
using (FileStream fileStream = new FileStream(saveFilePath, FileMode.Open))
{
using (StreamReader reader = new StreamReader(fileStream))
{
json = reader.ReadToEnd();
}
}
PlayerData playerData;
try
{
playerData = JsonUtility.FromJson<PlayerData>(json);
}
catch (Exception e)
{
// The JSON parsing failed - the save might be corrupted
throw new SaveCorruptedException(saveName, e);
}
// Check if the save version matches the current game version
if (playerData.GameVersion != currentGameVersion)
{
throw new SaveVersionMismatchException(playerData.GameVersion, currentGameVersion);
}
Console.WriteLine($"Game loaded successfully from {saveFilePath}");
return playerData;
}
catch (SaveCorruptedException)
{
// Try to load from backup
if (File.Exists(backupFilePath))
{
Console.WriteLine("Save file corrupted. Attempting to load from backup...");
try
{
string backupJson = File.ReadAllText(backupFilePath);
PlayerData backupData = JsonUtility.FromJson<PlayerData>(backupJson);
// Restore the backup as the main save
File.Copy(backupFilePath, saveFilePath, true);
Console.WriteLine("Successfully loaded from backup.");
return backupData;
}
catch (Exception backupException)
{
Console.WriteLine($"Backup is also corrupted: {backupException.Message}");
throw new SaveCorruptedException(saveName);
}
}
else
{
throw; // Rethrow the original exception
}
}
catch (SaveVersionMismatchException e)
{
Console.WriteLine($"Version mismatch: {e.Message}");
// For this exercise, we'll try to load the save anyway
Console.WriteLine("Attempting to load save from different version...");
string json = File.ReadAllText(saveFilePath);
PlayerData playerData = JsonUtility.FromJson<PlayerData>(json);
// Log a warning but return the data anyway
Console.WriteLine("Save loaded, but some features may not work correctly due to version mismatch.");
return playerData;
}
catch (UnauthorizedAccessException e)
{
Console.WriteLine($"Permission denied: {e.Message}");
throw new SaveSystemException("Could not load game: permission denied", e);
}
catch (IOException e)
{
Console.WriteLine($"IO error: {e.Message}");
throw new SaveSystemException("Could not load game: I/O error", e);
}
catch (Exception e)
{
Console.WriteLine($"Unexpected error: {e.Message}");
throw new SaveSystemException("Could not load game: unexpected error", e);
}
}
public bool DeleteSave(string saveName)
{
if (string.IsNullOrEmpty(saveName))
{
throw new ArgumentException("Save name cannot be null or empty", nameof(saveName));
}
string saveFilePath = Path.Combine(saveFolderPath, $"{saveName}.json");
string backupFilePath = Path.Combine(saveFolderPath, $"{saveName}.bak");
try
{
bool deleted = false;
if (File.Exists(saveFilePath))
{
File.Delete(saveFilePath);
deleted = true;
}
if (File.Exists(backupFilePath))
{
File.Delete(backupFilePath);
deleted = true;
}
return deleted;
}
catch (UnauthorizedAccessException e)
{
Console.WriteLine($"Permission denied: {e.Message}");
throw new SaveSystemException("Could not delete save: permission denied", e);
}
catch (IOException e)
{
Console.WriteLine($"IO error: {e.Message}");
throw new SaveSystemException("Could not delete save: I/O error", e);
}
catch (Exception e)
{
Console.WriteLine($"Unexpected error: {e.Message}");
throw new SaveSystemException("Could not delete save: unexpected error", e);
}
}
public List<string> GetSaveFiles()
{
try
{
List<string> saveFiles = new List<string>();
if (!Directory.Exists(saveFolderPath))
{
return saveFiles; // Return empty list if directory doesn't exist
}
string[] files = Directory.GetFiles(saveFolderPath, "*.json");
foreach (string file in files)
{
saveFiles.Add(Path.GetFileNameWithoutExtension(file));
}
return saveFiles;
}
catch (UnauthorizedAccessException e)
{
Console.WriteLine($"Permission denied: {e.Message}");
throw new SaveSystemException("Could not list save files: permission denied", e);
}
catch (IOException e)
{
Console.WriteLine($"IO error: {e.Message}");
throw new SaveSystemException("Could not list save files: I/O error", e);
}
catch (Exception e)
{
Console.WriteLine($"Unexpected error: {e.Message}");
throw new SaveSystemException("Could not list save files: unexpected error", e);
}
}
}
Step 4: Create a Game Manager to Use the Save System
Now, let's create a GameManager
class that uses our SaveSystem
:
public class GameManager
{
private SaveSystem saveSystem;
private PlayerData currentPlayerData;
private bool isGameLoaded = false;
public GameManager()
{
saveSystem = new SaveSystem("1.0.0");
currentPlayerData = new PlayerData
{
PlayerName = "Player",
Level = 1,
Experience = 0,
Health = 100,
Position = Vector3.zero,
Inventory = new List<string>(),
LastSaved = DateTime.Now,
GameVersion = "1.0.0"
};
}
public void SaveGame(string saveName)
{
try
{
saveSystem.SaveGame(currentPlayerData, saveName);
Console.WriteLine("Game saved successfully!");
}
catch (SaveSystemException e)
{
Console.WriteLine($"Failed to save game: {e.Message}");
// Show error message to the player
}
catch (Exception e)
{
Console.WriteLine($"Unexpected error: {e.Message}");
// Log the error and show a generic message to the player
}
}
public void LoadGame(string saveName)
{
try
{
currentPlayerData = saveSystem.LoadGame(saveName);
isGameLoaded = true;
Console.WriteLine($"Loaded game for {currentPlayerData.PlayerName} (Level {currentPlayerData.Level})");
}
catch (FileNotFoundException)
{
Console.WriteLine($"No save file found with name: {saveName}");
// Show message to the player
}
catch (SaveCorruptedException e)
{
Console.WriteLine($"Save file is corrupted: {e.SaveFileName}");
// Show error message and suggest starting a new game
}
catch (SaveVersionMismatchException e)
{
Console.WriteLine($"Save version ({e.SaveVersion}) doesn't match game version ({e.CurrentVersion})");
// Show warning to the player about potential issues
}
catch (SaveSystemException e)
{
Console.WriteLine($"Error loading game: {e.Message}");
// Show error message to the player
}
catch (Exception e)
{
Console.WriteLine($"Unexpected error: {e.Message}");
// Log the error and show a generic message to the player
}
}
public void DeleteSave(string saveName)
{
try
{
bool deleted = saveSystem.DeleteSave(saveName);
if (deleted)
{
Console.WriteLine($"Save '{saveName}' deleted successfully");
}
else
{
Console.WriteLine($"No save found with name: {saveName}");
}
}
catch (SaveSystemException e)
{
Console.WriteLine($"Failed to delete save: {e.Message}");
// Show error message to the player
}
catch (Exception e)
{
Console.WriteLine($"Unexpected error: {e.Message}");
// Log the error and show a generic message to the player
}
}
public void ListSaves()
{
try
{
List<string> saveFiles = saveSystem.GetSaveFiles();
if (saveFiles.Count == 0)
{
Console.WriteLine("No save files found");
return;
}
Console.WriteLine("Available save files:");
foreach (string save in saveFiles)
{
Console.WriteLine($"- {save}");
}
}
catch (SaveSystemException e)
{
Console.WriteLine($"Failed to list saves: {e.Message}");
// Show error message to the player
}
catch (Exception e)
{
Console.WriteLine($"Unexpected error: {e.Message}");
// Log the error and show a generic message to the player
}
}
// Additional game methods...
}
Step 5: Test the Save System
Finally, let's create a simple program to test our save system:
class Program
{
static void Main(string[] args)
{
GameManager gameManager = new GameManager();
while (true)
{
Console.WriteLine("\n===== Game Save System =====");
Console.WriteLine("1. Save Game");
Console.WriteLine("2. Load Game");
Console.WriteLine("3. Delete Save");
Console.WriteLine("4. List Saves");
Console.WriteLine("5. Exit");
Console.Write("Enter your choice: ");
string choice = Console.ReadLine();
switch (choice)
{
case "1":
Console.Write("Enter save name: ");
string saveName = Console.ReadLine();
gameManager.SaveGame(saveName);
break;
case "2":
Console.Write("Enter save name to load: ");
string loadName = Console.ReadLine();
gameManager.LoadGame(loadName);
break;
case "3":
Console.Write("Enter save name to delete: ");
string deleteName = Console.ReadLine();
gameManager.DeleteSave(deleteName);
break;
case "4":
gameManager.ListSaves();
break;
case "5":
Console.WriteLine("Exiting...");
return;
default:
Console.WriteLine("Invalid choice. Please try again.");
break;
}
}
}
}
Challenge Extensions
Once you've completed the basic exercise, try these extensions:
- Save Encryption: Add encryption to protect save files from tampering
- Compression: Implement save file compression to reduce file size
- Auto-Save: Add an auto-save feature that periodically saves the game
- Cloud Saves: Simulate cloud save functionality with a secondary save location
- Save Versioning: Implement a more sophisticated version compatibility system
Solution
You can find a complete solution to this exercise in the accompanying code files. Remember, there are many ways to implement error handling, and your solution might differ from the provided one while still being correct.
Key Takeaways
Through this exercise, you've practiced:
- Creating and using custom exception types
- Implementing proper exception handling with
try-catch-finally
blocks - Using the
using
statement for resource management - Providing fallback mechanisms for error recovery
- Designing a robust system that can handle various failure scenarios
These error handling skills are essential for creating reliable, user-friendly games that can recover gracefully from unexpected conditions.
Next Steps
Now that you've mastered error handling and exceptions, you're ready to move on to more advanced C# concepts. In the next module, we'll explore delegates, events, lambda expressions, and LINQ, which will give you powerful tools for creating flexible, maintainable game code.