Chris' Tutorials
Docs/Grid Placement

Grid Placement v6.0

Save and Load

Persist placed objects and painted terrain across game sessions.

StatusDraft
Versionv6.0
UpdatedDevelopment docs generated from GDScript source

This is unreleased documentation in active development. APIs, class names, and behavior may change before the final release.

Draft — Unreleased:This page is in active development. APIs, class names, and behavior may change before the release is finalized. Use it as a preview of what's coming — not as a stable integration target.

This guide covers how to persist placed objects and painted terrain across game sessions.

Version note: This guide is for Grid Placement 6.0.0. The plugin gives you object and terrain serialization helpers; it does not replace your game's save-file, profile, cloud-save, or migration system.


Core Concepts

Persistence relies on two plugin-provided object-placement pieces:

  1. PlaceableInstance: A component attached to placed objects. It remembers the ScenePlacementEntry reference and transform needed to recreate the object.
  2. PlaceableInstance group: Every PlaceableInstance joins this Godot group on creation. This is the normal way to discover placed objects in a scene.

The plugin does not provide a complete file I/O system, world-state manager, or player-state serializer. Those are game-specific concerns you implement yourself.


Discovering Placed Objects

The plugin tracks placed objects through the PlaceableInstance group.

var placed_nodes: Array[Node] = get_tree().get_nodes_in_group(PlaceableInstance.group_name)

Why groups instead of a central list?

  • PlaceableInstance._init() calls add_to_group(group_name) automatically.
  • PlaceableInstance.validate_setup() warns if a node is missing the group.
  • Godot handles group membership with the node's lifetime.
  • PlacementState does not maintain a separate canonical placed-object array.

Saving Objects

Iterate the PlaceableInstance group and call save() on each component.

func collect_placed_object_data() -> Array[Dictionary]:
    var save_data: Array[Dictionary] = []

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

        var entry: Dictionary = node.save(true)
        save_data.append(entry)

    return save_data

What PlaceableInstance.save() returns

The returned Dictionary uses these keys from 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 ScenePlacementEntry resource PlaceableInstance.Names.PLACEABLE

Do not change these built-in keys. If you need extra data, merge your own keys after calling save().


Loading Objects

Step 1: Clear existing placed objects

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

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)

PlaceableInstance.instance_from_save() loads the saved ScenePlacementEntry, instances its packed_scene, adds it to the parent, restores name/transform, and attaches a fresh PlaceableInstance if the scene did not already include one.


Adding Custom Object Save Data

If your placed objects have game-specific state such as health, inventory, owner id, or tile anchors, merge that data after calling the plugin's save().

func save_with_custom_data(node: PlaceableInstance) -> Dictionary:
    var data := node.save(true)
    var parent := node.get_parent()

    if parent != null and 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.

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"])

Object Save/Load Edge Cases

  • Missing resources: If a placeable resource path no longer exists, ScenePlacementEntry.load_resource() returns null and instance_from_save() emits an error. Always check for null returns.
  • 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.
  • Extra state: PlaceableInstance.save() recreates the object scene and transform. It does not automatically persist your game-specific state.

Demo Reference

The plugin ships with worked save/load examples in the demo scenes. These classes are not part of the addon API; they show one possible way to wire plugin 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

Use the demo as a starting point, but remember: PlaceableInstance.save() and PlaceableInstance.instance_from_save() are the object APIs you need. Everything else is your own architecture.


Terrain Save/Load

Painted terrain tiles are not saved automatically with your game state. Use TerrainPersistence to serialize and restore terrain cell data.

Saving terrain

var result := TerrainPersistence.save_terrain(tile_map, "user://terrain_save.res")
if result != OK:
    push_error("Failed to save terrain: %d" % result)

Save all terrain layers tracked by the targeting state:

var result := TerrainPersistence.save_all_terrain(
    placement_container.get_states().targeting,
    "user://all_terrain.res"
)

Loading terrain

var result := TerrainPersistence.load_terrain(tile_map, "user://terrain_save.res")
if result == ERR_FILE_NOT_FOUND:
    pass # No save file yet — fine for first launch.
elif result != OK:
    push_error("Failed to load terrain: %d" % result)

Load all tracked terrain layers:

var result := TerrainPersistence.load_all_terrain(
    placement_container.get_states().targeting,
    "user://all_terrain.res"
)

Save data format

TerrainSaveData is a Resource containing an array of cell records:

{
  "x": int,
  "y": int,
  "terrain_set": int,
  "terrain": int
}

Terrain indices reference the TileSet that was active at paint time. Make sure the same TileSet is assigned to the TileMapLayer when loading.

Using the host's terrain service

If you have a GridPlacementHost, ask the host for its terrain service:

var terrain_service := host.get_terrain_service()
if terrain_service != null:
    terrain_service.save_terrain("user://terrain.res")
    terrain_service.load_terrain("user://terrain.res")

Terrain Edge Cases

  • Empty layers: Saving a TileMapLayer with no terrain-painted cells produces a valid, empty TerrainSaveData resource. Loading it is a no-op.
  • Missing save file: load_terrain() returns ERR_FILE_NOT_FOUND; handle this gracefully on first launch.
  • Corrupt file: If the file exists but is not a TerrainSaveData resource, an error is returned.
  • TileSet mismatch: Loading terrain indices onto a TileMapLayer with a different TileSet may produce no visible result or garbled terrain.
  • Direct-tile cells: Cells placed via direct tile IDs rather than terrain-connected painting may not be captured by terrain persistence helpers.

Related Guides

Source

docs/v6-0/guides/save-and-load.md

Plugin docs root:gdscript/plugins/grid_placement_dev/docs