#include #include #include #include #include #include #include #include #include /// Maximum path length. #define MAX_PATH 256 /// 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; // ----------------------------------------------------------------------------- // Renderer state. // ----------------------------------------------------------------------------- typedef struct IsoCoordSystem { ivec2 o; // Origin. ivec2 x; ivec2 y; } IsoCoordSystem; typedef struct Screen { int width; int height; Pixel* pixels; } Screen; typedef struct SpriteInstance { struct SpriteInstance* next; const Ss_SpriteSheet* sheet; ivec2 position; int animation; // Current animation. int frame; // Current frame of animation. } SpriteInstance; typedef struct Gfx2d { Screen screen; IsoCoordSystem iso_space; ivec2 camera; double last_animation_time; Tile next_tile; // For procedurally-generated tiles. Tm_Map* map; Ts_TileSet* tileset; SpriteInstance* head_sprite; // Head of sprites list. memstack stack; size_t watermark; } Gfx2d; // ----------------------------------------------------------------------------- // 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 ivec2_neg(ivec2 a) { return (ivec2){.x = -a.x, .y = -a.y}; } static inline vec2 vec2_add(vec2 a, vec2 b) { return (vec2){.x = a.x + b.x, .y = a.y + b.y}; } static inline vec2 ivec2_to_vec2(ivec2 a) { return (vec2){a.x, a.y}; } /// Map map coordinates to screen coordinates, both Cartesian. static ivec2 map2screen( ivec2 camera, int tile_width, int tile_height, int map_x, int map_y) { return ivec2_add( ivec2_neg(camera), (ivec2){.x = map_x * tile_width, .y = map_y * tile_height}); } // Not actually used because we pre-compute the two axis vectors instead. // See make_iso_coord_system() and the other definition of iso2cart() below. // 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)}; // } /// Create the basis for the isometric coordinate system with origin and vectors /// expressed in the Cartesian system. static IsoCoordSystem make_iso_coord_system( const Tm_Map* const map, const Screen* const screen) { assert(map); assert(screen); const ivec2 o = {screen->width / 2, 0}; const ivec2 x = { .x = map->base_tile_width / 2, .y = map->base_tile_height / 2}; const ivec2 y = { .x = -map->base_tile_width / 2, .y = map->base_tile_height / 2}; return (IsoCoordSystem){o, x, y}; } /// Map isometric coordinates to Cartesian coordinates. /// /// For a tile, this gets the screen position of the top diamond-corner of the /// tile. /// /// Takes the camera displacement into account. static ivec2 iso2cart( const IsoCoordSystem iso_space, ivec2 camera, int iso_x, int iso_y) { const ivec2 vx_offset = ivec2_scale(iso_space.x, iso_x); const ivec2 vy_offset = ivec2_scale(iso_space.y, iso_y); const ivec2 origin_world_space = ivec2_add(iso_space.o, ivec2_add(vx_offset, vy_offset)); const ivec2 origin_view_space = ivec2_add(origin_world_space, ivec2_neg(camera)); return origin_view_space; } // 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 inline const Pixel* screen_xy_const_ref( const Screen* screen, int x, int y) { assert(screen); assert(x >= 0); assert(y >= 0); assert(x < screen->width); assert(y < screen->height); return &screen->pixels[y * screen->width + x]; } static inline Pixel screen_xy(Screen* screen, int x, int y) { return *screen_xy_const_ref(screen, x, y); } static inline Pixel* screen_xy_mut(Screen* screen, int x, int y) { return (Pixel*)screen_xy_const_ref(screen, x, y); } // ----------------------------------------------------------------------------- // Renderer, world and tile management. // ----------------------------------------------------------------------------- Gfx2d* gfx2d_new(const Gfx2dDesc* 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); Gfx2d tmp = {0}; if (!memstack_make(&tmp.stack, desc->memory_size, desc->memory)) { goto cleanup; } Gfx2d* gfx = memstack_alloc_aligned(&tmp.stack, sizeof(Gfx2d), alignof(Gfx2d)); *gfx = tmp; const size_t screen_size_bytes = desc->screen_width * desc->screen_height * sizeof(Pixel); Pixel* screen = memstack_alloc_aligned(&gfx->stack, screen_size_bytes, alignof(Pixel)); gfx->screen = (Screen){.width = desc->screen_width, .height = desc->screen_height, .pixels = screen}; gfx->last_animation_time = 0.0; gfx->watermark = memstack_watermark(&gfx->stack); return gfx; cleanup: gfx2d_del(&gfx); return nullptr; } void gfx2d_clear(Gfx2d* gfx) { assert(gfx); gfx->last_animation_time = 0.0; gfx->next_tile = 0; gfx->map = nullptr; gfx->tileset = nullptr; gfx->head_sprite = nullptr; // The base of the stack contains the Gfx2d and the screen buffer. Make sure // we don't clear them. memstack_set_watermark(&gfx->stack, gfx->watermark); } void gfx2d_del(Gfx2d** ppGfx) { assert(ppGfx); Gfx2d* gfx = *ppGfx; if (gfx) { memstack_del(&gfx->stack); *ppGfx = nullptr; } } void gfx2d_make_map(Gfx2d* gfx, const MapDesc* desc) { assert(gfx); 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); // World must be non-empty. assert(desc->world_width > 0); assert(desc->world_height > 0); // Must have >0 tiles. assert(desc->num_tiles > 0); // Handle recreation by destroying the previous world and sprites. gfx2d_clear(gfx); const int world_size = desc->world_width * desc->world_height; const size_t map_size_bytes = sizeof(Tm_Map) + (world_size * sizeof(Tile)); // This implies that all tiles are of the base tile dimensions. // We could enhance the API to allow for supertiles as well. Take in max tile // width and height and allocate enough space using those values. const size_t tile_size = desc->tile_width * desc->tile_height; const size_t tile_size_bytes = tile_size * sizeof(Pixel); const size_t tile_data_size_bytes = desc->num_tiles * tile_size_bytes; const size_t tileset_size_bytes = sizeof(Ts_TileSet) + (desc->num_tiles * sizeof(Ts_Tile)) + tile_data_size_bytes; gfx->map = memstack_alloc_aligned(&gfx->stack, map_size_bytes, 4); *gfx->map = (Tm_Map){ .world_width = desc->world_width, .world_height = desc->world_height, .base_tile_width = desc->tile_width, .base_tile_height = desc->tile_height, .num_layers = 1, .flags = (desc->orientation == MapOrthogonal) ? Tm_Orthogonal : Tm_Isometric, }; gfx->tileset = memstack_alloc_aligned(&gfx->stack, tileset_size_bytes, 4); *gfx->tileset = (Ts_TileSet){ .num_tiles = desc->num_tiles, }; gfx->iso_space = make_iso_coord_system(gfx->map, &gfx->screen); } bool gfx2d_load_map(Gfx2d* gfx, const char* filepath) { assert(gfx); assert(filepath); bool success = false; // Handle recreation by destroying the previous world and sprites. gfx2d_clear(gfx); // Load the map. printf("Load tile map: %s\n", filepath); WITH_FILE(filepath, { const size_t map_size = get_file_size_f(file); gfx->map = memstack_alloc_aligned(&gfx->stack, map_size, 4); success = read_file_f(file, gfx->map); }); if (!success) { goto cleanup; } Tm_Map* const map = gfx->map; printf("Map orientation: %d\n", ((Tm_Flags*)&map->flags)->orientation); // Load the tile set. // // Tile set path is relative to the tile map file. Make it relative to the // current working directory before loading. const char* ts_path = map->tileset_path; char ts_path_cwd[MAX_PATH] = {0}; if (!path_make_relative(filepath, ts_path, ts_path_cwd, MAX_PATH)) { goto cleanup; } printf("Load tile set: %s\n", ts_path_cwd); WITH_FILE(ts_path_cwd, { const size_t file_size = get_file_size_f(file); gfx->tileset = memstack_alloc_aligned(&gfx->stack, file_size, 4); success = read_file_f(file, gfx->tileset); }); if (!success) { // TODO: Log errors using the log library. goto cleanup; } const Ts_TileSet* const tileset = gfx->tileset; printf("Loaded tile set (%u tiles): %s\n", tileset->num_tiles, ts_path_cwd); // TODO: These assertions on input data should be library runtime errors. assert(ts_validate_tileset(tileset)); assert(tm_validate_map(map, tileset)); gfx->iso_space = make_iso_coord_system(gfx->map, &gfx->screen); success = true; cleanup: if (!success) { gfx2d_clear(gfx); } return success; } int gfx2d_world_width(const Gfx2d* gfx) { assert(gfx); return gfx->map->world_width; } int gfx2d_world_height(const Gfx2d* gfx) { assert(gfx); return gfx->map->world_height; } static void make_tile_from_colour( Pixel colour, const Ts_Tile* tile, Pixel* tile_pixels) { assert(tile); assert(tile_pixels); 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. *ts_tile_xy_mut(tile_pixels, tile, x, y) = val; // Bottom half reflects the top half. const int y_reflected = height - y - 1; *ts_tile_xy_mut(tile_pixels, tile, x, y_reflected) = val; } } } Tile gfx2d_make_tile(Gfx2d* gfx, const TileDesc* desc) { assert(gfx); assert(desc); // Client must create a world first. assert(gfx->map); assert(gfx->tileset); // Currently, procedural tiles must match the base tile size. assert(desc->width == gfx->map->base_tile_width); assert(desc->height == gfx->map->base_tile_height); // Cannot exceed max tiles. assert(gfx->next_tile < gfx->tileset->num_tiles); const Tile tile = gfx->next_tile++; const size_t tile_size_bytes = desc->width * desc->height * sizeof(Pixel); switch (desc->type) { case TileFromColour: { assert(desc->width > 0); assert(desc->height > 0); Ts_Tile* const ts_tile = ts_tileset_get_tile_mut(gfx->tileset, tile); *ts_tile = (Ts_Tile){ .width = gfx->map->base_tile_width, .height = gfx->map->base_tile_height, .pixels = tile * tile_size_bytes, }; Pixel* const tile_pixels = ts_tileset_get_tile_pixels_mut(gfx->tileset, tile); make_tile_from_colour(desc->colour, ts_tile, tile_pixels); break; } case TileFromFile: assert(false); // TODO break; case TileFromMemory: { assert(desc->width > 0); assert(desc->height > 0); assert(false); // TODO break; } } return tile; } void gfx2d_set_tile(Gfx2d* gfx, int x, int y, Tile tile) { assert(gfx); Tm_Layer* const layer = tm_map_get_layer_mut(gfx->map, 0); Tile* map_tile = tm_layer_get_tile_mut(gfx->map, layer, x, y); *map_tile = tile; } void gfx2d_set_tiles(Gfx2d* gfx, int x0, int y0, int x1, int y1, Tile tile) { assert(gfx); for (int y = y0; y < y1; ++y) { for (int x = x0; x < x1; ++x) { gfx2d_set_tile(gfx, x, y, tile); } } } SpriteSheet gfx2d_load_sprite_sheet(Gfx2d* gfx, const char* filepath) { assert(gfx); assert(filepath); bool success = false; SpriteSheet spriteSheet = 0; const size_t watermark = memstack_watermark(&gfx->stack); // Load sprite sheet file. printf("Load sprite sheet: %s\n", filepath); Ss_SpriteSheet* ss_sheet = nullptr; WITH_FILE(filepath, { const size_t file_size = get_file_size_f(file); ss_sheet = memstack_alloc_aligned(&gfx->stack, file_size, alignof(Ss_SpriteSheet)); success = read_file_f(file, ss_sheet); }); if (!success) { goto cleanup; } assert(ss_sheet); spriteSheet = (SpriteSheet)ss_sheet; cleanup: if (!success) { if (ss_sheet) { memstack_set_watermark(&gfx->stack, watermark); } } return spriteSheet; } Sprite gfx2d_make_sprite(Gfx2d* gfx, SpriteSheet sheet) { assert(gfx); assert(sheet); // TODO: Remove memstack_alloc() and replace it with a same-name macro that // calls memstack_alloc_aligned() with sizeof/alignof. No real point in // having unaligned allocations. SpriteInstance* sprite = memstack_alloc_aligned( &gfx->stack, sizeof(SpriteInstance), alignof(SpriteInstance)); sprite->sheet = (const Ss_SpriteSheet*)sheet; sprite->next = gfx->head_sprite; gfx->head_sprite = sprite; return (Sprite)sprite; } void gfx2d_set_sprite_position(Gfx2d* gfx, Sprite hSprite, int x, int y) { assert(gfx); SpriteInstance* sprite = (SpriteInstance*)hSprite; sprite->position.x = x; sprite->position.y = y; } void gfx2d_set_sprite_animation(Gfx2d* gfx, Sprite hSprite, int animation) { assert(gfx); SpriteInstance* sprite = (SpriteInstance*)hSprite; sprite->animation = animation; } void gfx2d_update(Gfx2d* gfx, double t) { assert(gfx); // If this is the first time update() is called after initialization, just // record the starting animation time. if (gfx->last_animation_time == 0.0) { gfx->last_animation_time = t; return; } if ((t - gfx->last_animation_time) >= ANIMATION_UPDATE_DELTA) { // TODO: Consider linking animated sprites in a separate list so that we // only walk over those here and not also the static sprites. for (SpriteInstance* sprite = gfx->head_sprite; sprite; sprite = sprite->next) { const Ss_SpriteSheet* sheet = sprite->sheet; const Ss_Row* row = ss_get_sprite_sheet_row(sheet, sprite->animation); sprite->frame = (sprite->frame + 1) % row->num_cols; } gfx->last_animation_time = t; } } // ----------------------------------------------------------------------------- // Rendering and picking. // ----------------------------------------------------------------------------- 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( Screen* screen, ivec2 top_left, int rect_width, int rect_height, const Pixel* pixels, const uint8_t* indices) { assert(screen); assert(pixels); #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 < screen->height); ++py) { const int sy = top_left.y + py; for (int px = px_offset; (px < rect_width) && (top_left.x + px < 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(screen, sx, sy); const Pixel final = alpha_blend(colour, dst); *screen_xy_mut(screen, sx, sy) = final; } } } } /// Draw a tile in an orthogonal map. static void draw_tile_ortho(Gfx2d* gfx, Tile tile, int x, int y) { assert(gfx); assert(gfx->tileset); assert(x >= 0); assert(y >= 0); assert(x < gfx->map->world_width); assert(y < gfx->map->world_height); const Ts_Tile* pTile = ts_tileset_get_tile(gfx->tileset, tile); const Pixel* pixels = ts_tileset_get_tile_pixels(gfx->tileset, tile); const ivec2 screen_origin = map2screen( gfx->camera, gfx->map->base_tile_width, gfx->map->base_tile_height, x, y); draw_rect( &gfx->screen, screen_origin, pTile->width, pTile->height, pixels, nullptr); } /// Draw a tile in an isometric map. static void draw_tile_iso(Gfx2d* gfx, Tile tile, int iso_x, int iso_y) { assert(gfx); assert(gfx->tileset); assert(iso_x >= 0); assert(iso_y >= 0); assert(iso_x < gfx->map->world_width); assert(iso_y < gfx->map->world_height); const Ts_Tile* pTile = ts_tileset_get_tile(gfx->tileset, tile); const Pixel* pixels = ts_tileset_get_tile_pixels(gfx->tileset, tile); // Compute the screen coordinates of the top diamond-corner of the tile (the // base tile for super tiles). // World (0, 0) -> (screen_width / 2, 0). const ivec2 screen_origin = iso2cart(gfx->iso_space, gfx->camera, iso_x, iso_y); // 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 = { -(gfx->map->base_tile_width / 2), pTile->height - gfx->map->base_tile_height}; const ivec2 top_left = ivec2_add(screen_origin, offset); draw_rect( &gfx->screen, top_left, pTile->width, pTile->height, pixels, nullptr); } static void draw_map_ortho(Gfx2d* gfx) { assert(gfx); assert(gfx->map); // TODO: Same TODOs as in draw_map_iso(). const Tm_Layer* layer = tm_map_get_layer(gfx->map, 0); for (int wy = 0; wy < gfx->map->world_height; ++wy) { for (int wx = 0; wx < gfx->map->world_width; ++wx) { const Tile tile = tm_layer_get_tile(gfx->map, layer, wx, wy); draw_tile_ortho(gfx, tile, wx, wy); } } } static void draw_map_iso(Gfx2d* gfx) { assert(gfx); assert(gfx->map); // TODO: Support for multiple layers. const Tm_Layer* layer = tm_map_get_layer(gfx->map, 0); // 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 < gfx->map->world_height; ++wy) { for (int wx = 0; wx < gfx->map->world_width; ++wx) { const Tile tile = tm_layer_get_tile(gfx->map, layer, wx, wy); draw_tile_iso(gfx, tile, wx, wy); } } } static void draw_map(Gfx2d* gfx) { assert(gfx); assert(gfx->map); assert(gfx->screen.pixels); const int W = gfx->screen.width; const int H = gfx->screen.height; memset(gfx->screen.pixels, 0, W * H * sizeof(Pixel)); const Tm_Flags* flags = (const Tm_Flags*)&gfx->map->flags; switch (flags->orientation) { case Tm_Orthogonal: draw_map_ortho(gfx); break; case Tm_Isometric: draw_map_iso(gfx); break; } } /// Draw a sprite in an orthogonal/Cartesian coordinate system. static void draw_sprite_ortho( Gfx2d* gfx, const SpriteInstance* sprite, const Ss_SpriteSheet* sheet) { assert(gfx); assert(sprite); assert(sheet); assert(sprite->animation >= 0); assert(sprite->animation < sheet->num_rows); assert(sprite->frame >= 0); // Apply an offset similarly to how we offset tiles. The sprite is offset by // -base_tile_width/2 along the x-axis to align the sprite with the leftmost // edge of the tile it is on. const ivec2 screen_origin = map2screen( gfx->camera, gfx->map->base_tile_width, gfx->map->base_tile_height, sprite->position.x, sprite->position.y); const Ss_Row* row = ss_get_sprite_sheet_row(sheet, sprite->animation); const uint8_t* frame = ss_get_sprite_sheet_sprite(sheet, row, sprite->frame); draw_rect( &gfx->screen, screen_origin, sheet->sprite_width, sheet->sprite_height, sheet->palette.colours, frame); } /// Draw a sprite in an isometric coordinate system. static void draw_sprite_iso( Gfx2d* gfx, const SpriteInstance* sprite, const Ss_SpriteSheet* sheet) { assert(gfx); assert(sprite); assert(sheet); assert(sprite->animation >= 0); assert(sprite->animation < sheet->num_rows); assert(sprite->frame >= 0); // Apply an offset similarly to how we offset tiles. The sprite is offset by // -base_tile_width/2 along the x-axis to align the sprite with the leftmost // edge of the tile it is on. const ivec2 screen_origin = iso2cart( gfx->iso_space, gfx->camera, sprite->position.x, sprite->position.y); const ivec2 offset = {-(gfx->map->base_tile_width / 2), 0}; const ivec2 top_left = ivec2_add(screen_origin, offset); const Ss_Row* row = ss_get_sprite_sheet_row(sheet, sprite->animation); const uint8_t* frame = ss_get_sprite_sheet_sprite(sheet, row, sprite->frame); draw_rect( &gfx->screen, top_left, sheet->sprite_width, sheet->sprite_height, sheet->palette.colours, frame); } static void draw_sprites(Gfx2d* gfx) { assert(gfx); assert(gfx->map); const Tm_Flags* flags = (const Tm_Flags*)&gfx->map->flags; switch (flags->orientation) { case Tm_Orthogonal: for (const SpriteInstance* sprite = gfx->head_sprite; sprite; sprite = sprite->next) { draw_sprite_ortho(gfx, sprite, sprite->sheet); } break; case Tm_Isometric: for (const SpriteInstance* sprite = gfx->head_sprite; sprite; sprite = sprite->next) { draw_sprite_iso(gfx, sprite, sprite->sheet); } break; } } void gfx2d_set_camera(Gfx2d* gfx, int x, int y) { assert(gfx); gfx->camera = (ivec2){x, y}; } void gfx2d_render(Gfx2d* gfx) { assert(gfx); draw_map(gfx); draw_sprites(gfx); } void gfx2d_draw_tile(Gfx2d* gfx, int x, int y, Tile tile) { assert(gfx); assert(gfx->map); const Tm_Flags* flags = (const Tm_Flags*)&gfx->map->flags; switch (flags->orientation) { case Tm_Orthogonal: draw_tile_ortho(gfx, tile, x, y); break; case Tm_Isometric: draw_tile_iso(gfx, tile, x, y); break; } } void gfx2d_get_screen_size(const Gfx2d* gfx, int* width, int* height) { assert(gfx); assert(width); assert(height); *width = gfx->screen.width; *height = gfx->screen.height; } const Pixel* gfx2d_get_screen_buffer(const Gfx2d* gfx) { assert(gfx); return gfx->screen.pixels; } void gfx2d_pick_tile( const Gfx2d* gfx, double xcart, double ycart, int* xiso, int* yiso) { assert(gfx); assert(xiso); assert(yiso); const vec2 camera = ivec2_to_vec2(gfx->camera); const vec2 xy_cart = vec2_add(camera, (vec2){xcart, ycart}); const vec2 xy_iso = cart2iso( xy_cart, gfx->map->base_tile_width, gfx->map->base_tile_height, gfx->screen.width); if ((0 <= xy_iso.x) && (xy_iso.x < gfx->map->world_width) && (0 <= xy_iso.y) && (xy_iso.y < gfx->map->world_height)) { *xiso = (int)xy_iso.x; *yiso = (int)xy_iso.y; } else { *xiso = -1; *yiso = -1; } }