diff options
| author | 3gg <3gg@shellblade.net> | 2023-07-19 08:35:00 -0700 |
|---|---|---|
| committer | 3gg <3gg@shellblade.net> | 2023-07-19 08:35:00 -0700 |
| commit | 48cef82988d6209987ae27fe29b72d7d5e402b3c (patch) | |
| tree | fe5df57729a61839322ae8c1226d134e317b049f /gfx-iso | |
| parent | 2c668763a1d6e645dcfaa713b924de26260542d0 (diff) | |
Add sprites.
Diffstat (limited to 'gfx-iso')
| -rw-r--r-- | gfx-iso/app/isogfx-demo.c | 15 | ||||
| -rw-r--r-- | gfx-iso/app/main.c | 2 | ||||
| -rw-r--r-- | gfx-iso/asset/mkasset.py | 128 | ||||
| -rw-r--r-- | gfx-iso/include/isogfx/isogfx.h | 35 | ||||
| -rw-r--r-- | 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 @@ | |||
| 9 | #include <stdlib.h> | 9 | #include <stdlib.h> |
| 10 | 10 | ||
| 11 | typedef struct State { | 11 | typedef struct State { |
| 12 | int xpick; | 12 | int xpick; |
| 13 | int ypick; | 13 | int ypick; |
| 14 | SpriteSheet stag_sheet; | ||
| 15 | Sprite stag; | ||
| 14 | } State; | 16 | } State; |
| 15 | 17 | ||
| 16 | static void shutdown(IsoGfx* iso, void* app_state) { | 18 | static void shutdown(IsoGfx* iso, void* app_state) { |
| @@ -54,6 +56,15 @@ bool make_demo_app(IsoGfx* iso, IsoGfxApp* app) { | |||
| 54 | goto cleanup; | 56 | goto cleanup; |
| 55 | } | 57 | } |
| 56 | 58 | ||
| 59 | if (!isogfx_load_sprite_sheet( | ||
| 60 | iso, "/home/jeanne/assets/tilesets/scrabling/critters/stag/stag.ss", | ||
| 61 | &state->stag_sheet)) { | ||
| 62 | goto cleanup; | ||
| 63 | } | ||
| 64 | |||
| 65 | state->stag = isogfx_make_sprite(iso, state->stag_sheet); | ||
| 66 | isogfx_set_sprite_position(iso, state->stag, 5, 4); | ||
| 67 | |||
| 57 | app->pixel_scale = 2; | 68 | app->pixel_scale = 2; |
| 58 | app->state = state; | 69 | app->state = state; |
| 59 | app->shutdown = shutdown; | 70 | 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) { | |||
| 138 | assert(app_state); | 138 | assert(app_state); |
| 139 | State* state = (State*)(app_state); | 139 | State* state = (State*)(app_state); |
| 140 | 140 | ||
| 141 | isogfx_update(state->iso, t); | ||
| 142 | |||
| 141 | assert(state->app.update); | 143 | assert(state->app.update); |
| 142 | (*state->app.update)(state->iso, state->app.state, t, dt); | 144 | (*state->app.update)(state->iso, state->app.state, t, dt); |
| 143 | } | 145 | } |
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 @@ | |||
| 1 | # Converts tile sets and tile maps to binary formats (.TS, .TM) for the engine. | 1 | # Converts assets to binary formats (.ts, .tm, .ss) for the engine. |
| 2 | # | 2 | # |
| 3 | # Currently handles Tiled's .tsx and .tmx file formats. | 3 | # Input file formats: |
| 4 | # - Tiled tile set (.tsx) | ||
| 5 | # - Tiled tile map (.tmx) | ||
| 6 | # - Sprite sheets (.jpg, .png, etc), 1 row per animation. | ||
| 7 | # | ||
| 8 | # Output file formats: | ||
| 9 | # - Binary tile set file (.ts) | ||
| 10 | # - Binary tile map file (.tm) | ||
| 11 | # - Binary sprite sheet file (.ss) | ||
| 4 | # | 12 | # |
| 5 | # The output is a binary tile set file (.TS) or a binary tile map file (.TM). | ||
| 6 | import argparse | 13 | import argparse |
| 7 | import ctypes | 14 | import ctypes |
| 15 | import os | ||
| 8 | from PIL import Image | 16 | from PIL import Image |
| 9 | import sys | 17 | import sys |
| 10 | from xml.etree import ElementTree | 18 | from xml.etree import ElementTree |
| 11 | 19 | ||
| 12 | # Maximum length of path strings in .TS and .TM files. | 20 | # Maximum length of path strings in .TS and .TM files. |
| 21 | # Must match the engine's value. | ||
| 13 | MAX_PATH_LENGTH = 128 | 22 | MAX_PATH_LENGTH = 128 |
| 14 | 23 | ||
| 15 | 24 | ||
| @@ -133,20 +142,127 @@ def convert_tmx(input_filepath, output_filepath): | |||
| 133 | output.write(ctypes.c_uint16(int(tile_id))) | 142 | output.write(ctypes.c_uint16(int(tile_id))) |
| 134 | 143 | ||
| 135 | 144 | ||
| 145 | def get_num_cols(image, sprite_width): | ||
| 146 | """Return the number of non-empty columns in the image. | ||
| 147 | |||
| 148 | Assumes no gaps in the columns. | ||
| 149 | """ | ||
| 150 | assert (image.width % sprite_width == 0) | ||
| 151 | num_cols = image.width // sprite_width | ||
| 152 | |||
| 153 | # Start the search from right to left. | ||
| 154 | for col in reversed(range(1, num_cols)): | ||
| 155 | left = (col - 1) * sprite_width | ||
| 156 | right = col * sprite_width | ||
| 157 | rect = image.crop((left, 0, right, image.height)) | ||
| 158 | min_max = rect.getextrema() | ||
| 159 | for (channel_min, channel_max) in min_max: | ||
| 160 | if channel_min != 0 or channel_max != 0: | ||
| 161 | # 'col' is the rightmost non-empty column. | ||
| 162 | # Assuming no gaps, col+1 is the number of non-empty columns. | ||
| 163 | return col + 1 | ||
| 164 | |||
| 165 | return 0 | ||
| 166 | |||
| 167 | |||
| 168 | def get_sprite_sheet_rows(input_filepath, sprite_width, sprite_height): | ||
| 169 | """Gets the individual rows of a sprite sheet. | ||
| 170 | |||
| 171 | The input sprite sheet can have any number of rows. | ||
| 172 | |||
| 173 | Returns a list of lists [[sprite bytes]], one inner list for the columns in | ||
| 174 | each row. | ||
| 175 | """ | ||
| 176 | with Image.open(input_filepath) as im: | ||
| 177 | # Sprite sheet's width and height must be integer multiples of the | ||
| 178 | # sprite's width and height. | ||
| 179 | assert (im.width % sprite_width == 0) | ||
| 180 | assert (im.height % sprite_height == 0) | ||
| 181 | |||
| 182 | num_rows = im.height // sprite_height | ||
| 183 | |||
| 184 | rows = [] | ||
| 185 | for row in range(num_rows): | ||
| 186 | # Get the number of columns. | ||
| 187 | upper = row * sprite_height | ||
| 188 | lower = (row + 1) * sprite_height | ||
| 189 | whole_row = im.crop((0, upper, im.width, lower)) | ||
| 190 | num_cols = get_num_cols(whole_row, sprite_width) | ||
| 191 | assert (num_cols > 0) | ||
| 192 | |||
| 193 | # Crop the row into N columns. | ||
| 194 | cols = [] | ||
| 195 | for i in range(num_cols): | ||
| 196 | left = i * sprite_width | ||
| 197 | right = (i + 1) * sprite_width | ||
| 198 | sprite = im.crop((left, upper, right, lower)) | ||
| 199 | cols.append(sprite) | ||
| 200 | |||
| 201 | sprite_bytes = [sprite.convert('RGBA').tobytes() for sprite in cols] | ||
| 202 | assert (len(sprite_bytes) == num_cols) | ||
| 203 | rows.append(sprite_bytes) | ||
| 204 | |||
| 205 | return rows | ||
| 206 | |||
| 207 | |||
| 208 | def convert_sprite_sheet(input_file_paths, sprite_width, sprite_height, | ||
| 209 | output_filepath): | ||
| 210 | """Converts a set of sprite sheet images into a binary sprite sheet file | ||
| 211 | (.ss). | ||
| 212 | |||
| 213 | The input sprite sheets can have any number of rows, one row per animation. | ||
| 214 | All rows from all sprite sheets are concatenated in the output file. | ||
| 215 | |||
| 216 | The sprite's width and height is assumed constant throughout the input | ||
| 217 | sprite sheets. | ||
| 218 | """ | ||
| 219 | rows = [] | ||
| 220 | |||
| 221 | for sprite_sheet in input_file_paths: | ||
| 222 | rows.extend( | ||
| 223 | get_sprite_sheet_rows(sprite_sheet, sprite_width, sprite_height)) | ||
| 224 | |||
| 225 | with open(output_filepath, 'bw') as output: | ||
| 226 | output.write(ctypes.c_uint16(sprite_width)) | ||
| 227 | output.write(ctypes.c_uint16(sprite_height)) | ||
| 228 | output.write(ctypes.c_uint16(len(rows))) | ||
| 229 | |||
| 230 | print(f"Sprite width: {sprite_width}") | ||
| 231 | print(f"Sprite height: {sprite_height}") | ||
| 232 | print(f"Rows: {len(rows)}") | ||
| 233 | |||
| 234 | for sprites in rows: | ||
| 235 | output.write(ctypes.c_uint16(len(sprites))) | ||
| 236 | for sprite_bytes in sprites: | ||
| 237 | output.write(sprite_bytes) | ||
| 238 | |||
| 239 | |||
| 136 | def main(): | 240 | def main(): |
| 137 | parser = argparse.ArgumentParser() | 241 | parser = argparse.ArgumentParser() |
| 138 | parser.add_argument("input", help="Input file (.tsx, .tmx)") | 242 | parser.add_argument("input", |
| 243 | nargs="+", | ||
| 244 | help="Input file (.tsx, .tmx) or path regex (sprite sheets)") | ||
| 245 | parser.add_argument("--width", type=int, help="Sprite width in pixels") | ||
| 246 | parser.add_argument("--height", type=int, help="Sprite height in pixels") | ||
| 247 | parser.add_argument("--out", help="Output file (sprite sheets)") | ||
| 139 | args = parser.parse_args() | 248 | args = parser.parse_args() |
| 140 | 249 | ||
| 141 | output_filepath_no_ext = drop_extension(args.input) | ||
| 142 | if ".tsx" in args.input: | 250 | if ".tsx" in args.input: |
| 251 | output_filepath_no_ext = drop_extension(args.input) | ||
| 143 | output_filepath = output_filepath_no_ext + ".ts" | 252 | output_filepath = output_filepath_no_ext + ".ts" |
| 144 | convert_tsx(args.input, output_filepath) | 253 | convert_tsx(args.input, output_filepath) |
| 145 | elif ".tmx" in args.input: | 254 | elif ".tmx" in args.input: |
| 255 | output_filepath_no_ext = drop_extension(args.input) | ||
| 146 | output_filepath = output_filepath_no_ext + ".tm" | 256 | output_filepath = output_filepath_no_ext + ".tm" |
| 147 | convert_tmx(args.input, output_filepath) | 257 | convert_tmx(args.input, output_filepath) |
| 148 | else: | 258 | else: |
| 149 | print(f"Unhandled file format: {args.input}") | 259 | # Sprite sheets. |
| 260 | if not args.width or not args.height: | ||
| 261 | print("Sprite width and height must be given") | ||
| 262 | return 1 | ||
| 263 | output_filepath = args.out if args.out else "out.ss" | ||
| 264 | convert_sprite_sheet(args.input, args.width, args.height, | ||
| 265 | output_filepath) | ||
| 150 | 266 | ||
| 151 | return 0 | 267 | return 0 |
| 152 | 268 | ||
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 @@ | |||
| 8 | 8 | ||
| 9 | typedef struct IsoGfx IsoGfx; | 9 | typedef struct IsoGfx IsoGfx; |
| 10 | 10 | ||
| 11 | /// Sprite sheet handle. | ||
| 12 | typedef uint16_t SpriteSheet; | ||
| 13 | |||
| 14 | /// Sprite handle. | ||
| 15 | typedef uint16_t Sprite; | ||
| 16 | |||
| 11 | /// Tile handle. | 17 | /// Tile handle. |
| 12 | typedef uint16_t Tile; | 18 | typedef uint16_t Tile; |
| 13 | 19 | ||
| @@ -48,8 +54,10 @@ typedef struct WorldDesc { | |||
| 48 | } WorldDesc; | 54 | } WorldDesc; |
| 49 | 55 | ||
| 50 | typedef struct IsoGfxDesc { | 56 | typedef struct IsoGfxDesc { |
| 51 | int screen_width; /// Screen width in pixels. | 57 | int screen_width; /// Screen width in pixels. |
| 52 | int screen_height; /// Screen height in pixels. | 58 | int screen_height; /// Screen height in pixels. |
| 59 | int max_num_sprites; /// 0 for an implementation-defined default. | ||
| 60 | int sprite_sheet_pool_size_bytes; /// 0 for an implementation-defined default. | ||
| 53 | } IsoGfxDesc; | 61 | } IsoGfxDesc; |
| 54 | 62 | ||
| 55 | /// Create a new isometric graphics engine. | 63 | /// Create a new isometric graphics engine. |
| @@ -79,6 +87,29 @@ void isogfx_set_tile(IsoGfx*, int x, int y, Tile); | |||
| 79 | /// Set the tiles in positions in the range (x0,y0) - (x1,y1). | 87 | /// Set the tiles in positions in the range (x0,y0) - (x1,y1). |
| 80 | void isogfx_set_tiles(IsoGfx*, int x0, int y0, int x1, int y1, Tile); | 88 | void isogfx_set_tiles(IsoGfx*, int x0, int y0, int x1, int y1, Tile); |
| 81 | 89 | ||
| 90 | /// Load a sprite sheet (.SS) file. | ||
| 91 | bool isogfx_load_sprite_sheet(IsoGfx*, const char* filepath, SpriteSheet*); | ||
| 92 | |||
| 93 | /// Create an animated sprite. | ||
| 94 | Sprite isogfx_make_sprite(IsoGfx*, SpriteSheet); | ||
| 95 | |||
| 96 | /// Destroy the sprite. | ||
| 97 | void isogfx_del_sprite(IsoGfx*, Sprite); | ||
| 98 | |||
| 99 | /// Destroy all the sprites. | ||
| 100 | void isogfx_del_sprites(IsoGfx*); | ||
| 101 | |||
| 102 | /// Set the sprite's position. | ||
| 103 | void isogfx_set_sprite_position(IsoGfx*, Sprite, int x, int y); | ||
| 104 | |||
| 105 | /// Set the sprite's current animation. | ||
| 106 | void isogfx_set_sprite_animation(IsoGfx*, Sprite, int animation); | ||
| 107 | |||
| 108 | /// Update the renderer. | ||
| 109 | /// | ||
| 110 | /// Currently this updates the sprite animations. | ||
| 111 | void isogfx_update(IsoGfx*, double t); | ||
| 112 | |||
| 82 | /// Render the world. | 113 | /// Render the world. |
| 83 | void isogfx_render(IsoGfx*); | 114 | void isogfx_render(IsoGfx*); |
| 84 | 115 | ||
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 @@ | |||
| 13 | #include <stdlib.h> | 13 | #include <stdlib.h> |
| 14 | #include <string.h> | 14 | #include <string.h> |
| 15 | 15 | ||
| 16 | /// Maximum number of tiles unless the user chooses a non-zero value. | 16 | /// Maximum number of tiles unless the user specifies a value. |
| 17 | #define DEFAULT_MAX_NUM_TILES 1024 | 17 | #define DEFAULT_MAX_NUM_TILES 1024 |
| 18 | 18 | ||
| 19 | /// Maximum number of sprites unless the user specifies a value. | ||
| 20 | #define DEFAULT_MAX_NUM_SPRITES 128 | ||
| 21 | |||
| 22 | /// Size of sprite sheet pool in bytes unless the user specifies a value. | ||
| 23 | #define DEFAULT_SPRITE_SHEET_POOL_SIZE_BYTES (8 * 1024 * 1024) | ||
| 24 | |||
| 25 | /// Default animation speed. | ||
| 26 | #define ANIMATION_FPS 10 | ||
| 27 | |||
| 28 | /// Time between animation updates. | ||
| 29 | #define ANIMATION_UPDATE_DELTA (1.0 / ANIMATION_FPS) | ||
| 30 | |||
| 31 | typedef struct ivec2 { | ||
| 32 | int x, y; | ||
| 33 | } ivec2; | ||
| 34 | |||
| 35 | typedef struct vec2 { | ||
| 36 | double x, y; | ||
| 37 | } vec2; | ||
| 38 | |||
| 19 | // ----------------------------------------------------------------------------- | 39 | // ----------------------------------------------------------------------------- |
| 20 | // Tile set (TS) and tile map (TM) file formats. | 40 | // Tile set (TS) and tile map (TM) file formats. |
| 21 | // ----------------------------------------------------------------------------- | 41 | // ----------------------------------------------------------------------------- |
| @@ -70,6 +90,39 @@ static inline const Ts_Tile* ts_tileset_get_next_tile( | |||
| 70 | } | 90 | } |
| 71 | 91 | ||
| 72 | // ----------------------------------------------------------------------------- | 92 | // ----------------------------------------------------------------------------- |
| 93 | // Sprite sheet file format. | ||
| 94 | // ----------------------------------------------------------------------------- | ||
| 95 | |||
| 96 | /// A row of sprites in a sprite sheet. | ||
| 97 | /// | ||
| 98 | /// Each row in a sprite sheet can have a different number of columns. | ||
| 99 | /// | ||
| 100 | /// The pixels of the row follow a "sprite-major" order. It contains the | ||
| 101 | /// 'sprite_width * sprite_height' pixels for the first column/sprite, then the | ||
| 102 | /// second column/sprite, etc. | ||
| 103 | typedef struct Ss_Row { | ||
| 104 | uint16_t num_cols; /// Number of columns in this row. | ||
| 105 | Pixel pixels[1]; /// Count: num_cols * sprite_width * sprite_height. | ||
| 106 | } Ss_Row; | ||
| 107 | |||
| 108 | /// Sprite sheet top-level data definition. | ||
| 109 | /// | ||
| 110 | /// Sprite width and height are assumed constant throughout the sprite sheet. | ||
| 111 | typedef struct Ss_SpriteSheet { | ||
| 112 | uint16_t sprite_width; /// Sprite width in pixels. | ||
| 113 | uint16_t sprite_height; /// Sprite height in pixels. | ||
| 114 | uint16_t num_rows; | ||
| 115 | Ss_Row rows[1]; /// Count: num_rows. | ||
| 116 | } Ss_SpriteSheet; | ||
| 117 | |||
| 118 | const Ss_Row* get_sprite_sheet_row(const Ss_SpriteSheet* sheet, int row) { | ||
| 119 | assert(sheet); | ||
| 120 | assert(row >= 0); | ||
| 121 | assert(row < sheet->num_rows); | ||
| 122 | return &sheet->rows[row]; | ||
| 123 | } | ||
| 124 | |||
| 125 | // ----------------------------------------------------------------------------- | ||
| 73 | // Renderer state. | 126 | // Renderer state. |
| 74 | // ----------------------------------------------------------------------------- | 127 | // ----------------------------------------------------------------------------- |
| 75 | 128 | ||
| @@ -79,34 +132,45 @@ typedef struct TileData { | |||
| 79 | uint16_t pixels_handle; // Handle to the tile's pixels in the pixel pool. | 132 | uint16_t pixels_handle; // Handle to the tile's pixels in the pixel pool. |
| 80 | } TileData; | 133 | } TileData; |
| 81 | 134 | ||
| 135 | // File format is already convenient for working in memory. | ||
| 136 | typedef Ss_Row SpriteSheetRow; | ||
| 137 | typedef Ss_SpriteSheet SpriteSheetData; | ||
| 138 | |||
| 139 | typedef struct SpriteData { | ||
| 140 | SpriteSheet sheet; // Handle to the sprite's sheet. | ||
| 141 | ivec2 position; | ||
| 142 | int animation; // Current animation. | ||
| 143 | int frame; // Current frame of animation. | ||
| 144 | } SpriteData; | ||
| 145 | |||
| 82 | DEF_MEMPOOL_DYN(TilePool, TileData) | 146 | DEF_MEMPOOL_DYN(TilePool, TileData) |
| 83 | DEF_MEM_DYN(PixelPool, Pixel) | 147 | DEF_MEM_DYN(PixelPool, Pixel) |
| 84 | 148 | ||
| 149 | DEF_MEMPOOL_DYN(SpritePool, SpriteData) | ||
| 150 | DEF_MEM_DYN(SpriteSheetPool, SpriteSheetData) | ||
| 151 | |||
| 85 | typedef struct IsoGfx { | 152 | typedef struct IsoGfx { |
| 86 | int screen_width; | 153 | int screen_width; |
| 87 | int screen_height; | 154 | int screen_height; |
| 88 | int tile_width; | 155 | int tile_width; |
| 89 | int tile_height; | 156 | int tile_height; |
| 90 | int world_width; | 157 | int world_width; |
| 91 | int world_height; | 158 | int world_height; |
| 92 | Tile* world; | 159 | int max_num_sprites; |
| 93 | Pixel* screen; | 160 | int sprite_sheet_pool_size_bytes; |
| 94 | TilePool tiles; | 161 | double last_animation_time; |
| 95 | PixelPool pixels; | 162 | Tile* world; |
| 163 | Pixel* screen; | ||
| 164 | TilePool tiles; | ||
| 165 | PixelPool pixels; | ||
| 166 | SpritePool sprites; | ||
| 167 | SpriteSheetPool sheets; | ||
| 96 | } IsoGfx; | 168 | } IsoGfx; |
| 97 | 169 | ||
| 98 | // ----------------------------------------------------------------------------- | 170 | // ----------------------------------------------------------------------------- |
| 99 | // Math and world / tile / screen access. | 171 | // Math and world / tile / screen access. |
| 100 | // ----------------------------------------------------------------------------- | 172 | // ----------------------------------------------------------------------------- |
| 101 | 173 | ||
| 102 | typedef struct ivec2 { | ||
| 103 | int x, y; | ||
| 104 | } ivec2; | ||
| 105 | |||
| 106 | typedef struct vec2 { | ||
| 107 | double x, y; | ||
| 108 | } vec2; | ||
| 109 | |||
| 110 | static inline ivec2 ivec2_add(ivec2 a, ivec2 b) { | 174 | static inline ivec2 ivec2_add(ivec2 a, ivec2 b) { |
| 111 | return (ivec2){.x = a.x + b.x, .y = a.y + b.y}; | 175 | return (ivec2){.x = a.x + b.x, .y = a.y + b.y}; |
| 112 | } | 176 | } |
| @@ -220,8 +284,15 @@ IsoGfx* isogfx_new(const IsoGfxDesc* desc) { | |||
| 220 | iso->screen_width = desc->screen_width; | 284 | iso->screen_width = desc->screen_width; |
| 221 | iso->screen_height = desc->screen_height; | 285 | iso->screen_height = desc->screen_height; |
| 222 | 286 | ||
| 223 | const int screen_size = desc->screen_width * desc->screen_height; | 287 | iso->last_animation_time = 0.0; |
| 288 | |||
| 289 | iso->max_num_sprites = desc->max_num_sprites == 0 ? DEFAULT_MAX_NUM_SPRITES | ||
| 290 | : desc->max_num_sprites; | ||
| 291 | iso->sprite_sheet_pool_size_bytes = desc->sprite_sheet_pool_size_bytes == 0 | ||
| 292 | ? DEFAULT_SPRITE_SHEET_POOL_SIZE_BYTES | ||
| 293 | : desc->sprite_sheet_pool_size_bytes; | ||
| 224 | 294 | ||
| 295 | const int screen_size = desc->screen_width * desc->screen_height; | ||
| 225 | if (!(iso->screen = calloc(screen_size, sizeof(Pixel)))) { | 296 | if (!(iso->screen = calloc(screen_size, sizeof(Pixel)))) { |
| 226 | goto cleanup; | 297 | goto cleanup; |
| 227 | } | 298 | } |
| @@ -233,7 +304,7 @@ cleanup: | |||
| 233 | return 0; | 304 | return 0; |
| 234 | } | 305 | } |
| 235 | 306 | ||
| 236 | /// Destroy the world and its tile set. | 307 | /// Destroy the world, its tile set, and the underlying pools. |
| 237 | static void destroy_world(IsoGfx* iso) { | 308 | static void destroy_world(IsoGfx* iso) { |
| 238 | assert(iso); | 309 | assert(iso); |
| 239 | if (iso->world) { | 310 | if (iso->world) { |
| @@ -244,11 +315,19 @@ static void destroy_world(IsoGfx* iso) { | |||
| 244 | mem_del(&iso->pixels); | 315 | mem_del(&iso->pixels); |
| 245 | } | 316 | } |
| 246 | 317 | ||
| 318 | /// Destroy all loaded sprites and the underlying pools. | ||
| 319 | static void destroy_sprites(IsoGfx* iso) { | ||
| 320 | assert(iso); | ||
| 321 | mempool_del(&iso->sprites); | ||
| 322 | mem_del(&iso->sheets); | ||
| 323 | } | ||
| 324 | |||
| 247 | void isogfx_del(IsoGfx** pIso) { | 325 | void isogfx_del(IsoGfx** pIso) { |
| 248 | assert(pIso); | 326 | assert(pIso); |
| 249 | IsoGfx* iso = *pIso; | 327 | IsoGfx* iso = *pIso; |
| 250 | if (iso) { | 328 | if (iso) { |
| 251 | destroy_world(iso); | 329 | destroy_world(iso); |
| 330 | destroy_sprites(iso); | ||
| 252 | if (iso->screen) { | 331 | if (iso->screen) { |
| 253 | free(iso->screen); | 332 | free(iso->screen); |
| 254 | iso->screen = 0; | 333 | iso->screen = 0; |
| @@ -341,7 +420,7 @@ bool isogfx_load_world(IsoGfx* iso, const char* filepath) { | |||
| 341 | // Tile set path is relative to the tile map file. Make it relative to the | 420 | // Tile set path is relative to the tile map file. Make it relative to the |
| 342 | // current working directory before loading. | 421 | // current working directory before loading. |
| 343 | char ts_path_cwd[PATH_MAX] = {0}; | 422 | char ts_path_cwd[PATH_MAX] = {0}; |
| 344 | if (!make_relative_path(MAX_PATH_LENGTH, filepath, ts_path, ts_path_cwd)) { | 423 | if (!make_relative_path(filepath, ts_path, ts_path_cwd, PATH_MAX)) { |
| 345 | goto cleanup; | 424 | goto cleanup; |
| 346 | } | 425 | } |
| 347 | 426 | ||
| @@ -498,36 +577,199 @@ void isogfx_set_tiles(IsoGfx* iso, int x0, int y0, int x1, int y1, Tile tile) { | |||
| 498 | } | 577 | } |
| 499 | } | 578 | } |
| 500 | 579 | ||
| 580 | bool isogfx_load_sprite_sheet( | ||
| 581 | IsoGfx* iso, const char* filepath, SpriteSheet* p_sheet) { | ||
| 582 | assert(iso); | ||
| 583 | assert(filepath); | ||
| 584 | assert(p_sheet); | ||
| 585 | |||
| 586 | bool success = false; | ||
| 587 | |||
| 588 | // Lazy initialization of sprite pools. | ||
| 589 | if (mempool_capacity(&iso->sprites) == 0) { | ||
| 590 | if (!mempool_make_dyn( | ||
| 591 | &iso->sprites, iso->max_num_sprites, sizeof(SpriteData))) { | ||
| 592 | return false; | ||
| 593 | } | ||
| 594 | } | ||
| 595 | if (mem_capacity(&iso->sheets) == 0) { | ||
| 596 | // Using a block size of 1 byte for sprite sheet data. | ||
| 597 | if (!mem_make_dyn(&iso->sheets, iso->sprite_sheet_pool_size_bytes, 1)) { | ||
| 598 | return false; | ||
| 599 | } | ||
| 600 | } | ||
| 601 | |||
| 602 | // Load sprite sheet file. | ||
| 603 | printf("Load sprite sheet: %s\n", filepath); | ||
| 604 | FILE* file = fopen(filepath, "rb"); | ||
| 605 | if (file == NULL) { | ||
| 606 | goto cleanup; | ||
| 607 | } | ||
| 608 | const size_t sheet_size = get_file_size(file); | ||
| 609 | SpriteSheetData* ss_sheet = mem_alloc(&iso->sheets, sheet_size); | ||
| 610 | if (!ss_sheet) { | ||
| 611 | goto cleanup; | ||
| 612 | } | ||
| 613 | if (fread(ss_sheet, sheet_size, 1, file) != 1) { | ||
| 614 | goto cleanup; | ||
| 615 | } | ||
| 616 | |||
| 617 | *p_sheet = mem_get_chunk_handle(&iso->sheets, ss_sheet); | ||
| 618 | success = true; | ||
| 619 | |||
| 620 | cleanup: | ||
| 621 | // Pools remain initialized since client may attempt to load other sprites. | ||
| 622 | if (file != NULL) { | ||
| 623 | fclose(file); | ||
| 624 | } | ||
| 625 | if (!success) { | ||
| 626 | if (ss_sheet) { | ||
| 627 | mem_free(&iso->sheets, &ss_sheet); | ||
| 628 | } | ||
| 629 | } | ||
| 630 | return success; | ||
| 631 | } | ||
| 632 | |||
| 633 | Sprite isogfx_make_sprite(IsoGfx* iso, SpriteSheet sheet) { | ||
| 634 | assert(iso); | ||
| 635 | |||
| 636 | SpriteData* sprite = mempool_alloc(&iso->sprites); | ||
| 637 | assert(sprite); | ||
| 638 | |||
| 639 | sprite->sheet = sheet; | ||
| 640 | |||
| 641 | return mempool_get_block_index(&iso->sprites, sprite); | ||
| 642 | } | ||
| 643 | |||
| 644 | #define with_sprite(SPRITE, BODY) \ | ||
| 645 | { \ | ||
| 646 | SpriteData* data = mempool_get_block(&iso->sprites, sprite); \ | ||
| 647 | assert(data); \ | ||
| 648 | BODY; \ | ||
| 649 | } | ||
| 650 | |||
| 651 | void isogfx_set_sprite_position(IsoGfx* iso, Sprite sprite, int x, int y) { | ||
| 652 | assert(iso); | ||
| 653 | with_sprite(sprite, { | ||
| 654 | data->position.x = x; | ||
| 655 | data->position.y = y; | ||
| 656 | }); | ||
| 657 | } | ||
| 658 | |||
| 659 | void isogfx_set_sprite_animation(IsoGfx* iso, Sprite sprite, int animation) { | ||
| 660 | assert(iso); | ||
| 661 | with_sprite(sprite, { data->animation = animation; }); | ||
| 662 | } | ||
| 663 | |||
| 664 | void isogfx_update(IsoGfx* iso, double t) { | ||
| 665 | assert(iso); | ||
| 666 | |||
| 667 | // If this is the first time update() is called after initialization, just | ||
| 668 | // record the starting animation time. | ||
| 669 | if (iso->last_animation_time == 0.0) { | ||
| 670 | iso->last_animation_time = t; | ||
| 671 | return; | ||
| 672 | } | ||
| 673 | |||
| 674 | if ((t - iso->last_animation_time) >= ANIMATION_UPDATE_DELTA) { | ||
| 675 | // TODO: Consider linking animated sprites in a list so that we only walk | ||
| 676 | // over those here and not also the static sprites. | ||
| 677 | mempool_foreach(&iso->sprites, sprite, { | ||
| 678 | const SpriteSheetData* sheet = mem_get_chunk(&iso->sheets, sprite->sheet); | ||
| 679 | assert(sheet); // TODO: Make this a hard assert inside the mem/pool. | ||
| 680 | const SpriteSheetRow* row = | ||
| 681 | get_sprite_sheet_row(sheet, sprite->animation); | ||
| 682 | sprite->frame = (sprite->frame + 1) % row->num_cols; | ||
| 683 | }); | ||
| 684 | |||
| 685 | iso->last_animation_time = t; | ||
| 686 | } | ||
| 687 | } | ||
| 688 | |||
| 501 | // ----------------------------------------------------------------------------- | 689 | // ----------------------------------------------------------------------------- |
| 502 | // Rendering and picking. | 690 | // Rendering and picking. |
| 503 | // ----------------------------------------------------------------------------- | 691 | // ----------------------------------------------------------------------------- |
| 504 | 692 | ||
| 505 | static void draw_tile(IsoGfx* iso, ivec2 origin, Tile tile) { | 693 | typedef struct CoordSystem { |
| 694 | ivec2 o; /// Origin. | ||
| 695 | ivec2 x; | ||
| 696 | ivec2 y; | ||
| 697 | } CoordSystem; | ||
| 698 | |||
| 699 | /// Create the basis for the isometric coordinate system with origin and vectors | ||
| 700 | /// expressed in the Cartesian system. | ||
| 701 | static CoordSystem make_iso_coord_system(const IsoGfx* iso) { | ||
| 506 | assert(iso); | 702 | assert(iso); |
| 703 | // const ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0}; | ||
| 704 | const ivec2 o = { | ||
| 705 | (iso->screen_width / 2) - (iso->tile_width / 2), iso->tile_height}; | ||
| 706 | const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2}; | ||
| 707 | const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2}; | ||
| 708 | return (CoordSystem){o, x, y}; | ||
| 709 | } | ||
| 507 | 710 | ||
| 508 | const TileData* tile_data = mempool_get_block(&iso->tiles, tile); | 711 | static Pixel alpha_blend(Pixel src, Pixel dst) { |
| 509 | assert(tile_data); | 712 | if ((src.a == 255) || (dst.a == 0)) { |
| 713 | return src; | ||
| 714 | } | ||
| 715 | const uint16_t one_minus_alpha = 255 - src.a; | ||
| 716 | #define blend(s, d) \ | ||
| 717 | (Channel)( \ | ||
| 718 | (double)((uint16_t)s * (uint16_t)src.a + \ | ||
| 719 | (uint16_t)d * one_minus_alpha) / \ | ||
| 720 | 255.0) | ||
| 721 | return (Pixel){ | ||
| 722 | .r = blend(src.r, dst.r), | ||
| 723 | .g = blend(src.g, dst.g), | ||
| 724 | .b = blend(src.b, dst.b), | ||
| 725 | .a = src.a}; | ||
| 726 | } | ||
| 727 | |||
| 728 | /// Draw a rectangle (tile or sprite). | ||
| 729 | /// | ||
| 730 | /// The rectangle's bottom-left corner is mapped to the given origin. The | ||
| 731 | /// rectangle then extends to the right and top of the origin. | ||
| 732 | /// | ||
| 733 | /// The rectangle's pixels are assumed to be arranged in a linear, row-major | ||
| 734 | /// fashion. | ||
| 735 | static void draw_rect( | ||
| 736 | IsoGfx* iso, ivec2 origin, int rect_width, int rect_height, | ||
| 737 | const Pixel* pixels) { | ||
| 738 | assert(iso); | ||
| 510 | 739 | ||
| 511 | // Tile can exceed screen bounds, so we must clip it. | 740 | // Rect can exceed screen bounds, so we must clip it. |
| 512 | #define max(a, b) (a > b ? a : b) | 741 | #define max(a, b) (a > b ? a : b) |
| 513 | const int py_offset = max(0, (int)tile_data->height - origin.y); | 742 | const int py_offset = max(0, rect_height - origin.y); |
| 514 | origin.y = max(0, origin.y - (int)tile_data->height); | 743 | origin.y = max(0, origin.y - rect_height); |
| 515 | 744 | ||
| 516 | // Clip along Y and X as we draw. | 745 | // Clip along Y and X as we draw. |
| 517 | for (int py = py_offset; | 746 | for (int py = py_offset; |
| 518 | (py < tile_data->height) && (origin.y + py < iso->screen_height); ++py) { | 747 | (py < rect_height) && (origin.y + py < iso->screen_height); ++py) { |
| 519 | const int sy = origin.y + py - py_offset; | 748 | const int sy = origin.y + py - py_offset; |
| 520 | for (int px = 0; | 749 | for (int px = 0; (px < rect_width) && (origin.x + px < iso->screen_width); |
| 521 | (px < tile_data->width) && (origin.x + px < iso->screen_width); ++px) { | 750 | ++px) { |
| 522 | const Pixel colour = tile_xy(iso, tile_data, px, py); | 751 | const Pixel colour = pixels[py * rect_width + px]; |
| 523 | if (colour.a > 0) { | 752 | if (colour.a > 0) { |
| 524 | const int sx = origin.x + px; | 753 | const int sx = origin.x + px; |
| 525 | *screen_xy_mut(iso, sx, sy) = colour; | 754 | const Pixel dst = screen_xy(iso, sx, sy); |
| 755 | const Pixel final = alpha_blend(colour, dst); | ||
| 756 | *screen_xy_mut(iso, sx, sy) = final; | ||
| 526 | } | 757 | } |
| 527 | } | 758 | } |
| 528 | } | 759 | } |
| 529 | } | 760 | } |
| 530 | 761 | ||
| 762 | static void draw_tile(IsoGfx* iso, ivec2 origin, Tile tile) { | ||
| 763 | assert(iso); | ||
| 764 | |||
| 765 | const TileData* tile_data = mempool_get_block(&iso->tiles, tile); | ||
| 766 | assert(tile_data); | ||
| 767 | |||
| 768 | const Pixel* pixels = tile_xy_const_ref(iso, tile_data, 0, 0); | ||
| 769 | |||
| 770 | draw_rect(iso, origin, tile_data->width, tile_data->height, pixels); | ||
| 771 | } | ||
| 772 | |||
| 531 | static void draw(IsoGfx* iso) { | 773 | static void draw(IsoGfx* iso) { |
| 532 | assert(iso); | 774 | assert(iso); |
| 533 | 775 | ||
| @@ -536,11 +778,7 @@ static void draw(IsoGfx* iso) { | |||
| 536 | 778 | ||
| 537 | memset(iso->screen, 0, W * H * sizeof(Pixel)); | 779 | memset(iso->screen, 0, W * H * sizeof(Pixel)); |
| 538 | 780 | ||
| 539 | // const ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0}; | 781 | const CoordSystem iso_space = make_iso_coord_system(iso); |
| 540 | const ivec2 o = { | ||
| 541 | (iso->screen_width / 2) - (iso->tile_width / 2), iso->tile_height}; | ||
| 542 | const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2}; | ||
| 543 | const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2}; | ||
| 544 | 782 | ||
| 545 | // TODO: Culling. | 783 | // TODO: Culling. |
| 546 | // Ex: map the screen corners to tile space to cull. | 784 | // Ex: map the screen corners to tile space to cull. |
| @@ -550,16 +788,58 @@ static void draw(IsoGfx* iso) { | |||
| 550 | for (int ty = 0; ty < iso->world_height; ++ty) { | 788 | for (int ty = 0; ty < iso->world_height; ++ty) { |
| 551 | for (int tx = 0; tx < iso->world_width; ++tx) { | 789 | for (int tx = 0; tx < iso->world_width; ++tx) { |
| 552 | const Tile tile = world_xy(iso, tx, ty); | 790 | const Tile tile = world_xy(iso, tx, ty); |
| 553 | const ivec2 so = | 791 | const ivec2 so = ivec2_add( |
| 554 | ivec2_add(o, ivec2_add(ivec2_scale(x, tx), ivec2_scale(y, ty))); | 792 | iso_space.o, |
| 793 | ivec2_add( | ||
| 794 | ivec2_scale(iso_space.x, tx), ivec2_scale(iso_space.y, ty))); | ||
| 555 | draw_tile(iso, so, tile); | 795 | draw_tile(iso, so, tile); |
| 556 | } | 796 | } |
| 557 | } | 797 | } |
| 558 | } | 798 | } |
| 559 | 799 | ||
| 800 | static void draw_sprite( | ||
| 801 | IsoGfx* iso, ivec2 origin, const SpriteData* sprite, | ||
| 802 | const SpriteSheetData* sheet) { | ||
| 803 | assert(iso); | ||
| 804 | assert(sprite); | ||
| 805 | assert(sheet); | ||
| 806 | assert(sprite->animation >= 0); | ||
| 807 | assert(sprite->animation < sheet->num_rows); | ||
| 808 | assert(sprite->frame >= 0); | ||
| 809 | |||
| 810 | const SpriteSheetRow* ss_row = &sheet->rows[sprite->animation]; | ||
| 811 | assert(sprite->frame < ss_row->num_cols); | ||
| 812 | |||
| 813 | const int sprite_offset = | ||
| 814 | sprite->frame * sheet->sprite_width * sheet->sprite_height; | ||
| 815 | |||
| 816 | const Pixel* frame = &ss_row->pixels[sprite_offset]; | ||
| 817 | |||
| 818 | draw_rect(iso, origin, sheet->sprite_width, sheet->sprite_height, frame); | ||
| 819 | } | ||
| 820 | |||
| 821 | static void draw_sprites(IsoGfx* iso) { | ||
| 822 | assert(iso); | ||
| 823 | |||
| 824 | const CoordSystem iso_space = make_iso_coord_system(iso); | ||
| 825 | |||
| 826 | mempool_foreach(&iso->sprites, sprite, { | ||
| 827 | const SpriteSheetData* sheet = mem_get_chunk(&iso->sheets, sprite->sheet); | ||
| 828 | assert(sheet); | ||
| 829 | |||
| 830 | const ivec2 so = ivec2_add( | ||
| 831 | iso_space.o, ivec2_add( | ||
| 832 | ivec2_scale(iso_space.x, sprite->position.x), | ||
| 833 | ivec2_scale(iso_space.y, sprite->position.y))); | ||
| 834 | |||
| 835 | draw_sprite(iso, so, sprite, sheet); | ||
| 836 | }); | ||
| 837 | } | ||
| 838 | |||
| 560 | void isogfx_render(IsoGfx* iso) { | 839 | void isogfx_render(IsoGfx* iso) { |
| 561 | assert(iso); | 840 | assert(iso); |
| 562 | draw(iso); | 841 | draw(iso); |
| 842 | draw_sprites(iso); | ||
| 563 | } | 843 | } |
| 564 | 844 | ||
| 565 | void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) { | 845 | void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) { |
