If you know Unity’s C#, you can bring your knowledge of variables, functions, and events to GDScript. The syntax changes, and so do scene structure and callback timing. This guide compares the common patterns and explains where a direct translation needs care.
Basic Syntax Differences
The biggest visual difference: GDScript uses indentation instead of curly braces, and lines don't end with semicolons. If you've ever written Python, this will feel natural. If not, most editors (including Godot's built-in one) handle indentation automatically.
# GDScript uses indentation instead of braces
func _ready():
var message = "Hello, Godot!"
print(message)
# Compare to C#:
# void Start() {
# string message = "Hello, Unity!";
# Debug.Log(message);
# }Use func to declare a function and extends to specify its base class. Call ClassName.new() to construct an object. Output uses print() in GDScript, in place of Unity’s Debug.Log().
Variables and the Type System
GDScript supports both dynamic and static typing. You can start with untyped code for quick prototyping, then add type hints as your project matures. Typed GDScript provides editor autocompletion, catches errors before runtime, and can improve performance.
# Dynamic typing (quick prototyping)
var health = 100
var player_name = "Hero"
# Static typing (recommended for production)
var health: int = 100
var player_name: String = "Hero"
var velocity: Vector2 = Vector2.ZERO
# Type inference with :=
var speed := 200.0 # Inferred as float
var position := Vector2(100, 200) # Inferred as Vector2
# Constants
const MAX_HEALTH: int = 100
const GRAVITY: float = 980.0The := operator infers a variable’s type from its initial value, much like C#’s var. GDScript does not enforce public, private, or protected access modifiers. An underscore prefix such as _my_var communicates internal use by convention.
Lifecycle Methods
Godot has callbacks for entering the tree, becoming ready, and processing frames. They serve some of the same purposes as Unity lifecycle methods, but their timing and ownership differ. Use the mapping below as a starting point and check when your dependencies become available.
# Unity -> Godot lifecycle mapping
# Awake() -> _init() # Constructor, called first
# Start() -> _ready() # Called when node enters tree
# Update() -> _process(delta) # Called every frame
# FixedUpdate() -> _physics_process(delta) # Fixed timestep (60/sec)
# OnDestroy() -> _exit_tree() # Called when removed from tree
# OnEnable() -> _enter_tree() # Called when added to tree
# Note: delta is already passed as a parameter — no Time.deltaTime needed!Godot passes delta to _process() and _physics_process(). A parent’s _ready() runs after its children’s _ready() callbacks. That ordering helps with child references, but it does not guarantee that an unrelated scene or a node created later is ready.
Signals: Godot's Event System
Signals let an object announce an event to connected listeners. Built-in examples include button presses, timer timeouts, and area overlaps. You can declare custom signals in scripts; the emitter does not need references to its listeners.
# Define a custom signal
signal health_changed(new_health: int)
signal died
# Emit signals
func take_damage(amount: int):
health -= amount
health_changed.emit(health)
if health <= 0:
died.emit()
# Connect in code
func _ready():
player.health_changed.connect(_on_health_changed)
player.died.connect(_on_player_died)
func _on_health_changed(new_health: int):
health_bar.value = new_health
func _on_player_died():
show_game_over_screen()You can connect signals in code or in the editor’s Node panel. Check the lifetime of both objects, especially when using captured variables or custom C# signal connections. Automatic cleanup does not cover every connection pattern.
Node References and @export
In Unity, you use [SerializeField] or public fields to expose variables in the Inspector, and GetComponent
# Get child nodes (like GetComponent but for scene tree)
@onready var sprite = $Sprite2D # Direct child
@onready var health_bar = $UI/HealthBar # Nested path
@onready var anim = $AnimationPlayer # Animation player
# Export to Inspector (like [SerializeField] in Unity)
@export var speed: float = 200.0
@export var jump_force: float = 400.0
@export var max_health: int = 100
# Exports with ranges and categories
@export_range(0, 100, 5) var volume: int = 50
@export_enum("Sword", "Bow", "Staff") var weapon: int = 0
@export_group("Movement")
@export var acceleration: float = 800.0
@export var friction: float = 1200.0@onready delays a variable’s initialization until just before _ready(). That is useful for references to children in an instantiated scene; entering the tree alone is a different lifecycle step. @export_group, @export_range, and related annotations organize Inspector fields.
Coroutines and Async: await vs StartCoroutine
GDScript uses await to suspend a function until a signal fires or an awaited coroutine completes. The rest of the game continues running. Use it for a delay or a sequence whose next step depends on an event.
# Wait for a timer (like yield return new WaitForSeconds)
func flash_damage():
sprite.modulate = Color.RED
await get_tree().create_timer(0.2).timeout
sprite.modulate = Color.WHITE
# Wait for an animation to finish
func play_death_animation():
anim.play("death")
await anim.animation_finished
queue_free()
# Wait for a signal from another node
func wait_for_player_input():
var choice = await dialog_box.choice_made
process_choice(choice)After the awaited signal fires, execution continues on the next line. If the sequence can be canceled, design that behavior explicitly: check whether the action is still valid before applying its result. Replacing StartCoroutine() with await does not provide automatic cancellation.
Classes and Inheritance
A GDScript file defines a class. Scripts attached to nodes extend a compatible node type, but scripts can also extend Resource or other object classes. Similarly, Unity C# classes do not all need to inherit from MonoBehaviour.
# Every script extends a node type (like inheriting MonoBehaviour)
extends CharacterBody2D
class_name Player # Optional: registers as a global type
# Inner classes (less common)
class Inventory:
var items: Array[String] = []
func add_item(item: String):
items.append(item)
# Using class_name lets you reference it like a type:
# var player: Player = Player.new()
# if node is Player: ...Common Gotchas for C# Developers
- Freed objects: GDScript has null, but a reference to a freed object needs an is_instance_valid(obj) check. A non-null reference is not proof that the node is still usable.
- No try-catch: GDScript doesn't have exception handling. Use push_error() and push_warning() for logging. Check return values and use assert() during development
- Arrays and dictionaries share references when assigned. duplicate() makes a shallow copy by default; duplicate(true) also copies nested arrays and dictionaries. Object and Resource sharing needs separate care.
- Enums are just ints: GDScript enums are syntactic sugar over integers, unlike C#'s type-safe enums. They work for simple cases but don't provide the same type safety
- No method overloading: GDScript doesn't support multiple methods with the same name but different parameters. Use default parameter values instead: func attack(damage: int = 10, is_critical: bool = false)
Next Steps
Rebuild one small Unity feature in Godot: a player controller, one UI update, and a signal connection. Check the node tree and callback order as well as the syntax. Once that feature works, reuse the pattern for the next system.
Use the interactive Code Lab for C# and GDScript examples, and keep the GDScript Cheat Sheet nearby for syntax lookups. Run the translated feature in Godot to check its actual behavior.
Frequently asked questions
- Is GDScript similar to Python?
- Yes, GDScript is Python-inspired and uses similar syntax like indentation-based blocks, dynamic typing, and familiar constructs. Python developers will feel at home. However, GDScript has game-specific features like signals, exports, and built-in vector math that Python doesn't have.
- Can I use C# in Godot instead of GDScript?
- Yes. Use the Godot .NET editor and a compatible .NET SDK. A project can mix C# and GDScript, but check export support first: Godot 4 C# projects currently cannot export to web.
- How long does it take to learn GDScript coming from C#?
- That depends on your experience and the feature. Start by translating a small script, then learn signals, exported properties, callback timing, and scene references as the project needs them.
- Does GDScript support static typing like C#?
- GDScript supports optional type hints for variables, parameters, and return values. They help the editor check assignments and provide completion. They do not reproduce every feature of C#’s type system.
Sources and revision context
Updated September 8, 2026. Clarified exported properties and lifecycle differences without promising a fixed learning time. This revision does not establish a full tutorial runtime test.