Save and Load (5.0.3)

This guide covers how to persist placed objects across game sessions in GridBuilding 5.0.3. While the maintenance version uses component-based persistence, it establishes patterns that carry forward into future versions.

Core Concepts

Persistence in 5.0.3 relies on three key elements:

  1. PlaceableInstance: A runtime component attached to every valid placement. It holds the “DNA” of the object (template ID, original resource path).
  2. Serialization: The process of converting the node hierarchy into a flat Dictionary.
  3. Deserialization: Reconstructing the node tree from data, ensuring all dependencies (scripts, resources) are relinked.

The Serialization Contract

A valid save entry in 5.0.3 looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func make_save_entry() -> Dictionary:
    return {
        "instance_name": "Chair_001",
        "transform": Transform2D.IDENTITY,
        "placeable_id": "res://placeables/chair.tres",
        "custom_data": {
            "durability": 50,
            "owner_id": "player_1"
        }
    }

PlaceableInstance Component

The PlaceableInstance script (res://addons/grid_building/placeable_instance.gd) exposes two critical methods:

  • save(with_custom_data: bool) -> Dictionary: serializes the node.
  • instance_from_save(data: Dictionary, parent: Node) -> Node: static factory method to rebuild the node.

Implementation Guide

Step 1: Saving the World

To save your level, iterate through the PlaceableInstance group. This group is automatically managed by the system.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func save_level() -> void:
    var save_data: Array[Dictionary] = []

    # Iterate all placed objects
    for node in get_tree().get_nodes_in_group("PlaceableInstance"):

        # 1. Validate it's a valid placement (not a preview)
        if node.get_parent().has_meta("gb_preview"):
            continue

        # 2. Serialize
        # Assumes the node structure: PlaceableRoot -> PlaceableInstance
        var data = node.save(true)

        # 3. Add custom game logic data (optional)
        if node.get_parent().has_method("get_custom_save_data"):
            data["custom_data"] = node.get_parent().get_custom_save_data()

        save_data.append(data)

    # Write to file
    _write_to_disk(save_data)

Step 2: Loading the World

Loading is a two-step process: Clear the existing world, then rebuild from data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func load_level(data: Array[Dictionary]) -> void:
    # 1. Clear existing
    get_tree().call_group("PlaceableInstance", "queue_free")
    await get_tree().process_frame

    # 2. Rebuild
    for entry in data:
        # Static factory method handles resource loading
        var new_node = PlaceableInstance.instance_from_save(entry, world_root)

        # Restore custom data if needed
        if "custom_data" in entry and new_node.has_method("restore_custom_data"):
            new_node.restore_custom_data(entry["custom_data"])

Handling Edge Cases

  • Missing Resources: If a placeable_id resource path no longer exists (e.g., you deleted the file), instance_from_save will fail gracefully. Ensure your loading logic handles null returns.
  • Version Migrations: 5.0.3 does not have a built-in schema migration system. If you change your data structure, you must handle version checking in your own code.

Validated By

The save/load architecture and serialization patterns are verified by the following test suites:

  • Serialization Protocol: res://addons/grid_building/test/building/placement/placement_report_unit_test.gd - Validates the serialization of placement metadata and report integrity.
  • Component Implementation: res://addons/grid_building/placeable_instance.gd - Core component that manages instance lifecycle and serialization hooks.
  • Metadata Integrity: res://addons/grid_building/test/utilities/data/composition_container_subresources_test.gd - Ensures that configuration data used during reconstruction is properly persisted.