Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 11 из 25 ~20 мин +50 XP

Анимация: кадры по времени

Анимация спрайтов превращает статичную картинку в живого героя: кадры сменяются один за другим, и нога делает шаг. В Ebitengine вся механика кадра — целочисленная арифметика над счётчиком тиков: разберём скорость анимации, ряды направлений из атласа и циклы, которые не дрожат ни на одном мониторе.

Содержание урока · 10 разделов

Урок про атласы оставил вас с вырезанными кадрами и честным вопросом: что делать дальше с срезом из тридцати двух картинок. Кадры сами себя не сменят — нужна формула, которая каждый момент времени отвечает на вопрос «какой кадр сейчас». Ответ в духе геймдева прост до неприличия: делить счётчик тиков на длительность кадра и брать остаток по числу кадров. Но вокруг этой формулы есть решения, которые отличают плавную анимацию от дёрганой: где живёт счётчик, сколько тиков отведено кадру и как переключаться между стойкой, ходьбой и ударом. В этом уроке анимация спрайтов собирается как арифметика — без таймеров, без time.Since и без единого замера реального времени.

Слово «анимация» здесь означает только смену кадров — не скелетные кости и не морфинг. Это не упрощение, а точность: пиксель-арт десятилетиями живёт сменой кадров, и большинство 2D-игр в Steam устроены так же. Сложность анимации лежит не в формуле, а в организации данных: какие кадры, в каком порядке, с какой скоростью — и всё это должно правиться без переписывания кода. К концу урока у героя будет срез кадров и формула, которая одна обслуживает все анимации.

Кадры ebiten: индекс из счётчика тиков#

Кадры ebiten получают из одной строки: frame := (g.tick / framesPer) % numFrames. Здесь g.tick — счётчик, который вы сами увеличиваете на единицу в каждом вызове Update, framesPer — сколько тиков длится один кадр, а numFrames — общее число кадров анимации. Деление целое, поэтому кадр держится framesPer тиков подряд, а остаток замыкает цикл: дошли до последнего кадра — вернулись к первому. При TPS 60 из урока про движение счётчик растёт ровно шестьдесят раз в секунду независимо от монитора, и анимация идёт с одной и той же скоростью у всех игроков.

Почему счётчик — именно ваше поле в структуре, а не что-то глобальное: у героя может быть несколько анимаций с разными скоростями, и каждой нужна своя фаза. Счётчик, увеличенный в Update, гарантированно растёт с частотой тиков; в Draw его трогать нельзя — частота Draw зависит от монитора, и мы это уже ловили в predict пятого урока. Формула полностью детерминирована: по номеру тика можно вычислить кадр без воспроизведения игры — это упрощает отладку, реплеи и тесты.

Детерминизм — не абстракция, а рабочая привычка. Представьте баг-репорт: «на тридцать второй секунде герой дёргается». С таймером реального времени воспроизведение зависит от машины, загрузки и момента запуска; со счётчиком тиков тридцать вторая секунда — это тик 1920, и формула говорит точно, какой кадр был на этом тике. Юнит-тест анимации тоже становится тривиальным: подали тик — получили кадр, без окна и рендера.

Своё поле не единственный вариант: движок ведёт глобальный счётчик сам. Функция ebiten.Tick() возвращает число тиков — оно стартует с нуля и растёт на единицу на каждом вызове Update, тип int64, вызов потокобезопасен. Именно на нём держится пример из таблицы ниже. Но глобальный тик нельзя ни сбросить, ни отложить, ни завести второй параллельно — а для смешения ходьбы и удара нужен именно свой счётчик у каждого типа анимации.

Ходьба одним циклом

GO
type Player struct {
        x, y   float64
        tick   int             // счётчик тиков этой анимации
        frame  int             // текущий кадр
        frames []*ebiten.Image // 8 кадров ходьбы из атласа
}

const framesPer = 6 // тиков на один кадр

func (p *Player) Update() {
        p.tick++
        p.frame = (p.tick / framesPer) % len(p.frames)
}

func (p *Player) Draw(screen *ebiten.Image) {
        op := &ebiten.DrawImageOptions{}
        op.GeoM.Translate(p.x, p.y)
        screen.DrawImage(p.frames[p.frame], op)
}

Счётчик растёт в Update, кадр вычисляется той же строкой, а Draw лишь рисует уже выбранную картинку из среза кадров десятого урока. При framesPer 6 и TPS 60 анимация показывает десять кадров в секунду. Срез frames вырезается из атласа один раз при загрузке, поэтому в горячем цикле нет ни вырезаний, ни аллокаций — только арифметика целых.

TPS по умолчанию, framesPer = 4. Сколько кадров в секунду показывает анимация?

Скорость анимации: сколько тиков живёт один кадр#

Скорость анимации выражается одним целым числом — тиками на кадр — и вся её настройка сводится к подбору этого числа. При TPS 60: framesPer 2 даёт тридцать кадров в секунду — бешеная вертлявость, годится для мелких эффектов; framesPer 4 — пятнадцать кадров, бодрая ходьба; framesPer 8 — семь с половиной, тяжёлая поступь большого монстра; framesPer 15 — четыре кадра в секунду, расслабленный Idle-покачивание. Пиксель-арт традиционно живёт на низких частотах: шесть-двенадцать кадров в секунду для ходьбы — норма, а не экономия.

Пара практических следствий из целочисленного деления. Первое: частота анимации привязана к TPS, и если вы поднимете тики через SetTPS для точной физики, все анимации ускорятся во столько же раз — держите framesPer рядом с TPS в конфиге, а не размазанным по коду. Второе: скорость не дрожит. Формулы на вещественном времени — elapsed * fps с накоплением дробей — дают рывки при пропущенных кадрах; целочисленная схема нечувствительна к пропускам Draw вообще: кадр вычисляется из номера тика, а тик по контракту движка приходит ровно TPS раз в секунду.

Подбирая framesPer, смотрите на размер и характер героя. Мелкий персонаж с короткими шагами живёт на восьми-десяти кадрах в секунду, крупный босс с размашистыми ударами — на шести-восьми: крупные движения читаются дольше, и спешка делает их суетными. Анимация урона, наоборот, выигрывает от скорости: три кадра за пять тиков читаются как удар, те же три кадра за пятнадцать — как заминка. Отдельный случай — покой: два кадра и медленный ритм framesPer пятнадцать-двадцать оживляют стоячего героя лучше, чем новые кадры ходьбы.

Три способа выбрать кадр#

СпособКак считаетГде уместен
(tick / framesPer) % numFramesваш счётчик, растущий в Updateосновной путь: состояние, скорость и сброс под вашим контролем
int(ebiten.Tick()/5) % frameCountглобальный счётчик тиков движкадемо и прототипы: так сделан официальный пример animation
time.Since(start) * fpsреальное время с плавающей точкойне стоит: дрожание, ломка при паузах, недетерминизм

Какой кадр рисуется на тике 47?

Прочитайте код и выберите, что он выведет:

const framesPer = 5
// в атласе 8 кадров ходьбы
// g.tick уже равен 47
g.frame = (g.tick / framesPer) % 8

Анимация спрайтов: ряды направлений в атласе#

Анимация спрайтов с видом сверху или сбоку почти всегда хранится рядами: каждый ряд листа — одно направление ходьбы, кадры внутри ряда — фазы шага. Первый ряд — шаг вниз, второй — влево, третий — вправо, четвёртый — вверх: раскладка, к которой привыкли редакторы и готовые паки спрайтов. Формула кадра дополняется выбором ряда: sprite := rows[row][frame], где rows — срез срезов кадров, нарезанных из атласа при загрузке. Направление выбирает ввод — клавиши из урока про клавиатуру и курсор из урока про мышь, а фаза шага по-прежнему считается по тикам.

Здесь проявляется выгода нарезки кадров один раз при загрузке: заготовьте rows при старте, и переключение направления станет сменой индекса среза — ноль работы в кадре. Внутри ряда анимация циклится модулем по числу кадров ряда, а не по общему числу ячеек: у направления может быть шесть кадров, у удара — четыре, у стойки — два. Если лист разрезан с зазорами, формулы margin и spacing из атласного урока добавляют по слагаемому в вырезание, но анимационная арифметика не меняется.

Четыре направления из одного листа

GO
const (
        frameW      = 32
        frameH      = 32
        framesInRow = 6 // фаз шага в каждом ряду
        rowDown     = 0
        rowLeft     = 1
        rowRight    = 2
        rowUp       = 3
)

// rows[dir] — кадры одного направления, нарезаны при загрузке
func loadRows(atlas *ebiten.Image) [][]*ebiten.Image {
        var rows [][]*ebiten.Image
        for row := 0; row < 4; row++ {
                var frames []*ebiten.Image
                for i := 0; i < framesInRow; i++ {
                        r := image.Rect(i*frameW, row*frameH,
                                (i+1)*frameW, (row+1)*frameH)
                        frames = append(frames,
                                atlas.SubImage(r).(*ebiten.Image))
                }
                rows = append(rows, frames)
        }
        return rows
}

Внешний цикл идёт по рядам-направлениям, внутренний — по фазам шага; прямоугольник кадра собирается из размеров ячейки и индексов, как в атласном уроке. Четыре вырезания по шесть кадров дают двадцать четыре спрайта из одного файла, а переключение направления в игре — это смена индекса rows, без пересчёта прямоугольников на лету.

Герой повернул влево. Что происходит с кадрами и счётчиком?

Циклы и однонаправленные анимации#

Две категории анимаций живут по разным правилам. Циклические — ходьба, стойка, взмахи крыльев — замыкаются модулем и крутятся бесконечно: последний кадр перетекает в первый, и разрыва не видно, если художник нарисовал петлю аккуратно: поза на последнем кадре обязана логически продолжаться в первом, иначе на стыке виден скачок, который лечится рисунком, а не формулой. Однонаправленные — удар, взрыв, вспышка смерти — проигрываются ровно один раз: взмах меча не должен повторяться сам по себе, пока игрок не нажал кнопку ещё раз. Для них формула другая: кадр растёт до последнего и остаётся на нём — f := tick / framesPer; if f > last { f = last }.

Однонаправленной анимацией управляет флаг: нажатие кнопки атаки заводит отдельный счётчик с нуля, Update прокручивает кадры удара, а по достижении последнего возвращает героя к ходьбе. Пока удар играет, ввод движения обычно игнорируется или копится — это уже игровое решение, но механика кадров от него не зависит. И держите числа в данных: таблица анимаций вида «имя, первый кадр, число кадров, framesPer» превращает добавление новой анимации в строку конфига вместо копирования формулы — пригодится, когда героев станет больше одного.

Правила анимации одним списком#

  • Кадр — производная счётчика тиков: (tick / framesPer) % numFrames, без таймеров реального времени
  • Счётчик растёт только в Update: частота Draw зависит от монитора и скорость не считается
  • Скорость анимации — одно число framesPer: TPS / framesPer кадров в секунду
  • Ряд атласа — направление движения, кадр внутри ряда — фаза шага
  • Циклические анимации замыкаются модулем, однонаправленные держат последний кадр
  • Смена типа анимации сбрасывает счётчик, смена направления внутри типа — нет
  • Таблица анимаций с числами в данных лучше формул, размазанных по коду

Соберите формулу кадра

Восстановите три пропуска: метод логики, длительность кадра и функцию длины среза.

func (p *Player) () error { p.tick++ p.frame = (p.tick / ) % (p.frames) return nil }

От кадра к состоянию: анимация как следствие#

Когда у героя появляется больше одной анимации, главный вопрос — не формула, а источник истины: какая анимация вообще должна играть прямо сейчас. Правильный ответ — состояние героя: если герой в воздухе — кадры прыжка, если рядом враг и нажата атака — удар, иначе ходьба или стойка по факту движения. Формула кадра становится хвостом этой логики: выбрали анимацию — взяли её кадры и framesPer — посчитали индекс. Так анимация перестаёт быть отдельной сущностью, с которой надо синхронизироваться, и становится следствием состояния, а про хранение состояния и HUD речь пойдёт в уроке про игровое состояние.

Тот же принцип работает на эффектах: искра, дым, след от пули — это те же кадры, но с собственной короткой жизнью. Систему частиц курса, где у каждой частицы свой возраст и свой кадр, разберёт урок про частицы. Пока же закрепите привычку: одна сущность — один счётчик, выбранная анимация — данные, кадр — арифметика. Три правила покрывают и героя, и врага, и монету, крутящуюся на месте.

Кадр от тика до отрисовки

Расставьте строки игрового кадра в правильном порядке.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Анимация взрыва из 5 кадров должна пройти один раз и остановиться. Какая формула подходит?

Слоёная база курса собралась: атласы дают кадры, счётчик тиков даёт время, а формула деления с остатком склеивает их в движение. Дальше по курсу кадры начнут работать на целые системы: тайлмапы нарежут мир повторяющимися картинками, камера поведёт экран за героем по большому уровню, и обе задачи решаются той же дисциплиной — логика в тиках, картинка в кадрах. Домашнее задание ниже соединяет атласный урок с этим: срез кадров уже есть, формулы есть, остаётся собрать и посмотреть, как герой впервые пойдёт. Если анимация дёргается, перечитайте живое место: почти всегда виноват счётчик не там, где место ему по контракту движка.

Термины урока

Термин

framesPer

нажмите, чтобы перевернуть
Определение

Число тиков на один кадр анимации. Скорость = TPS / framesPer кадров в секунду: при 60 тиках и framesPer 6 — десять кадров в секунду.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Кадр вычисляется из счётчика тиков формулой (tick / framesPer) % numFrames — без таймеров и замеров реального времени.
  • Скорость анимации — одно целое число framesPer: TPS, делённый на него, даёт кадры в секунду.
  • Счётчик растёт только в Update: в Draw анимация ускоряется пропорционально герцовке монитора.
  • Ряды атласа хранят направления, кадры внутри ряда — фазы шага; нарезка делается один раз при загрузке.
  • Циклические анимации замыкаются модулем, однонаправленные держат последний кадр до нового события.
  • Смена типа анимации сбрасывает счётчик; сама анимация — следствие состояния героя, а не отдельная сущность.

Домашнее задание

Возьмите срез кадров из атласного урока и заведите в герое поля tick и frame. Подключите формулу кадра в Update и подберите framesPer для трёх ритмов: торопливая ходьба, обычная и ленивая стойка — запишите выбранные числа и кадры в секунду для каждого. Затем добавьте второй ряд атласа и переключайте направление клавишами, проверяя, что фаза шага не сбрасывается на поворотах. Отдельно сделайте однонаправленный удар из четырёх кадров: по нажатию счётчик с нуля, по достижении последнего кадра — возврат к ходьбе. И последнее: перенесите формулу кадра в Draw, запустите игру и увеличьте частоту окна, если есть чем, — увиденное дёрганое ускорение и есть аргумент за дисциплину урока.

Личные заметкиПусто
Хранятся только в вашем браузере (Ebitengine · Анимация: кадры по времени)

Вопросы и ответы по уроку

Почему бы не считать кадр через time.Since и вещественные числа, как в других движках?

Фиксированный тик Ebitengine уже даёт ровную временную шкалу: счётчик растёт TPS раз в секунду по контракту движка. Вещественное время добавляет дрожание, ломается при паузах и лишает детерминизма — по номеру тика нельзя восстановить кадр задним числом.

Как ускорить или замедлить готовую анимацию, не трогая кадры?

Поменяйте framesPer — это единственная ручка скорости. Половина числа удваивает частоту смены кадров, удвоение — растягивает. Если разным анимациям нужны разные скорости, храните framesPer вместе с кадрами в таблице анимаций, а не в константе на весь код.

Герой при повороте начинает шаг с первой фазы — раздражает. Что проверить?

Скорее всего, при смене направления сбрасывается общий счётчик. Сбрасывать нужно только счётчик сменённой анимации: у ходьбы фаза шага переживает повороты, и кадры нового ряда должны продолжаться с той же фазы. Разведите счётчики по типам анимаций, если у ударной и ходьбы они путаются.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Chromedp25 уроков~11 чпрактика25 уроков уже доступны

Chromedp в Go: автоматизация Chrome без Selenium

Курс chromedp для Go: навигация, селекторы, клики и формы, скриншоты и PDF, JS-оценка, вкладки, прокси, пул браузеров и скрапинг. 25 уроков.

  • Запускать Chrome из Go и управлять страницами
  • Находить элементы селекторами и ждать их появления
  • Кликать, заполнять формы и извлекать данные
  • Делать скриншоты, PDF и выполнять JavaScript