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:
2026-07-13 16:14:31 +03:00
parent 0e935343d9
commit eb9ca0179d
15 changed files with 506 additions and 4 deletions
+10
View File
@@ -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;
+94
View File
@@ -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;
}
}
}
}
+2
View File
@@ -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;
}
+54
View File
@@ -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;
}
+27
View File
@@ -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);
}
}
+2
View File
@@ -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;
}
+19
View File
@@ -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;
}
}
+46
View File
@@ -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;
}
+6
View File
@@ -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();