Tutorials & Beginner-First May 16, 2026

Your First Godot 4.5 Floor-Load Coordinator in One Evening - A Beginner Pipeline for Smooth Roguelite Transitions (2026)

Beginner Godot 4.5 tutorial for 2026—build a FloorLoadCoordinator autoload, floor manifest JSON, progress UI, and portal transition in one evening so roguelite floor changes stop hitching.

By GamineAI Team

Your First Godot 4.5 Floor-Load Coordinator in One Evening (2026 Beginner Pipeline)

Cozy house pixel art - metaphor for loading the next floor room in a small roguelite

You do not need a bigger team to fix the floor portal freeze. You need one autoload, one JSON manifest, and one evening where you stop calling synchronous load() on the main thread.

This is a beginner-first companion to the deeper Godot 4.5 threaded ResourceLoader programming guide. That article explains why. This one walks you through build it tonight with copy-paste steps.

Why this matters now (May 2026)

October 2026 festival demos punish floor hitches harshly. Players experience a 600 ms freeze as a broken build—even if combat runs at 60 FPS. Godot 4.5 threaded loading is stable enough for indies to adopt now, before you lock your demo branch.

If your game also ships in the browser, finish this pipeline first, then read web export smoke tests for thread hosting rules.

What you will have at the end of the evening

  • FloorLoadCoordinator autoload that queues threaded loads
  • floor_manifest_floor_a.json listing paths for one floor
  • Portal scene that fades out, polls progress, spawns floor, fades in
  • floor_epoch guard so fast restarts do not spawn wrong floors
  • A note in release-evidence/floor-load-evening-pass.md with profiler screenshot

Time budget: about 4 hours for a minimal vertical slice (two rooms, one portal).

Prerequisites (honest)

  • Godot 4.5 project that already runs with two placeholder floor scenes
  • Basic GDScript (extends Node, signals, await)
  • One transition that currently uses load() or change_scene_to_file() and hitches

You do not need multiplayer, custom shaders, or a concept artist.

Who should skip this evening

  • Single-scene arcade score-chasers with no room changes
  • Teams without any portal transition yet (build core loop first)
  • Projects that already use a trusted additive streaming system

Everyone else with a roguelite-shaped portal freeze should try the four-hour path once.

Evening schedule (four hours)

Hour Outcome
1 Autoload coordinator + test path loads in empty scene
2 Manifest JSON + queue two floors
3 Portal fade + progress bar wired
4 Epoch test + profiler capture + doc note

Before you write code — inventory your hitch (15 minutes)

Open the scene that transitions floors. Search scripts for:

  • load(
  • preload( in _ready of transition-only scenes
  • change_scene_to_file

Write each path on paper. Circle the slowest one (usually the main .tscn).

That path becomes the first row in tonight’s manifest. Everything else is optional until the first portal is smooth.

Beginner mindset: You are not optimizing the whole game. You are fixing one portal.

Project folder layout (create empty folders now)

res://
  autoload/           # optional if script lives elsewhere
  data/floors/        # manifest JSON files
  floors/             # floor_a.tscn, floor_b.tscn
  audio/              # per-floor ambience
  ui/                 # fade_overlay.tscn (optional)
release-evidence/     # at repo root or docs/

Consistent folders prevent manifest typos that waste your evening.

Step 1 — Create the autoload (45 minutes)

Project → Project Settings → Autoload → add script floor_load_coordinator.gd, name FloorLoadCoordinator.

Paste the minimal coordinator from the programming guide (FloorLoadCoordinator section). For this evening, keep:

  • max_inflight = 4
  • Signal floor_asset_ready(path, resource, epoch)
  • Method progress_ratio() -> float

Smoke test scene: one Node2D with script:

extends Node2D

func _ready() -> void:
    FloorLoadCoordinator.queue_floor_pack(
        ["res://floors/test_floor.tscn"],
        1
    )

func _process(_d: float) -> void:
    print(FloorLoadCoordinator.progress_ratio())

Run. Confirm ratio reaches 1.0 without editor freeze longer than one frame spike.

Step 2 — Floor manifest JSON (30 minutes)

Create res://data/floors/floor_a.manifest.json:

{
  "floor_id": "floor_a",
  "paths": [
    "res://floors/floor_a.tscn",
    "res://audio/floor_a_ambience.ogg"
  ]
}

Add a one-line loader:

func load_manifest(floor_id: String) -> PackedStringArray:
    var f := FileAccess.open("res://data/floors/%s.manifest.json" % floor_id, FileAccess.READ)
    var data = JSON.parse_string(f.get_as_text())
    return PackedStringArray(data["paths"])

Beginner rule: if a path is not in the manifest, it does not exist for the coordinator tonight.

Step 3 — Progress bar UI (30 minutes)

Control root → ColorRect full screen (black, alpha 0.6) → ProgressBar centered.

@onready var bar: ProgressBar = $ProgressBar

func _process(_d: float) -> void:
    bar.value = FloorLoadCoordinator.progress_ratio() * 100.0

Hide overlay when ratio == 1.0. Do not use fake timers.

Step 4 — Portal transition script (60 minutes)

extends Area2D

@export var target_floor_id: String = "floor_b"
var _epoch: int = 0

func _on_body_entered(body: Node2D) -> void:
    if not body.is_in_group("player"):
        return
    _transition()

func _transition() -> void:
    _epoch = FloorLoadCoordinator.floor_epoch + 1
    FloorLoadCoordinator.floor_epoch = _epoch
    $CanvasLayer/FadeOverlay.show()
    $AnimationPlayer.play("fade_out")
    await $AnimationPlayer.animation_finished
    _unload_current_floor()
    var paths := load_manifest(target_floor_id)
    FloorLoadCoordinator.queue_floor_pack(paths, _epoch)
    while FloorLoadCoordinator.progress_ratio() < 1.0:
        if FloorLoadCoordinator.floor_epoch != _epoch:
            return
        await get_tree().process_frame
    _spawn_floor(target_floor_id, _epoch)
    $AnimationPlayer.play("fade_in")
    await $AnimationPlayer.animation_finished
    $CanvasLayer/FadeOverlay.hide()

Implement _unload_current_floor and _spawn_floor for your project structure (usually get_tree().current_scene.queue_free() then instantiate).

Step 5 — Epoch torture test (20 minutes)

  1. Enter portal
  2. Immediately restart run (your game’s restart button) before fade completes
  3. Confirm wrong floor does not appear
  4. Repeat three times

If wrong floor appears, your _spawn_floor lacks if epoch != FloorLoadCoordinator.floor_epoch: return.

Full coordinator reference (copy once, trim later)

If you want a single paste block for the evening, use this trimmed coordinator and expand later from the programming guide:

extends Node
class_name FloorLoadCoordinator

signal floor_asset_ready(path: String, resource: Resource, epoch: int)

@export var max_inflight: int = 4

var floor_epoch: int = 0
var _queue: Array = []
var _inflight: Dictionary = {}
var _ready_count: int = 0
var _expected_count: int = 0

func queue_floor_pack(paths: PackedStringArray, epoch: int) -> void:
    _ready_count = 0
    _expected_count = paths.size()
    for p in paths:
        _queue.append({"path": p, "epoch": epoch})

func progress_ratio() -> float:
    if _expected_count == 0:
        return 1.0
    return float(_ready_count) / float(_expected_count)

func _process(_delta: float) -> void:
    _pump()
    _start()

func _start() -> void:
    while _inflight.size() < max_inflight and _queue.size() > 0:
        var item = _queue.pop_front()
        if item["epoch"] != floor_epoch:
            continue
        if ResourceLoader.load_threaded_request(item["path"]) == OK:
            _inflight[item["path"]] = item

func _pump() -> void:
    var done: Array = []
    for path in _inflight.keys():
        var item = _inflight[path]
        var st = ResourceLoader.load_threaded_get_status(path)
        if st == ResourceLoader.THREAD_LOAD_LOADED:
            if item["epoch"] == floor_epoch:
                var res = ResourceLoader.load_threaded_get(path)
                _ready_count += 1
                emit_signal("floor_asset_ready", path, res, item["epoch"])
            done.append(path)
        elif st == ResourceLoader.THREAD_LOAD_FAILED:
            push_error("Load failed: %s" % path)
            done.append(path)
    for path in done:
        _inflight.erase(path)

Note: _ready_count increments per path, not per chunk. For two-path manifests, ratio hits 1.0 when both are ready.

Wiring _spawn_floor safely (30 minutes)

Typical pattern when a persistent Game root holds the player:

var _floor_holder: Node2D

func _spawn_floor(floor_id: String, epoch: int) -> void:
    if epoch != FloorLoadCoordinator.floor_epoch:
        return
    for child in _floor_holder.get_children():
        child.queue_free()
    var scene_path := "res://floors/%s.tscn" % floor_id
    var packed: PackedScene = null
    # Resource already loaded by coordinator — get from cache
    var res = ResourceLoader.load_threaded_get(scene_path)
    if res is PackedScene:
        packed = res
    if packed == null:
        push_error("Missing floor: %s" % floor_id)
        return
    var inst = packed.instantiate()
    _floor_holder.add_child(inst)

Adjust paths to match your manifest. The critical line is the epoch check at the top.

Minute-by-minute portal test script (read aloud while testing)

  1. Stand at portal entrance — note FPS
  2. Walk into portal — fade should start within one frame
  3. Watch progress bar — must move smoothly upward
  4. Floor appears — player collision works on new ground
  5. Press restart mid-fade — wrong floor must not appear
  6. Complete run portal A→B→A — memory in profiler should not climb unbounded

If step 5 fails, stop and fix epoch before adding art.

Exporting the evening build (15 minutes)

Project → Export your desktop preset. Confirm:

  • *.json manifests included (not filtered)
  • floors/ directory present
  • No accidental exclusion of audio/

Run exported exe once through portal test. Editor lies sometimes; exports tell truth.

When to call the evening “done”

You are done when:

  • Video clip shows moving progress bar
  • Epoch test documented pass
  • Markdown note exists with worst-frame ms
  • You can explain coordinator in one sentence to a friend

You are not required to refactor every load in the project tonight.

Fade overlay setup (20 minutes)

  1. Add CanvasLayer (layer 10) to portal or autoload UI scene
  2. ColorRect anchors full rect, color #000000, alpha 0.0 initially
  3. AnimationPlayer with two animations:
    • fade_out: alpha 0 → 1 over 0.25s
    • fade_in: alpha 1 → 0 over 0.25s

Track ColorRect → Modulate → Color → Alpha in the animation keys. If your theme uses modulate on a parent Control, animate that instead—one consistent property only.

Input: disable player movement at fade start; re-enable at fade end. Prevents walking into portal twice.

Audio: optional soft click when bar hits 100%—players associate sound with readiness even when loads are fast.

Lookahead optional stretch goal (if hour 4 has slack)

If floor B follows floor A linearly, queue floor B manifest when player kills the last enemy on A:

func _on_room_cleared() -> void:
    var next_epoch = FloorLoadCoordinator.floor_epoch
    FloorLoadCoordinator.queue_floor_pack(load_manifest("floor_b"), next_epoch)

Portal transition then feels instant—this is how 2026 roguelites fake “no load screens.”

Deck / slow storage profile (10 minutes)

Duplicate export preset deck with coordinator max_inflight = 3 if you use feature tags:

func _ready() -> void:
    if OS.has_feature("deck"):
        max_inflight = 3

Test once on Deck or HDD external drive—not required tonight, but high value before fest.

Release-evidence packet (10 minutes)

Minimum files after the evening:

release-evidence/
  floor-load-evening-pass.md
  floor-load-evening-pass.png   # profiler screenshot
  floor_a.manifest.json         # copy or link

Add to .gitignore only large captures—not the markdown note.

Troubleshooting table (beginner)

What you see Likely cause Fix tonight
Frozen game 1s Still sync load() Remove it
Bar stuck at 50% One path failed Check path spelling
Black forever _spawn_floor never called Connect ratio == 1
Wrong floor No epoch check Add guard
Instant crash instantiate null packed Wait for LOADED
Works in editor only Export missing file Include JSON in export

How this fits the “Your First X” series

Sibling tutorial What it proves
Telemetry event Players act
Color script Visual coherence
This pipeline Transitions do not hitch

Together they cover the three places beginners lose festival trust.

Unity developers switching engines

Unity’s LoadSceneAsync feels familiar. Godot’s threaded ResourceLoader is lower-level—you poll status yourself. The epoch idea maps to canceling AsyncOperation when players restart.

If you stay on Unity for SKU, use build manifest diff gates instead of this tutorial.

Phaser HTML5 sibling teams

Browser Phaser games should not copy this GDScript coordinator. Use the Phaser tilemap streaming preflight instead.

AI-assisted coding guardrails

If you use Cursor or Copilot to paste the coordinator:

  1. Ask for Godot 4.5 specifically
  2. Verify ResourceLoader.THREAD_LOAD_* enum names in your build
  3. Run epoch test manually—models forget guards

Same discipline as human-gated patch notes.

Ninety-minute compressed path (if four hours is not available)

Minutes Must-ship
0–25 Coordinator + one path smoke test
25–40 Manifest with two paths
40–60 Portal + progress bar
60–75 Epoch test
75–90 Profiler screenshot + markdown note

Skip lookahead and Deck profile until a second session.

Checklist — print this

  • [ ] Autoload registered
  • [ ] No sync load on transition path
  • [ ] Manifest JSON validates
  • [ ] Progress bar uses progress_ratio()
  • [ ] Epoch test pass ×3
  • [ ] Profiler screenshot saved
  • [ ] release-evidence note committed

Step 6 — Profiler proof (25 minutes)

Run with Debugger → Profiler during one transition.

Capture worst main-thread frame. Target: under 16 ms during poll loop on 60 FPS cap.

Save screenshot to release-evidence/floor-load-evening-pass.png.

Add three lines to release-evidence/floor-load-evening-pass.md:

# Floor load evening pass — 2026-05-16
- Build: debug/editor or export stamp
- Worst transition frame ms: ___
- Epoch test: pass/fail

Same folder pattern as color script and telemetry beginner pipelines.

Common beginner mistakes tonight

  1. Calling load() “just once” for the big scene — that is the hitch you are fixing
  2. Forgetting to add audio paths to manifest
  3. Progress bar on timer instead of progress_ratio()
  4. Not grouping player body for portal detection
  5. Skipping epoch test because “restart is rare”

When to stop for the night

Stop when one portal transition is smooth and documented. Do not refactor every scene tonight.

Tomorrow you can:

Pair with map streaming (optional second evening)

Large tilemaps need chunk discipline thinking even in Godot. This evening fixes scene packs; a second evening fixes tile regions.

Store and demo honesty

If web build cannot thread-load on your host, ship the non-threaded itch demo and document it per vertical slice labeling.

Building two test floors quickly (if you lack content)

If you only have one greybox room, duplicate it tonight:

  1. Save floor_a.tscn and floor_b.tscn with different wall colors
  2. Place portal at edge of A pointing to B
  3. Add Label with floor name for clarity in profiler clips

The coordinator does not care about art—only paths and epochs.

Signals optional upgrade (second week)

Connect floor_asset_ready to log slow paths:

func _ready() -> void:
    FloorLoadCoordinator.floor_asset_ready.connect(_on_asset)

func _on_asset(path: String, _res: Resource, ep: int) -> void:
    if ep != FloorLoadCoordinator.floor_epoch:
        return
    print("ready: ", path)

Print order reveals which asset finishes last—optimize that path first tomorrow.

Key takeaways

  1. One evening is enough for a defensible floor transition pipeline.
  2. Autoload coordinator + manifest + portal poll loop are the three pieces.
  3. Epoch prevents restart bugs.
  4. Progress bar must reflect real load state.
  5. Document proof in release-evidence/ like other “Your First X” tutorials.
  6. Deep architecture lives in the threaded ResourceLoader guide.
  7. Festival season is why May is the right month to do this, not October panic week.
  8. Inventory sync loads first—code comes second.
  9. Two-floor test is enough proof for a demo milestone.
  10. QA handoff is one sentence once coordinator exists.
  11. Phaser and Godot teams share epoch thinking, not copy-paste code.
  12. AI codegen still needs human epoch tests.
  13. Export preset must include manifest JSON—not only scripts.
  14. Screen capture before/after is the morale boost that keeps you refactoring tomorrow.
  15. Block: Floor load in the weekly operating review is how this habit survives post-launch pressure too.

What “smooth” feels like (calibrate expectations)

Before: Portal triggers → hard freeze → pop-in → player assumes bug.

After: Portal triggers → brief fade → progress bar moves → floor appears → fade in → player keeps playing.

You may still have 50–150 ms of instantiation cost. That is normal. You eliminated 500–900 ms sync load spikes.

Record a ten-second screen capture before and after. Paste both into release-evidence/—future you will forget the improvement.

Morning-after tasks (30 minutes, not tonight)

  1. Add floor C/D manifests using duplicate JSON
  2. Grep project again for sneaky load( in UI popups
  3. Share note in team chat with profiler ms number
  4. If using 21-day devlog habit, post “Tuesday tech detail: floor coordinator shipped”

QA handoff sentence

Give QA this line:

“Floor transitions should show progress bar; restart during fade should never spawn wrong floor; report ms freeze if bar stuck.”

That sentence replaces a thirty-minute explanation.

Co-op with save/load

If your roguelite saves mid-run:

  • Persist floor_id and floor_epoch in save data
  • On load, set FloorLoadCoordinator.floor_epoch before queueing manifest
  • Do not serialize loaded resources—serialize ids only

Cross-read save fuzz primer when persistence grows.

Accessibility

  • Progress overlay should not flash rapidly (photosensitivity)
  • Allow skip fade setting that keeps bar but shortens alpha animation
  • Screen reader text optional: “Loading next area” on overlay show

Legal / AI disclosure

This pipeline does not use AI at runtime. If you used AI to write scripts, still run human review before shipping—see AI disclosure checklist for store forms.

Glossary

  • Coordinator — Autoload that owns threaded load queue.
  • Manifest — JSON list of resource paths for one floor.
  • Epoch — Integer token invalidating stale loads.
  • Portal — Area that triggers transition sequence.
  • Hitch — Main-thread stall players perceive as freeze.

FAQ

I only have one scene. Do I need this?
Not until you add a second floor or heavy room swap.

Can I use change_scene_to_file instead?
You can, but you still need async load + epoch discipline for hitches.

Does this work in 4.4?
Mostly yes; verify enum names match your minor version.

What about additive scenes?
Same coordinator; spawn into a Node2D holder instead of replacing tree root.

I'm stuck at 0.5 progress forever.
One path failed threaded load—check Output for THREAD_LOAD_FAILED.

Can two portals share one coordinator?
Yes. Same autoload; different target_floor_id exports.

Should I thread-load UI scenes?
Usually no—UI is small. Floors are large.

Does this replace an loading screen scene?
It replaces sync work inside that scene. You may keep the scene as UI shell.

What if I use GLoot or other addons on floor spawn?
Initialize addons after epoch check in _spawn_floor.

Export size grew?
Manifest JSON is tiny. Growth means you duplicated floors—not coordinator fault.


Related reading (deep dives)


Close: Your first coordinator is a habit, not a hero script. Ship tonight’s portal smooth; let every future floor inherit the same room key.

If this evening saves one festival demo from a “crashed on floor change” review, it paid for itself. The programming guide is there when you scale from two floors to twenty—tonight you only need two rooms and an honest progress bar.