#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 CoordSystem { ivec2 o; // Origin. ivec2 x; ivec2 y; } CoordSystem; 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 IsoGfx { Screen screen; CoordSystem 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; } 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 ivec2_neg(ivec2 a) { return (ivec2){.x = -a.x, .y = -a.y}; } 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 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); } /// Create the basis for the isometric coordinate system with origin and vectors /// expressed in the Cartesian system. static CoordSystem 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 (CoordSystem){o, x, y}; } // ----------------------------------------------------------------------------- // 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 tmp = {0}; if (!memstack_make(&tmp.stack, desc->memory_size, desc->memory)) { goto cleanup; } IsoGfx* iso = memstack_alloc_aligned(&tmp.stack, sizeof(IsoGfx), alignof(IsoGfx)); *iso = tmp; const size_t screen_size_bytes = desc->screen_width * desc->screen_height * sizeof(Pixel); Pixel* screen = memstack_alloc_aligned(&iso->stack, screen_size_bytes, alignof(Pixel)); iso->screen = (Screen){.width = desc->screen_width, .height = desc->screen_height, .pixels = screen}; iso->last_animation_time = 0.0; iso->watermark = memstack_watermark(&iso->stack); return iso; cleanup: isogfx_del(&iso); return nullptr; } void isogfx_clear(IsoGfx* iso) { assert(iso); iso->last_animation_time = 0.0; iso->next_tile = 0; iso->map = nullptr; iso->tileset = nullptr; iso->head_sprite = nullptr; // The base of the stack contains the IsoGfx and the screen buffer. Make sure // we don't clear them. memstack_set_watermark(&iso->stack, iso->watermark); } void isogfx_del(IsoGfx** ppIso) { assert(ppIso); IsoGfx* iso = *ppIso; if (iso) { memstack_del(&iso->stack); *ppIso = nullptr; } } void 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); // 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. isogfx_clear(iso); 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; iso->map = memstack_alloc_aligned(&iso->stack, map_size_bytes, 4); *iso->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, }; iso->tileset = memstack_alloc_aligned(&iso->stack, tileset_size_bytes, 4); *iso->tileset = (Ts_TileSet){ .num_tiles = desc->num_tiles, }; iso->iso_space = make_iso_coord_system(iso->map, &iso->screen); } bool isogfx_load_world(IsoGfx* iso, const char* filepath) { assert(iso); assert(filepath); bool success = false; // Handle recreation by destroying the previous world and sprites. isogfx_clear(iso); // Load the map. printf("Load tile map: %s\n", filepath); WITH_FILE(filepath, { const size_t map_size = get_file_size_f(file); iso->map = memstack_alloc_aligned(&iso->stack, map_size, 4); success = read_file_f(file, iso->map); }); if (!success) { goto cleanup; } Tm_Map* const map = iso->map; // 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); iso->tileset = memstack_alloc_aligned(&iso->stack, file_size, 4); success = read_file_f(file, iso->tileset); }); if (!success) { // TODO: Log errors using the log library. goto cleanup; } const Ts_TileSet* const tileset = iso->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)); iso->iso_space = make_iso_coord_system(iso->map, &iso->screen); success = true; cleanup: if (!success) { isogfx_clear(iso); } return success; } int isogfx_world_width(const IsoGfx* iso) { assert(iso); return iso->map->world_width; } int isogfx_world_height(const IsoGfx* iso) { assert(iso); return iso->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 isogfx_make_tile(IsoGfx* iso, const TileDesc* desc) { assert(iso); assert(desc); // Client must create a world first. assert(iso->map); assert(iso->tileset); // Currently, procedural tiles must match the base tile size. assert(desc->width == iso->map->base_tile_width); assert(desc->height == iso->map->base_tile_height); // Cannot exceed max tiles. assert(iso->next_tile < iso->tileset->num_tiles); const Tile tile = iso->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(iso->tileset, tile); *ts_tile = (Ts_Tile){ .width = iso->map->base_tile_width, .height = iso->map->base_tile_height, .pixels = tile * tile_size_bytes, }; Pixel* const tile_pixels = ts_tileset_get_tile_pixels_mut(iso->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 isogfx_set_tile(IsoGfx* iso, int x, int y, Tile tile) { assert(iso); Tm_Layer* const layer = tm_map_get_layer_mut(iso->map, 0); Tile* map_tile = tm_layer_get_tile_mut(iso->map, layer, x, y); *map_tile = 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); } } } SpriteSheet isogfx_load_sprite_sheet(IsoGfx* iso, const char* filepath) { assert(iso); assert(filepath); bool success = false; SpriteSheet spriteSheet = 0; const size_t watermark = memstack_watermark(&iso->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(&iso->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(&iso->stack, watermark); } } return spriteSheet; } Sprite isogfx_make_sprite(IsoGfx* iso, SpriteSheet sheet) { assert(iso); 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( &iso->stack, sizeof(SpriteInstance), alignof(SpriteInstance)); sprite->sheet = (const Ss_SpriteSheet*)sheet; sprite->next = iso->head_sprite; iso->head_sprite = sprite; return (Sprite)sprite; } void isogfx_set_sprite_position(IsoGfx* iso, Sprite hSprite, int x, int y) { assert(iso); SpriteInstance* sprite = (SpriteInstance*)hSprite; sprite->position.x = x; sprite->position.y = y; } void isogfx_set_sprite_animation(IsoGfx* iso, Sprite hSprite, int animation) { assert(iso); SpriteInstance* sprite = (SpriteInstance*)hSprite; sprite->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 separate list so that we // only walk over those here and not also the static sprites. for (SpriteInstance* sprite = iso->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; } iso->last_animation_time = t; } } // ----------------------------------------------------------------------------- // Rendering and picking. // ----------------------------------------------------------------------------- /// Get the screen position of the top diamond-corner of the tile at world /// (x,y). static ivec2 GetTileScreenOrigin( const CoordSystem iso_space, ivec2 camera, 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)); const ivec2 origin_view_space = ivec2_add(screen_origin, ivec2_neg(camera)); return origin_view_space; } 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); #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. /// /// '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); assert(iso->tileset); const Ts_Tile* pTile = ts_tileset_get_tile(iso->tileset, tile); const Pixel* pixels = ts_tileset_get_tile_pixels(iso->tileset, tile); // 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->map->base_tile_width / 2), pTile->height - iso->map->base_tile_height}; const ivec2 top_left = ivec2_add(screen_origin, offset); draw_rect( &iso->screen, top_left, pTile->width, pTile->height, pixels, nullptr); } static void draw_world(IsoGfx* iso) { assert(iso); const int W = iso->screen.width; const int H = iso->screen.height; memset(iso->screen.pixels, 0, W * H * sizeof(Pixel)); const Tm_Layer* layer = tm_map_get_layer(iso->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 < iso->map->world_height; ++wy) { for (int wx = 0; wx < iso->map->world_width; ++wx) { const Tile tile = tm_layer_get_tile(iso->map, layer, wx, wy); const ivec2 screen_origin = GetTileScreenOrigin(iso->iso_space, iso->camera, wx, wy); draw_tile(iso, screen_origin, tile); } } } static void draw_sprite( IsoGfx* iso, ivec2 origin, const SpriteInstance* sprite, const Ss_SpriteSheet* sheet) { assert(iso); assert(sprite); assert(sheet); assert(sprite->animation >= 0); assert(sprite->animation < sheet->num_rows); assert(sprite->frame >= 0); 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( &iso->screen, origin, sheet->sprite_width, sheet->sprite_height, sheet->palette.colours, frame); } static void draw_sprites(IsoGfx* iso) { assert(iso); for (const SpriteInstance* sprite = iso->head_sprite; sprite; sprite = sprite->next) { const Ss_SpriteSheet* sheet = sprite->sheet; assert(sheet); const ivec2 screen_origin = GetTileScreenOrigin( iso->iso_space, iso->camera, sprite->position.x, sprite->position.y); draw_sprite(iso, screen_origin, sprite, sheet); } } void isogfx_set_camera(IsoGfx* iso, int x, int y) { assert(iso); iso->camera = (ivec2){x, y}; } 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->map->world_width); assert(y < iso->map->world_height); const ivec2 screen_origin = GetTileScreenOrigin(iso->iso_space, iso->camera, x, y); draw_tile(iso, screen_origin, tile); } 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.pixels; } 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->map->base_tile_width, iso->map->base_tile_height, iso->screen.width); if ((0 <= xy_iso.x) && (xy_iso.x < iso->map->world_width) && (0 <= xy_iso.y) && (xy_iso.y < iso->map->world_height)) { *xiso = (int)xy_iso.x; *yiso = (int)xy_iso.y; } else { *xiso = -1; *yiso = -1; } }