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.
Behind the scenes, SDL_RWops
objects maintain a simple integer value that gets updated as we perform read and write operations. This value is sometimes called the read/write position, cursor, or offset.
Its purpose is to keep track of the data we’ve already read, or already written. For example, the following program is reading a file containing 10 characters - HelloWorld
.
When we read 5 characters using SDL_RWread()
, the internal offset of the RWOps
object we’re using is updated, such that the next read begins where we left off:
// content.txt
HelloWorld
#include <SDL.h>
#include <iostream>
int main(int, char**) {
SDL_RWops* File{SDL_RWFromFile(
"content.txt", "rb")};
// 5 bytes + 1 for null terminator
char Content[6];
Content[5] = '\0';
SDL_RWread(File, Content, 1, 5);
std::cout << "First Read: " << Content;
SDL_RWread(File, Content, 1, 5);
std::cout << "\nSecond Read: " << Content;
SDL_RWclose(File);
return 0;
}
First Read: Hello
Second Read: World
SDL_RWtell()
We can retrieve the current offset of an SDL_RWops
object by passing it to the SDL_RWtell()
function. When we first create an SDL_RWops
, we expect its offset to be 0
, but this will change as we interact with the stream:
#include <SDL.h>
#include <iostream>
int main(int, char**) {
SDL_RWops* File{SDL_RWFromFile(
"content.txt", "rb")};
char Content[6];
Content[5] = '\0';
std::cout << "Read/Write Offset: "
<< SDL_RWtell(File);
SDL_RWread(File, Content, 1, 5);
std::cout << "\nFirst Read: " << Content;
std::cout << "\nRead/Write Offset: "
<< SDL_RWtell(File);
SDL_RWread(File, Content, 1, 5);
std::cout << "\nSecond Read: " << Content;
std::cout << "\nRead/Write Offset: "
<< SDL_RWtell(File);
SDL_RWclose(File);
return 0;
}
Read/Write Offset: 0
First Read: Hello
Read/Write Offset: 5
Second Read: World
Read/Write Offset: 10
The SDL_RWtell()
function can sometimes fail. When this happens, it will return a negative error code. We can check for this outcome and react to it as needed. We can also call SDL_GetError()
for more information on what went wrong:
if (SDL_RWtell(File) < 0) {
std::cout << "Couldn't determine read/write "
"offset: " << SDL_GetError();
}
SDL_RWseek()
We can manipulate the read/write offset of an SDL_RWops
object using the SDL_RWseek()
function. This function requires three arguments:
SDL_RWops
object whose read/write offset we want to updateThe second and third arguments work together to determine where the offset should be after our call to SDL_RWseek()
. The possible values for the third argument are:
RW_SEEK_SET
: Our seek value sets the offset relative to the start of the stream. We should provide a positive seek value in this context.RW_SEEK_CUR
: Our seek value moves the offset relative to the current position of the offset. Our seek value can be positive or negative in this context.RW_SEEK_END
: Our seek value sets the offset relative to the end of the stream. Our seek value should be negative in this context.Here’s an example:
#include <SDL.h>
#include <iostream>
int main(int, char**) {
SDL_RWops* File{SDL_RWFromFile(
"content.txt", "rb")};
char Content[6];
Content[5] = '\0';
std::cout << "Read/Write Offset: "
<< SDL_RWtell(File);
SDL_RWseek(File, 5, RW_SEEK_SET);
std::cout << "\nRead/Write Offset: "
<< SDL_RWtell(File);
SDL_RWread(File, Content, 1, 5);
std::cout << "\nFirst Read: " << Content;
SDL_RWclose(File);
return 0;
}
Read/Write Offset: 0
Read/Write Offset: 5
First Read: World
The SDL_RWseek()
function returns the new offset of the SDL_RWops
object, relative to the beginning of the stream:
#include <SDL.h>
#include <iostream>
int main(int, char**) {
SDL_RWops* File{SDL_RWFromFile(
"content.txt", "rb")};
std::cout << "Read/Write Offset: "
<< SDL_RWtell(File);
std::cout << "\nRead/Write Offset: "
<< SDL_RWseek(File, -3, RW_SEEK_END);
SDL_RWclose(File);
return 0;
}
Read/Write Offset: 0
Read/Write Offset: 7
The SDL_RWseek()
function can sometimes fail. When this happens, it will return -1
. We can check for this outcome and react to it as needed. We can also call SDL_GetError()
for more information on what went wrong:
#include <SDL.h>
#include <iostream>
int main(int, char**) {
SDL_RWops* File{SDL_RWFromFile(
"content.txt", "rb")};
if (SDL_RWseek(File, -5, RW_SEEK_SET) < 0) {
std::cout << "Error seeking: "
<< SDL_GetError();
}
SDL_RWclose(File);
return 0;
}
Error seeking: windows_file_seek: An attempt was made to move the file pointer before the beginning of the file.
In this section, we’ll create a simple example where we keep track of the player’s highest score as a 32-bit integer serialized in a highscore.dat
file on their hard drive.
Every time the player gets a new score, we’ll compare it to the score in that file and, if it’s higher, we’ll update the file.
Let’s handle the initial case first. The first time we call UpdateHighScore()
, the highscore.dat
file will not exist. So, we’ll call CreateAndInitializeFile()
to create it, and initialize it with the score the player received:
#include <SDL.h>
#include <iostream>
void CreateAndInitializeFile(
const char* Path, int32_t InitialScore
) {
SDL_RWops* File{SDL_RWFromFile(Path, "w+b")};
if (!File) {
// Handle errors
return;
}
SDL_RWwrite(File, &InitialScore, sizeof(int32_t), 1);
SDL_RWclose(File);
}
void UpdateHighScore(
const char* Path, int32_t NewScore
) {
SDL_RWops* File{SDL_RWFromFile(Path, "r+b")};
if (!File) {
CreateAndInitializeFile(Path, NewScore);
return;
}
// TODO
SDL_RWclose(File);
}
int main(int, char**) {
UpdateHighScore("highscore.dat", 5000);
return 0;
}
On subsequent calls to UpdateHighScore()
, there will be a highscore.dat
file available. We’ll open that file for reading and writing, and retrieve the current high score.
If our new score is higher than what is currently in the file, we’ll rewind our read/write offset to the beginning of the file using SDL_RWseek()
, and overwrite that integer with our higher score:
#include <SDL.h>
#include <iostream>
void CreateAndInitializeFile(
const char* Path, int32_t InitialScore
) {
SDL_RWops* File{SDL_RWFromFile(Path, "w+b")};
if (!File) {
return;
}
SDL_RWwrite(File, &InitialScore, sizeof(int32_t), 1);
SDL_RWclose(File);
}
void UpdateHighScore(
const char* Path, int32_t NewScore
) {
SDL_RWops* File{SDL_RWFromFile(Path, "r+b")};
if (!File) {
CreateAndInitializeFile(Path, NewScore);
return;
}
int32_t CurrentScore{0};
SDL_RWread(File, &CurrentScore, sizeof(int32_t), 1);
std::cout << "Current High Score: "
<< CurrentScore << "\n";
if (NewScore > CurrentScore) {
std::cout << NewScore << " is a new high score!"
" Updating file\n";
SDL_RWseek(File, 0, RW_SEEK_SET);
SDL_RWwrite(File, &NewScore, sizeof(int32_t), 1);
}
SDL_RWclose(File);
}
int main(int, char**) {
// New high score
UpdateHighScore("highscore.dat", 5500);
return 0;
}
Current High Score: 5000
5500 is a new high Score! Updating file
The examples in this chapter have focused on slow, manual implementations of serialization and deserialization so we can build a deeper understanding of what is going on.
In larger projects, we typically want to make it as easy as possible to add serialization and deserialization capabilities to the classes and structs we define. Additionally, we have to deal with more complex situations that include:
Character
with a Weapon*
member?Fortunately, these are largely solved problems. We have a wide range of open-source serialization libraries that we can add to our project for free.
Many of these libraries, like Cereal, also include support for serializing standard library types like std::string
and std::vector
, further reducing the amount of code we need to write. We cover Cereal in detail in our advanced course:
In this lesson, we explored the vital concept of the read/write offset in SDL_RWops
, learning how to use SDL_RWtell()
to get the current position and SDL_RWseek()
to move to different locations within a stream. Here are the key points:
SDL_RWops
stream.SDL_RWtell()
returns the current offset.SDL_RWseek()
allows you to move the offset to a specific location.RW_SEEK_SET
, RW_SEEK_CUR
, and RW_SEEK_END
define the anchor for seeking.SDL_RWtell()
and SDL_RWseek()
.Learn how to manipulate the read/write offset of an SDL_RWops
object to control stream interactions.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games