Save and Load

This guide covers how to persist placed objects across game sessions using the Grid Building plugin’s built-in save/load API.

Version note: This guide is validated for Grid Building 5.0.5.


Core Concepts

Persistence relies on two plugin-provided elements:

  1. PlaceableInstance: A component attached to every valid placement. It holds the “DNA” of the object (template reference, transform) and provides the save/load methods.
  2. PlaceableInstance group: Every PlaceableInstance automatically joins this Godot group on creation. This is the canonical way to discover all placed objects in the scene.

Note: The plugin does not provide a file I/O system, world state manager, or player-state serializer. Those are game-specific concerns you implement yourself (see Demo Reference for an example).


Discovering Placed Objects

The plugin tracks placed objects through the "PlaceableInstance" Godot group. Every time the building system places an object, it attaches a PlaceableInstance node that auto-registers itself:

1
2
3
# This is the canonical way to access all placed objects.
# PlaceableInstance.group_name == "PlaceableInstance"
var placed_nodes: Array[Node] = get_tree().get_nodes_in_group(PlaceableInstance.group_name)

Why groups instead of a central list?

  • Reliable: PlaceableInstance._init() calls add_to_group(group_name) automatically.
  • Validated: PlaceableInstance.validate_setup() warns if a node is missing the group.
  • Zero maintenance: You never need to manually register or unregister objects; Godot handles group membership with the node’s lifetime.
  • No central tracker: BuildingState and other plugin systems do not maintain a separate placed_objects array. The group is the source of truth.

Saving

Step 1: Collect data from placed objects

Iterate the PlaceableInstance group and call save() on each node:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func collect_placed_object_data() -> Array[Dictionary]:
    var save_data: Array[Dictionary] = []

    for node in get_tree().get_nodes_in_group(PlaceableInstance.group_name):
        # Skip transient objects (previews, manipulation ghosts)
        var parent := node.get_parent()
        if parent != null and parent.has_meta("gb_preview"):
            continue

        # PlaceableInstance.save(bool include_uid) -> Dictionary
        var entry: Dictionary = node.save(true)
        save_data.append(entry)

    return save_data

What PlaceableInstance.save() returns

The returned Dictionary uses these exact keys (defined in PlaceableInstance.Names):

| Key | Value | Constant | |–|-|******-| | "instance_name" | The placed object’s node name | PlaceableInstance.Names.INSTANCE_NAME | | "transform" | var_to_str(parent.transform) | PlaceableInstance.Names.TRANSFORM | | "placeable" | Load data for the Placeable resource | PlaceableInstance.Names.PLACEABLE |

Do not invent your own keys. If you need extra data (durability, owner ID, etc.), store it in a separate dictionary and merge it after calling save(). Changing the built-in keys will break instance_from_save().


Loading

Step 1: Clear existing placed objects

1
2
3
func clear_placed_objects() -> void:
    get_tree().call_group(PlaceableInstance.group_name, "queue_free")
    await get_tree().process_frame

Step 2: Rebuild from save data

1
2
3
4
5
func restore_placed_objects(data: Array[Dictionary], parent: Node) -> void:
    for entry in data:
        var instance := PlaceableInstance.instance_from_save(entry, parent)
        if instance == null:
            push_error("Failed to instance object from save entry: %s" % entry)

What PlaceableInstance.instance_from_save() does

  1. Loads the Placeable resource from the saved reference.
  2. Instantiates the resource’s packed_scene.
  3. Adds it to p_instance_parent.
  4. Restores name and transform from the save entry.
  5. Attaches a fresh PlaceableInstance component (only if the scene root doesn’t already have one).

Adding Custom Save Data

If your placed objects have game-specific state (health, inventory, rotation), merge it after calling the plugin’s save():

1
2
3
4
5
6
7
8
func save_with_custom_data(node: PlaceableInstance) -> Dictionary:
    var data := node.save(true)

    var parent := node.get_parent()
    if parent.has_method("get_custom_save_data"):
        data["custom_data"] = parent.get_custom_save_data()

    return data

On load, restore custom data after instance_from_save() returns:

1
2
3
4
5
6
7
func load_with_custom_data(entry: Dictionary, parent: Node) -> void:
    var instance := PlaceableInstance.instance_from_save(entry, parent)
    if instance == null:
        return

    if entry.has("custom_data") and instance.has_method("restore_custom_data"):
        instance.restore_custom_data(entry["custom_data"])

Edge Cases

  • Missing resources: If a placeable resource path no longer exists, Placeable.load_resource() returns null and instance_from_save() emits an error. Always check for null returns.
  • UID resolution: Placeable.load_resource() supports UID resolution in web builds (5.0.4+).
  • Schema changes: The plugin does not provide a migration system. If you change your custom data structure, handle version checking in your own save wrapper.
  • Previews and ghosts: Objects marked with "gb_preview" metadata are transient and should be skipped during save. The plugin’s demo scenes use this convention; adopt it in your own preview system if you want the same filtering behavior.

Demo Reference

The plugin ships with a fully worked save/load example in the demo scenes. These classes are not part of the addon API — they show one possible way to wire the plugin’s save/load into a complete game.

| Demo Class | Purpose | Location | |||***| | DemoSaveLoad | JSON file I/O, folder creation, error handling | demos/shared/save/demo_save_load.gd | | WorldState | Multi-level world persistence, level switching | demos/shared/world/world_state.gd | | Level | Per-level save/load, PlaceableInstance group iteration | demos/shared/world/level.gd | | LevelState | Resource-based save data container | demos/shared/save/level_state.gd | | PlayerState | Player position, inventory, etc. | demos/shared/save/player_state.gd |

How the demo layers on top of the plugin

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Demo Level.save() — game code, not plugin API
func save() -> Dictionary:
    var placed_states: Array[Dictionary] = []

    for placed in get_tree().get_nodes_in_group(PlaceableInstance.group_name):
        var parent := placed.get_parent()
        if parent != null and parent.has_meta("gb_preview"):
            continue

        placed_states.append(placed.save(true))

    level_state.placed_states = placed_states
    return level_state.as_dictionary()

Use the demo as a starting point, but remember: PlaceableInstance.save() and PlaceableInstance.instance_from_save() are the only plugin APIs you must call. Everything else (JSON, LevelState, WorldState, player data) is your own architecture.