GDScript Tutorial for Beginners: Godot's Language Explained

A GDScript tutorial for beginners: variables, functions, _ready and _process, input, @export, @onready, signals, typed code and fixes for common errors.

Home › Guides › GDScript Tutorial for Beginners: Godot's Language Explained

Illustration: GDScript for Beginners

GDScript is the programming language built into the Godot engine. It looks a lot like Python, uses indentation instead of curly brackets and was made for games, so a moving character takes about ten lines. This tutorial teaches the basics from zero. You will learn variables, functions, _ready and _process, input, @export and @onready, signals, typed code, arrays and dictionaries, plus fixes for the errors beginners hit most.

Time needed2 to 3 hours with practice
CostFree
You needGodot 4.4 or newer
Skill levelNo coding experience needed

What is GDScript?

A script is a text file full of instructions. In Godot, you attach a script to a node, and the script tells that node how to behave. GDScript files end in .gd, and the editor knows them well: it suggests names as you type and shows built-in help when you press F1 (Opt+Space on a Mac).

Godot also supports C#, but GDScript is the best first choice. It needs no extra setup, most Godot tutorials use it, and it works on every platform Godot can export to, including the web. It looks like Python, but it is its own language, so Python libraries do not work in it.

Your first script

Create a new scene with a Node2D root, click the Attach Script button at the top of the Scene dock, then click Create. Replace the code with this and press F6 (Cmd+R on a Mac) to run the scene. Godot asks you to save it first.

extends Node2D

# This runs once, when the node enters the game.
func _ready() -> void:
    print("Hello, Godot!")

"Hello, Godot!" appears in the Output panel at the bottom. Here is what each part means:

  • The line extends Node2D says this script adds behaviour to a Node2D. The script can use everything a Node2D already has, such as position.
  • A line starting with # is a comment: a note for humans that Godot ignores.
  • The word func starts a function, a named group of instructions. The -> void part means it gives nothing back.
  • The indented line belongs to the function. GDScript uses indentation (a tab) instead of brackets, so it matters where each line starts.

The GDScript style guide also suggests an order for the parts of a script. Following it makes every script easier to read:

The parts of a GDScript file, in order A short player script on the left with brackets pointing to five labelled parts on the right: 1 the class name and the node type it extends, 2 the signals it can send, 3 constants and variables, the node's memory, 4 built-in callbacks such as _ready that Godot calls, and 5 your own functions that you call. class_name Player extends CharacterBody2D signal died const SPEED := 200.0 @export var lives := 3 var coins := 0 @onready var sprite: Sprite2D = $Sprite2D func _ready() -> void: print("Hi!") func hurt() -> void: lives -= 1 1. Name and node type what this script is 2. Signals it can send 3. Constants and variables the node's memory 4. Built-in callbacks Godot calls these 5. Your own functions you call these
The usual order of a GDScript file. Godot does not force it, but readable scripts are easier to fix.

Variables, types and constants

A variable is a named box that holds a value, like a score or a speed. Each value has a type: text, a whole number, a decimal number and so on.

var player_name := "Sam"       # String: text
var health := 100              # int: a whole number
var speed := 250.5             # float: a decimal number
var is_alive := true           # bool: true or false
var spawn := Vector2(64, 128)  # Vector2: an x and a y
const MAX_HEALTH := 100        # a constant never changes

The := sign tells Godot to work out the type from the value and keep it. Vector2 holds two numbers and is used for positions and speeds in 2D. Variables at the top of a script belong to the node and keep their values. Variables made inside a function disappear when it ends. This is called scope.

To show a number inside text, use the % sign. For example, "Score: %d" % score puts the score where %d stands, and %s works for any value. You can also join text with + and str(), as in "Lives: " + str(lives).

Watch out

Dividing two whole numbers drops the decimals: 7 / 2 gives 3. Write 7 / 2.0 to get 3.5. Also remember that = sets a value, while == compares two values.

Functions, if and loops

Functions can take inputs, called parameters, and can give back a result with return. Inside them, if, elif and else make decisions, and loops repeat work.

func take_damage(amount: int) -> void:
    health -= amount  # same as: health = health - amount
    if health <= 0:
        print("Game over")
    elif health < 30:
        print("Low health!")
    else:
        print("Still fine")

func heal_amount(potions: int) -> int:
    return potions * 25

func _ready() -> void:
    take_damage(80)                # prints "Low health!"
    print(heal_amount(2))          # prints 50
    for i in 3:                    # i is 0, then 1, then 2
        print("Countdown: ", 3 - i)
    for enemy in ["Slime", "Bat"]:
        print(enemy, " appears!")

You can combine conditions with and, or and not, for example if is_alive and health < 30:. A while loop repeats as long as its condition is true, so make sure that something inside it changes. Otherwise the game freezes.

When a character can be in one of several states, an enum gives each option a name, and match runs the code for the current one. This is the start of a simple state machine:

enum State { IDLE, RUN, JUMP }  # three named options
var state := State.IDLE

func play_animation() -> void:
    match state:
        State.IDLE:
            $AnimatedSprite2D.play("idle")
        State.RUN:
            $AnimatedSprite2D.play("run")
        _:  # any other state
            $AnimatedSprite2D.play("jump")

_ready, _process and _physics_process

Some functions are never called by you. Godot calls them at the right moment, and their names start with an underscore. These callbacks run in a fixed order during a node's life:

The life of a node and its callbacks A timeline of the callbacks Godot calls. The node is created and _init runs, it is added to the scene tree and _enter_tree runs, then _ready runs once. After that, a large box shows what repeats every frame until the node is removed: _process each drawn frame, _physics_process sixty times a second and _input when a key, click or touch happens. Finally, when the node is removed, for example after queue_free, _exit_tree runs. Created _init() In the tree _enter_tree() Ready _ready() runs once Every frame, until removed: _process(delta) each drawn frame _physics_process(delta) 60 times a second _input(event) when a key, click or touch happens Removed _exit_tree() e.g. queue_free()
_ready runs once, and a node's children are ready before the node itself. The green box repeats until the node leaves the scene tree.
  • Use _ready() for setup, such as reading starting values. It runs once.
  • Use _process(delta) for things like animations and timers you count yourself. It runs every time the screen is drawn.
  • Use _physics_process(delta) for movement and anything that touches physics. It runs at a steady 60 times a second by default.

The delta value is the number of seconds since the last call, about 0.016 at 60 frames per second. Multiply speeds by delta, and your game runs at the same speed on fast and slow computers.

extends Sprite2D

var speed := 200.0  # pixels per second, not per frame

func _process(delta: float) -> void:
    position.x += speed * delta
Why you multiply movement by delta Three tracks show how far a sprite moves in one second. At 60 frames per second with delta, it takes many small steps and reaches 200 pixels. At 30 frames per second with delta, it takes half as many bigger steps and still reaches 200 pixels. At 30 frames per second without delta, it takes small steps and only reaches 100 pixels, so the game runs slower on slow computers. 60 FPS with delta many small steps 200 px 30 FPS with delta fewer, bigger steps 200 px 30 FPS, no delta half the distance! only 100 px start after 1 second
With delta, speed means "pixels per second", whatever the frame rate. Without it, slow computers run the game in slow motion.

Reading keys, mouse and touch

First, name your controls in Project > Project Settings > Input Map, for example move_left or jump. Then there are two ways to read them. You can ask the Input object every frame, which suits movement. Or you can react to single events in _unhandled_input(), which suits one-off actions like jumping or pausing. This example needs five actions: move_left, move_right, move_up, move_down and jump.

func _physics_process(delta: float) -> void:
    # A Vector2 such as (1, 0) while right is held.
    var dir := Input.get_vector("move_left", "move_right",
            "move_up", "move_down")
    position += dir * 200.0 * delta

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("jump"):
        print("Jump! This runs once per key press.")
    if event is InputEventMouseButton and event.pressed:
        print("Click at ", event.position)

Input.is_action_pressed("jump") is true for as long as the key is held. Input.is_action_just_pressed("jump") is true only in the first frame, which is what you want for a jump. Touch screens send InputEventScreenTouch events, which our mobile game guide covers.

Using @export and @onready

Words that start with @ are annotations: notes that change how Godot treats the next variable. These two are in almost every script:

extends CharacterBody2D

@export var speed := 200.0             # edit it in the Inspector
@export_range(1, 10) var lives := 3    # a slider from 1 to 10
@export var bullet_scene: PackedScene  # drag a .tscn file here

@onready var sprite: Sprite2D = $Sprite2D
@onready var jump_sound: AudioStreamPlayer2D = $JumpSound

The @export annotation shows the variable in the Inspector, so every copy of a node can have its own value. A value set in the Inspector wins over the number in your code. So if you change the code and nothing happens, click the little reset arrow next to the property in the Inspector.

The @onready annotation waits to fill the variable until the node's children exist, just before _ready() runs. Writing $Sprite2D is a short way to say get_node("Sprite2D"). Without @onready, that line would run too early, and the variable would hold null, which means nothing.

Signals in GDScript

A signal is a message a node sends when something happens. Other nodes can connect to it and run a function in response. Many nodes have built-in signals, such as a Button's pressed or a Timer's timeout. You can also make your own:

# player.gd
class_name Player
extends CharacterBody2D

signal health_changed(new_health: int)
signal died

var health := 3

func hurt() -> void:
    health -= 1
    health_changed.emit(health)  # tell anyone who listens
    if health <= 0:
        died.emit()
# hud.gd, on the HUD's CanvasLayer
extends CanvasLayer

@export var player: Player  # pick the Player in the Inspector
@onready var label: Label = $HealthLabel

func _ready() -> void:
    player.health_changed.connect(_on_health_changed)
    player.died.connect(func(): label.text = "Game over")

func _on_health_changed(new_health: int) -> void:
    label.text = "Health: %d" % new_health

The line class_name Player turns the script into a new type that other scripts can use. The player does not need to know the HUD exists: it only announces what happened. The func(): ... part is a lambda, a small function without a name. Signals also work with await, which pauses a function until the signal arrives:

func show_hint() -> void:
    await get_tree().create_timer(1.0).timeout  # wait 1 second
    print("One second later")

You can connect signals in the editor's Signals tab, too. Our guide to Godot nodes and scenes shows when to use signals, groups and autoloads.

Typed GDScript: catch bugs early

Typed GDScript means you state the type of each variable, parameter and return value. It is optional, but it is worth the habit. Godot then underlines mistakes in the editor before you even run the game, suggests better completions and runs your code faster.

var score: int = 0                     # stated type
var speed := 300.0                     # inferred type: float
var names: Array[String] = ["Ana", "Ben"]
var prices: Dictionary[String, int] = {"sword": 50}

func add_points(points: int) -> int:
    score += points
    return score

# add_points("ten") would be an error: "ten" is not an int.

Typed arrays like Array[String] only accept one type of item. Typed dictionaries like Dictionary[String, int] need Godot 4.4 or newer. To get reminders for untyped code, turn on Advanced Settings in Project Settings, open Debug > GDScript and set Untyped Declaration to Warn. Godot's static typing page lists every option.

Arrays and dictionaries

An array is an ordered list, like an inventory. A dictionary stores pairs of keys and values, like a character sheet where "hp" points to 10.

var inventory: Array[String] = ["sword", "potion"]
var stats := {"hp": 10, "attack": 3}

func _ready() -> void:
    inventory.append("key")        # add to the end
    print(inventory[0])            # "sword": counting starts at 0
    print(inventory.size())        # 3
    if "potion" in inventory:
        inventory.erase("potion")  # remove it
    for item in inventory:
        print("I have a ", item)
    print(inventory.pick_random()) # a random item

    stats["hp"] += 5               # change a value
    stats["speed"] = 2             # add a new key
    for key in stats:
        print(key, " = ", stats[key])

Counting starts at 0, so the last item of a three-item array is inventory[2]. Asking for inventory[3] stops the game with an error, because there is no fourth item. Arrays and dictionaries are the backbone of inventories, high scores, save files and level data.

Common GDScript errors and fixes

Errors show up in red at the bottom of the script editor, where a click jumps to the line. Errors that happen while the game runs appear in the Debugger panel, with the script and line number. These cause most beginner trouble:

The message starts with What it means How to fix it
Identifier "speeed" not declared in the current scope Godot does not know that name. It is a typo, or the variable was made inside another function. Check the spelling, or move the variable to the top of the script.
Node not found: "Sprit2D" The path after $ does not match any node. Copy the exact name from the Scene dock. Capital letters matter.
... on a base object of type 'null instance' The variable holds nothing, usually because of a wrong path or a deleted node. Fix the path, use @onready, and check that the node still exists.
Anything about indentation, tabs or spaces Mixed tabs and spaces, or a missing indent after a line that ends in : Indent the block. Use Edit > Indentation > Convert Indent to Tabs.
Too many arguments for "move_and_slide()" call Godot 3 code in a Godot 4 project. Set velocity, then call move_and_slide() with nothing inside the brackets.
No error, but nothing happens A signal is not connected, a callback is misspelled (_Ready), or the script is not attached. Check the connection icon and the spelling. Callbacks are lowercase.
Tip

When something behaves strangely, add print() lines to see what your variables really hold. Or click left of a line number to set a breakpoint, which pauses the game on that line so you can inspect every value.

How to practise

Reading code is not enough. You learn GDScript by typing it, breaking it and fixing it. Try these mini challenges, each about 15 minutes:

  1. Print your name five times with a for loop.
  2. Move a sprite to the right and make it wrap around to the left edge.
  3. Count how often Space is pressed and show the number in a Label.
  4. Give a player health, a health_changed signal and a HUD that listens.
  5. Store three item names in an array and print a random one when Enter is pressed.

Then build a whole small game with how to make your first game in Godot 4. AI chat tools can explain code you do not understand. Read how to use AI for Godot first, because they often write outdated Godot 3 code. For inspiration, browse the Godot games that players rank on Mutop100.

FAQ

Is GDScript easy to learn?

Yes, GDScript is one of the easiest game programming languages to start with. Its code is short and readable, and most beginners get a character moving on their first day.

Is GDScript the same as Python?

No, GDScript only looks similar to Python. It shares the indentation and some keywords, but it is a separate language made for Godot, and Python libraries do not work in it.

Should I learn GDScript or C# first?

Learn GDScript first if Godot is your main engine. It needs no setup and matches most Godot tutorials. If you also plan to use Unity, see Unity C# for beginners, since C# works in both engines.

Is GDScript fast enough for real games?

Yes, GDScript is fast enough for most indie games, and typed code runs faster still. Very heavy work can move to C# or C++ later, but beginners rarely need that.

What is the difference between _process and _physics_process?

The _process function runs once for every frame the screen draws, so its rate changes from computer to computer. The _physics_process function runs at a steady 60 times a second by default, so use it for movement and physics.

Where can I find the official GDScript reference?

The official reference is built into the editor: press F1 and search for any class or function. Online, start with Godot's GDScript reference page.

Turn your scripts into a real game

When your first Godot game is done, list it free on Mutop100. You get a game page, a spot among the Godot games and votes from players.

List your game free
← Bumalik sa MU Online Guides