The example game, room by room
The example game is examples/game/ in the repository: a village and the
rooms off it, a platformer where every sprite, tile, sound and song is a
document on a node. This page walks it a place at a time and meets each
JamChip class where the game needs it. RUNTIME.md, beside
this page, has the classes themselves and what holds in any game.
The village
The signs are Labels wearing a PixelFontFile over
arcade.font.tres, a font drawn in the Font editor. In the inspector: the
label’s Theme Overrides ▸ Fonts ▸ Font, New › PixelFontFile, then its
Document. The notice by the start is lettered the same way, and tells
you which document to draw on while the game runs.
The sky and the houses are PixelSpriteTextures on plain
TextureRects: one frame of a document, anywhere a Texture2D goes. The
village’s sky is one pixel wide and stretched; the houses are tiled. The
meadow’s sky and hills are tiled too. A tile repeats every pixel, so the
meadow’s sky can dither. The scene sets these in the inspector; in code,
the hills would be:
var hills := PixelSpriteTexture.new()hills.document = load("res://examples/art/meadow.jcsprite")$Meadow/Hills.texture = hills$Meadow/Hills.stretch_mode = TextureRect.STRETCH_TILETwo villagers, one document. The notice names villager.jcsprite.
Draw on it and save, and both villagers change; the second stays gold,
because its node’s palette is villager_gold.jcpalette. The slimes
in the meadow do the same thing in red.
The meadow
The player, the slimes, the coins and the flowers are PixelSprite2D. Set
Document to a .jcsprite and the node draws it. Each tag on the
document is an animation on the node, so the calls are the ones you’d make
on any AnimatedSprite2D:
player_sprite.play("run")player_sprite.flip_h = axis < 0.0speed_scale, flip_v, play_backwards, pause, stop, frame and
frame_progress are the parent’s too. The node adds what a tag needs and
the parent doesn’t have. Landing plays one tag and then the next, with no
handler:
player_sprite.play_then("land", "idle")queue(tag) lines tags up the same way.
The landing sound comes from the artwork. The artist put a land
marker on the frame the feet touch down, and the node says so when that
frame shows:
player_sprite.frame_event.connect(func(name: StringName) -> void: if name == &"land": $Sounds/Land.play())Two slimes, one document. The second slime’s node has palette set
to slime_red.jcpalette. The document stays green and the node draws
it red, swatch by swatch. Set it from code the same way:
slime.palette = load("res://examples/art/slime_red.jcpalette").
The sparkle is a plain AnimatedSprite2D. Its sprite_frames is
sparkle.frames.tres, a PixelSpriteFrames over a document. This is the
route for a node that isn’t ours: another addon’s sprite, or one your
game already has. PixelSpriteTexture is the same idea for one frame.
The sounds are ChiptuneSfxPlayer. Set Document to a .sfx.tres
and call play(). The pickup climbs a whole tone per coin in a run,
without rendering anything new:
var pickup := $Sounds/Pickup as ChiptuneSfxPlayerpickup.play_at((pickup.document as ChiptuneSfxSpec).pitch + coin_run * 2)play_at(pitch) takes a MIDI note, 60 being middle C. The hurt node has
variants set to 3, so a hit is a different hit three times running, and
the stomp is the same sound four notes lower through play_at.
pitch_variance spreads every play at random instead.
The cave
The cave is dark in its own documents. Its sky, stalactites and all,
is cave_sky.jcsprite, and the bank of rock behind the floor is
cave.jcsprite: two more documents on TextureRects through
PixelSpriteTexture, tiled like the meadow’s. The floor is mostly the
tileset’s stone tile. Nothing in the scene darkens any of it: draw on
cave.jcsprite in a lighter colour and save, and the rocks are lighter.
The ground is a PixelTileMapLayer over
examples/art/ground.tiles.tres. Paint it with Godot’s own tile editor.
Each tile brings its art, its collision and its custom data from the
tileset document, so the game asks a cell what it is:
var data := ground.get_cell_tile_data(ground.local_to_map(ground.to_local(point)))var kind := "" if data == null else str(data.get_custom_data("kind"))water slows the player and spike hurts; both are declared on the
tiles in the Tiles tab. The cave has a pit of spikes to jump, with a coin
over it, and a pool to wade. For a TileMapLayer you already have, put a
PixelTileSet in its Tile Set slot instead.
The tileset also has a terrain, earth, which the game doesn’t use yet:
sixteen tiles, one for each way its four corners can be filled. Pick it on
the Terrains tab of Godot’s tile editor and paint: Godot chooses the
tile for each cell. Redraw a tile in the Tiles tab and every cell painted
with it changes.
The lantern is a layer on the player’s document, hidden there. Picking it up shows it on this node only, and the document isn’t written:
player_sprite.set_layer_visible("Lantern", true)clear_layer_overrides() goes back to the document’s own.
The bat’s flutter is a ChiptuneSfxPlayer2D, so it pans as the bat
crosses the room. It plays on the wing’s downstroke, from a marker named
flap on the bat’s wings-down frame, while you’re in the cave:
($Cave/Bat/Sprite as PixelSprite2D).frame_event.connect(func(name: StringName) -> void: if name == &"flap" and room == CAVE and state == State.PLAYING: $Cave/Bat/Sound.play())Move the marker in the dock and the flap moves with it.
The cave has its own song, cavern.music.tres on a second
ChiptuneSongPlayer. Walking in stops the meadow’s and starts this one
from the top; walking out does the reverse:
music.stop()cave_music.play()Wading drops its lead, through the mixer in
RUNTIME.md, “Changing music while it plays”.
The workshop
The workshop is heard from where you stand. Everywhere else the camera
is the listener, in the middle of the room. Here an AudioListener2D on
the player takes over on the way in and hands back on the way out, so each
machine pans from one side to the other as you walk past it:
$Player/Ears.make_current()$Player/Ears.clear_current()The press’s clank and its danger are on its frames. press.jcsprite
has one tag, stamp, which the node plays on its own. It carries a
stamp marker on the frame where the ram lands and a lift marker on the
next, and the game listens for both:
$Workshop/Press/Sprite.frame_event.connect(_on_press_event)
func _on_press_event(name: StringName) -> void: if name == &"stamp": press_down = true $Workshop/Press/Sound.play() elif name == &"lift": press_down = falseMove a marker in the dock and save, and the clank and the danger move with
it. The press’s Sound is a ChiptuneSfxPlayer2D with variants set to 3,
so three stamps in a row each sound different.
The organ is one sound played five ways. Each pipe has a
ChiptuneSfxPlayer2D on pipe.sfx.tres, and walking through one plays the
document’s own pitch plus that pipe’s step of a major scale, do-re-mi-fa-so:
sound.play_at((sound.document as ChiptuneSfxSpec).pitch + PIPE_STEPS[step])pipe_sprite.play_then("toot", "rest")Walk left to right and the pipes climb the scale. No new sound is made for any of them.
The steam is a ChiptuneAudioStream on a plain AudioStreamPlayer.
In the inspector: the player’s Stream, New › ChiptuneAudioStream,
then its Document, here steam.sfx.tres. This is how a sound gets
into a node that isn’t ours. It loops while you are in the workshop
because its stream is told to:
($Workshop/Steam.stream as ChiptuneAudioStream).loop = true$Workshop/Steam.play()The band comes in as you go. The workshop plays the meadow’s song, but from the way in you only hear its bass and drums. The chords come in at the press, and the lead and its echo after the organ. Each change is a channel muted or unmuted on the song’s player, heard a moment later, on the next buffer:
music.set_channel_muted(2, false)The song itself is never changed. RUNTIME.md, “Changing
music while it plays”, has the mixer.
The castle
The finale, through the village’s gate once you hold a key from each room.
Its sky and wall work like the cave’s: two documents,
castle_sky.jcsprite and castle.jcsprite, each a
PixelSpriteTexture on a plain TextureRect, tiled. The torches are
one torch.jcsprite on two PixelSprite2Ds. Its one tag, flicker,
plays on its own, like the meadow’s flowers.
The key and the door are tags. The key plays shine. The door has
closed and open, a frame each, and opens once you hold the key:
$Castle/Door/Sprite.play("open")The meadow’s music is back, a ChiptuneSongPlayer holding
song.music.tres. It renders the song as it plays, so the scene
loads without a stall, and it loops on the beat with
no help. Each level has ninety seconds on the clock, from the way in,
and the village none. Under the last fifteen the song goes up a semitone,
one note higher, at the same tempo. On its own, a transpose waits for the
song’s next pattern, which can be seconds away. So the game also jumps to
the row the next buffer starts on, and you hear the change at once:
music.transpose = 1music.jump_to_row(music.pattern_index(), music.pattern_row() + ahead)ahead is the number of rows in one buffer, which game.gd works out
from the song’s tempo.
The end card comes up when you walk through the door or lose your last
life. The song fades out under it, a tween on the player’s volume_db,
and any key goes back to the title for another game.
The clock is a Label wearing a PixelFontFile over
arcade.font.tres, at 24, three times its eight pixel cell; the message
line and the cards’ lines wear the same one. Paint a glyph in the Font
editor while the game runs and every line has it after the save.
JamChip, by GlitterSnack. MIT licence. Built from the source on every merge. Its artwork and screenshots are made with JamChip.