Sprite scenes

#include "libtini/scene.h"

A scene is a band renderer for games that move many small sprites on a plain background. In each frame, the game makes a list of its sprites. ti_scene_draw then draws and publishes only the parts of one screen rectangle that changed since the last draw.

NamePurpose
TI_SCENE_STORAGEDefine the sprite list and the composition strip, one time in a program
ti_scene_beginStart a scene on a screen rectangle
ti_scene_frameStart the sprite list of the next frame
ti_scene_addAdd a sprite to the list, in draw order
ti_scene_drawCompose and publish all changed blocks
ti_scene_forgetTell the scene that the area shows only the background
ti_scene_invalidateDraw the full area at the next draw

How a scene draws

The scene divides its rectangle into blocks of 16 pixels (8 bytes) by 8 lines. ti_scene_add compares each sprite with the sprite at the same position in the last list. An identical sprite costs no time. A sprite can move, change its image or colour, or go away. Then it marks the blocks of its old position and the blocks of its new position.

ti_scene_draw examines each 8-line band that has marked blocks. It fills these blocks of an 8-line near strip with the background colour. It draws all listed sprites that touch the band, in list order. Then it publishes each run of adjacent marked blocks with usual CPU writes. A list without changes publishes nothing. The scene uses no banked surface. A draw needs an active video session.

The scene has no image of the other contents of the screen. It operates on the condition that its rectangle shows only the background and its own sprites. When other code draws in the rectangle, tell the scene with ti_scene_forget or ti_scene_invalidate.

Types and storage

ti_image

typedef struct ti_image { const uint8_t *pixels; uint8_t row_bytes, rows, stride; } ti_image;

A sprite image in near memory. It has packed 4-bit pixels, two in each byte, high nibble first. Colour 0 is transparent. row_bytes is the width in bytes, with two pixels in each byte. rows is the height. stride is the number of bytes from one row to the next. Thus an image can be a part of a larger sheet.

When pixels is NULL, the image is a solid rectangle of 2 * row_bytes by rows pixels. It has the colour that ti_scene_add gives. Its stride is not used.

ti_scene_config

typedef struct ti_scene_config {
    uint16_t x, y, width, height;  /* screen pixels: x and width even, height a multiple of 8 */
    const ti_image *images;        /* near, borrowed until the next begin */
    uint8_t image_count;
    uint8_t background;            /* colour 0..15 under every sprite */
    void (*service)(void);         /* called after every fourth band while drawing, or NULL */
} ti_scene_config;
x, y, width, height
The screen rectangle. x and width are even. height is a nonzero multiple of 8. The rectangle is fully in 320x200. width is a maximum of 2 * row_bytes of the storage.
images, image_count
The image table, 1-255 entries. Sprites identify images by their index.
background
The colour of the area under all sprites.
service
A function that the scene calls after bands 4, 8, 12 and the subsequent multiples of 4 in each draw. The scene calls it also when a band has no change. Use it to call ti_audio_update, thus the music continues during a long draw. The function must not call scene functions or draw in the area.

TI_SCENE_STORAGE

#define TI_SCENE_STORAGE(sprites, row_bytes) ...

This macro defines the sprite list and the composition strip as global variables. The renderer finds them at fixed addresses. Use the macro exactly one time in a program, at file scope.

sprites is the list capacity, 1-255. row_bytes is the width of the widest area in bytes. The macro uses 8 bytes for each sprite plus 8 * row_bytes for the strip, in BSS. To put this memory in the near arena, put the macro between #pragma bss-name (push, "NEARBSS") and #pragma bss-name (pop).

TI_SCENE_STORAGE(64, 120);   /* 64 sprites each frame, areas to 240 pixels wide:
                                 512 + 960 bytes */

Functions

ti_scene_begin

ti_error ti_scene_begin(const ti_scene_config *config);

This function starts a scene. It copies the configuration. The scene uses the image table and the pixels until the next ti_scene_begin.

The function checks each image one time. row_bytes and rows must not be zero. For an image with pixels, stride must be row_bytes or more, and (rows - 1) * stride + row_bytes near bytes must be readable.

The list starts empty. The scene operates on the condition that the area shows only the background. When the area shows other contents, call ti_scene_invalidate. The function does not need a video session. Only a draw needs a session.

Returns

TI_OK; TI_E_ARGUMENT for a missing configuration or image table, one that the function cannot read, or zero images; TI_E_RANGE for an incorrect area, a background above 15 or an incorrect image. After an error, no scene is active.

ti_scene_frame

void ti_scene_frame(void);

This function starts the list of the next frame. The scene keeps the last drawn list to compare. Thus the next draw erases the sprites that moved, changed or went away.

ti_scene_add

ti_error ti_scene_add(int16_t x, int16_t y, uint8_t image, uint8_t color);

This function adds a sprite. The top-left corner of the sprite is at the screen position x, y. Sprites that you add later cover the sprites before them. Positions are signed. The scene clips sprites to the area. The function rounds an odd x down, because sprites start on full bytes. Only rectangle images use color.

The scene compares sprites by their position in the list. Keep each object at the same list position from frame to frame. The scene does not see pixels that change in an image. Use a different image index, or call ti_scene_invalidate.

Returns

TI_OK; TI_E_FULL when the list is at its capacity (the function drops the sprite); TI_E_RANGE for an image index of image_count or more; TI_E_STATE when no ti_scene_begin occurred.

ti_scene_draw

ti_error ti_scene_draw(void);

This function erases the sprites past the end of the new list. It composes and publishes each band that has marked blocks. After each fourth band, it calls service. A draw does not use ti_gfx_budget or statistics. The list capacity and the area limit the work.

Returns

TI_OK, or TI_E_STATE without an active scene and video session. The function returns this error before it writes.

ti_scene_forget

void ti_scene_forget(void);

Call this function when other code drew in the area and left only the background there. An example is a canvas publication with a clear area at the scene position. The screen shows no sprite of the last list. The next draw draws all listed sprites and erases nothing.

ti_scene_invalidate

void ti_scene_invalidate(void);

Call this function when the area shows contents other than the background and the last list. The next draw draws the full area again, the background included.

Example

#include "libtini.h"

TI_SCENE_STORAGE(64, 120);                  /* 64 sprites, areas to 240 pixels wide */
extern const uint8_t ship[], sheet[];        /* packed pixels, colour 0 transparent */
extern int16_t ship_x, ship_y, shot_x[], shot_y[];   /* the state of the game */
extern uint8_t shots;

static void service(void) { ti_audio_update(); }

static const ti_image images[] = {
    {ship, 8, 16, 8},                         /* 16x16 */
    {sheet + 4, 2, 4, 16},                    /* 4x4 in a sheet of 32 pixels width */
    {0, 1, 2, 0},                             /* a 2x2 rectangle in the colour of the sprite */
};
static const ti_scene_config scene = {0, 16, 240, 184, images, 3, 0, service};

void play(void)
{
    uint8_t i;
    ti_scene_begin(&scene);
    for (;;) {
        /* ... update the game ... */
        ti_scene_frame();
        ti_scene_add(ship_x, ship_y, 0, 0);
        for (i = 0; i < shots; ++i) ti_scene_add(shot_x[i], shot_y[i], 2, 15);
        ti_scene_draw();
    }
}

Costs

The renderer adds 2,042 bytes of code and constants and 197 bytes of state to a program. The storage that TI_SCENE_STORAGE defines is in addition to this. The work increases with the number of marked blocks and with the number of sprites that touch marked bands.

STARFALL has a 240x184 playfield with a maximum of 112 sprites and its backdrop. In it, the scene draw uses approximately 15 ms for each frame at vtw33 and 17 ms with the turbo-f122 cost model. The sprite list uses 4.7 ms and 2.5 ms. These values are measured on the development emulator.