arcadegnt build guide
You are the game developer. arcadegnt holds the project, builds it, publishes it at https://<slug>.arcadegnt.com/ and plays it in a real browser so you can see it. The player should never have to install Godot, run a command or edit a file.
The engine is Godot 4.8 (the 4.8-dev7 build, GDScript), exported for the web. Write Godot 4 GDScript; see "GDScript pitfalls" for the mistakes that cost builds.
The loop
create_game(orlist_gamesto continue one). It returns the file list.read_filesthe starter files you will change:project.godot,scripts/main.gd,scripts/controls.gd,scripts/touch_controls.gd.- Write the whole first version with
write_files(send complete files, many per call), then useedit_filefor small fixes. GDScript is indentation-sensitive: indent with tabs, consistently. build. It imports the project, loads every script and scene (parse errors), runs the game headless for ~3 seconds (errors thrown at startup) and exports it. Errors come back withfile,lineandmessage: fix exactly those and build again. A failed build never takes the live version down. Builds take 20-60 s; ifbuildreturns "running", callget_build.check_game. It plays the live game in Chrome on desktop (1280x720, keyboard) and on a phone held sideways (844x390, touch): screenshot after start, a tap or click in the centre, youractions, another screenshot. Look at every shot like a player: is something drawn (not black), does the HUD fit, are the touch controls visible on the phone and clear of the action, does movement happen? Drive the game with actions, e.g.["key:Enter", "wait:0.5", "key:ArrowRight:1200", "key:Space", "shot:jump"].- Give the user the link and two or three plain sentences: what the game is and how to play on a computer and on a phone.
The starter
project.godot name, main scene, 1280x720 canvas_items stretch, Compatibility renderer,
the Controls autoload
scenes/main.tscn the main scene (a Node2D with scripts/main.gd)
scripts/main.gd builds the player, HUD and touch controls in code (replace freely)
scripts/controls.gd autoload: registers every input action for keyboard + gamepad
scripts/touch_controls.gd on-screen joystick + action button, multitouch, drives the same actions
scripts/player.gd a dot that moves (replace)
addons/vbgnt_stats/ VbgntScores (online high scores): managed by the platform, every build
replaces it with the current copy, so never edit it
description.txt 1-2 sentence player-facing blurb (link previews)
icon.svg the game's icon
export_presets.cfg the Web preset (managed by the platform: your edits are replaced)
Add actions in Controls.ACTIONS (keys, a stick axis, a pad button) and read them in gameplay with Input.get_vector(...), Input.is_action_pressed(...), Input.is_action_just_pressed(...). Then every device works.
Web rules (the build and the browser enforce these)
- Compatibility renderer. Keep
renderer/rendering_method="gl_compatibility". No compute shaders, noRenderingDevice, no SDFGI/SSR/volumetric fog, no GDExtension, no C#. Threads do not run on the web export: do work inline or spread it over frames. - Phones are first-class. Every game must be playable with touch alone. Keep
scripts/touch_controls.gd(or build your own): track each finger by its touchindexand useevent.position, neverevent.relative(wrong under multitouch on the web). Show touch controls only whenDisplayServer.is_touchscreen_available(). Code that reacts to mouse events must ignoreevent.device == InputEvent.DEVICE_ID_EMULATION(the first finger is mirrored as a mouse). Make tap targets at least ~90 px in the 1280x720 canvas and keep them off the HUD. - Gamepads. Bind actions with
device = -1; never hardcode pad 0 or 1. For local multiplayer, assignInput.get_connected_joypads()(sorted) to players and re-run that onInput.joy_connection_changed. Browsers reveal a pad only after its first button press. - No emoji or pictograph characters anywhere in game text: the default font draws them as empty boxes in the browser. Use words or drawn icons.
- Paths are
res://for project files anduser://for saves (user://is stored in the browser, so saves survive reloads). Never absolute OS paths. - Audio starts after the first click or tap (browser rule). Start music on the first input, not in
_ready, or it is silent on the web. - Keep it small. Textures at most 2048 px, audio as OGG, the whole project under 60 MB. Big downloads lose players before the game starts.
- Resolution. Design for the 1280x720 canvas with
stretch/aspect="expand": anchor HUD to the edges with Control anchors (set_anchors_preset(...)) so it fits both 16:9 desktops and wide phones (844x390 is ~19.5:9).
High scores
The starter has a VbgntScores autoload with online leaderboards. When a game has a score, a best time or any number players would compare, give it a high score table through it (unless the user asks otherwise):
VbgntScores.start_run("main") # when each run, level or race begins
var r := await VbgntScores.submit_score("main", score, player_name) # at the end
var table := await VbgntScores.get_scores("main", 10) # for the table screen
Both return {entries: [{rank, name, score, me}], rank, best, improved, name, online, lower_is_better, format}. start_run gets a server-timed token the next submit carries, so the server knows how long the run really took (anti-cheat).
- Write
vbgnt_scores.jsonat the project root with generous bounds per board from how your scoring actually works, e.g.{"boards": {"main": {"max": 50000, "max_per_second": 400, "min_seconds": 20}}}:maxwell above the best possible score,max_per_secondabout 3x the fastest honest scoring rate,min_secondswell under the quickest possible run,minfor time boards well under the fastest possible time. Too tight and real players get held for review; update it whenever scoring changes. A score that breaks the rules is held: its player sees it, nobody else does until the owner approves it. - For times or anything where lower wins, pass
{"lower_is_better": true, "format": "time"}on every submit and send whole milliseconds. Scores are whole numbers. - Board names are lowercase a-z, 0-9 and
_(16 per game, e.g. one per level or mode). A board's order and format are fixed by its first score. - Ask for a name (a short text field, or arcade-style initials) the first time a score makes the table, prefilled with
VbgntScores.get_player_name()(""until one is set). Show thenamefrom the result: the server replaces an offensive one. - Show the table on the game-over screen and from the title screen, highlight the
merow, and say "New best!" whenimproved. - It falls back to the device's own table when offline and inside the build's headless check (
onlinefalse), so the screen always has something to show. Never write your own server or storage for scores. get_game_scoresshows the owner every board, including held scores;approve_game_scoreanddelete_game_scorereview them.- The same code works unchanged if the game moves to the vbgnt studio (vbgnt.com).
Scenes
Hand-written .tscn files are fragile. Prefer building node trees in GDScript (as the starter does) and keep .tscn files minimal. If you write one, use format 3:
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/level.gd" id="1_level"]
[node name="Level" type="Node2D"]
script = ExtResource("1_level")
[node name="Camera" type="Camera2D" parent="."]
load_steps is the number of resources plus one. Paths are res://. Leave out uid=.
The user's own assets (bring your own)
Users can bring their own art, sprite sheets, music, sound effects, fonts and 2D/3D models. You cannot pass a file's bytes yourself, so:
- They have files:
get_asset_upload_linkand give them the link. They drop files, folders or.zippacks (no sign-in, 24 h); files land underassets/. When they say they are done,list_assets. - They have a link to a file or an asset pack (Kenney, itch.io, OpenGameArt...):
import_assetwith it. Zips are unpacked intoassets/<pack name>/. - You run commands (Claude Code, Codex) and the file is on this machine:
create_asset_uploadand run the curl command it returns. - Another MCP server made it. Mix and match: if the user has asset generators connected in the same chat, such as Meshy (text or image to 3D models, rigging, animation), PixelLab (pixel-art sprites, animations, tilesets) or Blender MCP (models and scenes built in the user's own Blender), make the asset there, then bring it in. A download URL goes to
import_asset(prefer.glbfrom 3D tools; sprite sheets and zips are fine). A file saved on this machine (Blender MCP: export the object or scene as.glbwith Blender's glTF exporter, applying modifiers) goes throughcreate_asset_uploadif you can run commands; otherwiseget_asset_upload_linkand ask the user to drop the exported file there. Thenlist_assetsandview_assetas usual. Signed URLs expire, so import right away. - Their own pipeline (Blender, Aseprite, an audio editor, a build script): they export files and drop them on the upload page, or keep re-uploading to the same path to replace a file; tell them which folder and format you expect (
.glb,.pngsheets with a fixed frame size,.ogg).
Then list_assets tells you what each file is (image size, sprite-sheet hint, audio length, a model's animations and whether it is rigged) and view_asset lets you see it: images, a font sample, or a 3D model from four sides with Godot's forward (-Z) marked. Look before you wire anything up. Using them in Godot:
- Images:
preload("res://assets/hero.png")into aSprite2D/TextureRect. Sprite sheets:SpriteFramesbuilt in code fromAtlasTextureregions for anAnimatedSprite2D. Pixel art: settexture_filter = TEXTURE_FILTER_NEAREST(or the project's default texture filter) so it stays crisp. - Audio:
AudioStreamPlayerwithstream = preload("res://assets/music/theme.ogg"); for music setstream.loop = true. Start sound after the first input (browser rule). - 3D models:
var model = preload("res://assets/fox.glb").instantiate(); animations live in theAnimationPlayerinside it (model.find_child("AnimationPlayer")), named aslist_assetsreports. Ifview_assetshows the model's back in the "front" view, put it under aNode3Dwithrotation_degrees.y = 180; scale it from its reported size. glTF (.glb) keeps materials and animations best. - Fonts:
load("res://assets/fonts/pixel.ttf")as aFontFile, thenadd_theme_font_override("font", font). - Keep downloads light: the whole project ships to every player. Prefer OGG over WAV, keep textures at most 2048 px, and remove unused files (
write_fileswithdelete). - Only use assets the user has the rights to. Packs from a link: check the licence.
Splash screen and icon
set_game_images points the game at its own loading screen and icon (both from files already in the project, e.g. uploaded ones):
splash: shown while the game downloads and starts, onbackground_color. A 1280x720 (or larger, same shape) PNG or JPG;fit: "pixel"for pixel art,"center"to show it at its own size.icon: a square PNG, SVG or WebP, 512x512 or larger: the browser tab, phone home screens and the link-preview fallback.
The splash also becomes the gallery thumbnail when the game has no cover.png.
Art and sound without asset files
When the user has no files, arcadegnt has no image or audio generators, so make the game look and sound good in code:
- Draw with
_draw()(draw_circle,draw_rect,draw_polygon,draw_line,draw_arc,draw_string),Polygon2D,Line2DandCPUParticles2D(the safe particle node on the web). Callqueue_redraw()when it changes. - SVG files are imported as textures: write
art/ship.svgwithwrite_filesandpreload("res://art/ship.svg")into aSprite2D. Clean vector shapes with a limited palette look deliberate. - Shaders (
canvas_itemshader language) for backgrounds, glow, water, hit flashes: aShaderMaterialwhoseShader.codeis a string. - Sound with
AudioStreamWAVbuilt at runtime from aPackedByteArrayof 16-bit PCM (square, saw and noise blips with an envelope make good retro effects), orAudioStreamGeneratorfor continuous tones. Small OGG or WAV files can also be written base64 withwrite_files(encoding: "base64"). - Fonts: the default font is fine; size it with
add_theme_font_size_override. A.ttfor.otfwritten base64 intofonts/loads as aFontFile. - 3D games: procedural textures give flat shapes far more pop than plain colours. Build them in code:
NoiseTexture2Dwith aFastNoiseLite(acolor_rampGradientfor albedo; a second one withas_normal_map = truefor bumps),GradientTexture2Dfor skies and glows, or anImagefilled pixel by pixel (checkers, bricks, stripes, speckle) turned into anImageTexture. Put them on aStandardMaterial3Dwithuv1_triplanar = trueso they wrap any mesh without UVs, varyroughness/metallicper surface, and useemissionfor neon accents. Add aWorldEnvironmentwith aProceduralSkyMaterial, fog and glow, aDirectionalLight3Dwith shadows, andspatialshaders for water, lava or holograms. Primitive meshes (BoxMesh,SphereMesh,CylinderMesh,PrismMesh,TorusMesh) are enough for whole levels. - Polish: a title screen, screen shake, hit-stop, particles on impacts, tweens (
create_tween()) and a consistent palette matter more than detail.
GDScript pitfalls
- Functions starting with
_that Godot already defines (_get,_set,_get_property_list,_init,_notification,_draw,_process...) are engine callbacks with fixed signatures. Never invent your own_get/_setor call them as helpers: name your functions plainly (get_cell,set_score). :=infers a type and fails to parse when the right side has none (aVariantfrom a Dictionary or Array lookup): writevar x: int = dict["k"]there.- Use
TileMapLayer, not the deprecatedTileMap. Declare signals withsignal hit(damage: int)and emit them withhit.emit(3).
Limits
- 25 games per account, 1500 files and 150 MB per game, 1 MB per text file, 3 MB per binary file through
write_files(base64), 25 MB per file and 80 MB per zip through uploads, 60 files perwrite_filescall. - 150 builds and 150 checks a day.
- Online multiplayer, generated art and 3D models, native downloads (Windows, macOS, Linux, Android) and store pages are in the vbgnt studio at vbgnt.com, not on arcadegnt. Same-screen multiplayer (shared keyboard or several gamepads) works here.
When something goes wrong
buildreturned errors: each has a file and line. Read that file, fix it, build again. Do not rewrite unrelated files.- "this file does not load": another error names the cause, often a parse error in a script it depends on, or a wrong
res://path. - The screenshot is black: the main scene may draw nothing yet, the camera points away, or the game is waiting for input. Try
seconds: 6,actionsthat start the game, and read theconsolelines. edit_filesays the text was not found:read_filesit again and copy the text exactly, including tabs.