Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Crafting a Saveable Game State

Hello! Welcome back to your JRPG development course.

In the last lesson, we built a fully functional save/load screen with multiple slots. To make that happen, we created a basic PersistenceManager and a placeholder GameData object. It was a practical first step, but now it's time to formalize the architecture. As you know from your experience in software engineering, a robust data model is crucial for the long-term health of any application, and games are no exception.

Today's lesson focuses on a critical design task: Design a serializable game state object that aggregates all data needed for saving. We will move beyond our simple placeholder and architect a comprehensive, clean, and serializable object that will serve as the single source of truth for a player's entire journey.

1. What Does "Serializable" Mean?

First, let's clarify what we mean by a "serializable object." At its core, serialization is the process of converting an object's state from its representation in memory (with complex pointers, references, and methods) into a format that can be stored (like a file on disk) or transmitted (over a network). Deserialization is the reverse process: reconstructing the object from that stored format.

Serialization and Deserialization Flow
This diagram shows the core concept of serialization. An object in memory is converted into a stream of bytes (in our case, a JSON string), which can be saved to a file or database. Deserialization reads that stream and reconstructs the object.

Why not just take a "snapshot" of the game's memory? A forum post provides a concise answer.

Help me understand how Saving game data works

A post by user PraetorBlue on a Unity discussion forum gives an excellent, high-level explanation of how game saving works and why a direct memory snapshot is impractical.

Read the first two paragraphs of PraetorBlue's post. Focus on the core idea: you only save the minimum amount of information necessary to reconstruct the game state.

As the post mentions, a modern game's memory footprint is huge, and most of it is redundant static data (like textures, sounds, and map definitions). Our goal is to save only the dynamic state that changes as a result of player actions.

For this process, we'll rely on a format you're very familiar with: JSON (JavaScript Object Notation). It's human-readable, and JavaScript provides native tools (JSON.stringify() and JSON.parse()) to handle serialization and deserialization effortlessly.

Serialization - A Crash Course

For a quick refresher on the fundamentals, the video 'Serialization - A Crash Course' offers a clear explanation of the concept, using JSON as a primary example.

Please watch the following parts: What is Serialization? (00:29 - 03:02): This establishes the core problem and solution. Serializing Complex Data Structures (02:52 - 04:59): This shows how nested data is flattened into a string format, which is exactly what JSON.stringify does for us. Popular Serialization Formats (05:28 - 07:13): This section reviews JSON syntax, confirming its suitability for our needs.

2. The Centralized State Store Pattern

In front-end development, managing application state is often solved using a centralized store, like Redux or Vuex. This "single source of truth" simplifies data flow and makes the application's state predictable. This pattern is exceptionally well-suited for game development, especially when it comes to saving and loading.

By concentrating all persistent game data into a single, large, serializable object, the act of saving the game becomes incredibly simple: you just serialize that one object.

Web App Inspired State Management in My Godot Game

The video 'Web App Inspired State Management in My Godot Game' by Firebelley Games makes a direct and compelling case for using a Redux-like centralized store in game development. While the context is the Godot engine, the architectural principles are universal and highly relevant to your background.

Watch these key sections: Introduction to Centralized State Store (01:31 - 02:30): This introduces the core concept, drawing the parallel to Redux. Benefits for Game Saving (03:38 - 04:26): This is the crucial part. It explains how having all your state in one plain object makes saving and loading trivial. Practical Example (04:46 - 07:45): This walkthrough of an ammo system demonstrates the data flow and reinforces how easy it is to save the state once it's centralized.

Our SerializableGameState object will be this centralized store.

3. Designing the Object: What Data to Save?

Now for the main design task. What data constitutes the "minimum information necessary" to restore a JRPG session? We can group the data into logical categories.

  • World & Player Position: Where is the player and what direction are they facing?
  • Party & Character Progression: Who is in the party? What are their current stats, level, HP/MP, equipment, and learned skills? This must include all playable characters, not just the active ones.
  • Game Progression Flags: This is a critical one for JRPGs. It's a collection of key-value pairs that track the story. Have you opened that specific treasure chest? Have you spoken to the king? What stage of the main quest are you on? These are often called "switches" and "variables" in tools like RPG Maker.
  • Inventory & Economy: How much gold does the party have? What items do they possess, and in what quantity?
  • Map-Specific State: Sometimes you need to remember the state of things on a map you've left. For example, the position of an NPC who wanders around, or a block that was pushed in a puzzle.
  • System Data: Things like total playtime, how many times the game has been saved, and a timestamp.

Critically, we must distinguish between persistent state (what we save) and transient state (what we don't).

Save This (Persistent) Don't Save This (Transient) Why?
currentMapId: 'town' The MapScene object instance The MapScene can be reconstructed from the map ID.
party.hero.currentHp: 50 The player's sprite flashing animation after taking damage. The animation is temporary visual feedback. The HP value is the persistent state.
gameFlags.opened_dungeon_chest: true The state of the treasure chest's opening animation. The flag is the permanent fact; the animation is a one-time effect.
activeEncounter: null (when on the map) The entire BattleScene object with all enemy data. Battle state is cleared upon victory or escape. If you save mid-battle (a rare feature), you'd need a dedicated battleState object.
The ID of the currently equipped weapon: equipped.weapon: 2 A direct reference to the IronSword object in the character's equipment property. Object references are memory pointers and cannot be serialized. This is a crucial point we'll explore next.

4. Handling Data Relationships with IDs

The last point in the table is the most important architectural challenge: how do you save a relationship between two pieces of data? A character has an equipped sword. An AI-controlled ally targets an enemy.

In code, this is often represented by a direct object reference or pointer. As you know, these are memory addresses, which are meaningless once the game is closed and re-opened. Attempting to JSON.stringify an object with circular references will throw an error.

The solution is to use serializable identifiers. Instead of storing a reference to the object, you store its unique ID. When you load the game, you use that ID to look up the object's data from your static game database (e.g., your items.json or skills.json files).

Managing Data Relationships

The article 'Managing Data Relationships' from the blog Games From Within provides an excellent, in-depth look at this exact problem. It's written from a C++ perspective, but the concepts of pointers, indices, and handles are directly transferable to how we think about object references in JavaScript.

Please read the following subsections: Pointing The Way: Focus on the paragraph explaining why pointers are not serializable ('The biggest drawback is...'). Indexing: Understand how using an index into an array is a step towards a serializable solution. Handle-Ing The Problem: This describes a more robust ID system. While we won't implement this exact C++ handle manager, the core idea is the same: use a unique, stable ID to refer to data instead of a memory address.

For our engine, we will use simple, unique string or number IDs for all our database entities (items, skills, characters, etc.).

Test your understanding!

Your hero character has a Potion in their inventory and the Fireball skill. How would you represent the following states in a serializable GameState object?

  1. The party has 10 potions.
  2. The hero has the Fireball skill available.
  3. The hero has a "Steel Sword" equipped.
Show answer

Assuming your game database has defined potion as item_001, Fireball as skill_010, and Steel Sword as item_152:

  1. Inventory: The inventory object would not reference the hero directly. It's a party-wide resource.
    "inventory": { "item_001": 10 }
    
  2. Skills: The character's data object would contain a list of learned skill IDs.
    "characters": { "hero": { "learnedSkills": ["skill_010"] } }
    
  3. Equipment: The character's data object would store the item ID in the appropriate equipment slot.
    "characters": { "hero": { "equipment": { "weapon": "item_152" } } }
    

When the game loads, it sees weapon: "item_152" and looks up the stats for the "Steel Sword" from its static item database to apply them to the hero.

5. Final Design: The SerializableGameState

Putting all these principles together, here is a well-designed, serializable game state object for our JRPG. This structure is pure data, contains no methods, uses IDs for relationships, and is perfectly suited for JSON.stringify.

// This is a blueprint for the data structure. In practice, it would be
// constructed by a 'newGame' function and populated by a 'saveGame' function.
const serializableGameState = {
    // --- System Data ---
    system: {
        playtimeInSeconds: 3600, // e.g., 1 hour
        saveTimestamp: "2023-10-27T10:00:00Z",
    },

    // --- World State ---
    world: {
        currentMapId: "dungeon_level_2",
        player: {
            position: { x: 34, y: 12 },
            direction: "up"
        },
        // State of objects/NPCs on maps the player has visited
        mapOverrides: {
            "town_main": {
                // The current position of a wandering NPC
                "npc_wanderer_1": { position: { x: 22, y: 15 } } 
            }
        }
    },

    // --- Progression ---
    progression: {
        gameFlags: {
            "main_quest_stage": 3,
            "met_the_king": true,
            "unlocked_airship": false,
            "opened_town_potion_chest": true
        }
    },

    // --- Party & Characters ---
    party: {
        // IDs of characters in the active battle/map party
        activeMemberIds: ["cid", "rosa", "kain"],
        gold: 15230,
        inventory: {
            "item_potion": 25,
            "item_phoenix_down": 10,
            "item_tent": 3,
            "key_castle_key": 1
        },
        // All characters the player has recruited
        characters: {
            "cid": {
                level: 25,
                xp: 85000,
                currentHp: 350,
                currentMp: 80,
                equipment: {
                    weapon: "equip_axe_mythril",
                    shield: null,
                    head: "equip_helm_iron",
                    body: "equip_armor_iron",
                    accessory: "equip_ring_power"
                }
            },
            "rosa": {
                level: 24,
                xp: 79000,
                currentHp: 280,
                currentMp: 150,
                equipment: {
                    weapon: "equip_bow_killer",
                    shield: null,
                    head: "equip_hat_wizard",
                    body: "equip_robe_light",
                    accessory: "equip_ring_mana"
                }
            },
            // ... more characters
        }
    }
};

Conclusion

In this lesson, we laid the architectural foundation for a robust save/load system. We've moved from a simple concept to a detailed, professional design that leverages patterns familiar from your web development career.

Key Takeaways:

  • Serialize, Don't Snapshot: A game's save state is a minimal set of instructions to reconstruct the game, not a raw memory dump.
  • Centralize Your State: Using a single, comprehensive object as the "source of truth" for all persistent data greatly simplifies state management and serialization. This is a direct application of the data-oriented design principle that separating data from code leads to simpler systems.
  • IDs are King: All relationships between data entities (like equipment, skills, and targets) must be represented by serializable identifiers (IDs), not direct memory references.
  • Pure Data: The final save object should be "plain old data"—a nested structure of strings, numbers, and booleans that can be flawlessly converted to JSON.

Preview of the next lesson:
Now that we have designed our ideal SerializableGameState object, the next step is to fully implement the logic that creates, populates, and persists it. We will implement save/load functionality using the browser's localStorage API, refining the PersistenceManager from the previous lesson to gather data from all game systems and assemble this complete state object.

Can't find a good explanation? Sign up and we'll make it for you

Sign up