Game Programming Using Qt Beginner's Guide
上QQ阅读APP看书,第一时间看更新

Data storage

When implementing games, you will often have to work with persistent data—you will need to store the saved game data, load maps, and so on. For that, you have to learn about the mechanisms that let you use the data stored on digital media.

Files and devices

The most basic and low-level mechanism that is used to access data is to save and load it from the files. While you can use the classic file access approaches provided by C and C++, such as stdio or iostream, Qt provides its own wrapper over the file abstraction that hides platform-dependent details and provides a clean API that works across all platforms in a uniform manner.

The two basic classes that you will work with when using files are QDir and QFile. The former represents the contents of a directory, lets you traverse filesystems, creates and remove directories, and finally, access all files in a particular directory.

Traversing directories

Traversing directories with QDir is really easy. The first thing to do is to have an instance of QDir in the first place. The easiest way to do this is to pass the directory path to the QDir constructor.

Tip

Qt handles file paths in a platform-independent way. Even though the regular directory separator on Windows is a backwards slash character (\) and on other platforms it is the forward slash (/), Qt accepts forward slash as a directory separator on Windows platforms as well. Therefore, you can always use / to separate directories when you pass paths to Qt functions.

You can learn the native directory separator for the current platform is by calling the QDir::separator()static function. You can transform between native and non-native separators with the QDir::toNativeSeparators() and QDir::fromNativeSeparators()functions.

Qt provides a number of static methods to access some special directories. The following table lists these special directories and functions that access them:

When you already have a valid QDir object, you can start moving between directories. To do that, you can use the cd() and cdUp() methods. The former moves to the named subdirectory, while the latter moves to the parent directory.

To list files and subdirectories in a particular directory, you can use the entryList() method, which returns a list of entries in the directory that match the criteria passed to entryList(). This method has two overloads. The basic version takes a list of flags that correspond to the different attributes that an entry needs to have to be included in the result and a set of flags that determine the order in which entries are included in the set. The other overload also accepts a list of file name patterns in the form of QStringList as its first parameter. The most commonly used filter and sort flags are listed as follows:

Here is an example call that returns all JPEG files in the user's home directory sorted by size:

QDir dir = QDir::home();
QStringList nameFilters;
nameFilters << QStringLiteral("*.jpg") << QStringLiteral("*.jpeg");
QStringList entries = dir.entryList(nameFilters,
                      QDir::Files|QDir::Readable, QDir::Size);

Tip

The << operator is a nice and fast way to append entries to QStringList.

Getting access to the basic file

Once you know the path to a file (either by using QDir::entryList(), from some external source, or even by hardcoding the file path in code), you can pass it to QFile to receive an object that acts as a handle to the file. Before the file contents can be accessed, the file needs to be opened using the open() method. The basic variant of this method takes a mode in which we need to open the file. The following table explains the modes that are available:

The open() method returns true or false depending on whether the file was opened or not. The current status of the file can be checked by calling isOpen() on the file object. Once the file is open, it can be read from or written to depending on the options that are passed when the file is opened. Reading and writing is done using the read() and write() methods. These methods have a number of overloads, but I suggest that you focus on using those variants that accept or return a QByteArray object, which is essentially a series of bytes—it can hold both textual and nontextual data. If you are working with plain text, then a useful overload for write is the one that accepts the text directly as input. Just remember that the text has to be null or terminated. When reading from a file, Qt offers a number of other methods that might come in handy in some situations. One of these methods is readLine(), which tries to read from the file until it encounters a new line character. If you use it together with the atEnd() method that tells you whether you have reached the end of the file, you can realize the line-by-line reading of a text file:

QStringList lines;
while(!file.atEnd()) {
  QByteArray line = file.readLine();
  lines.append(QString::fromUtf8(line));
}

Another useful method is readAll(), which simply returns the file content, starting from the current position of the file pointer until the end of the file.

You have to remember though that when using these helper methods, you should be really careful if you don't know how much data the file contains. It might happen that when reading line by line or trying to read the whole file into memory in one step, you exhaust the amount of memory that is available for your process (you can check the size of the file by calling size() on the QFile instance). Instead, you should process the file's data in steps, reading only as much as you require at a time. This makes the code more complex but allows us to better manage the available resources. If you require constant access to some part of the file, you can use the map() and unmap() calls that add and remove mappings of the parts of a file to a memory address that you can then use like a regular array of bytes:

QFile f("myfile");
if(!f.open(QFile::ReadWrite)) return;
uchar *addr = f.map(0, f.size());
if(!addr) return;
f.close();
doSomeComplexOperationOn(addr);
f.unmap(addr);

Devices

QFile is really a descendant class of QIODevice, which is a Qt interface that is used to abstract entities related to reading and writing. There are two types of devices: sequential and random access devices. QFile belongs to the latter group—it has the concepts of start, end, size, and current position that can be changed by the user with the seek() method. Sequential devices, such as sockets and pipes, represent streams of data—there is no way to rewind the stream or check its size; you can only keep reading the data sequentially—piece by piece, and you can check how far away you currently are from the end of data.

All I/O devices can be opened and closed. They all implement open(), read(), and write() interfaces. Writing to the device queues the data for writing; when the data is actually written, the bytesWritten() signal is emitted that carries the amount of data that was written to the device. If more data becomes available in the sequential device, it emits the readyRead() signal, which informs you that if you call read now, you can expect to receive some data from the device.