If you’re eager to dive into the exciting world of Godot development, it’s important to get a handle on the basics of programming! You don’t need to be a coding wizard to make a game, you just need to have a solid understanding the key concepts like variables, functions, loops, and arrays.
I previously put together the Book of Nodes (which I’m currently updating), covering the common nodes you’ll encounter on your game development journey. I thought it would be super helpful to create a companion resource — a friendly coding guide!
This guide covers the fundamentals of coding in Godot. We won’t delve into all the aspects of coding or GDScript, but we will cover the core structures necessary to begin coding.
What is GDScript?
GDScript is Godot’s own programming language, made just for games. It’s easy to read, beginner-friendly, and designed to integrate tightly with the Godot Editor.
Why use GDScript?
- It’s simple, fast, and built for Godot.
- It’s similar to Python (clean syntax, no extra symbols).
- You can write code directly on any node to control how it behaves .
A lot of developers switch over from Unity and therefore codes in Godot using C#. For that reason, we will be covering both GDScript and C# in this resource.
Navigation
This resource is broken down into the following sections:
1. Fundamentals
- Variables
- Data Types
- Constants
- Functions
- Comments
- Scope
2. Logic & Control
- If / Else
- Match (Switch)
- Loops (For / While)
- Break, Continue, & Return
3. Collections
- Arrays
- Dictionaries
- Resources
4. Gameplay Mechanics
- Signals (Events)
- Input
- Timers
- Physics Process vs. Process
- Random Numbers
- The Ready Function
5. Object & Scene Control
- Instancing Scenes
- Accessing Nodes
- Inheritance
- Export Variables
- Groups
6. Bonus: Polish & Organization
- Code Style Tips
- Debugging
- Autoloads (Singletons)
To make it easier to read, you can download the (free) Offline PDF version of this post here.
Part I: Fundamentals
Before embarking on a complex project, it’s crucial to grasp the foundational elements of coding. The Fundamentals section covers the core concepts that you will frequently use in nearly every Godot script. These principles form the basis of all programming, not just within Godot.
In this section, we will cover the following topics:
- In this section, we will cover the following topics:
- Variables — storing and reusing data
- Data Types — understanding different kinds of information
- Constants — setting fixed, unchanging values
- Functions — organizing your logic into reusable actions
- Comments — explaining your code for yourself and others
- Scope — understanding where and how your data exists
1. Variables
Variables are containers used to store reusable pieces of data such as numbers, text, or even entire nodes. You can think of them as your code’s “memory slots,” holding information like the player’s name, health, or the sound that plays when they take damage.
Variables can be defined globally, making them accessible in every piece of the script (and sometimes other scripts), or locally, within functions, where they exist only while that function runs.
Why Variables are Needed
Games constantly track information:
- Player health, score, or ammo
- Enemy positions
- Whether a door is open or not
- The name of the NPC you’re talking to
Without variables, a game wouldn’t know what happened before or what should happen next. It couldn’t reference or change values , making it impossible to keep track of states. Variables are the backbone of all programming.
Syntax
GDScript:
var variable_name = value
var player_health = 100
C#:
type variableName = value;
int playerHealth = 100
Best Practices
- Use clear, descriptive names — for example, player_health or enemy_speed, so it’s easy to understand what each variable represents.
- Follow snake_case naming in GDScript — write variable names in lowercase with underscores, like player_score or door_open.
- Follow camelCase naming in C# — write variable names in camel case, like playerScore or doorOpen.
- Avoid reserved keywords — don’t use words that Godot or GDScript already reserve for internal use (e.g. name, class, print), as this can cause errors or unexpected behavior.
Example
Here’s how you might define variables throughout your game.
The below code creates a player with the name “Bob”, who has 100 health and a sword.
GDScript:
var player_health = 100
var player_name = "Bob"
var has_sword = true
C#:
int playerHealth = 100;
string playerName = "Bob";
bool hasSword = true;
2. Data Types
Data types define the kind of information that a variable can store, such as numbers, text, boolean values (true/false), or even complex objects like nodes or images. They are essential for ensuring that your game can correctly utilize and process data while maintaining a standardized approach to handling and processing that data.
For example, by specifying the data type, we can prevent the addition of a string to a numeric data type. This means that you cannot assign “Bob” as a value for your health; it must always be a number or a float!
Why Data Types Are Needed
Every game needs to handle different kinds of information:
- Numbers: Used for health, speed, or score. These can be either integers (whole numbers like 1 or 99) or floats (decimal values, such as 1.42 or 9.87).
- Text: Refers to player names, dialogues, or menus. Text is defined as strings.
- Boolean Values: Represent true/false states, such as “Is the door open?” or “Is the player jumping?”
- Complex Types: Include structures like Vector2, Color, or Node, which are used for movement, visual representation, and object references.
Without data types, your game wouldn’t know how to compare, process, calculate, or display information properly. This could potentially lead to to confusing bugs and broken logic.
Types of Data
Syntax
GDScript:
var variable_name: Type = value
var player_speed: float = 4.5
C#:
Type variableName = value;
float playerSpeed = 4.5f;
Best Practices
- Always use the correct type for what you’re storing — use integers (int) for whole numbers, floats (float) for decimals, and strings (String) for text.
- Be consistent — don’t change a variable’s type mid-script (e.g., storing a number and later replacing it with text).
- Use type hints (GDScript 2.0+) or explicit types (C#) to make your code safer and easier to read.
- Learn Godot’s built-in types — like Vector2, Vector3, Color, Node, and Array, as they’re core to building any game.
Example
Here’s how you might define different data types for a player’s stats and position.
The below code creates a player with a name of “Bob” (value of string), a health of 100 and speed of 4.5 (numeric values), a sword (boolean), and position (Vector2).
GDScript:
var player_name: String = "Bob" #string
var player_health: int = 100 #integer
var player_speed: float = 4.5 #float
var has_sword: bool = true #boolean
var player_position: Vector2 = Vector2(200, 100) # complex type
C#:
string playerName = "Bob"; //string
int playerHealth = 100; //integer
float playerSpeed = 4.5f; //float
bool hasSword = true; //boolean
Vector2 playerPosition = new Vector2(200, 100); //complex type
3. Constants
Constants are values that never change while your game runs. They’re like permanent markers in your code — once defined, they stay the same no matter what happens.
You might use constants for things like maximum health, gravity, default speed, or level names. These are all values that should stay fixed and not be altered during gameplay.
Why Constants Are Needed
Constants make your code more organized, predictable, and easier to maintain.
They’re especially useful for:
- Fixed values that should never change, like MAX_HEALTH = 100 (we want our health to change, but never go over 100%)
- Game settings like gravity, jump force, or map size
- Avoiding “magic numbers” — instead of random hard-coded numbers, you give them clear names
- Preventing accidental changes to important values during gameplay
Without constants, you might end up reusing the same number in multiple places, and if you ever need to change it, you’ll have to hunt it down everywhere in your code.
Syntax
GDScript:
const CONSTANT_NAME = value
const MAX_HEALTH = 100
C#:
const Type CONSTANT_NAME = value;
const int MAX_HEALTH = 100;
Best Practices
- Use constants for fixed or shared values — especially ones that appear multiple times in your code.
- Name constants in ALL_CAPS to make them stand out (e.g., MAX_SPEED, GRAVITY).
- Group related constants together at the top of your script for better organization.
- Never modify a constant after it’s defined in your code. That defeats its purpose, so if you define it at the beginning of your code, avoid modifying its value later in a function.
Example
Here’s how you might define constants for player attributes and physics.
The code below creates constants that ensure the maximum health never surpasses 100%, sets a gravity force of 9.8, assigns the final player name as Bob, and establishes a spawn position that will never change.
GDScript:
const MAX_HEALTH = 100
const GRAVITY = 9.8
const PLAYER_NAME = "Bob"
const START_POSITION = Vector2(100, 200)
C#:
const int MAX_HEALTH = 100;
const float GRAVITY = 9.8f;
const string PLAYER_NAME = "Bob";
readonly Vector2 START_POSITION = new Vector2(100, 200);
4. Functions
Functions are reusable blocks of code that perform specific tasks. They let you organize your logic into smaller, manageable pieces, almost like instructions you can call at anytime.
Think of a function as a mini-program inside your script: instead of writing the same code over and over, you define it once and call it whenever needed.
Why Functions Are Needed
Games rely on repeated actions and logic. For example, an NPC should know when to walk and when to stop. Functions make that process clean and efficient.
For example:
- Moving a character
- Playing a sound when the player takes damage
- Spawning an enemy or item
- Saving or loading game data
Without functions, you’d need to duplicate the same lines of code in multiple places — making your game harder to read, debug, and maintain. Functions promote clean, modular, and reusable code.
Syntax
GDScript:
func function_name(parameters):
# code to run
return value
func add(a, b):
return a + b
C#:
ReturnType FunctionName(parameters)
{
// code to run
return value;
}
int Add(int a, int b) {
return a + b;
}
Best Practices
- Use clear, action-based names (e.g., move_player(), take_damage(), play_music()), so it’s obvious what the function does.
- Keep functions focused — each one should do one thing well.
- Use parameters to pass data into a function, and returns to get data back.
- Avoid long functions — break them into smaller pieces for readability and reuse.
- In GDScript, function names use snake_case; in C#, use PascalCase.
Example
The code defines a variable which stores the player’s health, and in the function, whenever the player takes damage, the health value changes. Thus, the function controls the logic for when the player takes damage.
GDScript:
# variable
var player_health = 100
# function to change variable
func take_damage(amount):
player_health -= amount
print("Player health:", player_health)
C#:
// variable
int playerHealth = 100;
// function to change variable
void TakeDamage(int amount)
{
playerHealth -= amount;
GD.Print("Player health: " + playerHealth);
}
5. Comments
Comments are notes that you leave inside your code that the computer completely ignores. They’re meant for you and other developers to explain what your code does, why you wrote it that way, or to temporarily disable certain lines during testing.
Think of comments as sticky notes for your future self. They help make your code readable, teachable, and easier to maintain.
Why Comments Are Needed
As you grow your game, your code may become confusing after a few weeks (or to someone new reading it). Comments help you remember the reasoning behind your decisions, and take notes on what you need to change or refer back to.
You might use comments to:
- Explain what a function or variable does
- Clarify complex logic or math
- Leave notes for future improvements
- Temporarily disable code without deleting it
Without comments, your codebase can quickly become a maze of unexplained logic — especially in large projects with multiple scripts and systems.
Syntax
GDScript:
# GDScript
# Single-line comment
# This explains a line of code
C#:
// CSharp
// Single-line comment
/* Multi-line
comment */
// This explains a line of code
Best Practices
- Write comments for clarity, not decoration. Use them to explain why something exists, not just what it does.
- Keep comments up to date. Outdated comments can be more confusing than no comments at all.
- Use comments to break your code into sections. It helps you navigate long scripts easily.
- In GDScript , comments start with #.
- In C# , comments start with //.
Example
Here’s how you might use comments to explain your code in both languages.
GDScript:
# Player starts with 100 health
var player_health = 100
# Function to apply damage to the player
func take_damage(amount):
player_health -= amount
print("Player health:", player_health) # Debug print to console
C#:
// Player starts with 100 health
int playerHealth = 100;
/* Function to apply damage
and display remaining health */
void TakeDamage(int amount)
{
playerHealth -= amount;
GD.Print("Player health: " + playerHealth); // Debug print
}
6. Scope
Scope determines where a variable or function can be accessed in your code. It’s like defining whether a certain piece of logic is available everywhere or only inside a specific block of code.
Why Scope Is Needed
Scope keeps your code clean, predictable, and safe from unwanted changes.
In games, you might want some data to be accessible everywhere (like the player’s score), while other data should only exist temporarily inside a function (like a loop counter or a temporary position).
Without scope rules, variables could overwrite each other, cause bugs, or leak data across scripts unintentionally.
Here’s the most common scopes:
- Global scope: Variables and functions that can be accessed from anywhere in the script, and sometimes by other scripts.
- Local scope: Variables that only exist inside a specific function or block. They’re created when the function runs and destroyed when it ends.
- Access modifiers (C# only): Define who can access a variable or function from other scripts.
Syntax
GDScript:
# Global variable
var global_var = value
func some_function():
var local_var = value # Local to this function - you cannot call it outside of here
C#:
// Global variable
int globalVar = 100;
void SomeFunction()
{
int localVar = 50; // Only accessible inside this function
}
// Access modifiers (C# only)
public int localVar = 100; // Accessible by all scripts
private int localVar = 30; // Accessible only in this class
protected int localVar = 10; // Accessible in this class and subclasses
internal int localVar = 0; // Accessible within the same assembly
Best Practices
- Use local scope whenever possible — it keeps variables self-contained and prevents accidental changes elsewhere.
- Use global scope only for shared data like settings, player stats, or managers that must be accessed by multiple scripts.
- Avoid naming conflicts — two variables with the same name in different scopes can cause confusion.
- In GDScript , define globals outside of functions (like at the top of your script), and locals inside.
- In C# , use access modifiers like public, private, and protected to control visibility.
Example
Here’s a simple example showing the difference between global and local scope.
GDScript:
# Global variable - any function can access this, even other scripts
var player_health = 100
func take_damage(amount):
# Local variable (only exists while this function runs)
var new_health = player_health - amount
print("After damage:", new_health)
C#:
// Global variable - any function can access this, even other scripts
public int playerHealth = 100; // Accessible by all scripts
private int ammoCount = 30; // Accessible only in this class
protected int armor = 10; // Accessible in this class and subclasses
internal int score = 0; // Accessible within the same assembly
void TakeDamage(int amount)
{
// Local variable (only exists within this function)
int newHealth = playerHealth - amount;
GD.Print("After damage: " + newHealth);
}
Part II: Logic & Control
Games and programs are filled with decisions — should the player take damage? Should the door open? Should the enemy chase or retreat?
Logic and control structures enable your game to think, react, and adapt to various situations. These tools instruct your code on what actions to take and when to take them. They serve as the brain behind your game’s behavior, controlling the flow of the game, responding to player input, and determining outcomes.
In this section, we will explore the following concepts:
- If / Else : Making choices
- Match (Switch) : Handling multiple possibilities
- Loops (For / While) : Repeating actions efficiently
- Break, Return, & Continue : Controlling loop & function behavior
1. If / Else
If / Else statements are the foundation of decision-making in programming. They let your game choose different paths depending on whether certain conditions are true or false.
You can think of them like branching roads — your game checks a condition, and depending on the result, it takes one route or another.
Why If / Else Is Needed
Every game relies on conditions, for example:
- If the player’s health reaches zero → trigger game over
- If the score is high enough → unlock a new level
- If a key is collected → open a door
- If the player presses jump → play jump animation
Without conditional logic, your game would run in a straight line with no reaction to what happens — no decisions, no consequences, and no interactivity.
Syntax
GDScript:
if condition:
# code
elif other_condition:
# code
else:
# code
C#:
if (condition)
{
// code
}
else if (otherCondition)
{
// code
}
else
{
// code
}
Best Practices
- Keep conditions clear and readable — use descriptive variable names so your logic reads like a sentence (if player_health <= 0:).
- Avoid deep nesting — too many nested if statements make code hard to follow. Consider using match or early returns instead.
- Use elif for multiple related conditions instead of chaining separate ifs.
- Test edge cases — ensure your logic works at boundaries (e.g., 0 health, max score).
Example
Here’s a simple block of code that uses if/else statements to determine whether the player is alive, low on health, or defeated.
GDScript:
var player_health = 50
# If health is more than zero, the player is alive.
if player_health > 50:
print("Player is alive!")
# If health is between 1 and 50, warn that it's low.
elif player_health > 0 and player_health <= 50:
print("Player health low")
# Otherwise, the player is defeated.
else:
print("Player is defeated.")
C#:
int playerHealth = 50;
// If health is more than zero, the player is alive.
if (playerHealth > 50)
{
GD.Print("Player is alive!");
}
// If health is between 1 and 50, warn that it's low.
else if (playerHealth > 0 && playerHealth <= 50)
{
GD.Print("Player health low");
}
// Otherwise, the player is defeated.
else
{
GD.Print("Player is defeated.");
}
2. Match (Switch)
The Match statement (called Switch in many other languages) is a cleaner, more organized way to handle multiple possible outcomes for a single value.
Instead of writing a long chain of if and elif statements, match lets your code check one variable against several conditions, thus keeping your logic simple and easy to read.
Think of it like a menu of possibilities: the program picks the one that matches and runs the code for that case.
Why Match Is Needed
When your game needs to react differently to multiple options — such as player input, item types, or enemy states — match keeps your logic tidy.
For example:
- Checking which key was pressed
- Determining what item the player picked up
- Reacting to an enemy’s state (idle, chasing, attacking)
- Handling dialog choices or menu selections
Without match, you’d end up with repetitive if/elif blocks that are harder to maintain.
Syntax
GDScript:
match value:
pattern1:
# code
pattern2:
# code
_:
# default case
C#:
switch (value)
{
case pattern1:
// code
break;
case pattern2:
// code
break;
default:
// default case
break;
}
Best Practices
- Use match for clear, single-variable comparisons — it’s cleaner than stacking if/elif.
- Include a _ (default) case to catch unexpected values or errors.
- Keep cases short and direct — avoid nesting too much logic inside each one.
- In C# , use the switch statement (and consider switch expressions in newer C# versions for concise code).
Example
Here’s how you might use match to handle player actions. The logic of the game will change depending on the state of the player.
GDScript:
# Player state
var action = "jump"
# Change state based on action
match action:
"attack":
print("Player attacks!")
"jump":
print("Player jumps!")
"block":
print("Player blocks!")
_:
print("Unknown action!")
C#:
// Player state
string action = "jump";
// Change state based on action
switch (action)
{
case "attack":
GD.Print("Player attacks!");
break;
case "jump":
GD.Print("Player jumps!");
break;
case "block":
GD.Print("Player blocks!");
break;
default:
GD.Print("Unknown action!");
break;
}
3. Loops (For / While)
Loops allow your code to repeat actions automatically without writing the same line over and over. They’re essential for anything that needs to run multiple times — like moving enemies, checking objects, or counting through a list.
Think of loops as automated cycles: you define what should happen and how long it should continue
Why Loops Are Needed
Games rely on repetition — from drawing frames to checking collisions. Loops make that process simple and efficient.
You might use them to:
- Move every enemy in a list
- Spawn several coins or projectiles
- Update player stats each frame
- Repeat something until a condition is met
Without loops, you’d have to manually duplicate code for every instance — wasting time and creating room for errors.
Syntax
GDScript:
for element in collection:
# repeated code
while condition:
# repeated code
C#:
foreach (var element in collection)
{
// repeated code
}
while (condition)
{
// repeated code
}
Best Practices
- Use for loops when you know how many times something should run (e.g., counting or iterating through arrays).
- Use while loops when you don’t know how long something will continue (e.g., waiting for an event or condition).
- Avoid infinite loops — always include a clear condition or break.
- Use descriptive loop variables (for enemy in enemies: instead of for i in range(…)).
- Keep loop bodies small — too much logic inside can hurt performance.
Example
Here’s how you might use both types of loops to manage enemies in a scene.
The for loop will iterate over a list of enemies (Goblin, Orc, and Troll), and for each enemy, it will print a message indicating that they are ready to attack.
The while loop will decrease the player’s health as long as their current health is more than 0. Once the player’s health reaches 0, the loop will stop.
GDScript:
# Array of enemies
var enemies = ["Goblin", "Orc", "Troll"]
# FOR LOOP: Go through a list of enemies
for enemy in enemies:
print(enemy, "is ready to attack!")
# WHILE LOOP: Reduce player health over time
var player_health = 100
while player_health > 0:
player_health -= 10
print("Player health:", player_health)
C#:
// Array of enemies
string[] enemies = { "Goblin", "Orc", "Troll" };
// FOR LOOP: Go through a list of enemies
foreach (string enemy in enemies)
{
GD.Print(enemy + " is ready to attack!");
}
// WHILE LOOP: Reduce player health over time
int playerHealth = 100;
while (playerHealth > 0)
{
playerHealth -= 10;
GD.Print("Player health: " + playerHealth);
}
4. Break, Continue, & Return
Inside loops and functions, break, continue , and return are special keywords that give you precise control over how and when your code stops or skips certain actions.
- break — immediately stops a loop , even if it hasn’t finished all its cycles.
- continue — skips the rest of the current loop cycle and jumps to the next one.
- return — exits a function immediately and optionally sends a value back.
Think of them as traffic signs for your logic flow: “Stop here,” “Skip ahead,” or “Exit and report back.”
Why They’re Needed
Games constantly run logic loops and functions that depend on changing conditions. These keywords give you fine-grained control over when that logic should stop, skip, or exit.
You might use them to:
- break when you’ve found the first matching item in a list
- continue to skip inactive enemies or irrelevant data
- return to exit a function early (like when the player is already dead)
- Save performance by stopping unnecessary calculations
Without these control statements, loops and functions would run to completion every time — wasting cycles and cluttering logic.
Syntax
GDScript:
break # Exit loop immediately
continue # Skip to next loop iteration
return value # Exit function and return value
C#:
break; // Exit loop immediately
continue; // Skip to next loop iteration
return value; // Exit function and return value
Best Practices
- Use break only when needed to exit loops early.
- Use continue for filtering logic , such as skipping invalid data.
- Use return for early exits in functions , especially for edge-case checks.
- Avoid multiple returns in long functions — too many can make code flow harder to follow.
- Keep conditions clear and simple to prevent confusion when jumping out of loops or functions.
Example
Here’s how you might combine all three in a single gameplay function.
In the code below, we loop through our list of enemies. If we encounter an invalid enemy, we use the continue statement to skip to the next iteration. If we find an Orc in the list, we print a message and continue with the remaining logic (checking for dragon). If we come across a dragon, we print a message and exit the loop using the break statement. Once the loop is complete, we return, which stops the function from executing further.
GDScript:
func find_enemy(enemies):
for enemy in enemies:
if enemy == null:
continue # Skip missing entries
if enemy == "Orc":
print("Ignoring Orc.")
continue # Skip missing entries
if enemy == "Dragon":
print("Boss found! Stopping search.")
break # Stop loop
print("Spotted:", enemy)
return "Search complete." # Exit function and return a message
C#:
string FindEnemy(string[] enemies)
{
foreach (string enemy in enemies)
{
if (enemy == null)
continue; // Skip missing entries
if (enemy == "Orc")
{
GD.Print("Ignoring Orc.");
continue; // Skip missing entries
}
if (enemy == "Dragon")
{
GD.Print("Boss found! Stopping search.");
break; // Stop loop
}
GD.Print("Spotted: " + enemy);
}
return "Search complete."; // Exit function and return a message
}
Part III: Collections
As your games develop, you’ll soon realize that single variables, like player_name and enemy, aren’t sufficient. You will need ways to store, organize, and manage groups of data, such as multiple enemies, items, or lines of dialogue.
Collections are special data structures that enable you to handle many values simultaneously, making your scripts more dynamic, efficient, and powerful.
In this section, we’ll cover:
- Arrays — ordered lists of values
- Dictionaries — key-value pairs for labelled data
- Resources — saved or reusable data objects
1. Arrays
Arrays are ordered lists that can store multiple values in a single variable.
You can think of them as containers or shelves, where each slot holds an item — like a list of enemies, levels, or collected items.
Each item in an array is stored at a numbered position called an index , starting at 0.
Why Arrays Are Needed
Arrays are essential whenever you need to handle more than one piece of related data.
For example:
- A list of enemies in a level
- All items in the player’s inventory
- A sequence of checkpoints or spawn points
- A queue of dialog lines or sound effects
Without arrays, you’d need a separate variable for every element (enemy1, enemy2, enemy3…), which quickly becomes unmanageable and performance heavy.
Syntax
GDScript:
var array_name = [value1, value2, value3]
array_name.append(value)
array_name[index]
C#:
Type[] arrayName = { value1, value2, value3 };
arrayName[index];
var listName = new List<Type>() { value1, value2, value3 };
listName.Add(value);
GDScript Methods
C# Methods
Best Practices
- Use arrays for ordered data where position matters.
- Access elements by index — array[0] gets the first item.
- Use loops to efficiently process all items.
- Be careful with indices — trying to access an index that doesn’t exist causes an error.
- Use methods like append(), erase(), or size() to manage array contents.
Example
Here’s how you might use arrays to handle multiple enemies.
We start by defining an array named enemies, which holds a list of enemy names. Next, we print the first element in the array (at index 0), which is “Goblin”. After that, we add a new enemy “Dragon” to the list. When we print the array again, it will then display the updated list: [“Goblin”, “Orc”, “Troll”, “Dragon”]
GDScript:
var enemies = ["Goblin", "Orc", "Troll"]
# Access the first element
print(enemies[0]) # Output: Goblin
# Add a new enemy
enemies.append("Dragon")
# Loop through the list
for enemy in enemies:
print(enemy)
C#:
// Create an array of enemies
string[] enemies = { "Goblin", "Orc", "Troll" };
// Access the first element
GD.Print(enemies[0]); // Output: Goblin
// Convert to a dynamic list to add new items
var enemyList = new System.Collections.Generic.List<string>(enemies);
enemyList.Add("Dragon");
// Loop through the list
foreach (string enemy in enemyList)
{
GD.Print(enemy);
}
2. Dictionaries
Dictionaries store data as key–value pairs, allowing you to label each piece of information instead of relying on numerical positions.
Think of a dictionary as a filing cabinet — the key is the label on the drawer, and the value is what’s inside. This makes your data easier to understand and access, especially when order isn’t important but meaning is.
Why Dictionaries Are Needed
Dictionaries are perfect for data that needs names instead of numbers.
For example:
- Storing player stats like {“health”: 100, “mana”: 50}
- Tracking inventory items and their quantities
- Keeping NPC dialog by character name
- Mapping key bindings or configuration settings
Without dictionaries, you’d need separate variables or parallel arrays for every related value — which quickly gets messy.
Syntax
GDScript:
var dict_name = {
"key1": value1,
"key2": value2
}
dict_name["key3"] = value3
C#:
var dictName = new Dictionary<string, Type>()
{
{ "key1", value1 },
{ "key2", value2 }
};
dictName["key3"] = value3;
GDScript Methods
C# Methods
Best Practices
- Use dictionaries when labels make data clearer — e.g., player["health"] is more readable than player[0].
- Access values by key , not index — use dict["key_name"].
- Check for existence before accessing a key to avoid errors (if “health” in player:).
- Use nested dictionaries for structured data (like characters, stats, or items).
- In C# , use Dictionary for type-safe collections.
Example
Here’s how you might use dictionaries to store player stats and inventory.
In the code below, we create a new dictionary for our players values — storing their name, health, and mana. We then add a new value to the dictionary, which will store the amount of gold that they have. Finally, we iterate over the dictionary to return the updated dictionary, which will be: {“name”: “Bob”, “health”: 100, “mana”: 50, “gold”: 250}
GDScript:
# Create a dictionary of player stats
var player = {
"name": "Bob",
"health": 100,
"mana": 50
}
# Access a value by key
print(player["health"]) # Output: 100
# Add a new key-value pair
player["gold"] = 250
# Loop through keys and values
for key in player:
print(key, ":", player[key])
C#:
using System.Collections.Generic;
// Create a dictionary of player stats
Dictionary<string, object> player = new Dictionary<string, object>()
{
{ "name", "Bob" },
{ "health", 100 },
{ "mana", 50 }
};
// Access a value by key
GD.Print(player["health"]); // Output: 100
// Add a new key-value pair
player["gold"] = 250;
// Loop through all keys and values
foreach (var stat in player)
{
GD.Print(stat.Key + ": " + stat.Value);
}
3. Resources
Resources in Godot are data objects that can be saved, loaded, and reused across your project. They’re stored as .tres (text-based) or .res (binary) files, and can contain variables, configurations, or even scripts — anything from item stats and materials to player data or configuration settings.
Think of them as data containers that live outside your scripts, allowing you to manage information in a clean, modular way.
Why Resources Are Needed
Resources make your game more modular, reusable, and data-driven. Instead of hardcoding every item and managing a list of arrays or dictionaries, you can store that information in Resource files and reuse them across multiple scenes or objects.
You’ll typically use Resources to:
- Store item stats (like damage, rarity, or price).
- Define enemy attributes or character abilities.
- Create configuration files for tuning and game balance.
- Create NPC, enemy, or character data which can be loaded into scenes.
- Save and load player progress or inventory data.
- Define materials, audio effects, shaders, and particle settings.
Without resources, you’d have to duplicate data across scripts or scenes, making your project harder to update, maintain, and balance.
Custom Resource Example
You can create your own resource classes to store structured data, such as an RPG item database or character stats.
GDScript
# ItemData.gd
extends Resource
@export var name: String
@export var damage: int
@export var price: int
Once you’ve created your custom Resource script (for example, ItemData.gd), you can easily create Resource files directly inside the Godot Editor — no code needed.
To create a Resource file:
- In the FileSystem panel, right-click anywhere in your data/ folder (or wherever you want to store it).
- Select New Resource → Create Resource.
- In the window that appears, scroll down and select your custom class (e.g., ItemData).
- Click Create , then assign your script (ItemData.gd) if it’s not already linked.
- You’ll now see your custom exported variables (like name, damage, and price) in the Inspector.
- Fill in their values — e.g. name = "Sword", damage = 10, price = 100.
- Save it as sword.tres.
For example, you could create multiple item resources:
data/
sword.tres
axe.tres
potion.tres
Then load them in-game:
var sword = load("res://data/sword.tres")
print(sword.name, "does", sword.damage, "damage")
C
// ItemData.cs
using Godot;
[GlobalClass]
public partial class ItemData : Resource
{
[Export] public string Name { get; set; }
[Export] public int Damage { get; set; }
[Export] public int Price { get; set; }
}
You can now create .tres resources from this script directly in the editor — each one representing an item.
Best Practices
- Use .tres for text-based resources (readable and version control friendly).
- Keep reusable data in res://data/ or a similar dedicated folder.
- Use custom Resource scripts for structured data (like items, quests, or NPCs).
- Don’t hardcode data — load and reference Resource files instead.
- Reuse Resources between multiple nodes or scenes whenever possible.
- Avoid circular references (when Resources reference each other in a loop).
Example
Here’s how you can create, load, and use Resources dynamically to power your game’s item system.
GDScript
# Game.gd
extends Node
func _ready():
var sword = load("res://data/sword.tres")
print("Picked up:", sword.name)
print("Damage:", sword.damage)
C
using Godot;
public partial class Game : Node
{
public override void _Ready()
{
var sword = (ItemData)GD.Load("res://data/sword.tres");
GD.Print($"Picked up: {sword.Name}");
GD.Print($"Damage: {sword.Damage}");
}
}
Part IV: Gameplay Mechanics
Now that you understand how to store data and control logic, it’s time to explore the coding features that can help make your game interactive. Gameplay mechanics are what bring your project to life by connecting player actions, objects, and events in real time. They’re what make doors open, characters jump, and music change when you take damage.
In this section, we’ll explore:
- Signals (Events) — letting nodes communicate with each other
- Input — handling player actions and controls
- Timers — triggering delayed or repeated actions
- Physics Process vs. Process — controlling frame updates and physics logic
- Random Numbers — adding unpredictability and variety
1. Signals (Events)
Signals are Godot’s way of letting nodes communicate without being directly connected. Think of them as events, where one node or part of the code “emits” a signal, and another “listens” and reacts to it. This makes your game more modular and organized, since nodes don’t need to directly reference each other to interact.
You can use signals for all sorts of gameplay interactions:
- Notify the game that the Start button was pressed → start the game
- Have the UI flash red when the player takes damage
- When an action is pressed, tell the door to open
- When an animation is complete, switch to the next one
Signals turn isolated nodes into a living system that reacts to events in real time, which is a key concept for dynamic, responsive gameplay.
Why Signals Are Needed
Signals allow for clean, modular communication between game elements.
You’ll use them to:
- Detect button presses in UI
- React when a player’s health changes
- Trigger events when a collision shape is entered or exited
- Notify scripts when timers finish or animations end
Without signals, nodes would need to constantly “poll” each other, ultimately creating messy code.
Syntax
I like to think that signals are comprised of three main parts: Event → Listener → Response
GDScript:
# Emitting a signal (Event)
signal health_changed(new_health)
health_changed.emit_signal(80)
# Connecting a signal (Listener)
button.pressed.connect(_on_button_pressed)
# Defining the action (Response)
func _on_button_pressed():
print("Button was pressed!")
C#:
// Emitting a signal (Event)
[Signal]
public delegate void HealthChangedEventHandler(int newHealth);
EmitSignal(nameof(HealthChanged), 80);
// Connecting a signal (Listener)
button.Pressed += OnButtonPressed;
// Response method (Response)
private void OnButtonPressed()
{
GD.Print("Button was pressed!");
}
Best Practices
- Use signals to decouple logic — nodes shouldn’t depend directly on each other
- Connect signals in _ready() or in the editor for clarity.
- Name custom signals descriptively , like player_died or door_opened.
- Disconnect unused signals to prevent unexpected behavior.
Example
Here’s a simple setup where pressing a button updates the player’s health bar.
In the code below, we define a signal called health_changed in the Player script. When our player takes damage, we emit this signal to notify the other parts in our game that our health has changed.
When the signal is emitted, the other parts of our code which are connected to it, should execute their logic. In this case, we have another script called UI, which connects to this signal in the ready() function. We are connecting this signal to a new function in this script called on_health_changed. This tells the game our UI script is listening for events (emits) from the Player script, and whenever that event occurs, the logic within the function will execute - which in this case is a simple print statement notifying us of our new health value.
GDScript:
# Player.gd
# Signal creation
signal health_changed(new_health)
var health = 100
# Signal emission (Event)
func take_damage(amount):
health -= amount
health_changed.emit(health)
# UI.gd
# Signal connection (Listener)
func _ready():
var player = get_node("../Player")
player.health_changed.connect(_on_health_changed)
# Signal action (Response)
func _on_health_changed(new_health):
print("Player health:", new_health)
C#:
// Player.cs
// Signal creation
[Signal]
public delegate void HealthChangedEventHandler(int newHealth);
int health = 100;
// Signal emission (Event)
public void TakeDamage(int amount)
{
health -= amount;
EmitSignal(nameof(HealthChanged), health);
}
// UI.cs
// Signal connection (Listener)
public override void _Ready()
{
var player = GetNode("../Player");
player.Connect("HealthChanged", this, nameof(OnHealthChanged));
}
// Signal action (Response)
private void OnHealthChanged(int newHealth)
{
GD.Print("Player health: " + newHealth);
}
2. Input
Input is how players interact with your game. This can be via pressing keys, clicking, or touching the screen to control characters and trigger actions. Godot’s input system makes it easy to detect and respond to player actions through code or the Input Map (found in Project → Project Settings → Input Map).
You can think of Input as the player’s choices, and your game listens and reacts accordingly.
Why Input Is Needed
Every interactive game depends on Input:
- Moving the player character
- Jumping, attacking, or using abilities
- Navigating menus or UI
- Detecting gamepad or touchscreen actions
Without Input handling, your game wouldn’t know what the player wants to do, and the player won’t have any way of interacting with the game, they would just sit idle!
Syntax
GDScript:
# Detecting input actions continuously (we'll move right as long as we hold key)
func _process(delta):
if Input.is_action_pressed("move_right"):
# logic
# Detecting input actions once-off (we'll jump once and have to press key again)
func _input(event):
if event.is_action_pressed("jump"):
# logic
C#:
// Detecting input actions continuously (we'll move right as long as we hold key)
public override void _Process(double delta)
{
if (Input.IsActionPressed("move_right"))
// logic
}
// Detecting input actions once-off (we'll jump once and have to press key again)
public override void _Input(InputEvent @event)
{
if (@event.IsActionPressed("jump"))
// logic
}
Input Methods
Best Practices
- Use the Input Map instead of hardcoding keys — this lets players rebind controls easily.
- Check for actions (like "ui_accept" or "move_left") rather than specific keys.
- Use _process() for continuous input (movement), and _input() for one-time actions (button presses).
- Support multiple devices (keyboard, controller, touchscreen) whenever possible.
- Keep input logic separate from gameplay logic — use signals or dedicated functions to handle actions cleanly.
Example
Here’s a simple movement script that lets a player move left and right, and jump when the spacebar is pressed.
In the code below, the move_right and move_left keys have been assigned to the ← → keys in the Project Settings , and the jump key to the spacebar.
GDScript:
extpends CharacterBody2D
const SPEED = 200
const JUMP_FORCE = -400
func _physics_process(delta):
var velocity = Vector2.ZERO
# Move player left and right
if Input.is_action_pressed("move_right"):
velocity.x += SPEED
elif Input.is_action_pressed("move_left"):
velocity.x -= SPEED
# Move player up (jump)
if is_on_floor() and Input.is_action_just_pressed("jump"):
velocity.y = JUMP_FORCE
velocity = move_and_slide(velocity, Vector2.UP)
C#:
using Godot;
public partial class Player : CharacterBody2D
{
const float Speed = 200f;
const float JumpForce = -400f;
public override void _PhysicsProcess(double delta)
{
Vector2 velocity = Velocity;
// Move player left and right
if (Input.IsActionPressed("move_right"))
velocity.X = Speed;
else if (Input.IsActionPressed("move_left"))
velocity.X = -Speed;
else
velocity.X = 0;
// Move player up (jump)
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
velocity.Y = JumpForce;
Velocity = velocity;
MoveAndSlide();
}
}
3. Timers
Timers are nodes that let you run code after a delay or repeatedly at fixed intervals. They’re essential for creating time-based events such as cooldowns, countdowns, enemy spawns, or temporary effects.
Think of a timer as an alarm clock inside your game: you set it, wait, and when it rings, something happens.
Why Timers Are Needed
Games constantly rely on timing to feel dynamic and balanced:
- Countdown before starting a match
- Enemy attack intervals
- Power-up durations or cooldowns
- Auto-saving or periodic events
Without timers, you’d need to manually track time using variables, which quickly becomes messy and unreliable.
Syntax
GDScript:
# Creating and starting a timer
var timer = Timer.new()
add_child(timer)
timer.wait_time = 2.0 # how long it will execute for (2 seconds)
timer.one_shot = true # will execute once, set to false for continuous execution
timer.start() # start the timer
# Connecting timeout signal
timer.timeout.connect(_on_timer_timeout)
# When timer times out, do this logic
func _on_timer_timeout():
print("Time's up!")
C#:
// Creating and starting a timer
var timer = new Timer();
AddChild(timer);
timer.WaitTime = 2.0f; // how long it will execute for (2 seconds)
timer.OneShot = true; // will execute once, set to false for continuous execution
timer.Start(); // start the timer
timer.Timeout += OnTimerTimeout;
// When timer times out, do this logic
private void OnTimerTimeout()
{
GD.Print("Time's up!");
}
Best Practices
- Use Timer nodes for clarity instead of manually counting seconds in _process().
- Connect the timeout signal to trigger actions cleanly when the timer finishes.
- Set one_shot to true for single-use timers, or false to make them loop.
- Use start() and stop() to control timers in code.
- Reuse timers for repeating events rather than creating new ones each time.
Example
Here’s how you might use a Timer to respawn an enemy 3 seconds after it’s defeated.
In the code below, we create a timer as soon as our game starts. When it’s created, we also connect its timeout signal to our script (this can be done in the editor itself, instead of via code). When the enemy is defeated, say their health reaches below zero, we start the timer. The timer will run for however long we’ve set its wait_time, which is 3 seconds. When 3 seconds have passed, the timer will timeout , and the enemy will be respawned.
If the enemy dies again, the logic will start over again, unless we’ve set our timer to be one_shot enabled.
GDScript:
extends Node2D
# Create timer
func _ready():
var respawn_timer = $RespawnTimer
respawn_timer.wait_time = 3.0
respawn_timer.one_shot = false
respawn_timer.timeout.connect(_on_respawn_timer_timeout)
# Start timer
func on_enemy_defeated():
print("Enemy defeated! Respawning in 3 seconds...")
$RespawnTimer.start()
# Stop timer
func _on_respawn_timer_timeout():
print("Enemy has respawned!")
C#:
using Godot;
public partial class EnemySpawner : Node2D
{
// Create timer
private Timer RespawnTimer;
public override void _Ready()
{
RespawnTimer = GetNode<Timer>("RespawnTimer");
RespawnTimer.WaitTime = 3b.0f;
RespawnTimer.OneShot = false;
RespawnTimer.Timeout += OnRespawnTimerTimeout;
}
// Start timer
public void OnEnemyDefeated()
{
GD.Print("Enemy defeated! Respawning in 3 seconds...");
RespawnTimer.Start();
}
// Stop timer
private void OnRespawnTimerTimeout()
{
GD.Print("Enemy has respawned!");
}
}
4. Physics Process vs. Process
In Godot, both _process() and _physics_process() are special built-in functions that run every frame — but they serve different purposes.
- _process(delta) runs every rendered frame — perfect for animations, UI, and non-physics logic.
- _physics_process(delta) runs at a fixed time step — perfect for movement, collisions, and anything that interacts with the physics engine.
Why It Matters
Games depend on predictable, consistent updates.
You’ll typically use:
- _process() → for animations, timers, and UI transitions.
- _physics_process() → for movement, gravity, and collisions.
Syntax
GDScript:
# Called every frame (variable time step)
func _process(delta):
# do framerate related logic
# Called at a fixed time step (default: 60 times per second)
func _physics_process(delta):
# do physics related logic
# usually ends in move_and_slide() or move_and_collide()
C#:
// Called every frame
public override void _Process(double delta)
{
// do framerate related logic
}
// Called at a fixed physics tick rate
public override void _PhysicsProcess(double delta)
{
// do physics related logic
// usually ends in MoveAndSlide() or MoveAndCollide()
}
Best Practices
- Use _physics_process() for movement and collisions.
- Use _process() for visual effects and non-physics updates.
- Always multiply by delta to keep movement frame-rate independent.
- Don’t mix physics logic in _process() — it can cause jitter or inconsistencies.
- Pause logic carefully — timers and physics might still run if not managed properly.
Example
Here’s a basic player script that plays a characters run animations continuously whilst the input keys are being held down in the process() function. It also then moves the character around whilst the input keys are being held down in the physics_process() function.
GDScript:
# This will play the run animations on inputs
func _process(delta):
if Input.is_action_pressed("move_right") or Input.is_action_pressed("move_left"):
$AnimatedSprite2D.play("run")
else:
$AnimatedSprite2D.play("idle")
# This will move the player on inputs.
func _physics_process(delta):
var velocity = Vector2.ZERO
if Input.is_action_pressed("move_right"):
velocity.x += 200
elif Input.is_action_pressed("move_left"):
velocity.x -= 200
velocity = move_and_slide(velocity)
C#:
using Godot;
public partial class Player : CharacterBody2D
{
const float Speed = 200f;
private AnimatedSprite2D sprite;
public override void _Ready()
{
sprite = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
}
// This will play the run animations on inputs
public override void _Process(double delta)
{
if (Input.IsActionPressed("move_right") || Input.IsActionPressed("move_left"))
sprite.Play("run");
else
sprite.Play("idle");
}
// This will move the player on inputs.
public override void _PhysicsProcess(double delta)
{
Vector2 velocity = Vector2.Zero;
if (Input.IsActionPressed("move_right"))
velocity.X += Speed;
else if (Input.IsActionPressed("move_left"))
velocity.X -= Speed;
Velocity = velocity;
MoveAndSlide();
}
}
5. Random Numbers
Random numbers add variety and unpredictability to your game, thus ensuring no two playthroughs feel exactly the same. Godot’s random system lets you generate numbers, select random items from arrays, or even produce random directions for projectiles or enemies.
Randomness makes things feel dynamic, alive, and surprising.
Why Random Numbers Are Needed
Games rely on randomness for endless replayability:
- Loot drops or item rewards
- Random enemy spawns
- Critical hits or damage variation
- Procedural generation (maps, terrain, puzzles)
- Particle spread or visual effects
Without randomness, your game would feel repetitive and predictable.
Syntax
GDScript:
# Initialize random seed (usually in _ready)
randomize()
# Generate a random float between 0 and 1
var r = randf()
# Generate an integer between 0 and 10
var i = randi_range(0, 10)
# Pick a random element from an array
var enemies = ["Goblin", "Orc", "Troll"]
var random_enemy = enemies.pick_random()
# Random vector direction
var dir = Vector2(randf_range(-1, 1), randf_range(-1, 1)).normalized()
C#:
using Godot;
using System;
public partial class RandomExample : Node
{
private Random rand = new Random();
public override void _Ready()
{
// Random float between 0 and 1
float r = (float)rand.NextDouble();
// Random integer between 0 and 10
int i = rand.Next(0, 11);
// Random element from an array
string[] enemies = { "Goblin", "Orc", "Troll" };
string randomEnemy = enemies[rand.Next(enemies.Length)];
// Random direction
Vector2 dir = new Vector2(
(float)rand.NextDouble() * 2 - 1,
(float)rand.NextDouble() * 2 - 1
).Normalized();
GD.Print($"Enemy: {randomEnemy}, Direction: {dir}");
}
}
Best Practices
- Use randomize() once at startup (in _ready()) to avoid repeating patterns.
- Choose the right random function — Godot offers several for different data types.
- Use seeded randomness for predictable results (great for replays or testing).
- Clamp or round values if you only need whole numbers or limits.
- Keep randomness balanced — too much can frustrate players.
Example
Here’s how you might use randomness to spawn enemies at random positions on the map.
GDScript:
extends Node2D
func _ready():
randomize()
for i in range(5):
var enemy_scene = preload("res://Enemy.tscn").instantiate()
enemy_scene.position = Vector2(
randi_range(100, 800),
randi_range(100, 400)
)
add_child(enemy_scene)
C#:
using Godot;
using System;
public partial class Spawner : Node2D
{
private Random rand = new Random();
public override void _Ready()
{
for (int i = 0; i < 5; i++)
{
var enemyScene = GD.Load<PackedScene>("res://Enemy.tscn").Instantiate<Node2D>();
enemyScene.Position = new Vector2(
rand.Next(100, 801),
rand.Next(100, 401)
);
AddChild(enemyScene);
}
}
}
6. The Ready Function
In Godot, the _ready() function is called once when a node enters the scene tree and is fully initialized. It’s the go-to place for setup logic such as loading resources, connecting signals, setting initial values, or caching references to other nodes.
Why Its Needed
When a node is created, not all of its children or dependencies exist yet. If you try to reference them too early, your code can break.
_ready() ensures the entire node hierarchy is loaded before running your setup code.
You’ll typically use it to:
- Get references to child nodes
- Connect signals between nodes
- Load textures, sounds, or scenes
- Initialize variables or game states
Without _ready(), you’d risk calling nodes or data that don’t exist yet.
Syntax
GDScript:
# Called once when the node enters the scene tree
func _ready():
# Set values to be initialized
var sprite = $Sprite2D
var player_health = 100
C#:
using Godot;
public partial class Player : Node2D
{
public override void _Ready()
{
// Set values to be initialized
Sprite2D sprite = GetNode<Sprite2D>("Sprite2D");
int playerHealth = 100
}
}
Best Practices
- Use _ready() for one-time setup only — not for logic that runs continuously.
- Keep it clean and short — just initialization, no heavy loops or updates.
- Access child nodes here safely — all children are guaranteed to exist.
- Connect signals or timers in _ready(), not in _process().
- Avoid creating dependencies between unrelated nodes — keep setups modular.
Example
Here’s a simple player setup that uses _ready() to load assets, connect signals, and initialize variables before gameplay starts.
GDScript:
extends CharacterBody2D
var health = 100
func _ready():
$AnimatedSprite2D.play("idle")
$HealthBar.max_value = health
$Area2D.body_entered.connect(_on_body_entered)
func _on_body_entered(body):
print("Collided with:", body.name)
C#:
using Godot;
public partial class Player : CharacterBody2D
{
private int health = 100;
private AnimatedSprite2D sprite;
private ProgressBar healthBar;
private Area2D hitArea;
public override void _Ready()
{
sprite = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
sprite.Play("idle");
healthBar.MaxValue = health;
hitArea.BodyEntered += OnBodyEntered;
}
private void OnBodyEntered(Node body)
{
GD.Print($"Collided with: {body.Name}");
}
}
Part V: Scenes & Nodes
In Godot, everything is a node. From your player and camera to UI buttons, lights, and audio — every piece of your game inherits from the Node class.
Nodes are the building blocks of Godot. When you combine them, you create scenes, which are self-contained groups of nodes that can represent anything: a level, a character, or even a menu. Together, these form the Scene Tree, a hierarchy that defines how everything in your game is organized, updated, and connected.
In this section, we’ll cover:
- Node Basics — understanding what nodes are and how they work
- Scenes — reusable groups of nodes that form your game’s structure
- Inheritance — allows one scene or script to reuse another’s logic and structure.
- Export variables — makes a variable visible and editable in the Inspector panel.
- Groups — tagging nodes into similar categories
1. Accessing Nodes
A Node is the most fundamental building block in Godot. Each node has a name, a type, and optional children. Nodes within a scene creates a tree-like structure that defines how your game world is built.
Think of it like this:
- Nodes are the building blocks.
- Scenes are the containers which hold and organize nodes.
For a more visual reference on all the nodes and their use-cases, refer to my Book of Nodes .
View the online version of the above depiction of scenes and nodes here.
Why Nodes Are Needed
Without nodes, there would be no game. They’re the foundation of everything you create in Godot.
Nodes can represent almost anything:
- A collider that detects hits
- A sprite that displays a texture
- A camera that follows the player
- A light that illuminates the scene
- A script that controls behavior
Without nodes, every part of your game would have to be hard-coded, messy, unorganized, and nearly impossible to maintain.
Syntax
Nodes are usually added within the editor itself, but we can create and reference nodes directly within our code.
GDScript:
# Referencing an existing node and changing its property
var sprite = $Sprite2D
sprite.modulate = Color.RED
# Getting a node by path
var camera = get_node("Camera2D")
# Creating a new node
var new_label = Label.new()
new_label.text = "Hello World"
add_child(new_label)
C#:
// Referencing an existing node and changing its property
Sprite2D sprite = GetNode<Sprite2D>("Sprite2D");
sprite.Modulate = new Color(1, 0, 0);
// Getting a node by path
Camera2D camera = GetNode<Camera2D>("Camera2D");
// Creating a new node
Label newLabel = new Label();
newLabel.Text = "Hello World";
AddChild(newLabel);
Best Practices
- Name nodes clearly (e.g., PlayerSprite, Hitbox, MainCamera).
- Use composition — build functionality by combining nodes, not writing giant scripts.
- Leverage node types — use the right node for the job (Area2D for detection, Sprite2D for visuals, etc.).
- Keep the tree organized — indentation and naming go a long way.
- Don’t overload one scene — break your game into multiple smaller, reusable ones.
Example
Here’s how you can reference a node that already exists and create a new one dynamically in the same script.
In the code below, we assume that within the editor we’ve added a Sprite2D node called “Sprite2D”. We reference this node in our script, and then we change its texture (sprite). We also create a new Label node with the text “Player ready”, and position it to our screen. The add_child() method adds the newly created node to our scene via the script.
GDScript:
extends Node2D
func _ready():
# Reference an existing Sprite node
var sprite = $Sprite2D
sprite.texture = preload("res://sprites/player.png")
# Create and add a new Label node
var label = Label.new()
label.text = "Player ready!"
label.position = Vector2(10, 10)
add_child(label)
C#:
using Godot;
public partial class Player : Node2D
{
public override void _Ready()
{
// Reference an existing node
Sprite2D sprite = GetNode<Sprite2D>("Sprite2D");
sprite.Texture = (Texture2D)GD.Load("res://sprites/player.png");
// Create and add a new node dynamically
Label label = new Label();
label.Text = "Player ready!";
label.Position = new Vector2(10, 10);
AddChild(label);
}
}
2. Accessing Scenes
A Scene in Godot is a collection of nodes. It can represent this such as a character, a level, a user interface, or even a single reusable object.
Every scene has a root node , and that root defines the scene’s type. For example, a CharacterBody2D might be the root for a player scene, or a Control node might be the root for a UI screen.
Think of scenes as blueprints: you build them once in the editor, then reuse, instance, or switch between them dynamically during gameplay.
View the online version of the above depiction of scenes and nodes here.
Why Scenes Are Needed
Scenes make your game modular and reusable. They allow you to:
- Build individual pieces of your game separately.
- Reuse the same object (like enemies or items) multiple times.
- Load and unload levels dynamically.
- Transition between menus, gameplay, and cutscenes.
Without scenes, your entire game would live in one massive, unmanageable hierarchy, which makes updates, debugging, and testing a nightmare.
Syntax
GDScript:
# Load a scene from file
var enemy_scene = preload("res://scenes/Enemy.tscn")
# Create an instance of that scene
var enemy_instance = enemy_scene.instantiate()
# Add it to the current scene tree
add_child(enemy_instance)
# Change to another scene file
get_tree().change_scene_to_file("res://scenes/Level2.tscn")
# Reload the current scene
get_tree().reload_current_scene()
# Get a node from another scene (if it's loaded)
var player = get_tree().get_root().get_node("World/Player")
player.health = 50
C#:
// Load a scene from file
PackedScene enemyScene = (PackedScene)GD.Load("res://scenes/Enemy.tscn");
// Create an instance of that scene
Node enemyInstance = enemyScene.Instantiate();
// Add it to the current scene tree
AddChild(enemyInstance);
// Change to another scene file
GetTree().ChangeSceneToFile("res://scenes/Level2.tscn");
// Reload the current scene
GetTree().ReloadCurrentScene();
// Get a node from another scene (if it's loaded)
Node player = GetTree().Root.GetNode("World/Player");
player.Set("health", 50);
Best Practices
- Keep scenes focused — each should represent one clear purpose (e.g., Player, MainMenu, Enemy, Level1).
- Use the root node type wisely — it defines what the scene is and how it behaves.
- Instance scenes instead of duplicating — this makes updates propagate automatically.
- Use get_tree().change_scene_to_file() for transitions, and preload() or load() for instancing.
- Keep references clean — don’t rely on global paths unless necessary.
Example
The below code loads and spawns an enemy scene when the player presses a key, and transitions to a new scene when health reaches zero.
GDScript:
extends Node2D
# Load scene
var enemy_scene = preload("res://scenes/Enemy.tscn")
var health = 100
func _process(delta):
# Spawn enemy when pressing "E"
if Input.is_action_just_pressed("spawn_enemy"):
var enemy = enemy_scene.instantiate() # Spawn scene
enemy.position = Vector2(400, 200)
add_child(enemy)
# Change to game over scene when health reaches zero
if health <= 0:
get_tree().change_scene_to_file("res://scenes/GameOver.tscn") # Transition scene
C#:
using Godot;
public partial class Level : Node2D
{
// Load scene
private PackedScene enemyScene = (PackedScene)GD.Load("res://scenes/Enemy.tscn");
private int health = 100;
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("spawn_enemy"))
{
Node2D enemy = enemyScene.Instantiate<Node2D>(); // Spawn scene
enemy.Position = new Vector2(400, 200);
AddChild(enemy);
}
if (health <= 0)
GetTree().ChangeSceneToFile("res://scenes/GameOver.tscn"); // Transition scene
}
}
3. Inheritance
Inheritance allows you to create new scenes or scripts that reuse and extend the functionality of existing ones. It’s one of the most powerful features of coding, as it lets you define a base behavior (like a generic “Enemy”) and then build specialized versions (like “Goblin” or “Orc”) without rewriting everything from scratch.
There are two main types of inheritance in Godot:
- Script inheritance — extending another script’s code using the extends keyword.
- Scene inheritance — creating new scenes that build on existing ones in the editor.
Why Inheritance Is Needed
Inheritance helps you write cleaner, reusable, and modular code. It keeps your project organized and avoids duplication when many objects share similar traits.
For example:
- A base Enemy scene might define health and damage logic for all enemies.
- Derived scenes like Orc, Goblin, or Troll can inherit that and add their own unique animations or stats.
- A CharacterBody2D script might define general movement for all characters in your game, and the Player script can extend it to handle input.
Without inheritance, you’d have to duplicate large chunks of code or rebuild scenes from scratch every time.
Best Practices
- Use inheritance for shared functionality , not for one-off behaviors.
- Keep base classes simple and focused — they should define core rules, not everything.
- Override functions responsibly — always call super() or ._process(delta) if you still want the parent’s logic to run.
- For scenes , use “Inherit Scene” in the editor instead of duplicating.
- Use composition (adding child nodes) alongside inheritance for flexibility.
Syntax
Script Inheritance
GDScript:
# Base script: Base.gd
# The base class defines the basic values of your enemies
class_name Base
extends Node
var health = 100
func take_damage(amount):
health -= amount
print("Enemy took", amount, "damage. Health:", health)
# Derived script: Derived.gd
# The derived class gets everythign from the Base, such as health and take_damage() function
extends Base
func _ready():
print("Goblin ready!")
take_damage()
C#:
// Base script: Base.cs
// The base class defines the basic values of your enemies
using Godot;
public partial class Base : Node
{
public int Health = 100;
public virtual void TakeDamage(int amount)
{
Health -= amount;
GD.Print($"Enemy took {amount} damage. Health: {Health}");
}
}
// Derived script: Derived.cs
// The derived class gets everythign from the Base, such as health and take_damage() function
using Godot;
public partial class Derived : Base
{
public override void _Ready()
{
GD.Print("Goblin ready!");
base.TakeDamage(amount);
}
}
Scene Inheritance
You can also inherit scenes directly in the editor:
- Right-click an existing scene (e.g., Enemy.tscn).
- Choose “New Inherited Scene”.
- Godot creates a new version with all the parent nodes — ready for customization.
You can then:
- Add new child nodes (e.g., a weapon or animation).
- Override properties (like textures, speeds, or collision shapes).
- Change script references while keeping base functionality intact.
Example
The code below creates a base Enemy class , which defines the shared behavior for all of our enemies. We then create two derived classes (Orc and Troll) which extends from it. These classes will get all the base functionality, such as health and take_damage(), but will still be able to get their own unique features.
This means our damage logic is the same for all enemies, but different enemies can have different animations, sound, and behavior.
GDScript:
# Base Enemy.gd
class_name Enemy
extends CharacterBody2D
var health = 100
func _ready():
print("Enemy spawned with", health, "HP")
func take_damage(amount):
health -= amount
print("Enemy took damage:", amount)
# Orc.gd (inherits Enemy.gd)
# Orc plays a dance anim on taking damage, whilst still losing health
extends Enemy
func _ready():
print("Orc enters the battlefield!")
func take_damage(amount):
print("Orc roars in anger!")
$animated_sprite.play("dance_anim")
# Troll.gd (inherits Enemy.gd)
extends "res://scripts/Enemy.gd"
func take_damage(amount):
print("Troll regenerates some health!")
health += 5
4. Export Variables
Export variables let you expose script properties directly to the Godot Editor. They make your scripts customizable, flexible, and designer-friendly, allowing you to change values without modifying the code.
Think of them as editable parameters which appear in the Inspector just like any built-in node property.
Why Export Variables Are Needed
Export variables bridge the gap between code and design. Instead of opening scripts to adjust things like player speed or enemy health for testing, you can edit them visually in the editor.
You’ll typically use them for:
- Character attributes (health, speed, jump power)
- Enemy AI settings (vision range, attack delay)
- Visual tuning (colors, scale, offsets)
- Audio or effect configuration
- Linking external resources (textures, scenes, sounds)
Without exports, tuning gameplay would mean constant code edits and reloads, which slows development dramatically.
Syntax
GDScript:
# A variable visible and editable in the editor
@export var speed = 200
@export var player_name = "Hero"
# Choose a texture in the editor
@export var icon: Texture2D
C#:
[Export]
public float Speed = 200f;
[Export]
public string PlayerName = "Hero";
[Export]
public Texture2D Icon;
Best Practices
- Use clear names — make sure exports describe what they control.
- Set sensible defaults — keep values playable right away.
- Add type hints (int, float, Color, PackedScene) for editor validation.
- Use ranges and enums to restrict inputs and avoid mistakes.
- Keep export usage simple — avoid exporting everything just for convenience.
Example
The below script exports variables which will be exposed to the Inspector panel for editing. This means whatever value we assign in the inspector panel will be the value assigned to that script — meaning it will overwrite hard-coded values in our script!
GDScript:
extends CharacterBody2D
@export var speed: float = 200.0
@export var jump_force: float = 400.0
@export var sprite_texture: Texture2D
C#:
using Godot;
public partial class Player : CharacterBody2D
{
[Export] public float Speed = 200f;
[Export] public float JumpForce = 400f;
[Export] public Texture2D SpriteTexture;
}
4. Groups
Groups are a powerful way to organize and manage multiple nodes in Godot. They let you categorize nodes under a shared label (like “Enemies” or “Interactables”) so you can call functions, apply effects, or send signals to all of them at once.
Think of groups as tags for nodes. You can assign one or more groups to a node and then access them from anywhere in your game.
Why Groups Are Needed
Groups are essential when you have multiple instances of similar objects, like enemies, NPCs, or projectiles.
You’ll typically use them to:
- Apply damage or effects to every enemy.
- Check interactions dynamically — for example, showing an “Interact” prompt when focusing on an interactable group item, or triggering dialog when focusing on an NPC.
- Enable or disable all interactables at once (e.g., pausing player input or freezing AI).
- Trigger multiple lights, doors, or sounds with a single command.
- Broadcast global events, like GameOver, LevelCleared, or Pause
Without groups, you’d need to manually loop through every node or keep separate arrays, which is messy, slow, and error-prone.
Syntax
You can assign nodes directly to Groups within the editor, but below is how you can add objects to groups using code.
GDScript:
# Add the node to a group
add_to_group("Enemies")
# Check if the node belongs to a group
if is_in_group("Enemies"):
print("This node is an enemy!")
# Remove the node from a group
remove_from_group("Enemies")
C#:
// Add the node to a group
AddToGroup("Enemies");
// Check if the node belongs to a group
if (IsInGroup("Enemies"))
GD.Print("This node is an enemy!");
// Remove the node from a group
RemoveFromGroup("Enemies");
Best Practices
- Use groups to organize related nodes (e.g., “Enemies”, “Collectibles”, “UI”).
- Add groups via code or the editor (under the “Node → Groups” tab).
- Avoid too many overlapping groups — keep naming meaningful.
- Use is_in_group() to check membership before applying logic.
- Leverage signals and call_group() for clean global communication.
Example
Here’s how you can use groups to detect whether the player is focusing on an interactable object (like an item, door, or NPC). If the object is in the Interactable group, the player receives a message prompt.
GDScript:
# Player.gd
extends CharacterBody2D
func _process(delta):
# Cast a ray forward to detect what the player is looking at
var space_state = get_world_2d().direct_space_state
var result = space_state.intersect_ray(global_position, global_position + Vector2(50, 0))
# Get the collider of the object
if result and result.has("collider"):
var target = result.collider
# If its in interactable group, show prompt
if target.is_in_group("Interactable"):
print("You're focusing on an interactable. Press [E] to pick up.")
else:
print("You're looking at:", target.name)
C#:
// Player.cs
using Godot;
public partial class Player : CharacterBody2D
{
public override void _Process(double delta)
{
// Cast a ray forward to detect what the player is looking at
var spaceState = GetWorld2D().DirectSpaceState;
var result = spaceState.IntersectRay(GlobalPosition, GlobalPosition + new Vector2(50, 0));
// Get the collider of the object
if (result.Count > 0 && result.ContainsKey("collider"))
{
var target = result["collider"] as Node;
// If its in interactable group, show prompt
if (target.IsInGroup("Interactable"))
GD.Print("You're focusing on an interactable. Press [E] to pick up.");
else
GD.Print($"You're looking at: {target.Name}");
}
}
}
Part VI: Bonus
You’ve learned how to build the core gameplay systems of a Godot project. Now it’s time to focus on the finishing touches that make your project clean, stable, and scalable.
This section covers the tools and habits that keep your code readable, debuggable, and organized — especially as your project grows.
We’ll go over:
- Code Style Tips — write clear, consistent, and maintainable code.
- Debugging — identify and fix issues effectively.
- Autoloads (Singletons) — manage global data and cross-scene communication.
1. Code Style Tips
Clean code isn’t just about getting things to work, it’s about making your future self thank you later.
Readable scripts are easier to debug, share, and extend.
Why It Matters
As projects grow, consistency becomes critical. Having clear naming, structure, and commenting habits keeps your code easy to understand for everyone (including you, months later).
Best Practices
- Use consistent naming:
- GDScript → snake_case for variables and functions.
- C# → camelCase for variables and PascalCase for methods and classes.
- Comment with purpose: Explain why, not just what.
- Group related code: Keep setup, logic, and cleanup organized in sections OR code regions.
- Avoid magic numbers: Use constants or exported variables instead.
- Keep functions short: Each function should do one clear thing.
- Be descriptive: player_health is better than hp1.
Example (GDScript)
# Bad
var s = 10
func x():
s -= 1
if s == 0: print("Dead")
func phello():
print("Hello")
# Good
#region [Player Health Management]
var player_health = 10
func take_damage(amount):
player_health -= amount
if player_health <= 0:
print("Player defeated")
# Trigger game over sequence
#endregion
#region [Debugging]
func print_hello():
print("Hello")
#endregion
2. Debugging
Every developer runs into bugs, it’s just inevitable. The key is finding and understanding them quickly. Godot provides a range of built-in tools to help you diagnose problems effectively.
Best Practices
- Use print() or GD.Print() to log key values and track execution flow.
- Use assert(condition) to catch unexpected states early.
- Use the Debugger panel (bottom dock) to pause on errors and inspect variables.
- Leverage breakpoints to step through your code line by line.
- Enable “Visible Collision Shapes” in the Debug menu to check collisions.
- Don’t ignore warnings — they often point to subtle bugs before they crash your game.
Example
GDScript:
func _physics_process(delta):
assert(player != null, "Player reference missing!")
if player.health <= 0:
print("Player is dead")
C#:
public override void _PhysicsProcess(double delta)
{
GD.Assert(Player != null, "Player reference missing!");
if (Player.Health <= 0)
GD.Print("Player is dead");
}
3. Using Autoloads (Singletons)
Autoloads (also called Singletons) are special scripts that stay loaded across all scenes. They’re perfect for storing global data, such as player stats, game state, settings, or audio managers.
Once registered, they’re available from anywhere in your game using their name.
Why Autoloads Are Needed
Autoloads make it easy to share data between scenes without constantly passing references around.
They’re great for things like:
- Saving and loading game progress.
- Keeping player stats between levels.
- Handling global input or settings.
- Managing background music, pause menus, or transitions.
They’re not great for:
- Shared or per-instance values, like enemy health or temporary item states. If you store shared data such as enemy health in an autoload, then when one enemy dies, all others will die too.
- Objects that exist multiple times in the world, such as bullets, items, or NPCs.
- Autoloads are global, meaning every scene accesses the same data. Using them for per-object logic can cause synchronization issues or unexpected behavior between instances.
Setup
- Create a new script, e.g. Global.gd.
- Go to Project → Project Settings → Autoload.
- Add your script and give it a name (e.g., Global).
- Enable “Singleton.”
Now you can access it anywhere using that name.
Syntax
GDScript:
# GameManager.gd (autoload)
extends Node
var player_health = 100
var coins = 0
func add_coin():
coins += 1
# Access the autoload directly from any other script
GameManager.add_coin()
print(GameManager.coins)
C#:
// GameManager.cs (autoload)
using Godot;
public partial class GameManager : Node
{
public int PlayerHealth = 100;
public int Coins = 0;
public void AddCoin() => Coins++;
}
// Access the autoload directly from any other script
GameManager game = (GameManager)GD.Load("res://GameManager.cs");
game.AddCoin();
GD.Print(game.Coins);












































Top comments (0)