Skip to main content

9.1 - System.IO Namespace

In game development, you'll frequently need to interact with the file system to save player progress, load configuration settings, import custom assets, or export game data. The System.IO namespace in C# provides a comprehensive set of classes for working with files, directories, and various types of data streams.

Introduction to System.IO

The System.IO namespace is part of the .NET Framework's Base Class Library and contains types that enable reading and writing to files and data streams, as well as types that provide basic file and directory support.

To use the System.IO namespace, you need to include it at the top of your C# file:

using System.IO;

Key Classes in System.IO

The System.IO namespace contains many useful classes, but here are the most important ones you'll use in game development:

File and Directory Management

ClassPurposeCommon Use in Games
FileStatic methods for creating, copying, deleting, moving, and opening filesSaving/loading game data, checking if save files exist
DirectoryStatic methods for creating, moving, and enumerating through directories and subdirectoriesManaging game assets, organizing save files
PathMethods and properties for processing directory stringsBuilding cross-platform file paths
FileInfoInstance methods for creating, copying, deleting, moving, and opening filesWorking with specific files repeatedly
DirectoryInfoInstance methods for creating, moving, and enumerating through directories and subdirectoriesWorking with specific directories repeatedly

Stream Classes

ClassPurposeCommon Use in Games
StreamAbstract base class for all streamsBase class for custom stream implementations
FileStreamReading from and writing to filesDirect file access for binary data
MemoryStreamReading from and writing to memoryTemporary data storage, manipulating data in memory
BufferedStreamAdds buffering to streamsImproving performance when reading/writing large files
StreamReaderReading characters from a streamReading text files (e.g., configuration files)
StreamWriterWriting characters to a streamWriting text files (e.g., log files)
BinaryReaderReading primitive data types from a streamReading custom binary formats
BinaryWriterWriting primitive data types to a streamWriting custom binary formats

Working with Files

The File class provides static methods for file operations. Here are some of the most commonly used methods:

Checking if a File Exists

string savePath = "playerData.sav";

if (File.Exists(savePath))
{
Console.WriteLine("Save file found!");
}
else
{
Console.WriteLine("No save file found. Creating a new game.");
}

Creating a New File

// Creates a new file and opens it for writing
// If the file already exists, it will be overwritten
using (FileStream fs = File.Create("gameSettings.cfg"))
{
// We'll learn how to write to this file in the next section
}

The using statement ensures that the file is properly closed even if an exception occurs. We'll explore this more in the next section.

Copying, Moving, and Deleting Files

// Copy a file (overwrite if destination exists)
File.Copy("originalSave.sav", "backupSave.sav", true);

// Move/rename a file
File.Move("tempScores.dat", "finalScores.dat");

// Delete a file
File.Delete("oldLog.txt");

Getting File Information

// Get creation time
DateTime creationTime = File.GetCreationTime("playerData.sav");
Console.WriteLine($"Save created on: {creationTime}");

// Get last write time (useful for checking when a file was last modified)
DateTime lastModified = File.GetLastWriteTime("playerData.sav");
Console.WriteLine($"Last saved on: {lastModified}");

// Get file size in bytes
long fileSize = new FileInfo("levelData.dat").Length;
Console.WriteLine($"Level data size: {fileSize} bytes");

Working with Directories

The Directory class provides static methods for directory operations:

Checking if a Directory Exists

string savesDirectory = "Saves";

if (!Directory.Exists(savesDirectory))
{
// Create the directory if it doesn't exist
Directory.CreateDirectory(savesDirectory);
Console.WriteLine("Created saves directory.");
}

Getting Files in a Directory

// Get all .sav files in the Saves directory
string[] saveFiles = Directory.GetFiles("Saves", "*.sav");

Console.WriteLine("Available save files:");
foreach (string file in saveFiles)
{
// Get just the filename without the path
string fileName = Path.GetFileName(file);
Console.WriteLine(fileName);
}

Getting Subdirectories

// Get all subdirectories in the current directory
string[] subDirs = Directory.GetDirectories(".");

Console.WriteLine("Subdirectories:");
foreach (string dir in subDirs)
{
Console.WriteLine(dir);
}

The Path Class

The Path class is extremely useful for working with file and directory paths in a cross-platform way. It handles differences between operating systems (like different path separators) automatically.

Combining Paths

// Combines paths correctly regardless of whether the base path ends with a separator
string savesFolder = "GameData/Saves";
string fileName = "player1.sav";
string fullPath = Path.Combine(savesFolder, fileName);

Console.WriteLine(fullPath); // Outputs: GameData/Saves/player1.sav

Getting File Information from Paths

string filePath = "GameData/Saves/player1.sav";

// Get just the filename with extension
string fileName = Path.GetFileName(filePath); // player1.sav

// Get filename without extension
string fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath); // player1

// Get just the extension
string extension = Path.GetExtension(filePath); // .sav

// Get directory name
string directory = Path.GetDirectoryName(filePath); // GameData/Saves

Working with Absolute and Relative Paths

// Get the current directory
string currentDir = Directory.GetCurrentDirectory();
Console.WriteLine($"Current directory: {currentDir}");

// Convert a relative path to an absolute path
string relativePath = "Saves/player1.sav";
string absolutePath = Path.GetFullPath(relativePath);
Console.WriteLine($"Absolute path: {absolutePath}");

FileInfo and DirectoryInfo Classes

While the File and Directory classes provide static methods for one-time operations, the FileInfo and DirectoryInfo classes are better for performing multiple operations on the same file or directory:

// Create a FileInfo object
FileInfo saveFile = new FileInfo("playerData.sav");

// Check if the file exists
if (saveFile.Exists)
{
Console.WriteLine($"File size: {saveFile.Length} bytes");
Console.WriteLine($"Last modified: {saveFile.LastWriteTime}");

// Create a copy
saveFile.CopyTo("playerData_backup.sav", true);
}

// Create a DirectoryInfo object
DirectoryInfo savesDir = new DirectoryInfo("Saves");

// Check if the directory exists
if (savesDir.Exists)
{
// Get all .sav files
FileInfo[] saveFiles = savesDir.GetFiles("*.sav");

Console.WriteLine($"Found {saveFiles.Length} save files:");
foreach (FileInfo file in saveFiles)
{
Console.WriteLine($"{file.Name} - {file.Length} bytes");
}
}

Practical Example: Game Configuration System

Let's create a simple game configuration system that reads and writes settings to a file:

public class GameConfig
{
// Game settings
public bool FullScreen { get; set; } = true;
public int MusicVolume { get; set; } = 80;
public int SfxVolume { get; set; } = 100;
public string Difficulty { get; set; } = "Normal";

// File path for the config
private readonly string configPath = "gameConfig.txt";

// Check if config file exists
public bool ConfigExists()
{
return File.Exists(configPath);
}

// Save settings to file
public void SaveConfig()
{
try
{
// Create a string with each setting on a new line
string configData =
$"FullScreen={FullScreen}\n" +
$"MusicVolume={MusicVolume}\n" +
$"SfxVolume={SfxVolume}\n" +
$"Difficulty={Difficulty}";

// Write the string to the file
File.WriteAllText(configPath, configData);
Console.WriteLine("Game configuration saved successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"Error saving configuration: {ex.Message}");
}
}

// Load settings from file
public void LoadConfig()
{
try
{
if (!ConfigExists())
{
Console.WriteLine("No configuration file found. Using default settings.");
return;
}

// Read all lines from the file
string[] lines = File.ReadAllLines(configPath);

// Process each line
foreach (string line in lines)
{
// Split the line into key and value
string[] parts = line.Split('=');
if (parts.Length != 2) continue;

string key = parts[0].Trim();
string value = parts[1].Trim();

// Set the appropriate property based on the key
switch (key)
{
case "FullScreen":
FullScreen = bool.Parse(value);
break;
case "MusicVolume":
MusicVolume = int.Parse(value);
break;
case "SfxVolume":
SfxVolume = int.Parse(value);
break;
case "Difficulty":
Difficulty = value;
break;
}
}

Console.WriteLine("Game configuration loaded successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"Error loading configuration: {ex.Message}");
Console.WriteLine("Using default settings.");
}
}

// Display current settings
public void DisplaySettings()
{
Console.WriteLine("Current Game Settings:");
Console.WriteLine($"- Full Screen: {FullScreen}");
Console.WriteLine($"- Music Volume: {MusicVolume}%");
Console.WriteLine($"- SFX Volume: {SfxVolume}%");
Console.WriteLine($"- Difficulty: {Difficulty}");
}
}

// Usage example
public class Program
{
public static void Main()
{
GameConfig config = new GameConfig();

// Try to load existing config
config.LoadConfig();

// Display current settings
config.DisplaySettings();

// Change some settings
config.MusicVolume = 60;
config.Difficulty = "Hard";

// Save the updated config
config.SaveConfig();

Console.WriteLine("\nSettings updated!");
config.DisplaySettings();
}
}

This example demonstrates how to:

  1. Check if a configuration file exists
  2. Read settings from a file
  3. Parse the settings into appropriate data types
  4. Modify settings
  5. Save the updated settings back to the file

System.IO in Unity

Unity Relevance

In Unity, you'll often use System.IO for:

  1. Save Systems: Saving and loading player progress, high scores, and game state
  2. Configuration: Reading and writing game settings
  3. Custom Asset Importing: Reading custom data formats
  4. Debug Logging: Writing debug information to log files
  5. User-Generated Content: Saving player-created levels or characters

However, Unity provides its own file system API through Application.persistentDataPath and Application.dataPath to handle platform-specific file locations. When working with files in Unity, you should typically use these paths as the base directory for your file operations.

// Example of using Unity's paths with System.IO
string savePath = Path.Combine(Application.persistentDataPath, "saves", "playerData.sav");
Directory.CreateDirectory(Path.GetDirectoryName(savePath)); // Ensure directory exists
File.WriteAllText(savePath, "Player save data goes here");

Best Practices for File I/O

  1. Always use exception handling when working with files, as many things can go wrong (disk full, file locked, insufficient permissions, etc.)

  2. Use the using statement with disposable resources like streams to ensure they're properly closed even if exceptions occur

  3. Check if files exist before trying to read from them

  4. Create directories if they don't exist before writing files to them

  5. Use Path.Combine() instead of string concatenation to create file paths

  6. Avoid hardcoding absolute paths to make your code more portable

  7. Consider file access permissions, especially on different platforms

  8. Be mindful of file locking - don't keep files open longer than necessary

  9. Implement a backup system for important save data

  10. Use asynchronous I/O methods for large files to avoid freezing your game

Conclusion

The System.IO namespace provides powerful tools for working with files and directories in C#. In game development, these capabilities are essential for creating save systems, managing configuration settings, and handling user-generated content.

In the next section, we'll dive deeper into reading and writing different types of files, focusing on techniques specifically useful for game development.