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 заполняет указанную поверхность одним цветом, в данном случае черным, чтобы остатки анимации не мешались на экране.