Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 70

Implementing Save/Load in Your Game Loop

Recommended resources

Welcome back! In our previous lesson, you successfully implemented the loadGame function, completing the technical foundation for game state persistence. You now have the power to both serialize the living, dynamic state of your game into a binary file and deserialize it back into memory. This lesson is about bringing that power to the player.

Our focus now is to seamlessly integrate the saveGame and loadGame functions into the main game loop. This involves more than just hooking them up to command words; it requires thinking about how these powerful actions affect the flow of the game and the player's experience. We'll refine the command handlers and explore the design choices behind different persistence strategies.

Save, Load, and the Game Loop

At its heart, your game runs on a continuous loop: process input, update the game world, and display the result. Let's visualize this classic structure.

A standard game loop. In our text adventure, "Render" corresponds to printing text to the console.

How do our new save and load commands fit into this cycle?

  • Saving is an action like any other. When the player types save, it's processed during the "Update Game" phase. The saveGame function runs, writes to the disk, and upon completion, the game loop continues as normal, ready for the next command. It's a non-disruptive operation.

  • Loading is fundamentally different. It's a "disruptive" operation that halts the current game state and replaces it entirely with a new one from the disk. The entire collection of objects, their locations, and the player's inventory are wiped from memory and reconstructed. The game loop must handle this transition gracefully, ensuring that if the load succeeds, the player is placed into the restored world, and if it fails, the current game session can continue without crashing.

The executeLoad function you wrote in the previous lesson already accounts for this by calling executeLook after a successful load, immediately showing the player their new surroundings. This is the correct approach.


An Alternative Philosophy: Input Logging

The method we've implemented—serializing the entire game state—is the most common and versatile approach to saving games. However, for certain types of games, particularly traditional text adventures, there is an elegant alternative: input logging.

Instead of saving the state of all variables at a single point in time, the game simply records every command the player ever types into a log file. To "load" the game, the program restarts from its initial state and automatically re-plays every command from the log file at high speed, bringing the game back to where the player left off.

This technique has fascinating trade-offs. To explore this professional-level design consideration, let's analyze an implementation from a C text adventure tutorial.

16. Savegame

This article from a text adventure tutorial presents an alternative and insightful method for saving and loading a game: logging user input. While we have implemented state serialization, understanding this alternative approach deepens your knowledge of game persistence strategies.

Read the introduction to understand the concept of saving progress and the two main approaches described: state serialization and input logging. Study the list of advantages and challenges of the input logging technique. Consider how these trade-offs would apply to our game, especially regarding debugging and cheating. Examine the C code in the table for main.c. Focus on how the getInput function is modified to handle both keyboard input and file input. Pay close attention to the static FILE *fp variable which manages the input source. You can find the code block starting from the main.c table. Finally, read the detailed explanation of the code, especially the logic in lines 17 through 45. This section is key, as it describes how the game engine switches from replaying a log file to accepting live user input once the log is exhausted.

After reviewing that material, it's clear why state serialization was the right path for our project. While input logging is clever, it struggles with games that have random elements or complex state changes that aren't a direct result of player commands. Our method of serializing structs is far more robust and generalizable, aligning perfectly with your goal of mastering low-level data and memory management techniques that apply to a wider variety of game genres.


Finalizing the Integration

The code you wrote in the previous two lessons provides a solid foundation for the save and load commands. Let's formalize their implementation and ensure the user experience is clear and predictable.

Your main.c (or wherever your command processing loop resides) should contain the executeSave and executeLoad functions and the corresponding entries in your COMMAND array.

// In your main command processing file (e.g., main.c)



#include "save.h" // For saveGame
#include "load.h" // For loadGame

// Forward declaration if it's defined later in the file
bool executeLook(void);

bool executeSave(void) {
    if (saveGame("save.bin")) {
        printf("Game state saved.\n");
    } else {
        // The saveGame function should have printed a specific error
        printf("Failed to save game.\n");
    }
    return true; // The game continues after saving
}

bool executeLoad(void) {
    printf("Loading game state...\n");
    if (loadGame("save.bin")) {
        // On success, show the player their new surroundings
        return executeLook();
    } else {
        // On failure, inform the player and continue the current game
        printf("Failed to load game. Continuing current session.\n");
        return true; 
    }
}

// And in your commands array:
static const COMMAND commands[] = {
    /* ... your other commands ... */
    { "save",      executeSave },
    { "load",      executeLoad },
    { "quit",      executeQuit },
    { NULL,        NULL }
};

This implementation ensures:

  1. Clear Feedback: The player is explicitly told whether the save/load operation succeeded or failed.
  2. Robustness: A failed load doesn't crash the game; it allows the current session to continue.
  3. Good User Experience: A successful load immediately grounds the player in the new state by executing a look command.

Compile your project with these functions integrated. Run the game, move around, take and drop a few items, and then use the save command. Close the program, restart it, and immediately use the load command. Your game should be restored to the exact state you saved it in.


Conclusion

You have now fully integrated persistence into your game. The player can save their progress at any time and resume their adventure later. By implementing the save/load commands and considering the design trade-offs against alternative methods like input logging, you've engaged with a core aspect of professional game development. Your capstone project is now functionally complete.

Key Takeaways:

  • Integrating save and load commands requires careful consideration of their effect on the game loop.
  • save is a non-disruptive action, while load is a disruptive state replacement that requires careful handling.
  • State serialization (our method) and input logging are two primary strategies for game persistence, each with distinct advantages and disadvantages.
  • Providing clear user feedback and ensuring the game remains stable even if a file operation fails is crucial for a robust player experience.

Next Steps:
Your game works. But is it perfect? The final lesson in this course is dedicated to ensuring quality and correctness. We will perform a final, rigorous audit of the entire project using the professional-grade tools GDB and Valgrind. We will hunt for any subtle logic bugs, memory leaks, or invalid memory accesses to ensure your finished capstone project is not just complete, but polished and robust.