/** * @file sprite.h * @brief Spritesheets (one texture, many frames) and the animations cut out of them. * * The split is deliberate: an akgl_SpriteSheet owns the `SDL_Texture` and the * frame grid, and any number of akgl_Sprite animations point at the same sheet * and name the frame indices they use. Loading two sprites from the same image * loads the image once -- akgl_sprite_load_json looks the sheet up in * #AKGL_REGISTRY_SPRITESHEET by resolved path before creating one. * * Both are pool objects (akgl_heap_next_sprite, akgl_heap_next_spritesheet) and * both publish themselves in a registry under their name, which is how * akgl_character_sprite_add and the tilemap loader find them. Because loading a * sheet uploads a texture, the renderer has to exist first -- so * akgl_render_2d_init, or akgl_game_init, before any of this. */ #ifndef _AKGL_SPRITE_H_ #define _AKGL_SPRITE_H_ #include #include #include #define AKGL_SPRITE_MAX_FRAMES 16 #define AKGL_SPRITE_MAX_NAME_LENGTH 128 #define AKGL_SPRITE_MAX_REGISTRY_SIZE 1024 #define AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH 512 #define AKGL_MAX_HEAP_SPRITE (AKGL_MAX_HEAP_ACTOR * 16) #define AKGL_MAX_HEAP_SPRITESHEET AKGL_MAX_HEAP_SPRITE /** @brief Stores a loaded spritesheet texture and frame geometry. */ typedef struct { uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. Destroys the texture when it reaches 0. */ SDL_Texture *texture; /**< The whole sheet as one GPU texture. Owned by this struct. */ char name[AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH]; /**< Registry key: the resolved path the image was loaded from. */ uint16_t sprite_w; /**< Frame width. Vestigial: nothing in the library writes or reads it; the grid comes from akgl_Sprite::width. */ uint16_t sprite_h; /**< Frame height. Vestigial, same as sprite_w. */ } akgl_SpriteSheet; /** @brief Describes an animated sprite and its spritesheet reference. */ typedef struct { uint8_t refcount; /**< Pool bookkeeping; 0 means the slot is free. One reference per character that maps it. */ uint8_t frameids[AKGL_SPRITE_MAX_FRAMES]; /**< Frame numbers on the sheet, in playback order. Counted left to right, then wrapping to the next row. */ uint32_t frames; /**< How many entries of frameids are in use. */ uint32_t width; /**< Frame width in pixels; also the horizontal stride used to find a frame on the sheet. */ uint32_t height; /**< Frame height in pixels; the vertical stride once frames wrap to the next row. */ uint32_t speed; /**< Nanoseconds one frame is held. Read from JSON in milliseconds and scaled by #AKGL_TIME_ONEMS_NS. */ bool loop; /**< Restart from frame 0 when the last frame is reached, instead of holding it. */ bool loopReverse; /**< Play back down to frame 0 instead of jumping to it -- a ping-pong loop. */ char name[AKGL_SPRITE_MAX_NAME_LENGTH]; /**< Registry key, from the JSON `name` field. */ akgl_SpriteSheet *sheet; /**< The sheet these frames are cut from. Borrowed, not owned. */ } akgl_Sprite; // initializes a new sprite to use the given sheet and otherwise sets to zero /** * @brief Zero a pooled sprite, bind it to a sheet, and publish it in the sprite registry. * * Sets the name and the sheet and takes the first reference; everything else -- * frame list, geometry, speed, loop flags -- is left at zero for the caller, or * akgl_sprite_load_json, to fill in. The sheet is borrowed, so this does *not* * take a reference on it: releasing the sheet out from under a live sprite * leaves a dangling pointer. * * @param spr Pooled sprite to initialize, normally from akgl_heap_next_sprite. * Required. Any previous contents are discarded. * @param name Registry key, NUL-terminated. Required. Copied at a fixed * #AKGL_SPRITE_MAX_NAME_LENGTH bytes, so a shorter string reads * past its end -- pass a name from an akgl_String or another * buffer of at least that size. An existing entry with the same * name is silently replaced. * @param sheet The spritesheet this sprite's frames come from. Required. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p spr, @p name, or @p sheet is `NULL`. * @throws AKERR_KEY If the sprite cannot be written into #AKGL_REGISTRY_SPRITE * -- in practice, because akgl_registry_init has not run. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_initialize(akgl_Sprite *spr, char *name, akgl_SpriteSheet *sheet); // loads a given image file into a new spritesheet /** * @brief Load an image file as a texture and publish it as a spritesheet. * * Uploads @p filename through SDL_image using the global `akgl_renderer`, so the * renderer must already be initialized. The sheet is registered in * #AKGL_REGISTRY_SPRITESHEET under @p filename, which is the key * akgl_sprite_load_json looks it up by to avoid loading the same image twice. * * @param sheet Pooled spritesheet to initialize, normally from * akgl_heap_next_spritesheet. Required. Zeroed first, so a * texture it already held is leaked rather than destroyed. * @param sprite_w Frame width in pixels. Accepted and currently discarded -- * the frame grid actually used at draw time comes from the * akgl_Sprite's `width`/`height`, not from here. * @param sprite_h Frame height in pixels. Same caveat as @p sprite_w. * @param filename Path to the image, in any format SDL_image can decode. * Required. Doubles as the registry key, so callers should pass * an already-resolved path; two spellings of the same file are * two sheets. Truncated at * #AKGL_SPRITE_SHEET_MAX_FILENAME_LENGTH. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p sheet or @p filename is `NULL`. * @throws AKGL_ERR_SDL If the image cannot be loaded -- missing, unreadable, or * a format SDL_image was not built with. The message carries * `SDL_GetError()`. * @throws AKERR_KEY If the sheet cannot be written into * #AKGL_REGISTRY_SPRITESHEET. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_spritesheet_initialize(akgl_SpriteSheet *sheet, int sprite_w, int sprite_h, char *filename); /** * @brief Build a sprite -- and, if needed, its spritesheet -- from a JSON definition file. * * Reads `name`, `width`, `height`, `speed` (seconds, scaled to milliseconds), * `loop`, `loopReverse`, a `frames` array of frame indices, and a `spritesheet` * object holding `filename`, `frame_width`, and `frame_height`. The sheet's * `filename` is resolved *relative to the directory of @p filename*, so a sprite * definition can sit next to its image and move with it. * * If a spritesheet with that resolved path is already registered it is reused; * otherwise one is claimed from the pool and loaded. On failure both the sprite * and any sheet loaded for it are released again. * * @param filename Path to the JSON document. Required. Must be shorter than * #AKGL_MAX_STRING_LENGTH -- it is copied into a pooled string * so `dirname` can be taken without modifying the caller's * buffer. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p filename is `NULL`, or if the file cannot be * opened or does not parse. The message carries jansson's line number * and text. * @throws AKERR_OUTOFBOUNDS If @p filename is at least #AKGL_MAX_STRING_LENGTH * bytes long, or if the `frames` array is indexed past its end. * @throws AKERR_KEY If a required key is absent, or if the sprite or sheet * cannot be added to its registry. The message names the key. * @throws AKERR_TYPE If a key is present with the wrong JSON type -- `speed` as * a string, `frames` as an object. * @throws AKGL_ERR_SDL If the spritesheet image fails to load. * @throws AKGL_ERR_HEAP If the sprite, spritesheet, or string pool is exhausted. * * @note The `frames` array is not bounded against #AKGL_SPRITE_MAX_FRAMES. A * definition with more than 16 frames writes past `frameids` into the * rest of the struct. */ akerr_ErrorContext AKERR_NOIGNORE *akgl_sprite_load_json(char *filename); /** * @brief Work out where one animation frame sits on its spritesheet. * * Frames are numbered in reading order across the sheet: multiply the frame * number by the sprite width, then wrap whole rows off the right-hand edge of * the texture and step down by the sprite height for each. Callers use the * result as the source rectangle for `SDL_RenderTexture`. * * @param self The sprite whose sheet and frame geometry to use. Required, * along with its `sheet`. * @param srccoords Receives the frame's rectangle on the sheet, in pixels. * Required -- the return value is the error context. * @param frameid Index into the sprite's `frameids` array, not a frame number * on the sheet. Not bounds-checked against `frames` or * #AKGL_SPRITE_MAX_FRAMES; an out-of-range index reads a * neighbouring struct member and yields a nonsense rectangle * rather than an error. * @return `NULL` on success, otherwise an error context owned by the caller. * @throws AKERR_NULLPOINTER If @p self, @p srccoords, or `self->sheet` is * `NULL`. Note that `sheet->texture` is dereferenced without a check, * so a registered-but-unloaded sheet is a crash rather than an error. */ akerr_ErrorContext *akgl_spritesheet_coords_for_frame(akgl_Sprite *self, SDL_FRect *srccoords, uint8_t frameid); #endif //_AKGL_SPRITE_H_