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 ChiptuneSfxSpecspec.pitch += 3 # the node renders again on the next idle framespec.set_setting("decay", "0.2") # any line of spec.to_text(), as `sfx show` prints itvar twin := ChiptuneSfxSpec.from_text(spec.to_text()) # advanced, open to change until 1.0var song := $Music.document as ChiptuneSongsong.set_order(PackedInt32Array([0, 0, 1])) # heard from the next pattern; advanced, as are replace_pattern, resize_pattern and remove_patternvar redo: ChiptuneSong = ChiptuneSongText.read(ChiptuneSongText.write(song), song)["song"] # as `song show` prints it; advancedChiptuneSoundPalette.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 PixelDocumentvar grid := bat.cel_indices(0, 0) # layer 0, frame 0for pixel in grid.size(): if grid[pixel] == -1: # a colour no swatch holds grid[pixel] = 0 # write it clear, or a swatchgrid[5] = 2if 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_changedon every class; on aPixelTileMapLayer, connect to itstile_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.PixelSpriteTextureandPixelSpriteFrameshavepalette_changedtoo. - On both song players:
beat(index)on every beat,bar(index)on every bar line, andloopedas 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 buffermusic.tempo = 150.0 # from the next pattern; 0 is the song's ownmusic.jump_to_pattern(3) # at the next bar line- The transport.
jump_to_patternandjump_to_rowbeside the parent’splay(),stop()andstream_paused.position_bar(),pattern_index()andpattern_row()say where you are. - The mixer.
set_channel_muted,set_channel_solo,set_channel_volume, andis_channel_mutedto 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,transposeandset_instrument, cleared byclear_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 = 3slime.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 = $Villagervar gold := PixelPalette.load_palette("res://art/villager_gold.jcpalette")villager.palette = goldgold.set_swatch(2, Color("ffd75e")) # every node wearing gold redrawsgold.ramp_between(0, 7) # 1 to 6 refilled, dark to lightvar dusk := PixelPalette.new()dusk.colors = PixelPaletteMaths.ramp(Color("1b1f3b"), Color("f4e9b0"), 8)nudge_swatches,regenerateandsort_swatchestoo: the mathspalette nudge,mutateandedit --sortrun. Write a swatch withset_swatch, notgold.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.