From 48cef82988d6209987ae27fe29b72d7d5e402b3c Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Wed, 19 Jul 2023 08:35:00 -0700 Subject: Add sprites. --- gfx-iso/app/isogfx-demo.c | 15 +- gfx-iso/app/main.c | 2 + gfx-iso/asset/mkasset.py | 128 +++++++++++++- gfx-iso/include/isogfx/isogfx.h | 35 +++- gfx-iso/src/isogfx.c | 362 +++++++++++++++++++++++++++++++++++----- 5 files changed, 491 insertions(+), 51 deletions(-) diff --git a/gfx-iso/app/isogfx-demo.c b/gfx-iso/app/isogfx-demo.c index d463d1c..9889275 100644 --- a/gfx-iso/app/isogfx-demo.c +++ b/gfx-iso/app/isogfx-demo.c @@ -9,8 +9,10 @@ #include typedef struct State { - int xpick; - int ypick; + int xpick; + int ypick; + SpriteSheet stag_sheet; + Sprite stag; } State; static void shutdown(IsoGfx* iso, void* app_state) { @@ -54,6 +56,15 @@ bool make_demo_app(IsoGfx* iso, IsoGfxApp* app) { goto cleanup; } + if (!isogfx_load_sprite_sheet( + iso, "/home/jeanne/assets/tilesets/scrabling/critters/stag/stag.ss", + &state->stag_sheet)) { + goto cleanup; + } + + state->stag = isogfx_make_sprite(iso, state->stag_sheet); + isogfx_set_sprite_position(iso, state->stag, 5, 4); + app->pixel_scale = 2; app->state = state; app->shutdown = shutdown; diff --git a/gfx-iso/app/main.c b/gfx-iso/app/main.c index 5b441d3..ff8a266 100644 --- a/gfx-iso/app/main.c +++ b/gfx-iso/app/main.c @@ -138,6 +138,8 @@ static void update(void* app_state, double t, double dt) { assert(app_state); State* state = (State*)(app_state); + isogfx_update(state->iso, t); + assert(state->app.update); (*state->app.update)(state->iso, state->app.state, t, dt); } diff --git a/gfx-iso/asset/mkasset.py b/gfx-iso/asset/mkasset.py index 15f7912..b4e335f 100644 --- a/gfx-iso/asset/mkasset.py +++ b/gfx-iso/asset/mkasset.py @@ -1,15 +1,24 @@ -# Converts tile sets and tile maps to binary formats (.TS, .TM) for the engine. +# Converts assets to binary formats (.ts, .tm, .ss) for the engine. # -# Currently handles Tiled's .tsx and .tmx file formats. +# Input file formats: +# - Tiled tile set (.tsx) +# - Tiled tile map (.tmx) +# - Sprite sheets (.jpg, .png, etc), 1 row per animation. +# +# Output file formats: +# - Binary tile set file (.ts) +# - Binary tile map file (.tm) +# - Binary sprite sheet file (.ss) # -# The output is a binary tile set file (.TS) or a binary tile map file (.TM). import argparse import ctypes +import os from PIL import Image import sys from xml.etree import ElementTree # Maximum length of path strings in .TS and .TM files. +# Must match the engine's value. MAX_PATH_LENGTH = 128 @@ -133,20 +142,127 @@ def convert_tmx(input_filepath, output_filepath): output.write(ctypes.c_uint16(int(tile_id))) +def get_num_cols(image, sprite_width): + """Return the number of non-empty columns in the image. + + Assumes no gaps in the columns. + """ + assert (image.width % sprite_width == 0) + num_cols = image.width // sprite_width + + # Start the search from right to left. + for col in reversed(range(1, num_cols)): + left = (col - 1) * sprite_width + right = col * sprite_width + rect = image.crop((left, 0, right, image.height)) + min_max = rect.getextrema() + for (channel_min, channel_max) in min_max: + if channel_min != 0 or channel_max != 0: + # 'col' is the rightmost non-empty column. + # Assuming no gaps, col+1 is the number of non-empty columns. + return col + 1 + + return 0 + + +def get_sprite_sheet_rows(input_filepath, sprite_width, sprite_height): + """Gets the individual rows of a sprite sheet. + + The input sprite sheet can have any number of rows. + + Returns a list of lists [[sprite bytes]], one inner list for the columns in + each row. + """ + with Image.open(input_filepath) as im: + # Sprite sheet's width and height must be integer multiples of the + # sprite's width and height. + assert (im.width % sprite_width == 0) + assert (im.height % sprite_height == 0) + + num_rows = im.height // sprite_height + + rows = [] + for row in range(num_rows): + # Get the number of columns. + upper = row * sprite_height + lower = (row + 1) * sprite_height + whole_row = im.crop((0, upper, im.width, lower)) + num_cols = get_num_cols(whole_row, sprite_width) + assert (num_cols > 0) + + # Crop the row into N columns. + cols = [] + for i in range(num_cols): + left = i * sprite_width + right = (i + 1) * sprite_width + sprite = im.crop((left, upper, right, lower)) + cols.append(sprite) + + sprite_bytes = [sprite.convert('RGBA').tobytes() for sprite in cols] + assert (len(sprite_bytes) == num_cols) + rows.append(sprite_bytes) + + return rows + + +def convert_sprite_sheet(input_file_paths, sprite_width, sprite_height, + output_filepath): + """Converts a set of sprite sheet images into a binary sprite sheet file + (.ss). + + The input sprite sheets can have any number of rows, one row per animation. + All rows from all sprite sheets are concatenated in the output file. + + The sprite's width and height is assumed constant throughout the input + sprite sheets. + """ + rows = [] + + for sprite_sheet in input_file_paths: + rows.extend( + get_sprite_sheet_rows(sprite_sheet, sprite_width, sprite_height)) + + with open(output_filepath, 'bw') as output: + output.write(ctypes.c_uint16(sprite_width)) + output.write(ctypes.c_uint16(sprite_height)) + output.write(ctypes.c_uint16(len(rows))) + + print(f"Sprite width: {sprite_width}") + print(f"Sprite height: {sprite_height}") + print(f"Rows: {len(rows)}") + + for sprites in rows: + output.write(ctypes.c_uint16(len(sprites))) + for sprite_bytes in sprites: + output.write(sprite_bytes) + + def main(): parser = argparse.ArgumentParser() - parser.add_argument("input", help="Input file (.tsx, .tmx)") + parser.add_argument("input", + nargs="+", + help="Input file (.tsx, .tmx) or path regex (sprite sheets)") + parser.add_argument("--width", type=int, help="Sprite width in pixels") + parser.add_argument("--height", type=int, help="Sprite height in pixels") + parser.add_argument("--out", help="Output file (sprite sheets)") args = parser.parse_args() - output_filepath_no_ext = drop_extension(args.input) if ".tsx" in args.input: + output_filepath_no_ext = drop_extension(args.input) output_filepath = output_filepath_no_ext + ".ts" convert_tsx(args.input, output_filepath) elif ".tmx" in args.input: + output_filepath_no_ext = drop_extension(args.input) output_filepath = output_filepath_no_ext + ".tm" convert_tmx(args.input, output_filepath) else: - print(f"Unhandled file format: {args.input}") + # Sprite sheets. + if not args.width or not args.height: + print("Sprite width and height must be given") + return 1 + output_filepath = args.out if args.out else "out.ss" + convert_sprite_sheet(args.input, args.width, args.height, + output_filepath) return 0 diff --git a/gfx-iso/include/isogfx/isogfx.h b/gfx-iso/include/isogfx/isogfx.h index 6b7ce8e..e96606c 100644 --- a/gfx-iso/include/isogfx/isogfx.h +++ b/gfx-iso/include/isogfx/isogfx.h @@ -8,6 +8,12 @@ typedef struct IsoGfx IsoGfx; +/// Sprite sheet handle. +typedef uint16_t SpriteSheet; + +/// Sprite handle. +typedef uint16_t Sprite; + /// Tile handle. typedef uint16_t Tile; @@ -48,8 +54,10 @@ typedef struct WorldDesc { } WorldDesc; typedef struct IsoGfxDesc { - int screen_width; /// Screen width in pixels. - int screen_height; /// Screen height in pixels. + int screen_width; /// Screen width in pixels. + int screen_height; /// Screen height in pixels. + int max_num_sprites; /// 0 for an implementation-defined default. + int sprite_sheet_pool_size_bytes; /// 0 for an implementation-defined default. } IsoGfxDesc; /// Create a new isometric graphics engine. @@ -79,6 +87,29 @@ void isogfx_set_tile(IsoGfx*, int x, int y, Tile); /// Set the tiles in positions in the range (x0,y0) - (x1,y1). void isogfx_set_tiles(IsoGfx*, int x0, int y0, int x1, int y1, Tile); +/// Load a sprite sheet (.SS) file. +bool isogfx_load_sprite_sheet(IsoGfx*, const char* filepath, SpriteSheet*); + +/// Create an animated sprite. +Sprite isogfx_make_sprite(IsoGfx*, SpriteSheet); + +/// Destroy the sprite. +void isogfx_del_sprite(IsoGfx*, Sprite); + +/// Destroy all the sprites. +void isogfx_del_sprites(IsoGfx*); + +/// Set the sprite's position. +void isogfx_set_sprite_position(IsoGfx*, Sprite, int x, int y); + +/// Set the sprite's current animation. +void isogfx_set_sprite_animation(IsoGfx*, Sprite, int animation); + +/// Update the renderer. +/// +/// Currently this updates the sprite animations. +void isogfx_update(IsoGfx*, double t); + /// Render the world. void isogfx_render(IsoGfx*); diff --git a/gfx-iso/src/isogfx.c b/gfx-iso/src/isogfx.c index 3ed0fde..9ba1bec 100644 --- a/gfx-iso/src/isogfx.c +++ b/gfx-iso/src/isogfx.c @@ -13,9 +13,29 @@ #include #include -/// Maximum number of tiles unless the user chooses a non-zero value. +/// Maximum number of tiles unless the user specifies a value. #define DEFAULT_MAX_NUM_TILES 1024 +/// Maximum number of sprites unless the user specifies a value. +#define DEFAULT_MAX_NUM_SPRITES 128 + +/// Size of sprite sheet pool in bytes unless the user specifies a value. +#define DEFAULT_SPRITE_SHEET_POOL_SIZE_BYTES (8 * 1024 * 1024) + +/// Default animation speed. +#define ANIMATION_FPS 10 + +/// Time between animation updates. +#define ANIMATION_UPDATE_DELTA (1.0 / ANIMATION_FPS) + +typedef struct ivec2 { + int x, y; +} ivec2; + +typedef struct vec2 { + double x, y; +} vec2; + // ----------------------------------------------------------------------------- // Tile set (TS) and tile map (TM) file formats. // ----------------------------------------------------------------------------- @@ -69,6 +89,39 @@ static inline const Ts_Tile* ts_tileset_get_next_tile( ((tile->width * tile->height - 1) * sizeof(Pixel))); } +// ----------------------------------------------------------------------------- +// Sprite sheet file format. +// ----------------------------------------------------------------------------- + +/// A row of sprites in a sprite sheet. +/// +/// Each row in a sprite sheet can have a different number of columns. +/// +/// The pixels of the row follow a "sprite-major" order. It contains the +/// 'sprite_width * sprite_height' pixels for the first column/sprite, then the +/// second column/sprite, etc. +typedef struct Ss_Row { + uint16_t num_cols; /// Number of columns in this row. + Pixel pixels[1]; /// Count: num_cols * sprite_width * sprite_height. +} Ss_Row; + +/// Sprite sheet top-level data definition. +/// +/// Sprite width and height are assumed constant throughout the sprite sheet. +typedef struct Ss_SpriteSheet { + uint16_t sprite_width; /// Sprite width in pixels. + uint16_t sprite_height; /// Sprite height in pixels. + uint16_t num_rows; + Ss_Row rows[1]; /// Count: num_rows. +} Ss_SpriteSheet; + +const Ss_Row* get_sprite_sheet_row(const Ss_SpriteSheet* sheet, int row) { + assert(sheet); + assert(row >= 0); + assert(row < sheet->num_rows); + return &sheet->rows[row]; +} + // ----------------------------------------------------------------------------- // Renderer state. // ----------------------------------------------------------------------------- @@ -79,34 +132,45 @@ typedef struct TileData { uint16_t pixels_handle; // Handle to the tile's pixels in the pixel pool. } TileData; +// File format is already convenient for working in memory. +typedef Ss_Row SpriteSheetRow; +typedef Ss_SpriteSheet SpriteSheetData; + +typedef struct SpriteData { + SpriteSheet sheet; // Handle to the sprite's sheet. + ivec2 position; + int animation; // Current animation. + int frame; // Current frame of animation. +} SpriteData; + DEF_MEMPOOL_DYN(TilePool, TileData) DEF_MEM_DYN(PixelPool, Pixel) +DEF_MEMPOOL_DYN(SpritePool, SpriteData) +DEF_MEM_DYN(SpriteSheetPool, SpriteSheetData) + typedef struct IsoGfx { - int screen_width; - int screen_height; - int tile_width; - int tile_height; - int world_width; - int world_height; - Tile* world; - Pixel* screen; - TilePool tiles; - PixelPool pixels; + int screen_width; + int screen_height; + int tile_width; + int tile_height; + int world_width; + int world_height; + int max_num_sprites; + int sprite_sheet_pool_size_bytes; + double last_animation_time; + Tile* world; + Pixel* screen; + TilePool tiles; + PixelPool pixels; + SpritePool sprites; + SpriteSheetPool sheets; } IsoGfx; // ----------------------------------------------------------------------------- // Math and world / tile / screen access. // ----------------------------------------------------------------------------- -typedef struct ivec2 { - int x, y; -} ivec2; - -typedef struct vec2 { - double x, y; -} vec2; - static inline ivec2 ivec2_add(ivec2 a, ivec2 b) { return (ivec2){.x = a.x + b.x, .y = a.y + b.y}; } @@ -220,8 +284,15 @@ IsoGfx* isogfx_new(const IsoGfxDesc* desc) { iso->screen_width = desc->screen_width; iso->screen_height = desc->screen_height; - const int screen_size = desc->screen_width * desc->screen_height; + iso->last_animation_time = 0.0; + + iso->max_num_sprites = desc->max_num_sprites == 0 ? DEFAULT_MAX_NUM_SPRITES + : desc->max_num_sprites; + iso->sprite_sheet_pool_size_bytes = desc->sprite_sheet_pool_size_bytes == 0 + ? DEFAULT_SPRITE_SHEET_POOL_SIZE_BYTES + : desc->sprite_sheet_pool_size_bytes; + const int screen_size = desc->screen_width * desc->screen_height; if (!(iso->screen = calloc(screen_size, sizeof(Pixel)))) { goto cleanup; } @@ -233,7 +304,7 @@ cleanup: return 0; } -/// Destroy the world and its tile set. +/// Destroy the world, its tile set, and the underlying pools. static void destroy_world(IsoGfx* iso) { assert(iso); if (iso->world) { @@ -244,11 +315,19 @@ static void destroy_world(IsoGfx* iso) { mem_del(&iso->pixels); } +/// Destroy all loaded sprites and the underlying pools. +static void destroy_sprites(IsoGfx* iso) { + assert(iso); + mempool_del(&iso->sprites); + mem_del(&iso->sheets); +} + void isogfx_del(IsoGfx** pIso) { assert(pIso); IsoGfx* iso = *pIso; if (iso) { destroy_world(iso); + destroy_sprites(iso); if (iso->screen) { free(iso->screen); iso->screen = 0; @@ -341,7 +420,7 @@ bool isogfx_load_world(IsoGfx* iso, const char* filepath) { // Tile set path is relative to the tile map file. Make it relative to the // current working directory before loading. char ts_path_cwd[PATH_MAX] = {0}; - if (!make_relative_path(MAX_PATH_LENGTH, filepath, ts_path, ts_path_cwd)) { + if (!make_relative_path(filepath, ts_path, ts_path_cwd, PATH_MAX)) { goto cleanup; } @@ -498,36 +577,199 @@ void isogfx_set_tiles(IsoGfx* iso, int x0, int y0, int x1, int y1, Tile tile) { } } +bool isogfx_load_sprite_sheet( + IsoGfx* iso, const char* filepath, SpriteSheet* p_sheet) { + assert(iso); + assert(filepath); + assert(p_sheet); + + bool success = false; + + // Lazy initialization of sprite pools. + if (mempool_capacity(&iso->sprites) == 0) { + if (!mempool_make_dyn( + &iso->sprites, iso->max_num_sprites, sizeof(SpriteData))) { + return false; + } + } + if (mem_capacity(&iso->sheets) == 0) { + // Using a block size of 1 byte for sprite sheet data. + if (!mem_make_dyn(&iso->sheets, iso->sprite_sheet_pool_size_bytes, 1)) { + return false; + } + } + + // Load sprite sheet file. + printf("Load sprite sheet: %s\n", filepath); + FILE* file = fopen(filepath, "rb"); + if (file == NULL) { + goto cleanup; + } + const size_t sheet_size = get_file_size(file); + SpriteSheetData* ss_sheet = mem_alloc(&iso->sheets, sheet_size); + if (!ss_sheet) { + goto cleanup; + } + if (fread(ss_sheet, sheet_size, 1, file) != 1) { + goto cleanup; + } + + *p_sheet = mem_get_chunk_handle(&iso->sheets, ss_sheet); + success = true; + +cleanup: + // Pools remain initialized since client may attempt to load other sprites. + if (file != NULL) { + fclose(file); + } + if (!success) { + if (ss_sheet) { + mem_free(&iso->sheets, &ss_sheet); + } + } + return success; +} + +Sprite isogfx_make_sprite(IsoGfx* iso, SpriteSheet sheet) { + assert(iso); + + SpriteData* sprite = mempool_alloc(&iso->sprites); + assert(sprite); + + sprite->sheet = sheet; + + return mempool_get_block_index(&iso->sprites, sprite); +} + +#define with_sprite(SPRITE, BODY) \ + { \ + SpriteData* data = mempool_get_block(&iso->sprites, sprite); \ + assert(data); \ + BODY; \ + } + +void isogfx_set_sprite_position(IsoGfx* iso, Sprite sprite, int x, int y) { + assert(iso); + with_sprite(sprite, { + data->position.x = x; + data->position.y = y; + }); +} + +void isogfx_set_sprite_animation(IsoGfx* iso, Sprite sprite, int animation) { + assert(iso); + with_sprite(sprite, { data->animation = animation; }); +} + +void isogfx_update(IsoGfx* iso, double t) { + assert(iso); + + // If this is the first time update() is called after initialization, just + // record the starting animation time. + if (iso->last_animation_time == 0.0) { + iso->last_animation_time = t; + return; + } + + if ((t - iso->last_animation_time) >= ANIMATION_UPDATE_DELTA) { + // TODO: Consider linking animated sprites in a list so that we only walk + // over those here and not also the static sprites. + mempool_foreach(&iso->sprites, sprite, { + const SpriteSheetData* sheet = mem_get_chunk(&iso->sheets, sprite->sheet); + assert(sheet); // TODO: Make this a hard assert inside the mem/pool. + const SpriteSheetRow* row = + get_sprite_sheet_row(sheet, sprite->animation); + sprite->frame = (sprite->frame + 1) % row->num_cols; + }); + + iso->last_animation_time = t; + } +} + // ----------------------------------------------------------------------------- // Rendering and picking. // ----------------------------------------------------------------------------- -static void draw_tile(IsoGfx* iso, ivec2 origin, Tile tile) { +typedef struct CoordSystem { + ivec2 o; /// Origin. + ivec2 x; + ivec2 y; +} CoordSystem; + +/// Create the basis for the isometric coordinate system with origin and vectors +/// expressed in the Cartesian system. +static CoordSystem make_iso_coord_system(const IsoGfx* iso) { assert(iso); + // const ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0}; + const ivec2 o = { + (iso->screen_width / 2) - (iso->tile_width / 2), iso->tile_height}; + const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2}; + const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2}; + return (CoordSystem){o, x, y}; +} - const TileData* tile_data = mempool_get_block(&iso->tiles, tile); - assert(tile_data); +static Pixel alpha_blend(Pixel src, Pixel dst) { + if ((src.a == 255) || (dst.a == 0)) { + return src; + } + const uint16_t one_minus_alpha = 255 - src.a; +#define blend(s, d) \ + (Channel)( \ + (double)((uint16_t)s * (uint16_t)src.a + \ + (uint16_t)d * one_minus_alpha) / \ + 255.0) + return (Pixel){ + .r = blend(src.r, dst.r), + .g = blend(src.g, dst.g), + .b = blend(src.b, dst.b), + .a = src.a}; +} + +/// Draw a rectangle (tile or sprite). +/// +/// The rectangle's bottom-left corner is mapped to the given origin. The +/// rectangle then extends to the right and top of the origin. +/// +/// The rectangle's pixels are assumed to be arranged in a linear, row-major +/// fashion. +static void draw_rect( + IsoGfx* iso, ivec2 origin, int rect_width, int rect_height, + const Pixel* pixels) { + assert(iso); - // Tile can exceed screen bounds, so we must clip it. + // Rect can exceed screen bounds, so we must clip it. #define max(a, b) (a > b ? a : b) - const int py_offset = max(0, (int)tile_data->height - origin.y); - origin.y = max(0, origin.y - (int)tile_data->height); + const int py_offset = max(0, rect_height - origin.y); + origin.y = max(0, origin.y - rect_height); // Clip along Y and X as we draw. for (int py = py_offset; - (py < tile_data->height) && (origin.y + py < iso->screen_height); ++py) { + (py < rect_height) && (origin.y + py < iso->screen_height); ++py) { const int sy = origin.y + py - py_offset; - for (int px = 0; - (px < tile_data->width) && (origin.x + px < iso->screen_width); ++px) { - const Pixel colour = tile_xy(iso, tile_data, px, py); + for (int px = 0; (px < rect_width) && (origin.x + px < iso->screen_width); + ++px) { + const Pixel colour = pixels[py * rect_width + px]; if (colour.a > 0) { - const int sx = origin.x + px; - *screen_xy_mut(iso, sx, sy) = colour; + const int sx = origin.x + px; + const Pixel dst = screen_xy(iso, sx, sy); + const Pixel final = alpha_blend(colour, dst); + *screen_xy_mut(iso, sx, sy) = final; } } } } +static void draw_tile(IsoGfx* iso, ivec2 origin, Tile tile) { + assert(iso); + + const TileData* tile_data = mempool_get_block(&iso->tiles, tile); + assert(tile_data); + + const Pixel* pixels = tile_xy_const_ref(iso, tile_data, 0, 0); + + draw_rect(iso, origin, tile_data->width, tile_data->height, pixels); +} + static void draw(IsoGfx* iso) { assert(iso); @@ -536,11 +778,7 @@ static void draw(IsoGfx* iso) { memset(iso->screen, 0, W * H * sizeof(Pixel)); - // const ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0}; - const ivec2 o = { - (iso->screen_width / 2) - (iso->tile_width / 2), iso->tile_height}; - const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2}; - const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2}; + const CoordSystem iso_space = make_iso_coord_system(iso); // TODO: Culling. // Ex: map the screen corners to tile space to cull. @@ -550,16 +788,58 @@ static void draw(IsoGfx* iso) { for (int ty = 0; ty < iso->world_height; ++ty) { for (int tx = 0; tx < iso->world_width; ++tx) { const Tile tile = world_xy(iso, tx, ty); - const ivec2 so = - ivec2_add(o, ivec2_add(ivec2_scale(x, tx), ivec2_scale(y, ty))); + const ivec2 so = ivec2_add( + iso_space.o, + ivec2_add( + ivec2_scale(iso_space.x, tx), ivec2_scale(iso_space.y, ty))); draw_tile(iso, so, tile); } } } +static void draw_sprite( + IsoGfx* iso, ivec2 origin, const SpriteData* sprite, + const SpriteSheetData* sheet) { + assert(iso); + assert(sprite); + assert(sheet); + assert(sprite->animation >= 0); + assert(sprite->animation < sheet->num_rows); + assert(sprite->frame >= 0); + + const SpriteSheetRow* ss_row = &sheet->rows[sprite->animation]; + assert(sprite->frame < ss_row->num_cols); + + const int sprite_offset = + sprite->frame * sheet->sprite_width * sheet->sprite_height; + + const Pixel* frame = &ss_row->pixels[sprite_offset]; + + draw_rect(iso, origin, sheet->sprite_width, sheet->sprite_height, frame); +} + +static void draw_sprites(IsoGfx* iso) { + assert(iso); + + const CoordSystem iso_space = make_iso_coord_system(iso); + + mempool_foreach(&iso->sprites, sprite, { + const SpriteSheetData* sheet = mem_get_chunk(&iso->sheets, sprite->sheet); + assert(sheet); + + const ivec2 so = ivec2_add( + iso_space.o, ivec2_add( + ivec2_scale(iso_space.x, sprite->position.x), + ivec2_scale(iso_space.y, sprite->position.y))); + + draw_sprite(iso, so, sprite, sheet); + }); +} + void isogfx_render(IsoGfx* iso) { assert(iso); draw(iso); + draw_sprites(iso); } void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) { -- cgit v1.2.3