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
Class | Purpose | Common Use in Games |
---|---|---|
File | Static methods for creating, copying, deleting, moving, and opening files | Saving/loading game data, checking if save files exist |
Directory | Static methods for creating, moving, and enumerating through directories and subdirectories | Managing game assets, organizing save files |
Path | Methods and properties for processing directory strings | Building cross-platform file paths |
FileInfo | Instance methods for creating, copying, deleting, moving, and opening files | Working with specific files repeatedly |
DirectoryInfo | Instance methods for creating, moving, and enumerating through directories and subdirectories | Working with specific directories repeatedly |
Stream Classes
Class | Purpose | Common Use in Games |
---|---|---|
Stream | Abstract base class for all streams | Base class for custom stream implementations |
FileStream | Reading from and writing to files | Direct file access for binary data |
MemoryStream | Reading from and writing to memory | Temporary data storage, manipulating data in memory |
BufferedStream | Adds buffering to streams | Improving performance when reading/writing large files |
StreamReader | Reading characters from a stream | Reading text files (e.g., configuration files) |
StreamWriter | Writing characters to a stream | Writing text files (e.g., log files) |
BinaryReader | Reading primitive data types from a stream | Reading custom binary formats |
BinaryWriter | Writing primitive data types to a stream | Writing 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:
- Check if a configuration file exists
- Read settings from a file
- Parse the settings into appropriate data types
- Modify settings
- Save the updated settings back to the file
System.IO in Unity
In Unity, you'll often use System.IO
for:
- Save Systems: Saving and loading player progress, high scores, and game state
- Configuration: Reading and writing game settings
- Custom Asset Importing: Reading custom data formats
- Debug Logging: Writing debug information to log files
- 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
-
Always use exception handling when working with files, as many things can go wrong (disk full, file locked, insufficient permissions, etc.)
-
Use the
using
statement with disposable resources like streams to ensure they're properly closed even if exceptions occur -
Check if files exist before trying to read from them
-
Create directories if they don't exist before writing files to them
-
Use
Path.Combine()
instead of string concatenation to create file paths -
Avoid hardcoding absolute paths to make your code more portable
-
Consider file access permissions, especially on different platforms
-
Be mindful of file locking - don't keep files open longer than necessary
-
Implement a backup system for important save data
-
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.