libbgi: авто-анимация спрайтов — кадровая + tween (tests/spranim все PASS)
Реализация §9г по требованиям пользователя: - sprite_anim(first,last,speed,mode): ANIM_LOOP / ANIM_PINGPONG / ANIM_ONCE (one-shot замирает на last), | ANIM_HORIZ для горизонтальных лент. Смена кадра в тике = ±an_step к оси ленты — без умножений (осевое смещение first считается в setup циклом). - sprite_moveto(tx,ty,max_step,interval): Брезенхэм порциями ≤max_step вдоль большей оси (меньшая — err-аккумулятором, нелинейные шаги Y сами собой), деления нет. - Одновременность кадровой и tween — независимые поля/биты. - Статус: sprite_anim_status() — битовое поле SPR_ANIM_ON/DONE + SPR_MOVE_ON/DONE; sprite_anim_frame() — текущий индекс кадра; sprite_moving(). - Стопы: sprite_anim_stop(frame | -1 = текущий); sprite_move_stop(0 = замереть / 1 = прыжок в цель + DONE). - Тикер _sprite_tick — проход 0 sprite_update через funcptr _spr_tick_fn (DCE: без sprite_anim/moveto код не линкуется, цена — один if на кадр; +40 Б программе с движком, sprite_t +18 Б). tests/spranim (MAME dev, все PASS): LOOP/PINGPONG (разворот на границе без удвоения краёв)/ONCE+DONE; пример (0,0)→(100,50), шаг 5, интервал 4 → ровно 80 кадров, Y идёт 3/2/3/2; стопы обоих видов. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
accfill 3772
|
||||
argv 3431
|
||||
assrtest 3847
|
||||
atlas 9154
|
||||
atlas 9193
|
||||
attrprob 4087
|
||||
balls 6208
|
||||
balls 6249
|
||||
banked 1056
|
||||
bankedbg 1067
|
||||
banklocl 4683
|
||||
@@ -44,6 +44,7 @@ rt_test 4892
|
||||
seek 4188
|
||||
simple 955
|
||||
solidt 11531
|
||||
spranim 8752
|
||||
spriteclip 4119
|
||||
sprites 6791
|
||||
stattest 7549
|
||||
|
||||
|
@@ -700,10 +700,22 @@ Probe-тест tests/w0page (MAME dev, 2026-07-13, ВСЕ PASS):
|
||||
W1, но только tiny (в small/big/huge в W1 код) — отвергнута в пользу
|
||||
универсального W0.
|
||||
|
||||
## 9г. Авто-анимация спрайтов (ПРЕДЛОЖЕНИЕ 2026-07-13, обсуждается)
|
||||
## 9г. Авто-анимация спрайтов (РЕАЛИЗОВАНО 2026-07-13; tests/spranim PASS)
|
||||
|
||||
Идея пользователя: sprite_update (вызывается раз на кадр) сам тикает
|
||||
анимации — приложение только описывает их декларативно.
|
||||
анимации — приложение только описывает их декларативно. Реализация:
|
||||
sprite_anim(first,last,speed,mode: LOOP/PINGPONG/ONCE|HORIZ) /
|
||||
sprite_anim_stop(frame|-1) / sprite_moveto(tx,ty,max_step,interval) /
|
||||
sprite_move_stop(at_target) / статус sprite_anim_status() (биты
|
||||
SPR_ANIM_ON/DONE, SPR_MOVE_ON/DONE) / sprite_anim_frame() /
|
||||
sprite_moving(). Кадровая и tween независимы (одновременно — да);
|
||||
тикер _sprite_tick подключается указателем _spr_tick_fn (DCE),
|
||||
sprite_update зовёт его проходом 0. Кадровая — ±an_step к оси без
|
||||
умножений; tween — Брезенхэм порциями ≤max_step по большей оси.
|
||||
Проверено tests/spranim (T1 LOOP / T2 PINGPONG / T3 ONCE+DONE /
|
||||
T4 пример (0,0)→(100,50)/5/4 = ровно 80 кадров, Y 3/2/3/2 /
|
||||
T5 стопы) — все PASS. Цена: ~+40 Б программе с движком, sprite_t
|
||||
+18 Б. Ниже — исходный дизайн-разбор.
|
||||
|
||||
### Кадровая анимация (смена изображений)
|
||||
|
||||
|
||||
@@ -155,6 +155,15 @@ void _gfx_w0_isr(void);
|
||||
#define _ATL_DIR_OFF 0x68u /* каталог: count × 8 Б (макс 19) */
|
||||
#define _ATL_DATA_OFF 0x100u /* ленты; = резерв ISR-стаба */
|
||||
|
||||
/* ---- Авто-анимация спрайтов (§9г) --------------------------------- *
|
||||
* Тикер подключается указателем (DCE: программа без sprite_anim/
|
||||
* sprite_moveto не тянет его код): дефолт NULL (_BSS), ставят
|
||||
* sprite_anim()/sprite_moveto(). sprite_update зовёт в начале кадра.
|
||||
* Типы из <sprite.h> здесь недоступны (подключается после) — void*. */
|
||||
typedef void (*_spr_tick_fp)(void *arr, uint8_t count);
|
||||
extern _spr_tick_fp _spr_tick_fn; /* common/_spr_tick_fn.c */
|
||||
void _sprite_tick(void *arr, uint8_t count); /* common/_sprite_tick.c */
|
||||
|
||||
/* ---- Fill-burst'ы акселератора 256 (register-ABI, asm-only) ------ *
|
||||
* Вызываются ТОЛЬКО из asm raw-примитивов; аргументы в регистрах, без
|
||||
* глобального скретча в _DATA:
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* _spr_tick_fn — указатель на тикер авто-анимаций (data-модуль).
|
||||
* Дефолт NULL (_BSS, crt0 зануляет): sprite_update платит один if на
|
||||
* кадр; sprite_anim()/sprite_moveto() ставят _sprite_tick — только
|
||||
* тогда код тикера попадает в программу (DCE-паттерн funcptr, как
|
||||
* clip/noclip).
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
|
||||
_spr_tick_fp _spr_tick_fn;
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* _sprite_tick — тикер авто-анимаций (§9г): вызывается из sprite_update
|
||||
* (раз на КАДР, до heal/блит-проходов — dirty должен взводиться раньше).
|
||||
* Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ (идут одновременно);
|
||||
* тикаются и у скрытых спрайтов («время идёт», ветка дороже).
|
||||
*
|
||||
* Кадровая: смена кадра = ±an_step к оси ленты (sy; при _SPR_ANIM_HORZ
|
||||
* — sx) — БЕЗ умножения; wrap цикла — прыжок оси в an_lo (посчитан
|
||||
* sprite_anim'ом один раз). Маятник разворачивается НА границе (…2,3,
|
||||
* 2,1,0,1…, без удвоения крайних). ANIM_ONCE замирает на last:
|
||||
* SPR_ANIM_ON снимается, SPR_ANIM_DONE взводится.
|
||||
*
|
||||
* Перемещение: раз в mv_speed кадров — порция ≤mv_step итераций
|
||||
* классического Брезенхэма (err-аккумулятор: большая ось шагает каждую
|
||||
* итерацию, меньшая подтягивается — нелинейные 2/3/2/3 сами собой,
|
||||
* деления нет). Цель достигнута — SPR_MOVE_ON→off, SPR_MOVE_DONE.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
#include <sprite.h>
|
||||
|
||||
void _sprite_tick(void *arr, uint8_t count)
|
||||
{
|
||||
sprite_t *s = (sprite_t *)arr;
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < count; i++, s++) {
|
||||
/* ---- кадровая анимация ---- */
|
||||
if ((s->an_flags & SPR_ANIM_ON) && --s->an_timer == 0) {
|
||||
int *axis = (s->an_flags & _SPR_ANIM_HORZ) ? &s->sx : &s->sy;
|
||||
uint8_t moved = 1;
|
||||
|
||||
s->an_timer = s->an_speed;
|
||||
if (!(s->an_flags & _SPR_ANIM_BACK)) {
|
||||
if (s->an_frame < s->an_last) {
|
||||
s->an_frame++;
|
||||
*axis += s->an_step;
|
||||
} else if (s->an_flags & _SPR_ANIM_ONCE) {
|
||||
s->an_flags &= (uint8_t)~SPR_ANIM_ON;
|
||||
s->an_flags |= SPR_ANIM_DONE;
|
||||
moved = 0;
|
||||
} else if (s->an_flags & _SPR_ANIM_PP) {
|
||||
s->an_flags |= _SPR_ANIM_BACK; /* разворот на границе */
|
||||
if (s->an_frame > s->an_first) {
|
||||
s->an_frame--;
|
||||
*axis -= s->an_step;
|
||||
} else
|
||||
moved = 0; /* лента из 1 кадра */
|
||||
} else { /* ANIM_LOOP: wrap */
|
||||
s->an_frame = s->an_first;
|
||||
*axis = s->an_lo;
|
||||
}
|
||||
} else { /* маятник, назад */
|
||||
if (s->an_frame > s->an_first) {
|
||||
s->an_frame--;
|
||||
*axis -= s->an_step;
|
||||
} else {
|
||||
s->an_flags &= (uint8_t)~_SPR_ANIM_BACK;
|
||||
if (s->an_frame < s->an_last) {
|
||||
s->an_frame++;
|
||||
*axis += s->an_step;
|
||||
} else
|
||||
moved = 0;
|
||||
}
|
||||
}
|
||||
if (moved)
|
||||
s->flags |= _SPR_DIRTY;
|
||||
}
|
||||
|
||||
/* ---- tween-перемещение ---- */
|
||||
if ((s->an_flags & SPR_MOVE_ON) && --s->mv_timer == 0) {
|
||||
uint8_t n = s->mv_step;
|
||||
int x = s->x, y = s->y;
|
||||
|
||||
s->mv_timer = s->mv_speed;
|
||||
while (n--) {
|
||||
int e2;
|
||||
if (x == s->mv_tx && y == s->mv_ty)
|
||||
break;
|
||||
e2 = s->mv_err * 2;
|
||||
if (e2 >= s->mv_dy) { s->mv_err += s->mv_dy; x += s->mv_sgnx; }
|
||||
if (e2 <= s->mv_dx) { s->mv_err += s->mv_dx; y += s->mv_sgny; }
|
||||
}
|
||||
if (x != s->x || y != s->y) {
|
||||
s->x = x;
|
||||
s->y = y;
|
||||
s->flags |= _SPR_DIRTY;
|
||||
}
|
||||
if (x == s->mv_tx && y == s->mv_ty) {
|
||||
s->an_flags &= (uint8_t)~SPR_MOVE_ON;
|
||||
s->an_flags |= SPR_MOVE_DONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ void atlas_sprite_init(sprite_t *s, const atlas_t *a, uint8_t idx)
|
||||
s->sx = 0; s->sy = 0;
|
||||
s->flags = 0;
|
||||
s->page = a->page;
|
||||
s->an_flags = 0; /* авто-анимации выключены */
|
||||
s->an_frame = 0;
|
||||
s->drawn[0].on = 0;
|
||||
s->drawn[1].on = 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* sprite_anim — запустить кадровую авто-анимацию: кадры first..last
|
||||
* вдоль оси ленты (вертикальная; ANIM_HORIZ — вдоль X), смена раз в
|
||||
* speed кадров, mode = ANIM_LOOP / ANIM_PINGPONG / ANIM_ONCE.
|
||||
*
|
||||
* Всё дорогое считается ЗДЕСЬ один раз (не в тике): шаг оси = размер
|
||||
* кадра, осевое смещение first — циклом сложений (без __mulint).
|
||||
* Ставит текущий кадр на first, сбрасывает SPR_ANIM_DONE, подключает
|
||||
* тикер (_spr_tick_fn) — до первого вызова sprite_anim/sprite_moveto
|
||||
* код тикера в программу не линкуется. Требование: индексы ≤ 127,
|
||||
* first ≤ last. Move-статусы (SPR_MOVE_*) не трогаются.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
#include <sprite.h>
|
||||
|
||||
void sprite_anim(sprite_t *s, uint8_t first, uint8_t last,
|
||||
uint8_t speed, uint8_t mode)
|
||||
{
|
||||
uint8_t f = s->an_flags & (SPR_MOVE_ON | SPR_MOVE_DONE);
|
||||
uint8_t step = (mode & ANIM_HORIZ) ? s->w : s->h;
|
||||
int lo = 0;
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < first; i++)
|
||||
lo += step; /* first*step без умножения */
|
||||
|
||||
if (!speed)
|
||||
speed = 1;
|
||||
|
||||
s->an_step = step;
|
||||
s->an_lo = lo;
|
||||
s->an_first = first;
|
||||
s->an_last = last;
|
||||
s->an_frame = first;
|
||||
s->an_speed = speed;
|
||||
s->an_timer = speed;
|
||||
|
||||
f |= SPR_ANIM_ON;
|
||||
if (mode & ANIM_HORIZ)
|
||||
f |= _SPR_ANIM_HORZ;
|
||||
if ((mode & 0x03u) == ANIM_PINGPONG)
|
||||
f |= _SPR_ANIM_PP;
|
||||
if ((mode & 0x03u) == ANIM_ONCE)
|
||||
f |= _SPR_ANIM_ONCE;
|
||||
s->an_flags = f;
|
||||
|
||||
/* Ось — на кадр first (обычный путь sprite_frame: dirty при смене). */
|
||||
if (mode & ANIM_HORIZ)
|
||||
sprite_frame(s, lo, s->sy);
|
||||
else
|
||||
sprite_frame(s, s->sx, lo);
|
||||
|
||||
_spr_tick_fn = _sprite_tick;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* sprite_anim_stop — остановить кадровую анимацию. frame ≥ 0 — какой
|
||||
* кадр останется на экране (индекс вдоль оси ленты, ≤127); -1 —
|
||||
* замереть на текущем. Ось кадра считается циклом сложений (стоп —
|
||||
* редкий вызов, __mulint не тянем). Валидна после sprite_anim
|
||||
* (an_step/ось настроены им); SPR_ANIM_DONE не трогает.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
#include <sprite.h>
|
||||
|
||||
void sprite_anim_stop(sprite_t *s, int8_t frame)
|
||||
{
|
||||
s->an_flags &= (uint8_t)~(SPR_ANIM_ON | _SPR_ANIM_BACK);
|
||||
|
||||
if (frame >= 0 && (uint8_t)frame != s->an_frame) {
|
||||
int off = 0;
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0; i < (uint8_t)frame; i++)
|
||||
off += s->an_step;
|
||||
s->an_frame = (uint8_t)frame;
|
||||
if (s->an_flags & _SPR_ANIM_HORZ)
|
||||
sprite_frame(s, off, s->sy);
|
||||
else
|
||||
sprite_frame(s, s->sx, off);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,8 @@ void sprite_init(sprite_t *s, const void *img)
|
||||
s->h = p[2];
|
||||
s->flags = 0;
|
||||
s->page = 0; /* img в обычной памяти (не W0-атлас) */
|
||||
s->an_flags = 0; /* авто-анимации выключены */
|
||||
s->an_frame = 0;
|
||||
s->drawn[0].on = 0;
|
||||
s->drawn[1].on = 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* sprite_move_stop — остановить tween-перемещение: at_target = 0 —
|
||||
* замереть в текущих координатах (SPR_MOVE_DONE НЕ взводится — спрайт
|
||||
* цели не достиг); 1 — прыгнуть в цель и взвести SPR_MOVE_DONE.
|
||||
* Ничего не делает, если перемещение не шло.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
#include <sprite.h>
|
||||
|
||||
void sprite_move_stop(sprite_t *s, uint8_t at_target)
|
||||
{
|
||||
if (!(s->an_flags & SPR_MOVE_ON))
|
||||
return;
|
||||
s->an_flags &= (uint8_t)~SPR_MOVE_ON;
|
||||
if (at_target) {
|
||||
sprite_move(s, s->mv_tx, s->mv_ty);
|
||||
s->an_flags |= SPR_MOVE_DONE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* sprite_moveto — запустить tween-перемещение к (tx,ty): раз в interval
|
||||
* кадров тикер делает до max_step итераций Брезенхэма (большая ось —
|
||||
* итерация = пиксель; меньшая подтягивается err-аккумулятором).
|
||||
* Пример: (0,0)→(100,50), max_step 5, interval 4 → 20 шагов × 4 кадра
|
||||
* = 80 кадров (1.6 с), Y идёт 2/3/2/3.
|
||||
*
|
||||
* Дельты/знаки/аккумулятор считаются здесь один раз. Если цель ==
|
||||
* текущая позиция — сразу SPR_MOVE_DONE (тикер не включается).
|
||||
* Кадровые статусы (SPR_ANIM_*) не трогаются.
|
||||
*/
|
||||
#include "../_bgi.h"
|
||||
#include <sprite.h>
|
||||
|
||||
void sprite_moveto(sprite_t *s, int tx, int ty,
|
||||
uint8_t max_step, uint8_t interval)
|
||||
{
|
||||
int dx = tx - s->x;
|
||||
int dy = ty - s->y;
|
||||
|
||||
s->mv_sgnx = 1;
|
||||
if (dx < 0) { dx = -dx; s->mv_sgnx = -1; }
|
||||
s->mv_sgny = 1;
|
||||
if (dy < 0) { dy = -dy; s->mv_sgny = -1; }
|
||||
|
||||
s->an_flags &= (uint8_t)~SPR_MOVE_DONE;
|
||||
if (dx == 0 && dy == 0) {
|
||||
s->an_flags = (s->an_flags & (uint8_t)~SPR_MOVE_ON) | SPR_MOVE_DONE;
|
||||
return;
|
||||
}
|
||||
|
||||
s->mv_tx = tx;
|
||||
s->mv_ty = ty;
|
||||
s->mv_dx = dx;
|
||||
s->mv_dy = -dy;
|
||||
s->mv_err = dx - dy;
|
||||
|
||||
if (!max_step) max_step = 1;
|
||||
if (!interval) interval = 1;
|
||||
s->mv_step = max_step;
|
||||
s->mv_speed = interval;
|
||||
s->mv_timer = interval;
|
||||
|
||||
s->an_flags |= SPR_MOVE_ON;
|
||||
_spr_tick_fn = _sprite_tick;
|
||||
}
|
||||
@@ -35,6 +35,12 @@ void sprite_update(sprite_t *arr, uint8_t count)
|
||||
uint8_t i;
|
||||
sprite_t *s;
|
||||
|
||||
/* Проход 0: тик авто-анимаций (§9г) — ДО heal/блита (двигает x/y и
|
||||
* ось кадра, взводя dirty). Указатель ставят sprite_anim/
|
||||
* sprite_moveto; без них — один if на кадр, код тикера не линкуется. */
|
||||
if (_spr_tick_fn)
|
||||
(*_spr_tick_fn)(arr, count);
|
||||
|
||||
/* Проход 1: heal ВСЕХ старых позиций, требующих обновления. */
|
||||
gfx_set_bank(GFX_BANK_NORMAL);
|
||||
_bgi_begin();
|
||||
|
||||
@@ -46,6 +46,26 @@
|
||||
#define _SPR_DIRTY1 0x08u
|
||||
#define _SPR_DIRTY (_SPR_DIRTY0 | _SPR_DIRTY1)
|
||||
|
||||
/* Статус-биты sprite_t.an_flags (чтение — sprite_anim_status()):
|
||||
* *_ON — анимация активна (тикается в sprite_update); *_DONE — дошла
|
||||
* до конца сама (one-shot до last / перемещение до цели) или была
|
||||
* добита sprite_move_stop(at_target=1). DONE сбрасывается новым
|
||||
* запуском соответствующей анимации. */
|
||||
#define SPR_ANIM_ON 0x01u /* кадровая анимация идёт */
|
||||
#define SPR_ANIM_DONE 0x02u /* one-shot дошёл до last */
|
||||
#define SPR_MOVE_ON 0x04u /* tween-перемещение идёт */
|
||||
#define SPR_MOVE_DONE 0x08u /* перемещение достигло цели */
|
||||
#define _SPR_ANIM_BACK 0x10u /* внутр.: маятник идёт назад */
|
||||
#define _SPR_ANIM_HORZ 0x20u /* внутр.: лента горизонтальная (ось sx) */
|
||||
#define _SPR_ANIM_PP 0x40u /* внутр.: режим маятника */
|
||||
#define _SPR_ANIM_ONCE 0x80u /* внутр.: режим one-shot */
|
||||
|
||||
/* Режимы sprite_anim(). */
|
||||
#define ANIM_LOOP 0u /* 0,1,..,last,first,.. (цикл) */
|
||||
#define ANIM_PINGPONG 1u /* 0,1,..,last,last-1,..,first,1,.. */
|
||||
#define ANIM_ONCE 2u /* first..last, замереть на last + DONE */
|
||||
#define ANIM_HORIZ 0x80u /* ИЛИ-флаг: кадры вдоль X (сетка nx>1) */
|
||||
|
||||
typedef struct {
|
||||
const void *img; /* getimage-формат / атлас-лента (вне W3) */
|
||||
int x, y; /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра */
|
||||
@@ -55,6 +75,19 @@ typedef struct {
|
||||
uint8_t page; /* 0 = img в обычной памяти; иначе физ. страница *
|
||||
* W0-атласа (img — адрес 0x0100-0x3FFF, движок *
|
||||
* сам мапит страницу в W0 на время блита) */
|
||||
/* --- авто-анимация (владеет тикер; настраивать ТОЛЬКО через
|
||||
* sprite_anim../sprite_moveto.., читать — inline-аксессорами) --- */
|
||||
uint8_t an_flags; /* статус: SPR_ANIM_.. | SPR_MOVE_.. */
|
||||
uint8_t an_frame; /* ТЕКУЩИЙ индекс кадра (0-based по ленте) */
|
||||
uint8_t an_first, an_last; /* границы анимации (индексы) */
|
||||
uint8_t an_speed, an_timer; /* кадров между сменами / счётчик */
|
||||
uint8_t an_step; /* шаг оси = fh (верт. лента) или fw (гориз.) */
|
||||
int an_lo; /* осевое смещение кадра first (для wrap) */
|
||||
uint8_t mv_speed, mv_timer; /* кадров между шагами / счётчик */
|
||||
uint8_t mv_step; /* макс. пикселей вдоль большей оси за шаг */
|
||||
int mv_tx, mv_ty; /* цель */
|
||||
int mv_dx, mv_dy, mv_err;/* Брезенхэм: dx, -dy, аккумулятор */
|
||||
int8_t mv_sgnx, mv_sgny; /* знаки шага осей */
|
||||
/* --- внутреннее (владеет движок) --- */
|
||||
struct {
|
||||
int x, y; /* где нарисован на странице p */
|
||||
@@ -107,6 +140,47 @@ inline void sprite_hide(sprite_t *s) {
|
||||
* активный спрайт прошёл над статиком — приложение помечает статик). */
|
||||
inline void sprite_touch(sprite_t *s) { s->flags |= _SPR_DIRTY; }
|
||||
|
||||
/* ==== Авто-анимация (§9г дизайна) ====================================
|
||||
* Тикается САМА в начале каждого sprite_update (раз на кадр), код
|
||||
* тикера подключается через funcptr — программа без анимаций его не
|
||||
* линкует. Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ и могут
|
||||
* идти одновременно. Пока кадровая активна — не трогать sprite_frame
|
||||
* вручную (тикер ведёт ось сам); границы кадров должны лежать вдоль
|
||||
* ОДНОЙ оси ленты (вертикальная по умолчанию, ANIM_HORIZ — вдоль X). */
|
||||
|
||||
/* Запустить кадровую анимацию: кадры first..last (индексы вдоль оси
|
||||
* ленты), смена раз в speed кадров (1 = каждый кадр), mode = ANIM_LOOP/
|
||||
* ANIM_PINGPONG/ANIM_ONCE (| ANIM_HORIZ для горизонтальной ленты).
|
||||
* Старт с first; сбрасывает SPR_ANIM_DONE. */
|
||||
void sprite_anim(sprite_t *s, uint8_t first, uint8_t last,
|
||||
uint8_t speed, uint8_t mode);
|
||||
|
||||
/* Остановить кадровую анимацию: frame = индекс кадра, который останется
|
||||
* на экране, или -1 — замереть на текущем. */
|
||||
void sprite_anim_stop(sprite_t *s, int8_t frame);
|
||||
|
||||
/* Запустить перемещение к (tx,ty): раз в interval кадров — шаг до
|
||||
* max_step пикселей вдоль БОЛЬШЕЙ оси (меньшая тянется Брезенхэмом,
|
||||
* без деления). Сбрасывает SPR_MOVE_DONE. Пример: (0,0)→(100,50),
|
||||
* max_step 5, interval 4 → 20 шагов × 4 кадра = 80 кадров (1.6 с). */
|
||||
void sprite_moveto(sprite_t *s, int tx, int ty,
|
||||
uint8_t max_step, uint8_t interval);
|
||||
|
||||
/* Остановить перемещение: at_target = 0 — замереть где есть;
|
||||
* 1 — прыгнуть в цель (и взвести SPR_MOVE_DONE). */
|
||||
void sprite_move_stop(sprite_t *s, uint8_t at_target);
|
||||
|
||||
/* Статус анимаций: битовое поле SPR_ANIM_ON/DONE | SPR_MOVE_ON/DONE. */
|
||||
inline uint8_t sprite_anim_status(const sprite_t *s) { return s->an_flags; }
|
||||
|
||||
/* Текущий индекс кадра ленты (ведёт тикер; валиден и после stop). */
|
||||
inline uint8_t sprite_anim_frame(const sprite_t *s) { return s->an_frame; }
|
||||
|
||||
/* Перемещение ещё идёт? */
|
||||
inline uint8_t sprite_moving(const sprite_t *s) {
|
||||
return s->an_flags & SPR_MOVE_ON;
|
||||
}
|
||||
|
||||
/* ==== Атласы в EMM-страницах (W0) ====================================
|
||||
* Файл .atl (docs/sprite-api-design.md §3.1, вариант II): 0x100-байтовый
|
||||
* заголовок (магия 'SPA1', count; каталог с 0x68) + getimage-ленты;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := spranim
|
||||
EXTRA_FLAGS ?= --gfx 256 --memory tiny
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* spranim — тест авто-анимаций спрайтового движка (§9г):
|
||||
* sprite_anim (LOOP/PINGPONG/ONCE) + sprite_moveto + статусы + стопы.
|
||||
*
|
||||
* Логика проверяется ПРОГРАММНО по полям sprite_t: спрайты невидимы,
|
||||
* тикер работает независимо от видимости, блитов нет — sprite_update
|
||||
* гоняется в текстовом режиме (скобка W3 безвредна). Ожидания:
|
||||
*
|
||||
* T1 LOOP 0..3 speed=2: смена раз в 2 тика: после 8 тиков frame
|
||||
* снова 0 (0→1→2→3→0), sy вернулся в 0.
|
||||
* T2 PINGPONG 0..3 speed=1: 1,2,3,2,1,0,1 — на тике 7 frame==1,
|
||||
* разворот на границе без удвоения краёв.
|
||||
* T3 ONCE 0..2 speed=1: после 2 тиков frame==2 и ANIM_ON ещё стоит,
|
||||
* после 3-го — ON снят, DONE взведён, frame==2 (замер на last).
|
||||
* T4 MOVETO (0,0)→(100,50) step=5 interval=4 (пример из §9г):
|
||||
* тик 4: (5,3); тик 8: (10,5); тик 79: НЕ DONE; тик 80: (100,50),
|
||||
* MOVE_ON снят, MOVE_DONE взведён. Параллельно у спрайта идёт
|
||||
* LOOP-анимация (одновременность) — frame успел смениться.
|
||||
* T5 sprite_anim_stop(1): ON снят, frame==1, ось = 16.
|
||||
* sprite_move_stop(at_target=1) на полпути: прыжок в цель + DONE.
|
||||
*
|
||||
* Кадры: RAM-лента 16×16×4 (getimage), кадр k залит k — содержимое
|
||||
* не важно (блитов нет), важен размер h=16 → an_step.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sprite.h>
|
||||
|
||||
static uint8_t strip[4 + 16 * 16 * 4];
|
||||
static sprite_t sp[4];
|
||||
|
||||
static void build_strip(void)
|
||||
{
|
||||
memset(strip, 0, sizeof strip);
|
||||
strip[0] = 16; strip[2] = 64; /* w=16, h=64 (4 кадра) */
|
||||
}
|
||||
|
||||
static void tick(sprite_t *arr, uint8_t n, uint16_t times)
|
||||
{
|
||||
while (times--)
|
||||
sprite_update(arr, n);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint8_t ok;
|
||||
|
||||
puts("SPRANIM: frame/tween auto-animation");
|
||||
build_strip();
|
||||
|
||||
/* ---- T1: LOOP ---------------------------------------------------- */
|
||||
sprite_init(&sp[0], strip);
|
||||
sp[0].w = 16; sp[0].h = 16;
|
||||
sprite_anim(&sp[0], 0, 3, 2, ANIM_LOOP);
|
||||
tick(sp, 1, 2);
|
||||
ok = (sprite_anim_frame(&sp[0]) == 1 && sp[0].sy == 16);
|
||||
tick(sp, 1, 4); /* всего 6: 0→1→2→3 */
|
||||
ok = ok && (sprite_anim_frame(&sp[0]) == 3 && sp[0].sy == 48);
|
||||
tick(sp, 1, 2); /* всего 8: wrap → 0 */
|
||||
ok = ok && (sprite_anim_frame(&sp[0]) == 0 && sp[0].sy == 0);
|
||||
printf("T1 LOOP: %s\n", ok ? "PASS" : "FAIL");
|
||||
|
||||
/* ---- T2: PINGPONG ------------------------------------------------ */
|
||||
sprite_init(&sp[0], strip);
|
||||
sp[0].w = 16; sp[0].h = 16;
|
||||
sprite_anim(&sp[0], 0, 3, 1, ANIM_PINGPONG);
|
||||
tick(sp, 1, 3); /* 1,2,3 */
|
||||
ok = (sprite_anim_frame(&sp[0]) == 3);
|
||||
tick(sp, 1, 1); /* разворот: 2 */
|
||||
ok = ok && (sprite_anim_frame(&sp[0]) == 2 && sp[0].sy == 32);
|
||||
tick(sp, 1, 2); /* 1,0 */
|
||||
ok = ok && (sprite_anim_frame(&sp[0]) == 0);
|
||||
tick(sp, 1, 1); /* разворот: 1 */
|
||||
ok = ok && (sprite_anim_frame(&sp[0]) == 1 && sp[0].sy == 16);
|
||||
printf("T2 PINGPONG: %s\n", ok ? "PASS" : "FAIL");
|
||||
|
||||
/* ---- T3: ONCE ---------------------------------------------------- */
|
||||
sprite_init(&sp[0], strip);
|
||||
sp[0].w = 16; sp[0].h = 16;
|
||||
sprite_anim(&sp[0], 0, 2, 1, ANIM_ONCE);
|
||||
tick(sp, 1, 2); /* 1,2 — дошли до last */
|
||||
ok = (sprite_anim_frame(&sp[0]) == 2 &&
|
||||
(sprite_anim_status(&sp[0]) & SPR_ANIM_ON) &&
|
||||
!(sprite_anim_status(&sp[0]) & SPR_ANIM_DONE));
|
||||
tick(sp, 1, 1); /* фиксация: ON→off,DONE */
|
||||
ok = ok && (sprite_anim_frame(&sp[0]) == 2 && sp[0].sy == 32 &&
|
||||
!(sprite_anim_status(&sp[0]) & SPR_ANIM_ON) &&
|
||||
(sprite_anim_status(&sp[0]) & SPR_ANIM_DONE));
|
||||
tick(sp, 1, 5); /* стоит на месте */
|
||||
ok = ok && (sprite_anim_frame(&sp[0]) == 2);
|
||||
printf("T3 ONCE: %s\n", ok ? "PASS" : "FAIL");
|
||||
|
||||
/* ---- T4: MOVETO + одновременная кадровая ------------------------- */
|
||||
sprite_init(&sp[0], strip);
|
||||
sp[0].w = 16; sp[0].h = 16; /* x=y=0 после init */
|
||||
sprite_anim(&sp[0], 0, 3, 3, ANIM_LOOP);
|
||||
sprite_moveto(&sp[0], 100, 50, 5, 4);
|
||||
ok = (sprite_moving(&sp[0]) != 0);
|
||||
tick(sp, 1, 4);
|
||||
ok = ok && (sp[0].x == 5 && sp[0].y == 3); /* Y: 3/2/3/2 */
|
||||
tick(sp, 1, 4);
|
||||
ok = ok && (sp[0].x == 10 && sp[0].y == 5);
|
||||
tick(sp, 1, 71); /* всего 79 — ещё едет */
|
||||
ok = ok && sprite_moving(&sp[0]) &&
|
||||
!(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE);
|
||||
tick(sp, 1, 1); /* 80: приехали */
|
||||
ok = ok && (sp[0].x == 100 && sp[0].y == 50 &&
|
||||
!sprite_moving(&sp[0]) &&
|
||||
(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE));
|
||||
ok = ok && (sprite_anim_frame(&sp[0]) != 0 ||
|
||||
sp[0].an_timer != 3); /* кадровая тоже тикала */
|
||||
printf("T4 MOVETO: %s (x=%d y=%d)\n",
|
||||
ok ? "PASS" : "FAIL", sp[0].x, sp[0].y);
|
||||
|
||||
/* ---- T5: стопы ---------------------------------------------------- */
|
||||
sprite_init(&sp[0], strip);
|
||||
sp[0].w = 16; sp[0].h = 16;
|
||||
sprite_anim(&sp[0], 0, 3, 1, ANIM_LOOP);
|
||||
tick(sp, 1, 2); /* frame 2 */
|
||||
sprite_anim_stop(&sp[0], 1); /* остановить на кадре 1 */
|
||||
ok = (!(sprite_anim_status(&sp[0]) & SPR_ANIM_ON) &&
|
||||
sprite_anim_frame(&sp[0]) == 1 && sp[0].sy == 16);
|
||||
tick(sp, 1, 4); /* не двигается */
|
||||
ok = ok && (sprite_anim_frame(&sp[0]) == 1);
|
||||
|
||||
sprite_moveto(&sp[0], 60, 60, 3, 1);
|
||||
tick(sp, 1, 5); /* на полпути */
|
||||
sprite_move_stop(&sp[0], 1); /* добить в цель */
|
||||
ok = ok && (sp[0].x == 60 && sp[0].y == 60 &&
|
||||
!sprite_moving(&sp[0]) &&
|
||||
(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE));
|
||||
|
||||
sprite_moveto(&sp[0], 0, 0, 3, 1);
|
||||
tick(sp, 1, 5);
|
||||
sprite_move_stop(&sp[0], 0); /* замереть где есть */
|
||||
ok = ok && (sp[0].x != 0 && !sprite_moving(&sp[0]) &&
|
||||
!(sprite_anim_status(&sp[0]) & SPR_MOVE_DONE));
|
||||
printf("T5 STOPS: %s\n", ok ? "PASS" : "FAIL");
|
||||
|
||||
puts("done");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user