joeylib2/examples/scroll/scroll.c

344 lines
13 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// Scroll demo: a tile layer (jlTileLayer*) over a landscape three screens
// wide and two tall, with a ball drawn over it every frame -- no
// save-under, since the next layer draw covers it. The arrow keys or a
// joystick move the view; left alone it drifts. The two tile rows under
// the layer are a status bar whose bar grows with the frames drawn a
// second, so the demo shows how fast each port scrolls. Escape ends it.
//
// The tiles are painted with the drawing calls and snapped, since a
// jlTileT is in each port's own format; the map is generated.
#include <stdio.h>
#include <joey/joey.h>
#define MAP_W 96
#define MAP_H 40
#define VIEW_ROWS 23 // tile rows of the layer
#define BAR_TOP (VIEW_ROWS * JL_TILE_PIXELS_PER_SIDE)
#define BAR_H (JL_SURFACE_HEIGHT - BAR_TOP)
#define BAR_PIXELS_PER_FPS 5 // the bar's growth for each frame a second
#define BAR_INSET 4
#define TILE_SKY 0
#define TILE_GRASS 1
#define TILE_DIRT 2
#define TILE_BRICK 3
#define TILE_WATER 4
#define TILE_CLOUD 5
#define TILE_STONE 6
#define TILE_FLOWER 7
#define TILE_COUNT 8
#define COLOR_SKY 1
#define COLOR_DIRT 2
#define COLOR_GRASS 3
#define COLOR_DIRT_DARK 4
#define COLOR_BRICK 5
#define COLOR_GREY 6
#define COLOR_WATER 7
#define COLOR_FOAM 8
#define COLOR_CLOUD 9
#define COLOR_GREY_DARK 10
#define COLOR_PETAL 11
#define COLOR_BALL 12
#define COLOR_SHINE 13
#define COLOR_BAR_BACK 14
#define COLOR_BAR 15
#define GROUND_LOW 34 // the ground's highest and lowest rows
#define GROUND_HIGH 22
#define WATER_LINE 31 // ground below this row floods
#define STONE_DEPTH 4 // dirt rows above the stone
#define LEDGES 30 // brick ledges and clouds, turn about
#define LEDGE_MIN 2 // their length in tiles
#define LEDGE_MAX 6
#define LEDGE_TOP 4 // their highest row
#define LEDGE_GAP 4 // rows kept clear above the highest ground
#define BALL_TILES 2
#define BALL_SIDE (BALL_TILES * JL_TILE_PIXELS_PER_SIDE)
#define BALL_X ((JL_SURFACE_WIDTH - BALL_SIDE) / 2)
#define BALL_Y ((BAR_TOP - BALL_SIDE) / 2)
#define SPEED 90 // pixels per second, the same on every port
#define DRIFT_X 40
#define DRIFT_Y 12
#define STICK_THRESHOLD 32
#define SUBPIXEL_SHIFT 8 // positions are kept in 1/256 of a pixel
#define MAX_STEP_MILLIS 100 // after a stall, move no further than this
#define MILLIS_PER_SECOND 1000
#define MAP_PX_W ((int32_t)MAP_W * JL_TILE_PIXELS_PER_SIDE << SUBPIXEL_SHIFT)
#define MAP_PX_H ((int32_t)MAP_H * JL_TILE_PIXELS_PER_SIDE << SUBPIXEL_SHIFT)
#define CODEGEN_BYTES (32UL * 1024)
static const uint16_t gPalette[16] = {
0x000, 0x6AF, 0x852, 0x3B3, 0x531, 0xB33, 0x999, 0x24C,
0x8CF, 0xFFF, 0x555, 0xFC2, 0xFD0, 0xFFF, 0x113, 0x4E4
};
// The ball, 16x16 at 4 bits a pixel, high nibble left; colour 0 is clear.
static const uint8_t gBallRows[BALL_SIDE * BALL_SIDE / 2] = {
0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x00,
0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x00,
0x0C, 0xCC, 0xDC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0,
0x0C, 0xCD, 0xDC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0,
0xCC, 0xDD, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC,
0x0C, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0,
0x0C, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xC0,
0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x00,
0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x00, 0x00,
0x00, 0x00, 0x00, 0xCC, 0xCC, 0x00, 0x00, 0x00
};
static jlTileT gTiles[TILE_COUNT];
static uint8_t gMap[MAP_W * MAP_H];
static uint8_t gBallTiles[BALL_TILES * BALL_TILES * JL_TILE_BYTES];
static uint32_t gSeed = 2026u;
static void drawBar(jlSurfaceT *screen, uint16_t framesPerSecond);
static void makeBall(void);
static void makeMap(void);
static void makeTiles(jlSurfaceT *screen);
static uint16_t nextRandom(uint16_t below);
static int32_t stickAxis(int8_t stick, bool minus, bool plus, int32_t drift);
static int32_t wrap(int32_t v, int32_t size);
// The status bar under the layer: its bar is BAR_PIXELS_PER_FPS a frame a second.
static void drawBar(jlSurfaceT *screen, uint16_t framesPerSecond) {
uint16_t w;
w = (uint16_t)(framesPerSecond * BAR_PIXELS_PER_FPS);
if (w > JL_SURFACE_WIDTH - 2 * BAR_INSET) {
w = JL_SURFACE_WIDTH - 2 * BAR_INSET;
}
jlFillRect(screen, 0, BAR_TOP, JL_SURFACE_WIDTH, BAR_H, COLOR_BAR_BACK);
if (w > 0) {
jlFillRect(screen, BAR_INSET, BAR_TOP + BAR_INSET, w, BAR_H - 2 * BAR_INSET, COLOR_BAR);
}
}
// The ball's rows, repacked into the tile-major layout jlSpriteCreate takes.
static void makeBall(void) {
uint16_t tx;
uint16_t ty;
uint16_t row;
uint16_t b;
for (ty = 0; ty < BALL_TILES; ty++) {
for (tx = 0; tx < BALL_TILES; tx++) {
for (row = 0; row < JL_TILE_PIXELS_PER_SIDE; row++) {
for (b = 0; b < JL_TILE_BYTES_PER_ROW; b++) {
gBallTiles[(ty * BALL_TILES + tx) * JL_TILE_BYTES + row * JL_TILE_BYTES_PER_ROW + b] = gBallRows[(ty * JL_TILE_PIXELS_PER_SIDE + row) * (BALL_SIDE / 2) + tx * JL_TILE_BYTES_PER_ROW + b];
}
}
}
}
}
// Rolling ground with flooded hollows and flowers, and brick ledges and
// clouds above it.
static void makeMap(void) {
uint16_t tx;
uint16_t ty;
uint16_t ground;
uint16_t len;
uint16_t i;
uint16_t k;
uint8_t t;
ground = GROUND_HIGH + 4;
for (tx = 0; tx < MAP_W; tx++) {
if (nextRandom(3) == 0 && ground < GROUND_LOW) {
ground++;
} else if (nextRandom(3) == 0 && ground > GROUND_HIGH) {
ground--;
}
for (ty = 0; ty < MAP_H; ty++) {
if (ty < ground) {
t = (ty >= WATER_LINE) ? TILE_WATER : TILE_SKY;
} else if (ty == ground) {
t = (ty >= WATER_LINE) ? TILE_STONE : TILE_GRASS;
} else if (ty < ground + STONE_DEPTH) {
t = TILE_DIRT;
} else {
t = TILE_STONE;
}
gMap[ty * MAP_W + tx] = t;
}
if (ground < WATER_LINE && nextRandom(5) == 0) {
gMap[(ground - 1) * MAP_W + tx] = TILE_FLOWER;
}
}
for (i = 0; i < LEDGES; i++) {
tx = nextRandom(MAP_W - LEDGE_MAX);
ty = (uint16_t)(LEDGE_TOP + nextRandom(GROUND_HIGH - LEDGE_TOP - LEDGE_GAP));
len = (uint16_t)(LEDGE_MIN + nextRandom(LEDGE_MAX - LEDGE_MIN));
for (k = 0; k < len; k++) {
gMap[ty * MAP_W + tx + k] = ((i & 1u) != 0u) ? TILE_BRICK : TILE_CLOUD;
}
}
}
// Paints the eight tiles in block row 0 of the screen and snaps them.
static void makeTiles(jlSurfaceT *screen) {
jlFillRect(screen, 0, 0, 8, 8, COLOR_SKY);
jlFillRect(screen, 8, 0, 8, 8, COLOR_DIRT);
jlFillRect(screen, 8, 0, 8, 3, COLOR_GRASS);
jlDrawPixel(screen, 10, 3, COLOR_GRASS);
jlDrawPixel(screen, 13, 3, COLOR_GRASS);
jlFillRect(screen, 16, 0, 8, 8, COLOR_DIRT);
jlDrawPixel(screen, 17, 2, COLOR_DIRT_DARK);
jlDrawPixel(screen, 21, 5, COLOR_DIRT_DARK);
jlFillRect(screen, 24, 0, 8, 8, COLOR_BRICK);
jlDrawLine(screen, 24, 3, 31, 3, COLOR_GREY);
jlDrawLine(screen, 24, 7, 31, 7, COLOR_GREY);
jlDrawLine(screen, 24, 0, 24, 2, COLOR_GREY);
jlDrawLine(screen, 28, 4, 28, 6, COLOR_GREY);
jlFillRect(screen, 32, 0, 8, 8, COLOR_WATER);
jlDrawLine(screen, 32, 1, 34, 1, COLOR_FOAM);
jlDrawLine(screen, 36, 5, 38, 5, COLOR_FOAM);
jlFillRect(screen, 40, 0, 8, 8, COLOR_SKY);
jlFillRect(screen, 41, 3, 6, 3, COLOR_CLOUD);
jlFillRect(screen, 43, 2, 3, 1, COLOR_CLOUD);
jlFillRect(screen, 48, 0, 8, 8, COLOR_GREY);
jlDrawPixel(screen, 50, 2, COLOR_GREY_DARK);
jlDrawPixel(screen, 53, 5, COLOR_GREY_DARK);
jlDrawPixel(screen, 49, 6, COLOR_GREY_DARK);
jlFillRect(screen, 56, 0, 8, 8, COLOR_SKY);
jlDrawLine(screen, 59, 3, 59, 7, COLOR_GRASS);
jlFillRect(screen, 58, 1, 3, 2, COLOR_PETAL);
jlTileSnap(screen, TILE_SKY, 0, &gTiles[TILE_SKY]);
jlTileSnap(screen, TILE_GRASS, 0, &gTiles[TILE_GRASS]);
jlTileSnap(screen, TILE_DIRT, 0, &gTiles[TILE_DIRT]);
jlTileSnap(screen, TILE_BRICK, 0, &gTiles[TILE_BRICK]);
jlTileSnap(screen, TILE_WATER, 0, &gTiles[TILE_WATER]);
jlTileSnap(screen, TILE_CLOUD, 0, &gTiles[TILE_CLOUD]);
jlTileSnap(screen, TILE_STONE, 0, &gTiles[TILE_STONE]);
jlTileSnap(screen, TILE_FLOWER, 0, &gTiles[TILE_FLOWER]);
}
// A number in [0, below), from a fixed sequence so every port builds the
// same landscape.
static uint16_t nextRandom(uint16_t below) {
gSeed = gSeed * 1103515245u + 12345u;
return (uint16_t)((gSeed >> 16) % below);
}
// The view's speed along one axis, pixels per second: the stick or the two
// keys, else the drift.
static int32_t stickAxis(int8_t stick, bool minus, bool plus, int32_t drift) {
if (minus || stick < -STICK_THRESHOLD) {
return -SPEED;
}
if (plus || stick > STICK_THRESHOLD) {
return SPEED;
}
return drift;
}
// v moved into [0, size).
static int32_t wrap(int32_t v, int32_t size) {
while (v < 0) {
v += size;
}
while (v >= size) {
v -= size;
}
return v;
}
int main(void) {
jlConfigT config;
jlSurfaceT *screen;
jlSpriteT *ball;
jlTileLayerT *layer;
int32_t x; // the view's origin, 1/256 of a pixel
int32_t y;
int32_t vx; // pixels per second
int32_t vy;
uint32_t lastMillis;
uint32_t barMillis;
uint32_t now;
uint32_t elapsed;
uint16_t frames;
config.codegenBytes = CODEGEN_BYTES;
config.noAudio = true;
if (!jlInit(&config)) {
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
return 1;
}
screen = jlStageGet();
jlPaletteSet(screen, 0, gPalette);
jlScbSetRange(screen, 0, JL_SURFACE_HEIGHT - 1, 0);
makeTiles(screen);
makeMap();
makeBall();
ball = jlSpriteCreate(gBallTiles, BALL_TILES, BALL_TILES);
layer = jlTileLayerCreate(gTiles, gMap, MAP_W, MAP_H, 0, VIEW_ROWS);
if (ball == NULL || layer == NULL) {
fprintf(stderr, "out of memory\n");
jlShutdown();
return 1;
}
(void)jlSpriteCompile(ball);
jlSurfaceClear(screen, 0);
drawBar(screen, 0);
x = 0;
y = (int32_t)(GROUND_HIGH - VIEW_ROWS / 2) * JL_TILE_PIXELS_PER_SIDE << SUBPIXEL_SHIFT;
frames = 0;
lastMillis = jlMillisElapsed();
barMillis = lastMillis;
for (;;) {
jlInputPoll();
if (jlKeyPressed(JL_KEY_ESCAPE)) {
break;
}
vx = stickAxis(jlJoystickX(JL_JOYSTICK_1), jlKeyDown(JL_KEY_LEFT), jlKeyDown(JL_KEY_RIGHT), DRIFT_X);
vy = stickAxis(jlJoystickY(JL_JOYSTICK_1), jlKeyDown(JL_KEY_UP), jlKeyDown(JL_KEY_DOWN), DRIFT_Y);
now = jlMillisElapsed();
elapsed = now - lastMillis;
lastMillis = now;
if (elapsed > MAX_STEP_MILLIS) {
elapsed = MAX_STEP_MILLIS;
}
x = wrap(x + (vx * (int32_t)elapsed << SUBPIXEL_SHIFT) / MILLIS_PER_SECOND, MAP_PX_W);
y = wrap(y + (vy * (int32_t)elapsed << SUBPIXEL_SHIFT) / MILLIS_PER_SECOND, MAP_PX_H);
jlTileLayerDraw(screen, layer, (uint16_t)(x >> SUBPIXEL_SHIFT), (uint16_t)(y >> SUBPIXEL_SHIFT));
jlSpriteDraw(screen, ball, BALL_X, BALL_Y);
frames++;
if (now - barMillis >= MILLIS_PER_SECOND) {
drawBar(screen, (uint16_t)((uint32_t)frames * MILLIS_PER_SECOND / (now - barMillis)));
frames = 0;
barMillis = now;
}
jlStagePresent();
}
jlTileLayerDestroy(layer);
jlSpriteDestroy(ball);
jlShutdown();
return 0;
}