Lesson 2: GDScript Fundamentals and Project Architecture

In Lesson 1 you defined your core game concept and created a basic Godot 4 project with a main scene and a placeholder player. In this lesson you will learn the GDScript fundamentals you actually need for this course and shape your project into a clear, modular architecture.

The goal is not to memorize the entire language; it is to become comfortable enough with GDScript and scene structure that you can add new systems without rewriting everything later.

Step 1 – GDScript Basics You Will Use Constantly

GDScript is a lightweight, Python-like language tightly integrated with Godot. For most 2D action games, you will repeatedly use a small set of features:

  • Variables and types
  • Functions and return values
  • Signals and callbacks
  • Nodes and the scene tree

Variables and types

GDScript supports both dynamic and typed variables. For clarity and safety in a full project, use typed variables where it helps:

var health: int = 100
var speed: float = 200.0
var is_dead := false # type inferred as bool

You can declare constants for values that should not change:

const MAX_HEALTH := 100
const GRAVITY := 980.0

Functions and lifecycle methods

Every script can define custom functions and use Godot’s lifecycle callbacks:

func _ready() -> void:
    # Called when the node enters the scene tree
    initialize_player()

func _process(delta: float) -> void:
    # Called every frame
    update_ui(delta)

func _physics_process(delta: float) -> void:
    # Called at fixed intervals, ideal for movement and physics
    apply_movement(delta)

You will use _ready for one-time setup and _physics_process for movement and physics-heavy logic.

Step 2 – Understanding the Scene Tree and Node Composition

Godot’s power comes from the scene tree: every object in your game is a Node, and complex objects are made by composing smaller nodes.

Typical patterns:

  • A Player scene might contain:
    • CharacterBody2D root
    • Sprite2D for visuals
    • CollisionShape2D for physics
    • Optional AudioStreamPlayer2D for sounds

Instead of one script doing everything in a giant file, you can attach small scripts to specific nodes where it makes sense.

Decide what lives where

  • Character logic (movement, health, attacks) β†’ Script on the root CharacterBody2D
  • UI logic (updating health bars, showing prompts) β†’ Scripts on UI scenes
  • Game flow (starting levels, pausing, loading scenes) β†’ Scripts on a Game or GameManager node

Write down a quick mapping in your notes:

  • Which node controls game flow?
  • Which node owns player state?
  • Which nodes handle level transitions?

This mental map prevents you from sprinkling logic randomly in many scripts.

Step 3 – Create a GameManager Autoload

To coordinate your game across scenes, create a simple GameManager script and add it as an autoload (singleton).

  1. Create scripts/autoload/game_manager.gd:
extends Node

var current_level_name: String = ""
var player_max_health: int = 100
var player_current_health: int = 100

func _ready() -> void:
    print("GameManager ready")

func reset_player_state() -> void:
    player_current_health = player_max_health
  1. Add it as an autoload:
  • Go to Project β†’ Project Settings β†’ Autoload
  • Add game_manager.gd with the name GameManager
  • Click Add

Now GameManager is available globally:

GameManager.player_current_health -= 10

Use this for cross-scene state (current level, game difficulty, global flags), not for every tiny detail.

Step 4 – Organize Scripts by Role

To keep the project understandable, mirror your scenes with your scripts:

  • scripts/player/ – Movement, combat, animation controllers
  • scripts/enemies/ – AI, behavior, state machines
  • scripts/ui/ – Menus, HUD, overlays
  • scripts/levels/ – Level-specific triggers and logic
  • scripts/autoload/ – Singletons like GameManager

Adjust your folder structure if needed:

scripts/
  player/
    player_controller.gd
  enemies/
  ui/
  levels/
  autoload/
    game_manager.gd

This way, when you come back after a break, you know exactly where to look for logic.

Step 5 – Refine the Player Script with Clear Responsibilities

In Lesson 1 you added a basic movement script. Now rewrite it slightly to keep responsibilities clear and typed.

scripts/player/player_controller.gd:

extends CharacterBody2D

const SPEED := 200.0

var input_direction: Vector2 = Vector2.ZERO

func _ready() -> void:
    # Any one-time initialization for the player
    GameManager.reset_player_state()

func _physics_process(delta: float) -> void:
    read_input()
    move_player(delta)

func read_input() -> void:
    input_direction = Vector2.ZERO
    input_direction.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
    input_direction = input_direction.normalized()

func move_player(delta: float) -> void:
    velocity = input_direction * SPEED
    move_and_slide()

This small separation makes it easier to add things like dash, knockback, or status effects later.

Step 6 – Scene and Script Loading Patterns

You will often want to:

  • Load a level scene by name
  • Restart the current level
  • Switch between menus and gameplay

Add simple helper methods to GameManager:

func go_to_level(level_name: String) -> void:
    current_level_name = level_name
    var error := get_tree().change_scene_to_file("res://levels/%s.tscn" % level_name)
    if error != OK:
        push_error("Failed to load level: %s" % level_name)

func restart_level() -> void:
    if current_level_name == "":
        return
    go_to_level(current_level_name)

From UI or game flow scripts you can now do:

GameManager.go_to_level("level_01")

This centralizes level loading instead of scattering change_scene_to_file calls everywhere.

Step 7 – Mini Architecture Checklist

Before you move on to art and assets in Lesson 3, check that your architecture feels clear:

  • You have a GameManager autoload that stores cross-scene state.
  • Player logic lives in a dedicated player_controller.gd script.
  • Scripts are stored in logical folders (player, ui, levels, autoload).
  • You know which node is responsible for:
    • Game flow and scene changes
    • Player state
    • UI updates

If you cannot answer β€œwhere should this code live?” for a feature, note that down and refine your mental model before adding more systems.

Common Mistakes to Avoid in Lesson 2

  • Putting all game logic in one giant script. This makes refactoring painful and limits reuse.
  • Using autoloads for everything. Global singletons are helpful, but overuse leads to hidden dependencies.
  • Mixing UI and gameplay logic. Keep UI scripts focused on displaying state, not simulating the game.

Mini Challenge

By the end of this lesson:

  • GameManager is configured as an autoload and can track the current level.
  • Your player script is refactored into a clear player_controller.gd with typed fields and separate functions for input and movement.
  • Your scripts are reorganized into role-based folders under scripts/.

If all of this is in place, you will be ready for Lesson 3, where you will define an art pipeline and organize assets so they flow smoothly into your Godot 4 scenes.