diff options
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) { |