# ============================================================================ # atlas.ludic — a namespaced spritesheet / atlas API (#81). # # Sprite loading used to be a bare png_load("assets/sprites/floor0.png") — one # file per 16x16 sprite, and no way to load one sheet and address a cell by grid # coords or by name. This adds: # # Sprite.sheet(path, cellw, cellh) -> handle load a sheet, remember its grid # Sprite.cell(sheet, col, row) -> id one cell, addressed by grid coords # Sprite.cell_span(sheet, col, row, cols, rows) a sprite spanning >1 cell (tall/wide) # Sprite.define(name, sheet, col, row) -> id name a cell for later lookup # Sprite.named(name) -> id look a named sprite up # Sprite.draw(id, x, y) / draw_scaled(id,x,y,sc) blit an atlas sprite # Sprite.width(id) / height(id) its pixel size # Assets.image(path) -> id a whole file as one sprite # Assets.load(path) -> id alias of Assets.image # Assets.get(name) -> id alias of Sprite.named # # It stands on the variable-size image loader in image.ludic (rt_image_load keeps # the whole decoded PNG; a cell is a sub-rect of it), so a sprite is NOT restricted # to the 16x16 sprite table — a cell (and a multi-cell span) can be any size. Draws # go through rt_put_px, so the camera, zoom (#78) and clip all apply, exactly like # Screen.sprite. Spliced on demand when a program uses Sprite.sheet / Assets.*. # ============================================================================ const ATLAS_MAX_SHEET: int = 32 const ATLAS_MAX_SPR: int = 1024 const ATLAS_MAX_NAME: int = 512 var at_sheet_img: words = null # image id per sheet var at_sheet_cw: words = null # cell width per sheet var at_sheet_ch: words = null # cell height per sheet var at_nsheet: int = 0 var at_spr_sheet: words = null # owning sheet per atlas sprite var at_spr_sx: words = null # source x/y (px) of the sub-rect var at_spr_sy: words = null var at_spr_w: words = null # sub-rect width/height (px) var at_spr_h: words = null var at_nspr: int = 0 var at_name_str: pointers = null # name per named sprite var at_name_id: words = null # atlas id per name var at_nname: int = 0 # #82 — an incremental preload queue: enqueue named image files, then load a # bounded number per frame (Assets.pump) so a loading scene stays responsive and # a game only enters play once Assets.ready(). Deterministic: the same enqueue + # pump order loads the same assets in the same order on every run. import "audio.ludic" # the preload queue feeds .wav/.mp3 into the sound bank const ATLAS_MAX_QUEUE: int = 512 var at_q_name: pointers = null # name to register the loaded sprite under var at_q_path: pointers = null # file path to load var at_q_n: int = 0 # how many enqueued var at_q_pos: int = 0 # how many loaded so far function atlas_init() -> void { if at_sheet_img != null { return } at_sheet_img = words(ATLAS_MAX_SHEET) at_sheet_cw = words(ATLAS_MAX_SHEET) at_sheet_ch = words(ATLAS_MAX_SHEET) at_spr_sheet = words(ATLAS_MAX_SPR) at_spr_sx = words(ATLAS_MAX_SPR) at_spr_sy = words(ATLAS_MAX_SPR) at_spr_w = words(ATLAS_MAX_SPR) at_spr_h = words(ATLAS_MAX_SPR) at_name_str = pointers(ATLAS_MAX_NAME) # a pointer (8 bytes) per name slot at_name_id = words(ATLAS_MAX_NAME) at_q_name = pointers(ATLAS_MAX_QUEUE) at_q_path = pointers(ATLAS_MAX_QUEUE) } # register (name -> atlas id) in the name table so Assets.get / Sprite.named find it. function atlas_register_name(name: pointer, id: int) -> void { atlas_init() if at_nname >= ATLAS_MAX_NAME { return } at_name_str[at_nname] = name at_name_id[at_nname] = id at_nname += 1 } # load a spritesheet PNG whose cells are cw x ch px; returns a sheet handle (>= 0). function atlas_sheet(path: pointer, cw: int, ch: int) -> int { atlas_init() if at_nsheet >= ATLAS_MAX_SHEET { return -1 } let img = rt_image_load(path) if img < 0 { return -1 } let h = at_nsheet at_sheet_img[h] = img at_sheet_cw[h] = cw at_sheet_ch[h] = ch at_nsheet += 1 return h } # register an atlas sprite for a sub-rect (px) of a sheet's image. function atlas_make(sheet: int, sx: int, sy: int, w: int, h: int) -> int { atlas_init() if at_nspr >= ATLAS_MAX_SPR { return -1 } let id = at_nspr at_spr_sheet[id] = sheet at_spr_sx[id] = sx at_spr_sy[id] = sy at_spr_w[id] = w at_spr_h[id] = h at_nspr += 1 return id } # one cell (col, row) of a sheet, addressed by grid coords. function atlas_cell(sheet: int, col: int, row: int) -> int { if (sheet < 0) or (sheet >= at_nsheet) { return -1 } let cw = at_sheet_cw[sheet] let ch = at_sheet_ch[sheet] return atlas_make(sheet, col * cw, row * ch, cw, ch) } # a sprite spanning cols x rows cells from (col, row) — some sprites cover more # than one cell (a tall character, a wide object). function atlas_cell_span(sheet: int, col: int, row: int, cols: int, rows: int) -> int { if (sheet < 0) or (sheet >= at_nsheet) { return -1 } let cw = at_sheet_cw[sheet] let ch = at_sheet_ch[sheet] return atlas_make(sheet, col * cw, row * ch, cols * cw, rows * ch) } # name a cell so a game can look it up by name; returns the atlas id. function atlas_define(name: pointer, sheet: int, col: int, row: int) -> int { atlas_init() let id = atlas_cell(sheet, col, row) if id < 0 { return -1 } if at_nname < ATLAS_MAX_NAME { at_name_str[at_nname] = name at_name_id[at_nname] = id at_nname += 1 } return id } # look up a named sprite's atlas id (names compare by content), or -1. function atlas_named(name: pointer) -> int { atlas_init() var i = 0 while i < at_nname { if at_name_str[i] == name { return at_name_id[i] } i += 1 } return -1 } # load a whole image file as one atlas sprite (a 1x1 sheet the size of the image). function atlas_image(path: pointer) -> int { atlas_init() if at_nsheet >= ATLAS_MAX_SHEET { return -1 } let img = rt_image_load(path) if img < 0 { return -1 } let w = img_w[img] let h = img_h[img] let sh = at_nsheet at_sheet_img[sh] = img at_sheet_cw[sh] = w at_sheet_ch[sh] = h at_nsheet += 1 return atlas_make(sh, 0, 0, w, h) } # blit an atlas sprite at (dx, dy) through rt_put_px (camera / zoom / clip apply). # Only sufficiently-opaque pixels are drawn (the sprite's transparent border is # skipped), matching Screen.sprite. function atlas_draw(id: int, dx: int, dy: int) -> void { atlas_init() if (id < 0) or (id >= at_nspr) { return } let sheet = at_spr_sheet[id] let img = at_sheet_img[sheet] let iw = img_w[img] let ih = img_h[img] let s: words = img_px[img] let sx = at_spr_sx[id] let sy = at_spr_sy[id] let w = at_spr_w[id] let h = at_spr_h[id] var y = 0 while y < h { let srcy = sy + y if (srcy >= 0) and (srcy < ih) { var x = 0 while x < w { let srcx = sx + x if (srcx >= 0) and (srcx < iw) { let px = s[srcy * iw + srcx] if ((px >> 24) & 255) >= 128 { rt_put_px(dx + x, dy + y, px & 16777215) } } x += 1 } } y += 1 } } # blit an atlas sprite scaled by an integer factor (through rt_fill_rect blocks). function atlas_draw_scaled(id: int, dx: int, dy: int, sc: int) -> void { atlas_init() if (id < 0) or (id >= at_nspr) { return } if sc < 1 { return } let sheet = at_spr_sheet[id] let img = at_sheet_img[sheet] let iw = img_w[img] let ih = img_h[img] let s: words = img_px[img] let sx = at_spr_sx[id] let sy = at_spr_sy[id] let w = at_spr_w[id] let h = at_spr_h[id] var y = 0 while y < h { let srcy = sy + y if (srcy >= 0) and (srcy < ih) { var x = 0 while x < w { let srcx = sx + x if (srcx >= 0) and (srcx < iw) { let px = s[srcy * iw + srcx] if ((px >> 24) & 255) >= 128 { rt_fill_rect(dx + x * sc, dy + y * sc, sc, sc, px & 16777215) } } x += 1 } } y += 1 } } # blit an atlas sprite with the full Sprite-component treatment (#90): integer scale, # horizontal flip, and a solid tint (non-zero = every opaque pixel in that colour). # This is what the engine sprite-render system calls for `Sprite { atlas: 1 }`. function atlas_draw_ex(id: int, dx: int, dy: int, sc0: int, flip: int, tint: int) -> void { atlas_init() if (id < 0) or (id >= at_nspr) { return } var sc = sc0 if sc < 1 { sc = 1 } let sheet = at_spr_sheet[id] let img = at_sheet_img[sheet] let iw = img_w[img] let ih = img_h[img] let s: words = img_px[img] let sx = at_spr_sx[id] let sy = at_spr_sy[id] let w = at_spr_w[id] let h = at_spr_h[id] var y = 0 while y < h { let srcy = sy + y if (srcy >= 0) and (srcy < ih) { var x = 0 while x < w { var rx = x if flip != 0 { rx = w - 1 - x } let srcx = sx + rx if (srcx >= 0) and (srcx < iw) { let px = s[srcy * iw + srcx] if ((px >> 24) & 255) >= 128 { var col = px & 16777215 if tint != 0 { col = tint & 16777215 } if sc == 1 { rt_put_px(dx + x, dy + y, col) } else { rt_fill_rect(dx + x * sc, dy + y * sc, sc, sc, col) } } } x += 1 } } y += 1 } } # a strip of `count` frames starting at (col,row), each `rows` cells tall, registered # as consecutive ids so `first + SpriteAnim.frame` addresses the current frame. function atlas_strip(sheet: int, col: int, row: int, count: int, rows: int) -> int { var first = -1 var i = 0 while i < count { let id = atlas_cell_span(sheet, col + i, row, 1, rows) if first < 0 { first = id } i += 1 } return first } # an atlas sprite's pixel size — handy for centering / layout. function atlas_width(id: int) -> int { atlas_init(); if (id < 0) or (id >= at_nspr) { return 0 }; return at_spr_w[id] } function atlas_height(id: int) -> int { atlas_init(); if (id < 0) or (id >= at_nspr) { return 0 }; return at_spr_h[id] } # Sprite.draw_meter: `value` of `max` as a row of icons — full, half or empty per # `per_icon` points (hearts, stars, ammo pips), `spacing` px apart function atlas_draw_meter(x: int, y: int, value: int, max: int, per_icon: int, spacing: int, full: int, half: int, empty: int) -> void { if per_icon <= 0 { return } var i = 0 while i < max / per_icon { var id = empty if value >= i * per_icon + per_icon / 2 { id = half } if value >= (i + 1) * per_icon { id = full } atlas_draw(id, x + i * spacing, y) i += 1 } } # Assets.enqueue_dir: every file of a directory, named by its file name without the # extension (assets/audio/hit.wav -> "hit"); the queue sorts by extension as usual function assets_enqueue_dir(dir: pointer) -> void { let names = Fs.list(dir) if names == null { return } var i = 0 while i < len(names) { let file = names[i] var dot = len(file) - 1 while (dot > 0) and (file[dot] != '.') { dot -= 1 } if dot > 0 { assets_enqueue(file[0..dot], dir + ("/") + file) } i += 1 } } # ---- incremental preload (#82) -------------------------------------------- # Enqueue a named image file to load later (does not load it now). function assets_enqueue(name: pointer, path: pointer) -> void { atlas_init() if at_q_n >= ATLAS_MAX_QUEUE { return } at_q_name[at_q_n] = name at_q_path[at_q_n] = path at_q_n += 1 } # Load up to `max` queued assets this frame, registering each under its name, and # return how many were loaded by this call. Call it each frame in a loading scene # (a small `max` keeps the frame short); Assets.ready() flips true when done. event AssetsReady { } # fired once, the frame the queue finishes var at_q_announced: bool = false function assets_pump(max: int) -> int { atlas_init() var done = 0 while (done < max) and (at_q_pos < at_q_n) { assets_load_one(at_q_name[at_q_pos], at_q_path[at_q_pos]) at_q_pos += 1 done += 1 } if (at_q_pos >= at_q_n) and (not at_q_announced) { at_q_announced = true; emit AssetsReady() } return done } # the default loading bar of a `scene X loads then Y`: centred, a fifth of the screen wide function assets_draw_progress() -> void { let w = rt_fbw / 2 let x = rt_fbw / 4 let y = rt_fbh / 2 - 4 rt_fill_rect(x, y, w, 8, 0x303030) rt_fill_rect(x, y, w * assets_progress() / 100, 8, 0xffcc44) } # what kind of asset a path is, by its extension: sounds and fonts go to their own # managers (Audio.play(name:), Assets.font(name)); everything else is an image function path_has_suffix(path: pointer, suffix: pointer) -> bool { let n = len(path) let m = len(suffix) if m > n { return false } var i = 0 while i < m { if path[n - m + i] != suffix[i] { return false } i += 1 } return true } const ATLAS_MAX_FONT: int = 16 var at_font_name: pointers = null var at_font_id: words = null var at_font_n: int = 0 function assets_load_one(name: pointer, path: pointer) -> void { if path_has_suffix(path, ".wav") or path_has_suffix(path, ".mp3") or path_has_suffix(path, ".ogg") { audio_define(name, path) return } if path_has_suffix(path, ".ttf") or path_has_suffix(path, ".ttc") { if at_font_name == null { at_font_name = pointers(ATLAS_MAX_FONT); at_font_id = words(ATLAS_MAX_FONT) } if at_font_n < ATLAS_MAX_FONT { at_font_name[at_font_n] = name at_font_id[at_font_n] = rt_font_load(path) at_font_n += 1 } return } atlas_register_name(name, atlas_image(path)) } # the font handle registered under `name` (Assets.font), or 0 function assets_font(name: pointer) -> int { var i = 0 while i < at_font_n { if at_font_name[i] == name { return at_font_id[i] } i += 1 } return 0 } function assets_total() -> int { atlas_init(); return at_q_n } function assets_loaded() -> int { atlas_init(); return at_q_pos } function assets_ready() -> bool { atlas_init(); return at_q_pos >= at_q_n } # loading progress as a whole-number percent (0..100); an empty queue is 100. function assets_progress() -> int { atlas_init() if at_q_n <= 0 { return 100 } return at_q_pos * 100 / at_q_n }