/* * balls — демо спрайтового движка v2 (): N шаров 16×16 летают * над чёрно-белой шахматкой, отражаясь от краёв. Esc — выход. * * РАНЬШЕ (до движка) приложение вело drawn[2][N] координаты вручную и * само соблюдало дисциплину «heal ВСЕ → блит ВСЕ» на скрытой странице. * Теперь это работа движка: sprite_move меняет только состояние, * sprite_flip(sp, N) за кадр делает draw-page=скрытая → heal все старые → * блит все видимые (одна W3-скобка + один банк на проход) → vsync → флип. * Учёт drawn[2] спрятан в sprite_t. Профиль/дизайн: * docs/sprite-api-design.md §9.1. * * BOUNDARY-ТЕСТ клипа: диапазон движения выходит ЗА края экрана на * половину спрайта — шары штатно наполовину уходят за все 4 края, * включая ОТРИЦАТЕЛЬНЫЕ x/y слева/сверху. Клип _gfx_blit_clip (общий * для putsprite и движка) обязан авто-обрезать: правый/нижний — урезать * w/h, левый/верхний — сдвинуть начало источника (sx/sy) и урезать. * * Двойная буферизация: рисуем на скрытой странице, флип по vsync (иначе * heal+блит на видимой ловятся лучом — мерцание). Палитру страницы 1 * зеркалим из 0 (initgraph грузит EGA только в палитру 0). * * Скорость привязана к КАДРАМ (50 Гц): один проход цикла = один шаг. */ #include #include #include #include #include #define FPS_METER 1 /* 1 = счётчик кадров/с слева-сверху (диагностика) */ #define NBALLS 16 /* количество шаров */ #define BW 16 /* сторона спрайта */ #define CELL 16 /* клетка шахматки */ /* Диапазон отражения — на пол-спрайта ЗА каждым краем (boundary-тест): * x ∈ [-8 .. 312], y ∈ [-8 .. 248]. */ #define MINY (0) /* -8: половина за левым/верхним краем */ #define MINX (0) /* -8: половина за левым/верхним краем */ #define MAXX (320 - BW) /* 312: половина за правым краем */ #define MAXY (256 - BW) /* 248: половина за нижним краем */ /* Цвета шаров (не совпадают с фоном BLACK/WHITE) */ static const uint8_t ball_colors[] = { YELLOW, LIGHTRED, LIGHTGREEN, LIGHTCYAN, LIGHTMAGENTA, LIGHTBLUE, RED, GREEN }; #define NCOLORS ((uint8_t)sizeof(ball_colors)) static sprite_t sp[NBALLS]; /* спрайты — движок ведёт drawn[2] */ static int8_t vx[NBALLS], vy[NBALLS]; /* направление ±1 (физика — наша) */ static uint8_t spr[NCOLORS][4 + BW * BW]; /* по спрайту на цвет */ static uint8_t egapal[16 * 4]; static void build_sprites(void) { uint8_t c; int x, y; for (c = 0; c < NCOLORS; c++) { uint8_t *p = spr[c]; *p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0; for (y = 0; y < BW; y++) for (x = 0; x < BW; x++) { int dx = 2 * x - (BW - 1), dy = 2 * y - (BW - 1); /* ×2: центр между пикселями */ *p++ = (dx * dx + dy * dy <= 15 * 15) ? ball_colors[c] : GFX_TRANSPARENT; } } } static void draw_board(void) { int cx, cy; for (cy = 0; cy < 256; cy += CELL) for (cx = 0; cx < 320; cx += CELL) { setfillstyle(SOLID_FILL, (((cx ^ cy) >> 4) & 1) ? WHITE : BLACK); bar(cx, cy, cx + CELL - 1, cy + CELL - 1); } } static void init_balls(void) { uint8_t i; for (i = 0; i < NBALLS; i++) { sprite_init(&sp[i], spr[i % NCOLORS]); sp[i].x = (20 + i * 37) % (MAXX - 1) + 1; sp[i].y = (20 + i * 53) % (MAXY - 1) + 1; sprite_show(&sp[i]); vx[i] = (i & 1) ? 1 : -1; vy[i] = (i & 2) ? 1 : -1; } } #if FPS_METER /* Кадров за последнюю RTC-секунду — белым в чёрной плашке слева-сверху * на ТЕКУЩЕЙ draw-странице (рисуем ДО sprite_update, чтобы шары легли * поверх и heal учитывал плашку через ОЗУ-копию). */ static void show_fps(uint8_t v) { char buf[4]; buf[0] = (char)('0' + (v / 100) % 10); buf[1] = (char)('0' + (v / 10) % 10); buf[2] = (char)('0' + v % 10); buf[3] = 0; setcolor(WHITE); setfillstyle(SOLID_FILL, BLACK); bar(0, 0, 27, 9); outtextxy(2, 1, buf); } #endif int main(void) { uint8_t i, page, hidden; #if FPS_METER datetime_t dt; uint8_t frames = 0, last_sec; #endif build_sprites(); initgraph(); gfx_sprite_clip(0); /* Палитра страницы 1 = палитре страницы 0. */ gfx_pal_get(0, 0, 16, egapal); gfx_pal_load(1, 0, 16, egapal); init_balls(); /* Обе страницы: шахматка (в ОЗУ-копию — бэкап для heal) + шары в * стартовых позициях (sprite_update рисует на текущей draw-странице * и снимает её dirty-бит). */ for (page = 0; page < 2; page++) { gfx_set_draw_page(page); draw_board(); sprite_update(sp, NBALLS); } gfx_set_visible_page(0); #if FPS_METER getdatetime(&dt); last_sec = dt.second; #endif for (;;) { if (kbhit() && getch() == 27) break; /* Физика: обновить позиции (только состояние спрайтов). */ for (i = 0; i < NBALLS; i++) { int nx = sp[i].x + vx[i]; int ny = sp[i].y + vy[i]; if (nx <= MINX || nx >= MAXX) vx[i] = -vx[i]; if (ny <= MINY || ny >= MAXY) vy[i] = -vy[i]; sprite_move(&sp[i], nx, ny); } /* Кадр: рисуем на скрытой странице. sprite_flip развёрнут, чтобы * плашку FPS нарисовать на скрытой странице ДО sprite_update. */ hidden = gfx_get_visible_page() ^ 1; gfx_set_draw_page(hidden); #if FPS_METER frames++; getdatetime(&dt); if (dt.second != last_sec) { last_sec = dt.second; show_fps(frames); frames = 0; } #endif sprite_update(sp, NBALLS); gfx_wait_vsync(); gfx_set_visible_page(hidden); /* флип в vblank — без мерцания */ } closegraph(); return 0; }