The best way to understand G.js is to build something that actually moves. This guide walks through the complete example from the README — a text object that slides left and right forever with a background flash on every cycle — line by line. By the end you will have seen all of the core building blocks:Documentation Index
Fetch the complete documentation index at: https://mintlify.com/g-js-api/G.js/llms.txt
Use this file to discover all available pages before exploring further.
$.exportConfig, objects, groups, trigger functions, context management, and looping.
Geometry Dash uses small-step units internally. One “big block” in the editor equals 30 small steps. The coordinate values you pass to
.with(obj_props.X, 45) are in small-step units, so X: 45 places an object 1.5 big blocks from the origin. When working with group.move(), the distance arguments are also in small steps — move(30, 0, 0.5) moves an object exactly one big block to the right over 0.5 seconds.Steps
Every G.js script starts with an import followed immediately by
$.exportConfig(). The config call is async, so your script needs to await it before doing anything else GD-related.type: 'savefile' tells G.js to write the finished level directly into your GD save file. If you would rather inspect the raw levelstring first, use type: 'levelstring' instead and capture the return value. The info: true option prints a summary — object count, group count, and so on — when the script finishes.Change
type to 'live_editor' to stream objects into GD in real time while you iterate, using the WSLiveEditor mod.Before placing an object you need a group ID to target it with move triggers later.
unknown_g() allocates the next available group and returns it as a $group value.This is equivalent to clicking “Next Free” in the GD editor. G.js tracks all allocated IDs so you never accidentally reuse one.
GD text objects can be built from a plain JavaScript string using the
.to_obj() extension method. Chain .with(prop, value) calls to set object properties, then finish with .add() to place the object in the level.'Hello, World!'
.to_obj()
.with(obj_props.X, 45)
.with(obj_props.Y, 45)
.with(obj_props.GROUPS, my_text)
.add();
obj_props is a dictionary of every GD object property key. Using obj_props.X, obj_props.Y, and obj_props.GROUPS instead of raw numbers makes the code readable and keeps you safe against magic-number errors.The object is positioned at X 45, Y 45 (in small-step units) and assigned the group ID stored in
my_text so that move triggers can target it later.A trigger function is a named group of GD triggers you can spawn on demand. You define one by passing a callback to
trigger_function(). The callback runs at compile time to collect the triggers; the returned TriggerFunctionGroup value is what you call at runtime.Everything inside the callback represents a sequence of GD triggers that will execute in order when the group is called.
Inside the trigger function, use
$.trigger_fn_context() to capture the group ID of the current context before any spawn delays advance it. Then add two move triggers, a background flash inside ignore_context_change(), and a recursive call to create the loop.let moveloop = trigger_function(() => {
// Capture the original context group before any delays
let my_context = $.trigger_fn_context();
// Move right 30 small steps over 0.5 s, then back
my_text.move(30, 0, 0.5);
my_text.move(-30, 0, 0.5);
// Flash the background WITHOUT advancing the context
ignore_context_change(() => log.runtime.flash());
// Call the original context group to loop
my_context.call();
});
Why
$.trigger_fn_context()? Each wait or sequential move trigger advances the internal spawn-delay context, giving later triggers a different group ID. If you called moveloop directly at the end, G.js would target the new context group — not the one the loop started in. Capturing it upfront with $.trigger_fn_context() and calling that specific group closes the loop correctly.Why
ignore_context_change()? The flash should fire at the same time as the second move rather than waiting for it to complete. Wrapping it in ignore_context_change() tells G.js not to switch the active context for the triggers inside, so the flash runs in parallel with the surrounding sequence.The trigger function is defined but not yet active. Call
.call() on the returned group to spawn it once — after that it loops itself forever via my_context.call().Complete Example
Here is the full script assembled:info: true is set.
Export Type Reference
Thetype field in $.exportConfig() controls how the level reaches GD:
type | Behaviour |
|---|---|
levelstring | Returns the raw GD levelstring as a string — useful for debugging or piping into other tools |
savefile | Writes directly to your local GD save file — the level appears in your editor immediately |
live_editor | Streams objects to a running GD session via the WSLiveEditor mod for real-time preview |
gmd | Produces a .gmd file compatible with GD World and external level editors |
What’s Next
This example only scratches the surface. Explore the concept guides to go deeper:Objects and Triggers
Learn how
object(), trigger(), .with(), and .add() work in detail.Trigger Functions and Contexts
Understand contexts, spawn delays, and how
$.trigger_fn_context() keeps loops correct.Groups, Colors, and Blocks
Deep dive into ID allocation,
unknown_g(), and how groups target objects.Counters and Conditions
Use
counter(), arithmetic methods, and while_loop for dynamic level logic.