Welcome back to your capstone project. In the last lesson, you brought your game world to life by implementing the take and drop commands. Objects are no longer static decorations; they move through the world, changing their relationships to rooms and to the player. This dynamism is the essence of an adventure game, but it introduces a new, critical challenge: how do we preserve the player's progress?
This lesson tackles that very problem. We are moving into the domain of persistence, where you will learn how to save the entire game state to a file. This isn't just about writing text; it's about systematically converting the complex, interconnected web of data structures in your game's memory into a format that can be stored on disk and perfectly reconstructed later.
Our focus today is on the save part of this process. You will design a binary file format and write the C functions to serialize your game's object graph—the player, the rooms, and all the items with their current locations—into that format.
The Challenge of Persistence: What to Save?
Imagine a complex game world like the one depicted below. Every character, item, and environmental feature has a state and a location. Losing all that progress when the player quits would be unacceptable.

In complex games, the state of the world is constantly changing. A robust save system is essential to preserve the player's unique journey.
The core task of saving a game is called serialization: the process of translating a data structure into a format (typically a sequence of bytes) that can be stored or transmitted. When we load the game, we will perform deserialization to reconstruct the original data structure from that byte stream.
We'll be using binary files for this, primarily for efficiency. Compared to text files, binary files are more compact and can be read and written more quickly, as they represent data in a way that's closer to how it's stored in memory.
However, there's a major hurdle. We can't just use fwrite to dump our entire objects array and all the structs to a file. Why? Pointers.
Your game state is held together by pointers. An object's location is a pointer. The links in your PATH struct are pointers. These pointers store memory addresses, like 0x7ffee5a9b0a8. These addresses are temporary; they are only valid for a single run of the program. When you close the game and restart it, the operating system will assign your data completely different memory addresses. Saving a raw pointer value is therefore useless—it will point to garbage when you load it back.
This is the central problem we must solve: how do you save the relationships represented by pointers, without saving the pointers themselves?
Designing a Robust Save File Format
Since we can't save memory addresses, we must save information that allows us to reconstruct the pointers upon loading. This requires us to design a file format. A well-designed format is like a blueprint for your data. A professional approach involves several key ideas, which are brilliantly summarized in a forum post on GameDev.net.
Serialization libraries in C - Forums
This forum discussion offers a masterclass in practical C serialization. We will model our approach on the concepts presented here.
First, read the conceptual overview from the user 'Khatharr' in this section. Absorb the high-level strategies: File Header: Using a fourcc (a four-character code) as a "magic number" to identify your file type, followed by a version number. This makes your save system robust and forward-compatible. Fixed-length vs. Variable-length Data: Understanding the difference between structs containing only numbers (Plain Old Data or POD) and those containing pointers to variable-length data like strings. Strategies for Variable Data: Prefixing each piece of variable data (like a string) with its length. Next, carefully study the complete code example that follows. This is the blueprint for our implementation. Pay close attention to: The SaveHeader and VersionNumber structs. The saveString and loadString functions. Notice how saveString first writes the string's length (unsigned int len), then the character data. The saveParty function. It writes the fixed-size part of the Party struct first, then handles the dynamic arrays (inventory and members). It knows how many items to write because the counts (itemCt, memberCt) were part of the fixed-size data.
This approach gives us a clear plan:
- Define a Header: We'll start our file with a
SaveHeadercontaining afourcclike'ADVC'(for "Adventure C") and a version number. - Save Counts First: Before writing a list of things (like our game objects), we first write an integer telling us how many there are.
- Serialize by Attribute: For each object, instead of trying to write the
OBJECTstruct directly, we will write its fundamental attributes one by one. - Solve the Pointer Problem: To save the
locationpointer, we won't write the address. Instead, we'll write the uniquetagof the object it points to. When we load the game, we can find the object with that tag and reconstruct the pointer to its new memory address.
The Simpler Case: A Linked List
To see a different, simpler take on the pointer problem, let's consider how you would save a basic linked list. The video below tackles this exact scenario. The author's solution is different from ours but instructive.
Beginning C Programming - Part 58 - Read/Write Linked Lists to File
This video from Bluefever Software demonstrates writing a doubly linked list to a file.
Watch from the beginning of the problem description at the setup, where the author explains the issue with saving the next pointer. Then, skim through the implementation from the writing function. The key technique is on display: he iterates through the list, and for each node, he temporarily sets the next and prev pointers to NULL, writes the modified struct to the file using fwrite, and then immediately restores the pointers to keep the in-memory list intact. This works because the nodes are written to the file in their natural sequence. The loading process (which is also in the video but is a topic for our next lesson) can then read them in the same order and link them back together. Our game's object graph is more complex than a simple list, which is why our "save by tag" strategy is more suitable, but this video clearly illustrates the core problem of handling pointers during serialization.
Your Implementation Task: The saveGame Function
Your goal is to implement a saveGame function that serializes the entire state of your game world to a binary file named save.bin.
1. Create save.c and save.h
-
Create these new files and add them to your
Makefile. -
In
save.h, define your file header structs and declare thesaveGamefunction.// In save.h #pragma pack(1) // Ensure structs are packed tightly in the file typedef struct { unsigned short major; unsigned short minor; } VersionNumber; const VersionNumber VERSION = { 1, 0 }; typedef struct { unsigned int fourcc; VersionNumber ver; } SaveHeader; #pragma pack() // End packing void saveGame(const char *filename);Note: The
#pragma pack(1)directive tells the compiler not to add extra padding bytes within the structs. This is important to ensure the file layout is identical across different compilers and platforms.
2. Implement the saveString Helper
-
In
save.c, create a helper function to serialize a string by first writing its length, then its contents. This is essential for handling all the strings in yourOBJECTstruct.// In save.c #include <stdio.h> #include <string.h> #include "object.h" // Or wherever your objects array is declared #include "save.h" static void saveString(const char *str, FILE *fp) { unsigned int len = str ? strlen(str) : 0; fwrite(&len, sizeof(unsigned int), 1, fp); if (len > 0) { fwrite(str, sizeof(char), len, fp); } }
3. Implement the saveGame Function
-
This is the main function. It will orchestrate the entire save process.
// In save.c void saveGame(const char *filename) { FILE *fp = fopen(filename, "wb"); if (!fp) { perror("fopen"); return; } // 1. Write the header SaveHeader header; header.fourcc = 'ADVC'; // Or your own magic number memcpy(&header.ver, &VERSION, sizeof(header.ver)); fwrite(&header, sizeof(SaveHeader), 1, fp); // 2. Write the number of objects int numObjects = 0; // Assuming your objects are in a fixed-size array terminated by a NULL description or tag for (numObjects = 0; objects[numObjects].description != NULL; numObjects++); fwrite(&numObjects, sizeof(int), 1, fp); // 3. Loop and serialize each object's attributes for (int i = 0; i < numObjects; i++) { OBJECT *obj = &objects[i]; saveString(obj->tag, fp); saveString(obj->description, fp); saveString(obj->details, fp); // This is the key: save the location by its tag! saveString(obj->location ? obj->location->tag : "", fp); } fclose(fp); printf("Game state saved to %s.\n", filename); }
4. Integrate with the Parser
- Finally, add a
savecommand to your game. In the file where you define yourCOMMANDarray (e.g.,main.corparsexec.c):- Include
save.h. - Create a simple
executeSavefunction:bool executeSave(void) { saveGame("save.bin"); return true; } - Add the command to your
commandsarray:static const COMMAND commands[] = { /* ... your other commands ... */ { "save", executeSave }, { NULL, NULL } };
- Include
Compile and run your game. Use the take and drop commands to change the location of a few items, then type save. You should see a save.bin file appear. While you can't easily read its contents in a text editor, you have successfully serialized your game's state.
Conclusion
You have just implemented one of the most intellectually satisfying features of game development: persistence. You designed a binary file format and wrote the logic to convert your game's in-memory data graph into a storable sequence of bytes, skillfully navigating the challenge of handling pointers.
Key Takeaways:
- Serialization is the process of converting data structures into a storable format.
- Pointers cannot be saved directly to a file, as their memory addresses are not persistent across program runs.
- A robust save format includes a header with a "magic number" (
fourcc) and versioning information. - The key to saving pointer-based relationships is to store a unique identifier (like an object's tag) instead of the pointer itself.
- Variable-length data like strings should be saved by first writing their length, followed by the data.
You have laid a solid foundation. In the next lesson, you will complete the circle by writing the loadGame function. You will read your save.bin file, dynamically reconstruct the objects, and, most importantly, use the saved tags to rebuild the location pointers, restoring the game state exactly as the player left it.