#include #include #include #include #include #include #include #include #include #include #include #include /// 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. // ----------------------------------------------------------------------------- /// Maximum length of path strings in .TS and .TM files. #define MAX_PATH_LENGTH 128 typedef struct Ts_Tile { uint16_t width; /// Tile width in pixels. uint16_t height; /// Tile height in pixels. Pixel pixels[1]; /// Count: width * height. } Ts_Tile; typedef struct Ts_TileSet { uint16_t num_tiles; uint16_t max_tile_width; /// Maximum tile width in pixels. uint16_t max_tile_height; /// Maximum tile height in pixels. Ts_Tile tiles[1]; /// Count: num_tiles. } Ts_TileSet; typedef struct Tm_Layer { union { char tileset_path[MAX_PATH_LENGTH]; // Relative to the Tm_Map file. }; Tile tiles[1]; /// Count: world_width * world_height. } Tm_Layer; typedef struct Tm_Map { uint16_t world_width; /// World width in number of tiles. uint16_t world_height; /// World height in number of tiles. uint16_t base_tile_width; uint16_t base_tile_height; uint16_t num_layers; Tm_Layer layers[1]; // Count: num_layers. } Tm_Map; static inline const Tm_Layer* tm_map_get_next_layer( const Tm_Map* map, const Tm_Layer* layer) { assert(map); assert(layer); return (const Tm_Layer*)((const uint8_t*)layer + sizeof(Tm_Layer) + ((map->world_width * map->world_height - 1) * sizeof(Tile))); } static inline const Ts_Tile* ts_tileset_get_next_tile( const Ts_TileSet* tileset, const Ts_Tile* tile) { assert(tileset); assert(tile); return (const Ts_Tile*)((const uint8_t*)tile + sizeof(Ts_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. /// /// Pixels are 8-bit indices into the sprite sheet's colour palette. typedef struct Ss_Row { uint16_t num_cols; /// Number of columns in this row. uint8_t pixels[1]; /// Count: num_cols * sprite_width * sprite_height. } Ss_Row; typedef struct Ss_Palette { uint16_t num_colours; Pixel colours[1]; /// Count: num_colors. } Ss_Palette; /// 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_Palette palette; /// Variable size. Ss_Row rows[1]; /// Count: num_rows. Variable offset. } Ss_SpriteSheet; static inline const Ss_Row* get_sprite_sheet_row( const Ss_SpriteSheet* sheet, int row) { assert(sheet); assert(row >= 0); assert(row < sheet->num_rows); // Skip over the palette. const Ss_Row* rows = (const Ss_Row*)(&sheet->palette.colours[0] + sheet->palette.num_colours); return &rows[row]; } static inline const uint8_t* get_sprite_sheet_sprite( const Ss_SpriteSheet* sheet, const Ss_Row* row, int col) { assert(sheet); assert(row); assert(col >= 0); assert(col < row->num_cols); const int sprite_offset = col * sheet->sprite_width * sheet->sprite_height; const uint8_t* sprite = &row->pixels[sprite_offset]; return sprite; } // ----------------------------------------------------------------------------- // Renderer state. // ----------------------------------------------------------------------------- typedef struct TileData { uint16_t width; uint16_t height; 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; 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. // ----------------------------------------------------------------------------- static inline ivec2 ivec2_add(ivec2 a, ivec2 b) { return (ivec2){.x = a.x + b.x, .y = a.y + b.y}; } static inline ivec2 ivec2_scale(ivec2 a, int s) { return (ivec2){.x = a.x * s, .y = a.y * s}; } static inline ivec2 iso2cart(ivec2 iso, int s, int t, int w) { return (ivec2){ .x = (iso.x - iso.y) * (s / 2) + (w / 2), .y = (iso.x + iso.y) * (t / 2)}; } // Method 1. // static inline vec2 cart2iso(vec2 cart, int s, int t, int w) { // const double x = cart.x - (double)(w / 2); // const double xiso = (x * t + cart.y * s) / (double)(s * t); // return (vec2){ // .x = (int)(xiso), .y = (int)((2.0 / (double)t) * cart.y - xiso)}; //} // Method 2. static inline vec2 cart2iso(vec2 cart, int s, int t, int w) { const double one_over_s = 1. / (double)s; const double one_over_t = 1. / (double)t; const double x = cart.x - (double)(w / 2); return (vec2){ .x = (one_over_s * x + one_over_t * cart.y), .y = (-one_over_s * x + one_over_t * cart.y)}; } static const Pixel* tile_xy_const_ref( const IsoGfx* iso, const TileData* tile, int x, int y) { assert(iso); assert(tile); assert(x >= 0); assert(y >= 0); assert(x < tile->width); assert(y < tile->height); return &mem_get_chunk(&iso->pixels, tile->pixels_handle)[y * tile->width + x]; } // static Pixel tile_xy(const IsoGfx* iso, const TileData* tile, int x, int y) { // return *tile_xy_const_ref(iso, tile, x, y); // } static Pixel* tile_xy_mut(const IsoGfx* iso, TileData* tile, int x, int y) { return (Pixel*)tile_xy_const_ref(iso, tile, x, y); } static inline const Tile* world_xy_const_ref(const IsoGfx* iso, int x, int y) { assert(iso); assert(x >= 0); assert(y >= 0); assert(x < iso->world_width); assert(y < iso->world_height); return &iso->world[y * iso->world_width + x]; } static inline Tile world_xy(const IsoGfx* iso, int x, int y) { return *world_xy_const_ref(iso, x, y); } static inline Tile* world_xy_mut(IsoGfx* iso, int x, int y) { return (Tile*)world_xy_const_ref(iso, x, y); } static inline const Pixel* screen_xy_const_ref( const IsoGfx* iso, int x, int y) { assert(iso); assert(x >= 0); assert(y >= 0); assert(x < iso->screen_width); assert(y < iso->screen_height); return &iso->screen[y * iso->screen_width + x]; } static inline Pixel screen_xy(IsoGfx* iso, int x, int y) { return *screen_xy_const_ref(iso, x, y); } static inline Pixel* screen_xy_mut(IsoGfx* iso, int x, int y) { return (Pixel*)screen_xy_const_ref(iso, x, y); } static int calc_num_tile_blocks( int base_tile_width, int base_tile_height, int tile_width, int tile_height) { const int base_tile_size = base_tile_width * base_tile_height; const int tile_size = tile_width * tile_height; const int num_blocks = tile_size / base_tile_size; return num_blocks; } // ----------------------------------------------------------------------------- // Renderer, world and tile management. // ----------------------------------------------------------------------------- IsoGfx* isogfx_new(const IsoGfxDesc* desc) { assert(desc->screen_width > 0); assert(desc->screen_height > 0); // Part of our implementation assumes even widths and heights for precision. assert((desc->screen_width & 1) == 0); assert((desc->screen_height & 1) == 0); IsoGfx* iso = calloc(1, sizeof(IsoGfx)); if (!iso) { return 0; } iso->screen_width = desc->screen_width; iso->screen_height = 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; } return iso; cleanup: isogfx_del(&iso); return 0; } /// Destroy the world, its tile set, and the underlying pools. static void destroy_world(IsoGfx* iso) { assert(iso); if (iso->world) { free(iso->world); iso->world = 0; } mempool_del(&iso->tiles); 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; } free(iso); *pIso = 0; } } bool isogfx_make_world(IsoGfx* iso, const WorldDesc* desc) { assert(iso); assert(desc); assert(desc->tile_width > 0); assert(desc->tile_height > 0); // Part of our implementation assumes even widths and heights for greater // precision. assert((desc->tile_width & 1) == 0); assert((desc->tile_height & 1) == 0); // Handle recreation by destroying the previous world. destroy_world(iso); iso->tile_width = desc->tile_width; iso->tile_height = desc->tile_height; iso->world_width = desc->world_width; iso->world_height = desc->world_height; const int world_size = desc->world_width * desc->world_height; const int tile_size = desc->tile_width * desc->tile_height; const int tile_size_bytes = tile_size * (int)sizeof(Pixel); const int tile_pool_size = desc->max_num_tiles > 0 ? desc->max_num_tiles : DEFAULT_MAX_NUM_TILES; if (!(iso->world = calloc(world_size, sizeof(Tile)))) { goto cleanup; } if (!mempool_make_dyn(&iso->tiles, world_size, sizeof(TileData))) { goto cleanup; } if (!mem_make_dyn(&iso->pixels, tile_pool_size, tile_size_bytes)) { goto cleanup; } return true; cleanup: destroy_world(iso); return false; } bool isogfx_load_world(IsoGfx* iso, const char* filepath) { assert(iso); assert(filepath); bool success = false; // Handle recreation by destroying the previous world. destroy_world(iso); // Load the map. printf("Load tile map: %s\n", filepath); Tm_Map* map = read_file(filepath); if (!map) { goto cleanup; } // Allocate memory for the map and tile sets. const int world_size = map->world_width * map->world_height; const int base_tile_size = map->base_tile_width * map->base_tile_height; const int base_tile_size_bytes = base_tile_size * (int)sizeof(Pixel); // TODO: Need to get the total number of tiles from the map. const int tile_pool_size = DEFAULT_MAX_NUM_TILES; if (!(iso->world = calloc(world_size, sizeof(Tile)))) { goto cleanup; } if (!mempool_make_dyn(&iso->tiles, tile_pool_size, sizeof(TileData))) { goto cleanup; } if (!mem_make_dyn(&iso->pixels, tile_pool_size, base_tile_size_bytes)) { goto cleanup; } // Load the tile sets. const Tm_Layer* layer = &map->layers[0]; // TODO: Handle num_layers layers. for (int i = 0; i < 1; ++i) { const char* ts_path = layer->tileset_path; // 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 (!path_make_relative(filepath, ts_path, ts_path_cwd, PATH_MAX)) { goto cleanup; } Ts_TileSet* tileset = read_file(ts_path_cwd); if (!tileset) { goto cleanup; }; // Load tile data. const Ts_Tile* tile = &tileset->tiles[0]; for (uint16_t j = 0; j < tileset->num_tiles; ++j) { // Tile dimensions should be a multiple of the base tile size. assert((tile->width % map->base_tile_width) == 0); assert((tile->height % map->base_tile_height) == 0); // Allocate N base tile size blocks for the tile. const uint16_t tile_size = tile->width * tile->height; const int num_blocks = tile_size / base_tile_size; Pixel* pixels = mem_alloc(&iso->pixels, num_blocks); assert(pixels); memcpy(pixels, tile->pixels, tile_size * sizeof(Pixel)); // Allocate the tile data. TileData* tile_data = mempool_alloc(&iso->tiles); assert(tile_data); tile_data->width = tile->width; tile_data->height = tile->height; tile_data->pixels_handle = (uint16_t)mem_get_chunk_handle(&iso->pixels, pixels); tile = ts_tileset_get_next_tile(tileset, tile); } printf("Loaded tile set (%u tiles): %s\n", tileset->num_tiles, ts_path_cwd); free(tileset); layer = tm_map_get_next_layer(map, layer); } // Load the map into the world. layer = &map->layers[0]; // TODO: Handle num_layers layers. for (int i = 0; i < 1; ++i) { memcpy(iso->world, layer->tiles, world_size * sizeof(Tile)); // TODO: We need to handle 'firsgid' in TMX files. for (int j = 0; j < world_size; ++j) { iso->world[j] -= 1; } layer = tm_map_get_next_layer(map, layer); } iso->world_width = map->world_width; iso->world_height = map->world_height; iso->tile_width = map->base_tile_width; iso->tile_height = map->base_tile_height; success = true; cleanup: if (map) { free(map); } if (!success) { destroy_world(iso); } return success; } int isogfx_world_width(const IsoGfx* iso) { assert(iso); return iso->world_width; } int isogfx_world_height(const IsoGfx* iso) { assert(iso); return iso->world_height; } /// Create a tile mask procedurally. static void make_tile_from_colour( const IsoGfx* iso, Pixel colour, TileData* tile) { assert(iso); assert(tile); const int width = tile->width; const int height = tile->height; const int r = width / height; for (int y = 0; y < height / 2; ++y) { const int mask_start = width / 2 - r * y - 1; const int mask_end = width / 2 + r * y + 1; for (int x = 0; x < width; ++x) { const bool mask = (mask_start <= x) && (x <= mask_end); const Pixel val = mask ? colour : (Pixel){.r = 0, .g = 0, .b = 0, .a = 0}; // Top half. *tile_xy_mut(iso, tile, x, y) = val; // Bottom half reflects the top half. const int y_reflected = height - y - 1; *tile_xy_mut(iso, tile, x, y_reflected) = val; } } } Tile isogfx_make_tile(IsoGfx* iso, const TileDesc* desc) { assert(iso); assert(desc); // Client must create world before creating tiles. assert(iso->tile_width > 0); assert(iso->tile_height > 0); TileData* tile = mempool_alloc(&iso->tiles); assert(tile); // TODO: Make this a hard assert. const int num_blocks = calc_num_tile_blocks( iso->tile_width, iso->tile_height, desc->width, desc->height); Pixel* pixels = mem_alloc(&iso->pixels, num_blocks); assert(pixels); // TODO: Make this a hard assert. tile->width = desc->width; tile->height = desc->height; tile->pixels_handle = mem_get_chunk_handle(&iso->pixels, pixels); switch (desc->type) { case TileFromColour: make_tile_from_colour(iso, desc->colour, tile); break; case TileFromFile: assert(false); // TODO break; case TileFromMemory: assert(false); // TODO break; } return (Tile)mempool_get_block_index(&iso->tiles, tile); } void isogfx_set_tile(IsoGfx* iso, int x, int y, Tile tile) { assert(iso); *world_xy_mut(iso, x, y) = tile; } void isogfx_set_tiles(IsoGfx* iso, int x0, int y0, int x1, int y1, Tile tile) { assert(iso); for (int y = y0; y < y1; ++y) { for (int x = x0; x < x1; ++x) { isogfx_set_tile(iso, x, y, 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. // ----------------------------------------------------------------------------- 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, 0}; 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}; } /// Get the screen position of the top diamond-corner of the tile at world /// (x,y). static ivec2 GetTileScreenOrigin( const CoordSystem iso_space, int world_x, int world_y) { const ivec2 vx_offset = ivec2_scale(iso_space.x, world_x); const ivec2 vy_offset = ivec2_scale(iso_space.y, world_y); const ivec2 screen_origin = ivec2_add(iso_space.o, ivec2_add(vx_offset, vy_offset)); return screen_origin; } 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 top-left corner is mapped to the screen space position given /// by 'top_left'. /// /// The rectangle's pixels are assumed to be arranged in a linear, row-major /// fashion. /// /// If indices are given, then the image is assumed to be colour-paletted, where /// 'pixels' is the palette and 'indices' the pixel indices. Otherwise, the /// image is assumed to be in plain RGBA format. static void draw_rect( IsoGfx* iso, ivec2 top_left, int rect_width, int rect_height, const Pixel* pixels, const uint8_t* indices) { assert(iso); #define rect_pixel(X, Y) \ (indices ? pixels[indices[Y * rect_width + X]] : pixels[Y * rect_width + X]) // Rect origin can be outside screen bounds, so we must offset accordingly to // draw only the visible portion. #define max(a, b) (a > b ? a : b) const int px_offset = max(0, -top_left.x); const int py_offset = max(0, -top_left.y); // Rect can exceed screen bounds, so clip along Y and X as we draw. for (int py = py_offset; (py < rect_height) && (top_left.y + py < iso->screen_height); ++py) { const int sy = top_left.y + py; for (int px = px_offset; (px < rect_width) && (top_left.x + px < iso->screen_width); ++px) { const Pixel colour = rect_pixel(px, py); if (colour.a > 0) { const int sx = top_left.x + px; const Pixel dst = screen_xy(iso, sx, sy); const Pixel final = alpha_blend(colour, dst); *screen_xy_mut(iso, sx, sy) = final; } } } } /// Draw a tile. /// /// 'screen_origin' is the screen coordinates of the top diamond-corner of the /// tile (the base tile for super tiles). /// World (0, 0) -> (screen_width / 2, 0). static void draw_tile(IsoGfx* iso, ivec2 screen_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); // Move from the top diamond-corner to the top-left corner of the tile image. // For regular tiles, tile height == base tile height, so the y offset is 0. // For super tiles, move as high up as the height of the tile. const ivec2 offset = { -(iso->tile_width / 2), tile_data->height - iso->tile_height}; const ivec2 top_left = ivec2_add(screen_origin, offset); draw_rect(iso, top_left, tile_data->width, tile_data->height, pixels, 0); } static void draw_world(IsoGfx* iso) { assert(iso); const int W = iso->screen_width; const int H = iso->screen_height; memset(iso->screen, 0, W * H * sizeof(Pixel)); const CoordSystem iso_space = make_iso_coord_system(iso); // TODO: Culling. // Ex: map the screen corners to tile space to cull. // Ex: walk in screen space and fetch the tile. // The tile-centric approach might be more cache-friendly since the // screen-centric approach would juggle multiple tiles throughout the scan. for (int wy = 0; wy < iso->world_height; ++wy) { for (int wx = 0; wx < iso->world_width; ++wx) { const Tile tile = world_xy(iso, wx, wy); const ivec2 screen_origin = GetTileScreenOrigin(iso_space, wx, wy); draw_tile(iso, screen_origin, 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* row = get_sprite_sheet_row(sheet, sprite->animation); const uint8_t* frame = get_sprite_sheet_sprite(sheet, row, sprite->frame); draw_rect( iso, origin, sheet->sprite_width, sheet->sprite_height, sheet->palette.colours, 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 screen_origin = GetTileScreenOrigin(iso_space, sprite->position.x, sprite->position.y); draw_sprite(iso, screen_origin, sprite, sheet); }); } void isogfx_render(IsoGfx* iso) { assert(iso); draw_world(iso); draw_sprites(iso); } void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) { assert(iso); assert(x >= 0); assert(y >= 0); assert(x < iso->world_width); assert(y < iso->world_height); const CoordSystem iso_space = make_iso_coord_system(iso); const ivec2 screen_origin = GetTileScreenOrigin(iso_space, x, y); draw_tile(iso, screen_origin, tile); } bool isogfx_resize(IsoGfx* iso, int screen_width, int screen_height) { assert(iso); assert(iso->screen); const int current_size = iso->screen_width * iso->screen_height; const int new_size = screen_width * screen_height; if (new_size > current_size) { Pixel* new_screen = calloc(new_size, sizeof(Pixel)); if (new_screen) { free(iso->screen); iso->screen = new_screen; } else { return false; } } iso->screen_width = screen_width; iso->screen_height = screen_height; return true; } void isogfx_get_screen_size(const IsoGfx* iso, int* width, int* height) { assert(iso); assert(width); assert(height); *width = iso->screen_width; *height = iso->screen_height; } const Pixel* isogfx_get_screen_buffer(const IsoGfx* iso) { assert(iso); return iso->screen; } void isogfx_pick_tile( const IsoGfx* iso, double xcart, double ycart, int* xiso, int* yiso) { assert(iso); assert(xiso); assert(yiso); const vec2 xy_iso = cart2iso( (vec2){.x = xcart, .y = ycart}, iso->tile_width, iso->tile_height, iso->screen_width); if ((0 <= xy_iso.x) && (xy_iso.x < iso->world_width) && (0 <= xy_iso.y) && (xy_iso.y < iso->world_height)) { *xiso = (int)xy_iso.x; *yiso = (int)xy_iso.y; } else { *xiso = -1; *yiso = -1; } }