You now have a controllable player, a working combat loop, and enemies that can fight back. The next step is to give that gameplay a home: actual levels that feel intentional rather than just a test room.

In this lesson you will:

  • Design small, focused 2D levels that play to your core mechanics.
  • Turn those layouts into Godot scenes using Tilemaps and reusable props.
  • Build a scene management system that can:
    • Load levels by name.
    • Restart the current level.
    • Move to the next level in a sequence.

By the end, you will have a simple but production‑ready level and scene‑flow system that we will keep reusing and extending in later lessons.


1. Define the Role of Your First Levels

Before opening Godot, decide what your first real levels should teach and test.

For this course, a sensible sequence looks like:

  • Level 1 – Movement sandbox

    • A small room that encourages moving, jumping (if you have it), and basic navigation.
    • Light enemy pressure or even no enemies at all.
  • Level 2 – Combat introduction

    • A corridor or arena where the player can practice attacking a few enemies with plenty of space.
    • Limited hazards so mistakes are recoverable.
  • Level 3 – Combined challenge

    • A layout that mixes:
    • Navigation (platforms, gaps, chokepoints).
    • Combat (enemies placed in interesting spots).

For this lesson, focus on Level 1 and Level 2. You can add more later using the same patterns.

Pro tip:

  • Keep levels shorter than you think.
  • It is better to have 3 tight levels than 1 oversized one that players never finish.

2. Build a Reusable Level Scene Template

Instead of building each level from scratch, create a base level scene you can duplicate.

  1. In Godot, create a new Node2D scene called BaseLevel.
  2. Add children to it:
    • TileMap (for ground and walls).
    • Node2D called Props (for barrels, decorations, etc.).
    • Node2D called Enemies (spawn positions or enemy instances).
    • Node2D called Markers (for spawn points, exits).
  3. Save it as:
scenes/levels/base_level.tscn

This template makes it easy to keep:

  • A consistent hierarchy across levels.
  • Clear locations for things like enemies and exits.

Later, scripts that search for child nodes (for example a scene manager that looks for a PlayerSpawn marker) can rely on that structure.


3. Create Your First Level Scene

With BaseLevel ready, you can create your first actual level.

  1. Right‑click base_level.tscn in the FileSystem dock and choose New Inherited Scene.
  2. Name it:
scenes/levels/level_01.tscn
  1. Select the TileMap and:

    • Assign the same Tileset you used earlier in the course.
    • Paint a simple room:
      • Solid floor.
      • Walls that enclose the space.
      • A door or opening that visually reads as an exit.
  2. Add a player spawn marker:

    • Under Markers, add a Marker2D and name it PlayerSpawn.
    • Position it where you want the player to start.
  3. Optionally add a level exit marker:

    • Under Markers, add another Marker2D named LevelExit.
    • Place it near the door or the far side of the room.

Your goal is not “perfect level design” but a clean, readable space that lets your core systems breathe.


4. Connect Levels to Your Existing Game Loop

Right now, your player and enemies probably live in a test scene. We want to load them into level_01 instead.

One simple pattern is to use a Game or Main scene that:

  • Instantiates the player.
  • Loads the current level.
  • Connects the two (for example spawns the player at PlayerSpawn).

4.1. Create a Game Scene

  1. Create a new scene called Game with a root Node2D.
  2. Add two children:
    • Node2D called LevelRoot.
    • Node2D called PlayerRoot.
  3. Save it as:
scenes/game.tscn

You will use this scene as your main entry point instead of dropping directly into a level scene.

4.2. Write a Simple Scene Manager Script

Attach a new script to the Game root, for example Game.gd:

extends Node2D

@onready var level_root: Node2D = $LevelRoot
@onready var player_root: Node2D = $PlayerRoot

var current_level_scene: PackedScene
var current_level_instance: Node2D

var current_level_index := 0
var level_paths := [
    "res://scenes/levels/level_01.tscn",
    "res://scenes/levels/level_02.tscn"
]

func _ready() -> void:
    load_level(current_level_index)

func load_level(index: int) -> void:
    if index < 0 or index >= level_paths.size():
        push_warning("Level index out of range")
        return

    # Clean up previous level
    if is_instance_valid(current_level_instance):
        current_level_instance.queue_free()

    current_level_scene = load(level_paths[index])
    current_level_instance = current_level_scene.instantiate()
    level_root.add_child(current_level_instance)

    spawn_player()

func spawn_player() -> void:
    # Assume you have a Player scene and autoload or factory for it.
    var player := get_or_create_player()

    var spawn := current_level_instance.get_node_or_null("Markers/PlayerSpawn")
    if spawn:
        player.global_position = spawn.global_position

    if player.get_parent() != player_root:
        player_root.add_child(player)

func restart_level() -> void:
    load_level(current_level_index)

func go_to_next_level() -> void:
    current_level_index += 1
    if current_level_index >= level_paths.size():
        current_level_index = 0  # or show a victory screen
    load_level(current_level_index)

Adapt get_or_create_player() to match how you currently instantiate your player. The important part is:

  • load_level() always:
    • Clears the previous level.
    • Instantiates the new level.
    • Calls spawn_player() to position the player at PlayerSpawn.

5. Wiring Exit Logic and Transitions

Your level exit marker should trigger a move to the next level. One straightforward integration is to let the player detect when it overlaps the exit.

5.1. Add an Exit Area

In level_01:

  1. Under Markers, add an Area2D called LevelExitArea.
  2. Add a CollisionShape2D to define the trigger zone.
  3. Place it where the player should stand to finish the level.

5.2. Connect to the Game Scene

In your player script (or a small trigger script), you can:

func _on_body_entered(body: Node) -> void:
    if body.name == "Player":
        var game := get_tree().get_first_node_in_group("GameRoot")
        if game:
            game.go_to_next_level()

Alternatively, you can connect the Area2D’s body_entered signal directly to a method on the Game node if you add it to a known group or path.

The exact connection depends on how you structured your scenes earlier in the course; the core idea is:

  • Exit → calls go_to_next_level() on the scene manager.

6. Testing and Iterating on Level Flow

With the basics wired up, test your level structure like a player would:

  • Start a new run and verify:
    • The player always spawns at PlayerSpawn in level_01.
    • Restarting a level returns you to a clean state (no leftover enemies).
    • Reaching the exit successfully loads the next level and respawns the player.

If anything feels fragile:

  • Add asserts and logging in Game.gd:
    • For missing markers.
    • For invalid level indices.
  • Keep your level scenes small and focused.
  • Name nodes consistently (PlayerSpawn, LevelExitArea, Enemies, Props).

This kind of discipline prevents “mystery bugs” later in the course when levels get more complex.


7. Troubleshooting and Common Mistakes

Player spawns at (0, 0) or off-screen

  • Check that:
    • Markers/PlayerSpawn exists in your level scene.
    • The path in get_node_or_null("Markers/PlayerSpawn") matches your hierarchy exactly.
    • You are using global_position, not position, if your level is nested under another node with transforms.

Level exit does nothing

  • Confirm that:
    • The Area2D for the exit has a collision shape.
    • Its collision layer/mask includes the player’s layer.
    • The signal is connected to a method that actually calls go_to_next_level().

Restarting the level leaves old enemies or props behind

  • Make sure load_level() frees the entire level instance before instancing a new one.
  • Avoid manually freeing children in multiple places; centralize cleanup in the scene manager.

8. What’s Next

In this lesson you:

  • Created a reusable level template with a clear node hierarchy.
  • Built your first real level scenes with Tilemaps and markers.
  • Implemented a simple scene management system to:
    • Load, restart, and advance levels.
    • Spawn the player correctly in each level.

In the next lesson, you will focus on UI and HUD design for your Godot 4 action game:

  • Health bars and damage feedback.
  • Ammo or resource displays (if applicable).
  • A pause menu that plays nice with your scene manager.

If you have time before moving on, design a second level with slightly more enemy pressure and make sure it slots into your level_paths array cleanly.