PONYλM2Modula-2
CodeCompared
for GDScript programmers

You already know GDScript.Now explore other languages.

Side-by-side, interactive cheatsheets for GDScript programmers
comparing GDScript to other languages. Every example runs live in your browser — no setup, no installation.

▶ Start with JavaScriptBrowse comparisons ↓Explore the language map ↗

Choose your own path by reordering languages

JavaScriptAlpha⚡ Works Offline⚡ Offline

Godot exports to the web, and when it does, your game is running next to JavaScript. The official JavaScriptBridge singleton is the seam: eval(), get_interface(), create_object() and create_callback() let a GDScript game reach the page it is embedded in. That is the concrete reason a Godot developer ends up reading JavaScript, and everything else here follows from meeting the browser's own language.

  • Objects and prototypes replace the scene tree's class hierarchy, and this means whatever the call site decided rather than the object you wrote it in
  • One event loop, no _process — nothing calls your code each frame unless you ask, and requestAnimationFrame is what asks
  • await is the same word for a different machine: a Promise rather than a signal, and every async function returns one whether you meant it to or not
  • Truthiness is broader and stranger — [] is truthy, 0 is falsy, and == converts types before comparing, which is why everyone writes ===
  • There is one number type and it is a float, so integer arithmetic is exact only up to 2^53
  • What you gain is reach: the same language runs the page, the build tooling, and the server
PythonBeta⚡ Works Offline⚡ Offline

GDScript was designed to feel like Python, and it is not Python. The indentation, the colons, the for x in thing — all the same. Then the differences start, and they are the awkward kind: the same shape with different behavior rather than something obviously new. This page is mostly about the false friends, because those are what actually cost you an afternoon.

  • func is def, and the return arrow moves from mandatory-ish to a hint Python does not enforce at all
  • Integer division is the first thing to bite7 / 2 is 3 in GDScript and 3.5 in Python, and nothing warns you
  • One Array becomes three things worth knowing apart: list, tuple, and a set you have never had
  • Truthiness is broader — an empty list, an empty string, an empty dict and 0 are all falsy, where GDScript only gives you the numeric and null cases
  • There is no scene tree, no signals and no _process: outside a game engine, something else has to decide when your code runs
  • What you gain is the libraryjson, re, datetime, itertools and comprehensions, which is why the tooling around your game tends to end up written here
RustPre-Alpha

The binding is real and people ship with it. godot-rust (gdext) registers a Rust struct as a node type the editor lists, inspects and instantiates, the same way GDExtension does for C++ — but with a compiler that refuses to build the class of bug you cannot currently see. What you trade for that is a build step, no hot reload, and a borrow checker that has opinions about the reference you were going to keep.

  • Ownership replaces "the engine will sort it out" — every value has exactly one owner, and passing it either moves it or lends it, which the compiler tracks rather than trusting you
  • There is no null. A value that might be missing is Option<T>, and the compiler will not let you use it without handling the empty case
  • Result<T, E> is what push_error wishes it were: a failure the caller cannot silently ignore, since ignoring it is a warning and unwrapping it is a decision you wrote down
  • No inheritance at all — a gdext node holds a Base<Node2D> rather than extending it, and shared behavior comes from traits instead of a parent class
  • match is checked for exhaustiveness, so adding a state to an enum turns every place that forgot it into a compile error rather than a silent fallthrough
  • Everything is immutable until you write mut, which is the reverse of every habit you have — and the reason data races are a compile error rather than a heisenbug
SwiftPre-Alpha

SwiftGodot is a real binding, and the one that reaches Apple platforms natively. Miguel de Icaza's project registers a Swift class as a Godot node type through GDExtension, and its sibling SwiftGodotKit embeds the engine inside a Swift app instead. If your game is going to iOS and you already live in Xcode, this is the path that does not ask you to leave.

  • Optionals replace null entirely — a value that might be missing is T?, and the compiler will not let you use it without unwrapping
  • enum carries associated values, so a state and its data travel together — and switch is checked for exhaustiveness, which match is not
  • struct copies and class shares, so the Vector2-versus-Array distinction you already feel becomes something you declare
  • Protocols and extensions replace inheritance for most jobs, and you can add methods to a type you did not write
  • guard let is the early-return shape that if x != null: return keeps almost expressing
  • 🚨 Check the version pairing before committing. SwiftGodot tracks Godot 4.6 on main with older engines on branches, and it targets iOS, macOS, Linux and Windows — not web
C#Pre-Alpha

Godot's other official language, in the same editor, on the same scene tree. You keep every node, signal and lifecycle callback you already know — _ready becomes _Ready and $Sprite2D becomes GetNode<Sprite2D>("Sprite2D"). What changes is everything around them: types are checked before the game runs, a build step stands between you and pressing play, and the engine's reference counting now has .NET's garbage collector living beside it.

  • Types stop being optional. GDScript lets you write var speed or var speed := 5.0 and checks the annotated one at runtime; C# has no untyped option, and the error arrives at build time instead of on the frame that touches it
  • Truthiness is gone — a condition must be a bool, so if (health) does not compile where if health: quietly meant "not zero"
  • Signals become two things at once: [Signal] delegate void HealthChangedEventHandler(int amount) declares one, and connecting is HealthChanged += OnHealthChanged — a C# event, checked for arity and type at compile time rather than at emit_signal
  • @export var speed := 200.0 becomes [Export] public float Speed = 200.0f; and still appears in the inspector, because the attribute is what the editor reads
  • Two lifetimes now overlap. A Node you QueueFree() is destroyed by the engine while its C# wrapper is collected by the GC, so a disposed node reached through a stale reference throws instead of returning null
  • LINQ replaces the for-loop-into-a-fresh-Array that GDScript makes you write for every filter, map and group
  • await get_tree().create_timer(1.0).timeout is await ToSignal(GetTree().CreateTimer(1.0), "timeout") — the same idea, with the compiler checking that the method returning it is async
LuaPre-Alpha⚡ Works Offline⚡ Offline

You can script Godot in Lua, and Lua is the language most other engines script in. gilzoide's lua-gdextension registers Lua as an alternative to GDScript and C#, and separately gives you sandboxed LuaStates — which is the modding story Godot otherwise makes you build yourself. Meanwhile Love2D, Defold and Roblox are all Lua, so this is the one comparison on this anchor that is sideways rather than downward.

  • There is exactly one data structure. Array and Dictionary are the same thing — a table — and {1, 2, 3} is just a table whose keys happen to be 1, 2 and 3
  • 🚨 Indices start at 1, and #list is the length. Every loop you have ever written has an off-by-one waiting in it
  • 🚨 0 is TRUE. Only nil and false are falsy, so if 0 then runs — the exact opposite of the check you write in GDScript
  • Variables are global unless you write local, which is the reverse of every scoping habit you have and the most common source of action-at-a-distance bugs
  • There are no classes. Metatables and __index are how inheritance is built, and a "class" is a table you wrote the plumbing for
  • Coroutines are explicit and general — coroutine.yield passes values both ways, where await only resumes
C++Pre-Alpha

The engine you already use is written in this. Godot's whole scene tree, its physics, its renderer and every built-in node are C++ — and since Godot 4, GDExtension lets you add to that layer without forking the engine. A class you register there appears in the editor's node list and is instantiated, inspected and connected exactly like a built-in one.

  • GDExtension is a shared library the editor loads, not a script it runs — so there is a build step, a .gdextension file, and no reloading a class by pressing play
  • Memory becomes yours: memnew and memdelete, Ref<T> for the reference-counted types, and a Node whose lifetime belongs to the parent you added it to
  • Pointers and references are the genuinely new idea — GDScript hands you an object, C++ makes you say whether you hold the thing, a name for it, or an address of it
  • Templates replace the Array-of-anything you reach for now, and they are checked when the library is built rather than when the frame runs
  • _bind_methods() is the price of talking to the engine: anything GDScript, a signal, or the inspector needs to reach has to be declared there by name
  • Reach for it where the work is per element, per frame — a particle solver, a mesh build, a pathfinder over thousands of cells. A row of engine calls buys nothing but a build step
Drag cards to reorder · your order is saved locally