In this lesson, we’ll introduce how to write data from our program to external sources. We’ll focus on writing to files for now, but the techniques we cover lay the foundations for working with other data streams, such as network traffic.
As we might expect, SDL’s mechanism for writing data shares much in common with their API for reading data. We’ll rely on SDL_RWops
objects, and the functions we use will be familiar to what we learned in the previous lesson.
Like before, we’ll start with a basic main
function that initializes SDL, and calls Write()
and Read()
functions within an included File
 namespace.
// main.cpp
#include <SDL.h>
#include "File.h"
int main(int argc, char** argv) {
SDL_Init(0);
File::Write("output.txt");
File::Read("output.txt");
return 0;
}
Our Read()
function logs out the file’s contents, using techniques we covered in the previous lesson. In this lesson, we’ll work on the Write()
function, which is currently empty:
// File.h
#pragma once
#include <iostream>
#include <SDL.h>
namespace File {
void Read(const std::string& Path) {
char* Content{static_cast<char*>(
SDL_LoadFile(Path.c_str(), nullptr)
)};
if (Content) {
std::cout << "Content: " << Content;
} else {
std::cout << "Error loading file: "
<< SDL_GetError();
}
SDL_free(Content);
}
void Write(const std::string& Path) {
// ...
}
}
Running our program, we should see an error output from our Read()
function, as it’s trying to read a file that we haven’t created yet:
Error loading file: Parameter 'src' is invalid
When we’re working on a mouse-based game, creating an engaging user interface typically requires us to change the visual appearance of the cursor.
For example, we might customize the cursor to fit the theme of our game. We may also want to apply further modifications to the cursor based on what the user is pointing at, like changing the cursor to a sword if the pointer is hovering over an enemy. This helps players quickly understand what effect clicking will have.
This lesson will guide you through the various cursor customization options available in SDL2. We’ll cover:
std::string
std::string
and SDL_RWops
When we’re loading text files into our program, we need the ability to programmatically understand that text data. This process is commonly called parsing the data.
In this lesson, we’ll imagine the configuration of our program is provided in a serialized form - perhaps a file or network response - that looks like this:
WINDOW_TITLE: Example Window
WINDOW_WIDTH: 800
WINDOW_HEIGHT: 600
LEVELS: 1, 2, 3, 4
We want to deserialize this payload into C++ content, defined by a struct that looks like this:
struct Config {
std::string WindowTitle;
int WindowWidth;
int WindowHeight;
std::vector<int> Levels;
};
We’ll cover the most important techniques to achieve results like this, using the standard library’s std::string
 type.
So far, we’ve been converting the data we want to serialize into a string-based form. For example, when we’ve serialized a number like 42
, we’ve converted it into a std::string
. Then, when we deserialize that data back into memory, we convert it back to its original int
 form.
This is a useful technique and is applicable to many scenarios, but these conversions incur a performance cost. In situations where performance matters, we’d prefer to avoid this cost and use binary serialization instead.
Binary serialization creates data that represents our objects in the exact same way that type is represented in memory. To fully understand this, we first need to familiarise ourselves with some important concepts that affect how data is stored in memory.
SDL_RWops
object to control stream interactions.This lesson focuses on two essential functions for working with SDL_RWops
: SDL_RWtell()
and SDL_RWseek()
.
SDL_RWtell()
provides the current read/write offsetSDL_RWseek()
lets you modify itThis gives us fine-grained control over stream interactions. We'll examine how these functions work together and apply them to a practical example of managing a game's high score.
In our earlier lessons where we covered mouse events and mouse tracking, we were primarily concerned with what the mouse is doing whilst it is hovering over our windows. That is, when one of our windows has mouse focus.
However, in certain scenarios, we may need to understand what’s going on with the mouse even when we don’t have mouse focus. This lesson covers the techniques we can use to accomplish this, and some of the scenarios where we may need to.
As we’ve seen, when we create variables and instantiate classes in C++, the data in these objects are stored in a block of memory, which our program can freely read and update. To build more advanced features, we need the ability to convert those memory representations into other forms. This process is typically called serialization.
The output of this serialization can be used in many ways - we might store it on our player’s hard drive to use later, send it to some other program, or send it to some other computer over the internet. When a program later uses that serialized data, it will need to convert back into an understandable form in memory. This process is called deserialization.
In this lesson, we’ll give a brief introduction to why serialization and deserialization are important, and the types of capabilities that they unlock. Through the rest of the chapter, we’ll walk through how to implement these techniques with C++ and SDL, and the things we need to consider when doing so.
We’ll then use these principles to create a basic save and reload system from scratch. Finally, we’ll introduce a free, open-source library that applies these concepts to provide a feature-complete solution, allowing us to quickly add these capabilities to much more complex projects.
Previously, we’ve focused on serializing single bytes of data at a time - usually the 8-bit char
type. However, when we start to serialize multi-byte objects, such as int
and float
values, things can get slightly more complex.
We need to pay careful attention to the order of those bytes, especially when working across different systems.
In this lesson, we'll explore how computers store multi-byte values, understand the challenges of different byte orderings, and learn practical techniques for handling these differences using SDL's binary manipulation functions.
When we write code, we often think about memory as a simple sequence of bytes. However, modern processors work with memory in larger chunks for efficiency. Two key concepts drive this behavior: cache lines and memory pages.
Cache lines, typically 64 bytes, are the smallest unit of data that can be transferred between the CPU cache and main memory. Similarly, memory pages are the smallest unit managed by the operating system's virtual memory system.
For optimal performance, we typically want our data to be aligned to minimise the frequency with which a single value crosses one of these boundaries. An example of a boundary cross might be a 4-byte integer where it’s first two bytes are at the end of one cache line, and the last two bytes are at the start of the next.
The boundary between our two cache lines might look like the following, where X
represents the integer we’re interested in, and A
and B
represent other arbitrary variables:
Line 1 | Line 2
A A X X | X X B B
Our systems can typically handle this - it can perform multiple reads to grab both blocks of memory, then take the appropriate bytes from each and combine them to to reconstruct our integer X
. However, this comes at a performance cost. Instead, we want to align our data to maximise the chances that it is stored entirely within the same cache line or page, eliminating the need for this additional processing.
In this lesson, we'll explore SDL's window management features, learning how to implement both desktop and exclusive fullscreen modes. We’ll cover:
SDL_WINDOW_FULLSCREEN_DESKTOP
SDL_WINDOW_FULLSCREEN
SDL_SetWindowFullscreen()
lets you change modes at runtime