SDLЭто вольный перевод и переосмысление статьи, SDL Tutorial - Animation за авторством Tim Jones. Готовый код я буду выкладывать на Github, откуда вы сможете скачать так же ZIP-архив. Если вы увидите ошибку, неточность, или у вас возникнут проблемы - обращайтесь: вы можете оставить комментарий или написать мне на почту, она указана внизу страницы.

В последней статье мы создали прототип игры в крестики-нолики. Надеюсь, многие из вас смогли заставить его работать. Если нет, то не волнуйтесь, рано или поздно у вас все получится. В этой статье мы собираемся научиться делать анимацию с помощью SDL. Как и прежде, мы будем код предыдущих статей SDL, за исключением кода из Крестиков-ноликов. Я отменил почти все изменения, сделанные в той статье, оставил лишь Transparent функцию для класса Sprite.

Мы создадим новый класс для обработки анимации, и в следующем уроке мы создадим класс для сущностей (Entities). Имейте в виду, что эти две разные вещи, и, хотя я знаю, они могут быть выражены через один класс, я не хочу использовать такой подход. Поэтому, пожалуйста, попридержите вашу критику.

Создайте два новых файла с названиями Animation.h и Animation.cpp. В будущем, наш Entity-класс будет наследовать этот класс, но пока что мы будем пользоваться им напрямую. Давайте сразу подключим файл в App.h:

#include "Animation.h"

Теперь откройте Animation.h и добавьте базовую структуру класса в файл:

#ifndef _ANIMATION_H_
#define _ANIMATION_H_

#include <SDL.h>

class Animation {
    private:
        int    CurrentFrame;
        int     FrameInc;

        int     FrameRate;
        long    OldTime;

    public:
        int    MaxFrames;
        bool    Oscillate;

    public:
        Animation();
        void Animate();

    public:
        void SetFrameRate(int Rate);
        void SetCurrentFrame(int Frame);
        int GetCurrentFrame();
};

#endif

Теперь откройте Animation.cpp и добавьте следующий код:

#include "Animation.h"

Animation::Animation() {
    CurrentFrame    = 0;
    MaxFrames       = 0;
    FrameInc        = 1;

    FrameRate       = 100; //Milliseconds
    OldTime         = 0;

    Oscillate       = false;
}

void Animation::Animate() {
    if(OldTime + FrameRate > SDL_GetTicks()) {
        return;
    }

    OldTime = SDL_GetTicks();

    CurrentFrame += FrameInc;

    if(Oscillate) {
        if(FrameInc > 0) {
            if(CurrentFrame >= MaxFrames) {
                FrameInc = -FrameInc;
            }
        }else{
            if(CurrentFrame <= 0) {
                FrameInc = -FrameInc;
            }
        }
    }else{
        if(CurrentFrame >= MaxFrames) {
            CurrentFrame = 0;
        }
    }
}

void Animation::SetFrameRate(int Rate) {
    FrameRate = Rate;
}

void Animation::SetCurrentFrame(int Frame) {
    if(Frame < 0 || Frame >= MaxFrames) return;

    CurrentFrame = Frame;
}

int Animation::GetCurrentFrame() {
    return CurrentFrame;
}

Теперь объясню что же делает этот класс. Существует один основной элемент анимации, который мы должны обрабатывать - текущий кадр анимации. Возьмите изображение Йоши ниже (мы будем использовать его в этой статье). Вы увидите, у нас есть 8 кадров Йоши на одном изображении. Каждый кадр будет пронумерован 0, 1, 2 сверху вниз.

Помните, статью про координаты, где мы создаели функцию, которая отрисовывает часть изображения? И если взять эту функцию в сочетании с номером кадра анимации, вуаля!

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

Sprite::Draw(Screen, Image, 0, 0, 0, Anim_Yoshi.GetCurrentFrame() * 64, 64, 64);

Так как наш Йоши размером 64 х 64 пикселей, столько мы и захватим для вывода, это же и является нашим кадром анимации. Посмотрите на иллюстрацию ниже.

Когда CurrentFrame увеличивается на 1, мы прыгаем вниз на 64 пикселя (высота кадра Yoshi), и рисуем этот кадр.

Другая часть этого класса нужна нам для того, чтобы определять, сколько кадров в анимации - MaxFrames. Наконец важно так же знать, сколько кадров в секунду, или, скорее, как быстро будет меняться анимированное изображение. Для этого мы используем вот этот кусок кода в функции Animate().

if(OldTime + FrameRate > SDL_GetTicks()) {
    return;
}

Если взять OldTime в миллисекундах плюс желаемую частоту кадров, мы можем сравнить это значение с количеством миллисекунл, которые SDL был запущен. Например, мы только что запустили нашу программу, SDL_GetTicks = 0, и OldTime = 0. Желаемая частота кадров будет 1 кадр в секунду. Итак FrameRate = 1000 (миллисекунд). 0 + 1000 больше 0? Да, так что мы не будем ничего делать и ждать. Но как только 0 + 1000 станет меньше, чем SDL_GetTicks, что означает, что 1 секунда прошла, мы увеличим номер кадра, а затем сбросим OldTime на текущее время, и начнем все сначала.

Следующий интересный момент это Oscillate и FrameInc. Я не хотел бы никого путать лишними переменными, но мне кажется, что это пригодится нам в будущем. В общем, когда Oscillate = true, в анимация будет увеличиваться номер кадра, а затем уменьшаться в обратном порядке. Если бы в анимации было 10 кадров, было бы как-то так:

0 1 2 3 4 5 6 7 8 9 8 7 6 5 4 3 2 1 2 ...

Видите, номер кадра идет до 9, а затем уменьшается обратно до 0, и так далее. Есть несколько интересных применений для этого, но мы этим займемся в других статьях. Так, как это работает? Взгляните на функцию Animate. Если Oscillate = false, то мы просто проверяем превысил ли номер текущего кадра максимальное количество кадров. Если да - сбрасываем на 0, а затем ниже, мы переходим к следующему кадру. В противположном же случае, у нас есть переменная FrameInc, которая принимает значения 1 или -1, в зависимости от того, увеличиваем или уменьшаем номер кадра. То есть, если FrameInc больше 0, мы увеличиваем номер кадра, иначе - уменьшаем. И если мы достигли первого или последнего кадра - меняем FrameInc на противоположный.

Теперь, когда мы обо всем позаботились, используем этот класс. Создайте новый объект типа Animation в App.h:

Animation      Anim_Yoshi;

Теперь, давайте установиv MaxFrames, добавьте в App::Init():

Sprite::Transparent(Test, 255, 0, 255);
Anim_Yoshi.MaxFrames = 8;

И теперь, чтобы анимация двигалась в App::Loop():

Anim_Yoshi.Animate();

И наконец, нарисуем на экране App::Render():

void App::Render() {
    SDL_FillRect(Screen, NULL, 0x000000);
    Sprite::Draw(Screen, Test, 290, 220, 0, Anim_Yoshi.GetCurrentFrame() * 64, 64, 64);

    SDL_Flip(Screen);
}

Теперь скомпилируйте и смотрить как работает наша анимация! SDL_FillRect заполняет указанную поверхность одним цветом, в данном случае черным, чтобы остатки анимации не мешались на экране.