344 lines
13 KiB
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_GRAY 6
|
|
#define COLOR_WATER 7
|
|
#define COLOR_FOAM 8
|
|
#define COLOR_CLOUD 9
|
|
#define COLOR_GRAY_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; color 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_GRAY);
|
|
jlDrawLine(screen, 24, 7, 31, 7, COLOR_GRAY);
|
|
jlDrawLine(screen, 24, 0, 24, 2, COLOR_GRAY);
|
|
jlDrawLine(screen, 28, 4, 28, 6, COLOR_GRAY);
|
|
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_GRAY);
|
|
jlDrawPixel(screen, 50, 2, COLOR_GRAY_DARK);
|
|
jlDrawPixel(screen, 53, 5, COLOR_GRAY_DARK);
|
|
jlDrawPixel(screen, 49, 6, COLOR_GRAY_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;
|
|
}
|