Godot 4 · 3D · GDScript · Game Systems

Door & Teleport
System Documentation

// personal reference · 3D scene navigation & world mapping

01 Core Concept

Two separate systems handle all scene navigation in the game. They share the same GameState autoload for condition checking.

Door System
Fixed two-way connection
between specific points
Teleport System
One-to-many, triggered by
NPC / item / magic circle
Key Principle All conditions are read from a single GameState autoload. Never hardcode conditions locally inside scenes. One source of truth for everything.

02 Door System SYSTEM

Each row in the connection table represents one bidirectional connection between two doors. This prevents missing return doors.

// How a door works

Player touches door_X
Check condition_X_Y
Load scene_Y
Spawn at door_Y

// Door node properties (Inspector)

Each door node in your scene only needs one exported variable — its own door_id. Everything else is looked up from the table at runtime.

# Door.gd — attach to each door node (Area3D)
extends Area3D

@export var door_id: String = ""

func _on_body_entered(body):
    if body.is_in_group("player"):
        GameState.enter_door(door_id)
Friendly to Edit Adding a new door = drag Door node into scene, set door_id in Inspector. No other code changes needed.

03 Connection Table GOOGLE SHEET

Maintain this as a Google Sheet. Use dropdown validation on door_id_x and door_id_y columns to prevent typos. Export as CSV → convert to JSON.

connection_id door_id_x scene_x door_id_y scene_y condition_x_y condition_y_x
001 town_01_door_A town_01.tscn dungeon_01_door_B dungeon_01.tscn (empty) (empty)
002 town_01_door_C town_01.tscn dungeon_02_door_A dungeon_02.tscn quest_B_done (empty)
003 town_02_door_A town_02.tscn dungeon_03_door_A dungeon_03.tscn key_obtained key_obtained
Important Empty condition = always accessible. Both directions can have different conditions — for example X→Y requires a key, but Y→X is always open (player already inside).

04 Door Code — Godot SYSTEM

// GameState.gd (Autoload)

extends Node

var door_database = {}
var spawn_door_id: String = ""

# Load door table from JSON on game start
func _ready():
    var file = FileAccess.open("res://data/doors.json", FileAccess.READ)
    var data = JSON.parse_string(file.get_as_text())
    for row in data["connections"]:
        door_database[row["door_id_x"]] = row
        door_database[row["door_id_y"]] = row

# Called when player touches a door
func enter_door(player_door_id: String):
    if not door_database.has(player_door_id):
        print("ERROR: door_id not found → ", player_door_id)
        return

    var row = door_database[player_door_id]
    var is_x_side = (player_door_id == row["door_id_x"])

    var condition = row["condition_x_y"] if is_x_side else row["condition_y_x"]
    var dest_scene = row["scene_y"] if is_x_side else row["scene_x"]
    var dest_door = row["door_id_y"] if is_x_side else row["door_id_x"]

    # Check condition
    if condition != "" and not get(condition):
        show_locked_message()
        return

    # Store destination and load scene
    spawn_door_id = dest_door
    get_tree().change_scene_to_file("res://maps/" + dest_scene)

// Spawn player at destination door

# In each map scene _ready()
func _ready():
    var spawn_id = GameState.spawn_door_id
    if spawn_id != "":
        var door_node = get_node_or_null(spawn_id)
        if door_node:
            $Player.global_position = door_node.global_position  # Vector3 in 3D

05 Teleport System SYSTEM

Teleport is one direction only. Used by guild NPCs, items, or magic circles. One trigger can show a menu of multiple destinations.

// How teleport works

Player talks to Guild NPC
Show available destinations
Check condition
Load scene → spawn
Key Difference from Door Teleport has no "return" row. The return trip is handled by another teleport entry or the door system. Each entry is one direction only.

06 Teleport Table GOOGLE SHEET

teleport_id label destination_scene destination_spawn condition teleport_type
guild_to_dungeon_01 Forest Dungeon dungeon_01.tscn dungeon_01_spawn_A quest_A_done guild
guild_to_dungeon_02 Ice Cave dungeon_02.tscn dungeon_02_spawn_A quest_B_done guild
item_return_town Return to Town town_01.tscn town_01_center (empty) item
circle_dungeon_03 Ancient Ruins dungeon_03.tscn dungeon_03_spawn_A relic_obtained magic_circle

07 Teleport Code — Godot SYSTEM

// Load teleport table (in GameState.gd)

var teleport_database = {}

func load_teleports():
    var file = FileAccess.open("res://data/teleports.json", FileAccess.READ)
    var data = JSON.parse_string(file.get_as_text())
    for entry in data["teleports"]:
        teleport_database[entry["teleport_id"]] = entry

# Get all available teleports for a specific type
func get_available_teleports(type: String) -> Array:
    var result = []
    for id in teleport_database:
        var entry = teleport_database[id]
        if entry["teleport_type"] == type:
            var cond = entry["condition"]
            if cond == "" or get(cond):
                result.append(entry)
    return result

# Execute a teleport
func use_teleport(teleport_id: String):
    var entry = teleport_database[teleport_id]
    spawn_door_id = entry["destination_spawn"]
    get_tree().change_scene_to_file("res://maps/" + entry["destination_scene"])

// Guild NPC usage example

# GuildNPC.gd
func talk_to_player():
    var options = GameState.get_available_teleports("guild")
    # pass options to your dialog/menu system
    $TeleportMenu.show_options(options)

# When player selects a destination
func _on_option_selected(teleport_id: String):
    GameState.use_teleport(teleport_id)

08 Validator

Run this script to catch missing or broken connections before they become runtime bugs.

# Run this in _ready() during development only
func validate_doors():
    for door_id in door_database:
        var row = door_database[door_id]

        # Check destination door exists
        var dest = row["door_id_y"] if door_id == row["door_id_x"] else row["door_id_x"]
        if not door_database.has(dest):
            push_error("DOOR ERROR: " + door_id + " → destination not found: " + dest)

        # Check condition variable exists in GameState
        for cond_key in ["condition_x_y", "condition_y_x"]:
            var cond = row[cond_key]
            if cond != "" and not (cond in GameState.get_property_list().map(func(p): return p["name"])):
                push_warning("CONDITION WARNING: " + cond + " not found in GameState")

    print("Door validation complete.")
Remember Disable or remove validator calls before shipping. Only run during development.

09 Summary

Foundation Goal Build and test this system in a small labyrinth puzzle game first. Once solid, reuse unchanged in the dream game — no matter how many dungeons or towns are added.