#include #include #include #include #include #include #include /// Maximum number of tiles unless the user chooses a non-zero value. #define DEFAULT_MAX_NUM_TILES 1024 typedef struct TileData { Pixel pixels[1]; // Dynamically allocated. } TileData; DEF_MEMPOOL_DYN(TilePool, TileData) typedef struct IsoGfx { Tile* world; Pixel* screen; uint8_t* tile_mask; TilePool tiles; int screen_width; int screen_height; int tile_width; int tile_height; int world_width; int world_height; int max_num_tiles; } IsoGfx; 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}; } 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)}; } 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 = (int)(one_over_s * x + one_over_t * cart.y), .y = (int)(-one_over_s * x + one_over_t * cart.y)}; } Pixel* tile_xy_mut(const IsoGfx* iso, TileData* tile, int x, int y) { assert(iso); assert(tile); assert(tile->pixels); assert(x >= 0); assert(y >= 0); assert(x < iso->tile_width); assert(y < iso->tile_height); return &tile->pixels[y * iso->tile_width + x]; } Pixel tile_xy(const IsoGfx* iso, const TileData* tile, int x, int y) { assert(iso); assert(tile); assert(tile->pixels); assert(x >= 0); assert(y >= 0); assert(x < iso->tile_width); assert(y < iso->tile_height); return tile->pixels[y * iso->tile_width + x]; } static inline Tile world_xy(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_mut(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 Pixel screen_xy(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_mut(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 void draw_tile(IsoGfx* iso, ivec2 so, Tile tile) { assert(iso); const TileData* data = mempool_get_block(&iso->tiles, tile); assert(data); for (int py = 0; py < iso->tile_height; ++py) { for (int px = 0; px < iso->tile_width; ++px) { const Pixel colour = tile_xy(iso, data, px, py); const int sx = so.x + px; const int sy = so.y + py; if ((sx >= 0) && (sy >= 0) && (sx < iso->screen_width) && (sy < iso->screen_height)) { const uint8_t mask = iso->tile_mask[py * iso->tile_width + px]; if (mask == 1) { *screen_xy_mut(iso, sx, sy) = colour; } } } } } static void draw(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 ivec2 o = {(iso->screen_width / 2) - (iso->tile_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}; // TODO: Since the world will generally be larger than the screen, it // would be best to walk in screen space and fetch the tile. // The tile-centric approach might be more cache-friendly, however, since the // screen-centric approach would juggle multiple tiles throughout the scan. 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))); draw_tile(iso, so, tile); } } } /// Creates a tile mask procedurally. static void make_tile_mask(IsoGfx* iso) { assert(iso); assert(iso->tile_mask); for (int y = 0; y < iso->tile_height / 2; ++y) { const int mask_start = iso->tile_width / 2 - 2 * y - 1; const int mask_end = iso->tile_width / 2 + 2 * y + 1; for (int x = 0; x < iso->tile_width; ++x) { const bool masked = (mask_start <= x) && (x <= mask_end); const uint8_t val = masked ? 1 : 0; // Top half. iso->tile_mask[y * iso->tile_width + x] = val; // Bottom half reflects the top half. const int y_reflected = iso->tile_height - y - 1; iso->tile_mask[y_reflected * iso->tile_width + x] = val; } } } /// Creates a tile with a constant colour. static void make_tile_from_colour( const IsoGfx* iso, Pixel colour, TileData* tile) { assert(iso); assert(tile); for (int y = 0; y < iso->tile_height; ++y) { for (int x = 0; x < iso->tile_width; ++x) { *tile_xy_mut(iso, tile, x, y) = colour; } } } IsoGfx* isogfx_new(const IsoGfxDesc* desc) { assert(desc->screen_width > 0); assert(desc->screen_height > 0); assert(desc->tile_width > 0); assert(desc->tile_height > 0); // Part of our implementation assumes even widths and heights for greater // precision. assert((desc->screen_width & 1) == 0); assert((desc->screen_height & 1) == 0); assert((desc->tile_width & 1) == 0); assert((desc->tile_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->tile_width = desc->tile_width; iso->tile_height = desc->tile_height; iso->world_width = desc->world_width; iso->world_height = desc->world_height; iso->max_num_tiles = desc->max_num_tiles > 0 ? desc->max_num_tiles : DEFAULT_MAX_NUM_TILES; const int world_size = desc->world_width * desc->world_height; const int screen_size = desc->screen_width * desc->screen_height; const int tile_size = desc->tile_width * desc->tile_height; const int tile_size_bytes = tile_size * (int)sizeof(Pixel); if (!(iso->world = calloc(world_size, sizeof(Tile)))) { goto cleanup; } if (!(iso->screen = calloc(screen_size, sizeof(Pixel)))) { goto cleanup; } if (!(iso->tile_mask = calloc(tile_size, sizeof(uint8_t)))) { goto cleanup; } if (!mempool_make_dyn(&iso->tiles, iso->max_num_tiles, tile_size_bytes)) { goto cleanup; } make_tile_mask(iso); return iso; cleanup: isogfx_del(&iso); return 0; } void isogfx_del(IsoGfx** pIso) { assert(pIso); IsoGfx* iso = *pIso; if (iso) { if (iso->world) { free(iso->world); } if (iso->screen) { free(iso->screen); } if (iso->tile_mask) { free(iso->tile_mask); } mempool_del(&iso->tiles); free(iso); } } Tile isogfx_make_tile(IsoGfx* iso, const TileDesc* desc) { assert(iso); assert(desc); TileData* tile = mempool_alloc(&iso->tiles); assert(tile); // TODO: Make this a hard assert. 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_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); const int x = (int)xy_iso.x; const int y = (int)xy_iso.y; if ((0 <= x) && (x < iso->world_width) && (0 <= y) && (y < iso->world_height)) { *xiso = x; *yiso = y; } else { *xiso = -1; } } void isogfx_render(IsoGfx* iso) { assert(iso); draw(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 ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0}; const ivec2 vx = {.x = iso->tile_width / 2, .y = iso->tile_height / 2}; const ivec2 vy = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2}; const ivec2 so = ivec2_add(o, ivec2_add(ivec2_scale(vx, x), ivec2_scale(vy, y))); draw_tile(iso, so, tile); } const Pixel* isogfx_get_screen_buffer(const IsoGfx* iso) { assert(iso); return iso->screen; } 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; }