Skip to content
ct

Grid Placement v6.0

Save and Load

Persist placed objects and painted terrain across game sessions.

Status
Draft
Version
v6.0
Updated
Development docs generated from GDScript source

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

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 nodein 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 starts with restored game-owned fields, then writes these plugin-owned 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
"placement_instance_id" Stable UUID-style identifier, when assigned PlaceableInstance.Names.PLACEMENT_INSTANCE_ID

Do not change these built-in keys. If you need extra data, merge your own keys after calling save(). The mount record used by the 3D demo is persistence-layer metadata added after this call; it is not emitted by PlaceableInstance.save() itself.


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 entryin 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: callers must merge live game-specific state after calling save(). Unknown game-owned keys loaded by instance_from_save() are retained on the restored component and included in its next save() call.
  • Mount metadata: a demo or game persistence layer may add a mount record after save() and restore it through saved_mount_data; the base component treats that record as opaque.

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.

PlaceableInstance save format

Each PlaceableInstance serializes itself into a dictionary via save(). The dictionary contains:

Key Type Description 5.x compat?
instance_name String The parent node's name Yes
transform String var_to_str() of the parent's transform Yes
placeable Dictionary Resource path + optional UID Yes
mount Dictionary Demo-owned cell, edge, face, or mount metadata Caller-defined
placement_instance_id StringName Stable UUID-style identifier (36 chars, 8-4-4-4-12 hex) No (6.0+)

5.x to 6.0 migration

Saves from Grid Placement 5.x do not contain placement_instance_id. When a 5.x save is loaded through PlaceableInstance.instance_from_save(), a new UUID is generated automatically during restoration. All other component fields (transform, placeable reference, and custom game-owned fields) are preserved unchanged. Demo-owned mount metadata is preserved opaquely when the save layer supplies it.

The generated ID is written on the next successful save(). The original save file is never mutated by the load operation alone.

Supported 5.x versions: 5.0.x through 5.0.8 6.0 minimum: 6.0.0

Duplicate placement_instance_id values within one load operation are rejected. If two restored objects claim the same identity, the second restoration fails with an actionable error and returns null.

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