Skip to content

JamChip in a game

A JamChip node is a Godot node you’d use anyway, built for the game side: art and sound that change while the game runs, that follow the dock while you draw, that ship without an export step, and whose job you can tell from the name.

Class Over The job
PixelSprite2D AnimatedSprite2D Artwork in a scene: a document, animated by its tags, following the dock.
PixelSpriteTexture ImageTexture One frame of a document anywhere a Texture2D goes: a TextureRect, a Sprite2D, a TileSet atlas.
PixelSpriteFrames SpriteFrames Every frame of a document, for an AnimatedSprite2D that isn’t ours.
ChiptuneAudioStream AudioStreamWAV A .sfx.tres or a .music.tres rendered for any audio player, ours or not, Godot’s AudioStreamPlayer3D among them.
ChiptuneSfxPlayer AudioStreamPlayer A .sfx.tres in a scene. Set document and it plays.
ChiptuneSfxPlayer2D AudioStreamPlayer2D A .sfx.tres that comes from somewhere in 2D.
ChiptuneSongPlayer AudioStreamPlayer A .music.tres, from the Tracker: it loops on the bar, and a game changes the music on it while it plays.
ChiptuneSongPlayer2D AudioStreamPlayer2D A .music.tres that comes from somewhere in 2D.
PixelTileSet TileSet A tileset document as a TileSet, for a TileMapLayer you already have.
PixelTileMapLayer TileMapLayer The ground of a level, painted with a tileset document’s tiles.
PixelFontFile FontVariation A font document anywhere a Font goes: a Label, a Button, a theme’s default font.
PixelPalette Resource A .jcpalette: a sprite’s palette, to show it in other colours, and colours you change in code.

Every member is on the site, at jamchip.com/api. EXAMPLE-GAME.md, beside this page, walks the example game, examples/game/game.gd in the repository, room by room, and meets each class where the game needs it. This page is what holds in any game.

Which pixel node

Node Document For In the example game
PixelSprite2D .jcsprite Artwork in a scene that animates, like a player, a pickup or a door. Player/Sprite
PixelSpriteTexture, a resource .jcsprite One frame anywhere a Texture2D goes: a TextureRect, a Sprite2D, a TileSet atlas. Hub/Sky
PixelSpriteFrames, a resource .jcsprite Every frame, one animation per tag, for an AnimatedSprite2D that isn’t ours. Meadow/Coin/Sparkle

Which sound node

Node Document For
ChiptuneSfxPlayer .sfx.tres A sound the game fires, like a jump, a hit or a coin.
ChiptuneSfxPlayer2D .sfx.tres The same, from a place on screen, panned and quieter with distance.
ChiptuneSongPlayer, Tracker .music.tres Music the game steers while it plays.
ChiptuneSongPlayer2D, Tracker .music.tres The same, from a place on screen, panned and quieter with distance.
ChiptuneAudioStream, a resource either Any other slot: an AnimationPlayer audio track, an AudioStreamRandomizer, an AudioStreamPlayer3D. A song renders whole: it plays, with no beat signals and nothing to steer it.

For a sound in 3D, put a ChiptuneAudioStream in the stream of Godot’s own AudioStreamPlayer3D and set its document.

When the document changes

A node follows its document. You don’t tell it to.

var spec := $Sounds/Jump.document as ChiptuneSfxSpec
spec.pitch += 3 # the node renders again on the next idle frame
spec.set_setting("decay", "0.2") # any line of spec.to_text(), as `sfx show` prints it
var twin := ChiptuneSfxSpec.from_text(spec.to_text()) # advanced, open to change until 1.0
var song := $Music.document as ChiptuneSong
song.set_order(PackedInt32Array([0, 0, 1])) # heard from the next pattern; advanced, as are replace_pattern, resize_pattern and remove_pattern
var redo: ChiptuneSong = ChiptuneSongText.read(ChiptuneSongText.write(song), song)["song"] # as `song show` prints it; advanced
ChiptuneSoundPalette.use_for_song(song, ChiptuneSoundPalette.style_set()) # on the style's sound palette; advanced, as is detach_instrument
The change Sprites and tiles Sounds Songs on a song player
Made in game code Redrawn on the next idle frame. Rendered again on the next idle frame. Heard from the next pattern.
Saved in the dock The scene open in the editor redraws as you draw, before the save. The scene’s nodes hold the new sound once it’s saved. The scene takes the saved song the next time it loads.
Saved while the game runs Redrawn in place, on the same animation and frame. Rendered again; the next play is the new sound. Heard from the next pattern; the music carries on.

A song on any other player is rendered again and starts on the bar nearest where it had got to.

Call rebuild() to see a change in the frame you made it, or one made behind the document’s back, like a cel written directly. Every class has it. On a tileset it’s the set’s: (ground.tile_set as PixelTileSet).rebuild().

Saves reach the game over the debugger connection the editor already has, so an exported build has nothing listening. To turn it off, the project setting jamchip/push_to_game, Push To Game under Jamchip.

A document is one loaded resource, shared by every node holding its file. An edit in code is heard on all of them. Turn on unique on a sound or song player to give it a copy of its own; for a sprite, palette and set_layer_visible change one node without touching the document.

Drawing from code

Advanced, and open to change until 1.0. A sprite’s cel, one layer on one frame, can be read and written whole as swatch numbers, counted from 1, with 0 for clear: the same calls the command line’s sprite cel makes.

var bat := load("res://art/bat.jcsprite") as PixelDocument
var grid := bat.cel_indices(0, 0) # layer 0, frame 0
for pixel in grid.size():
if grid[pixel] == -1: # a colour no swatch holds
grid[pixel] = 0 # write it clear, or a swatch
grid[5] = 2
if bat.set_cel_indices(0, 0, grid) != OK: # redrawn on the next idle frame
push_warning("the grid doesn't fit the bat")
var wings := (load("res://art/wings.png") as Texture2D).get_image()
bat.paste_into_cel(0, 0, wings, Vector2i(4, 0))
bat.paint_shape(0, 0, &"ellipse", Vector2i(1, 1), Vector2i(6, 5), bat.palette[2], {"filled": true})
bat.edit_cel(0, 0, &"flip_horizontal", PixelSelection.from_rect(Rect2i(0, 0, 4, 8)))

set_cel_indices refuses a grid of the wrong size or a number with no swatch, leaving the cel alone. paint_stroke, paint_shape (&"line", &"rectangle" or &"ellipse") and paint_fill are the tab’s drawing tools, sprite paint’s calls; a clear colour erases. paint_polygon takes three corners or more and paints the outline, or with filled the pixels PixelSelection.from_polygon selects through them. Options: brush, round, pixel_perfect, filled, tolerance, whole_layer, symmetry, a PixelSymmetry.Mode, axis, a Vector2 in pixels, halves allowed, dither, a pattern’s text: "bayer4:8", "tile:1" for the sprite’s first drawn tile, or rows like "#./.#", and selection, a PixelSelection every tool paints only inside; PixelSelection.from_opaque(cel) is what is drawn on a cel. smooth draws a stroke or a polygon as a smooth curve through its points, and from_polygon’s smooth selects inside one. An ellipse also takes thickness, a ring that many pixels thick, and arc, a Vector2 of degrees, start and end, clockwise from the right, keeping only that part of it, past 360 when the end is the smaller; the tab has neither. Both are refused off an ellipse, and thickness with filled, a brush over 1 or round. edit_cel is the Edit menu’s &"delete", &"flip_horizontal", &"flip_vertical", &"rotate_left" and &"rotate_right", or the select tool’s &"move" with {"offset": Vector2i(2, 0), "all_layers": true}, over a PixelSelection, the whole cel when null: sprite cel’s calls. Make one with PixelSelection.from_rect or from_polygon, or the wand’s PixelFloodFill.select. outline_cel(layer, frame, color, selection, {"width": 2, "inside": false, "round": true}) is sprite cel --outline: a line 1 to 32 pixels wide round the cel’s drawing, or the part a selection holds, on the clear pixels outside it unless inside. It’s written into the cel and doesn’t follow later drawing. The paint calls, edit_cel and outline_cel return the Rect2i they changed, empty for none or a locked layer. add_frame, duplicate_frame, move_frame, set_frame_duration, add_frame_event, add_layer, move_layer, set_layer_visible, add_tag, set_tag_range and the rest change frames, layers and tags: the same calls sprite frames, layers and tags make, frames and layers counted from 0 and layer 0 the bottom. For drawing that changes every frame, draw into an Image or in _draw instead: each call here is a whole cel. A sprite’s files are PixelSpriteFiles’s, the calls sprite export and sprite import make: write_png, write_sheet, write_tileset and write_aseprite write what the tab’s Export writes and answer {}, or {path, reason} when one could not, and import_aseprite, import_sheet and import_grid, the one --cell makes, answer {document, error, notes}.

A tileset changes through PixelTilesDocument‘s set_cell_image, rename_tile, set_collision, set_custom_data, set_terrain, copy_tile, wrap_shift_tile, add_terrain and regrid, among the tiles verbs’ calls; a level fills by name, exported too: $Forest.place_tile(Vector2i(3, 4), "tree"), as tiles layer lays one. A tileset’s files are PixelTilesFiles’s, the calls tiles export and tiles import make: write_tileset writes the tab’s PNG and TileSet and answers {} or {path, reason}, a done dictionary given getting wrote, fresh, merged or replaced, and import_png, import_tile_set and import_aseprite answer a PixelTileSetImporter, its document, or error when it could not.

A font changes through PixelFontDocument‘s set_characters, add_character, remove_character, resize_cell, set_guides, set_advance and paint, the font verbs’ calls; a glyph is found by index_of("A"), and its place moves when a character before it comes or goes. A font’s files are PixelFontFiles’s, the calls font export and font import make: write_font writes the tab’s atlas and FontFile and answers {} or {path, reason, error}, and import_png and import_font answer a PixelFontImporter, its document, or error when it could not. import_font takes the size to draw the face at, in pixels, apart from the cell: PixelFontImporter.OWN_SIZE for the size PixelFontImporter.pixel_size_of(bytes) finds its pixels land whole at, and Vector2i.ZERO for a cell that holds every glyph.

Signals

What the parents emit still fires: animation_finished, animation_looped and frame_changed on a sprite, finished on a player. JamChip adds:

  • document_changed on every class; on a PixelTileMapLayer, connect to its tile_set’s. A rebuild has landed, whatever caused it.
  • On PixelSprite2D: tag_finished(tag) when a tag ends, once per loop for a looping one; frame_event(name) when a frame with a marker shows; palette_changed(palette) once a node’s palette has landed. PixelSpriteTexture and PixelSpriteFrames have palette_changed too.
  • On both song players: beat(index) on every beat, bar(index) on every bar line, and looped as the loop comes round, each as you hear it.

Changing music while it plays

Everything here is on the node and leaves the song alone, so two nodes playing one song can sound different.

music.set_channel_muted(2, true) # the drums drop out, from the next buffer
music.tempo = 150.0 # from the next pattern; 0 is the song's own
music.jump_to_pattern(3) # at the next bar line
  • The transport. jump_to_pattern and jump_to_row beside the parent’s play(), stop() and stream_paused. position_bar(), pattern_index() and pattern_row() say where you are.
  • The mixer. set_channel_muted, set_channel_solo, set_channel_volume, and is_channel_muted to ask. They land on the next buffer, a fifth of a second at most.
  • The echo. set_send(channel, &"echo", amount, seconds).
  • The overrides. tempo, transpose and set_instrument, cleared by clear_instrument. They land at the start of the next pattern.

The chiptune guide has the details, under “Changing a sound or a song while the game runs”.

A hit flash

A PixelSprite2D flashes to one swatch of its palette: flash from 0, no flash, to 1, solid, and flash_swatch counted from 0. Tween it; there’s no shader to write, and the node’s material stays yours and leaves the flash alone.

slime.flash_swatch = 3
slime.create_tween().tween_property(slime, "flash", 0.0, 0.2).from(1.0)

The colour is the swatch of palette when the node wears one, else of the sprite’s own colours, so a palette switch mid-flash carries on in the new colours. A swatch past the end flashes nothing. Clear pixels stay clear.

Palettes in code

Advanced, and open to change until 1.0. Load a palette, make one, or change one while the game runs; a sprite wearing it redraws on the next idle frame when the colours go through colors or the palette’s methods.

var villager: PixelSprite2D = $Villager
var gold := PixelPalette.load_palette("res://art/villager_gold.jcpalette")
villager.palette = gold
gold.set_swatch(2, Color("ffd75e")) # every node wearing gold redraws
gold.ramp_between(0, 7) # 1 to 6 refilled, dark to light
var dusk := PixelPalette.new()
dusk.colors = PixelPaletteMaths.ramp(Color("1b1f3b"), Color("f4e9b0"), 8)
  • nudge_swatches, regenerate and sort_swatches too: the maths palette nudge, mutate and edit --sort run. Write a swatch with set_swatch, not gold.colors[2] = …, which Godot doesn’t report.
  • A loaded palette is shared by every node wearing it; duplicate() one to change it alone. Nothing is written to the file.
  • document.recolor(old, new) writes new colours into the drawing itself.

The style in an export

Godot imports your .jcstyle and the palettes it links like any other resource, so a preset that exports all resources carries them. The editors start new work from it, and JamChip’s sound reads one thing from it: how loud the music plays against the sound effects, every song at its music_db and every sound at its sounds_db, on any player. A palette in a node’s palette ships anyway. To leave the style out, add *.jcstyle, and *.style.tres for an older one, to the preset’s exclude filter; a script asking for the style then gets the built-in one, and every sound plays at full level. A script reads it with JamChipStyle.current(): its palettes, main first, then its swaps, sprite_size, grid, channels and waves. Advanced, open to change until 1.0.

When to export

A game ships on its documents, and the export carries the runtime that plays them. Export when that’s the wrong trade, with each tab’s File menu: a .wav for a long song on a plain player, which Godot’s importer compresses and loads instead of rendering; a PNG or a sheet when the layer data shouldn’t travel with the game.

JamChip, by GlitterSnack. MIT licence. Built from the source on every merge. Its artwork and screenshots are made with JamChip.