Вместо предисловия
В моей недавней статье Lazarus — пишем компонент для анимации спрайтов я описал процесс создания простого компонента TImageFragment, позволяющего отображать заданный фрагмент изображения.
Продолжая выбранную тему, в этой статье я хочу показать, как легко можно сделать анимацию спрайтов в среде разработки Lazarus (официальный сайт) при помощи этого компонента.
При таком подходе отдельные кадры анимации в разных проекциях размещаются на одном изображении, а компонент для отображения спрайта показывает только один выбранный фрагмент этого изображения, используя свойства OffsetX и OffsetY (смещение левого верхнего угла фрагмента изображения по горизонтали и по вертикали).
Множество таких готовых изображений можно найти в Сети — например, вот на этом сайте.
Выбираем (и подготавливаем) изображение
Для своего примера я выбрал вот это изображение:
— очень уж выразительно эта птица Феникс машет крыльями.
Как можно заметить, каждая строка содержит по 4 кадра для каждой из 4-х проекций. Меняя только OffsetX, можно заставить птицу махать крыльями, а для смены проекции достаточно только менять OffsetY. Такое разделение кадров по строкам очень упрощает программирование анимации.
Размер этого изображения — 384х384, а размер каждого кадра — 96х96. К сожалению, прямое использование этого изображение огорчило нас артефактами: некоторые кадры изображения размещены так, что их края попадают на соседние кадры, и во время анимации на краях спрайта мелькают желтые штрихи.
Для устранения этих дефектов я использовал бесплатный кросс-платформенный графический редактор GIMP (официальный сайт). Все, что нужно было сделать — удалить выступающие пиксели изображений в тех местах, где они попадают на соседний кадр.
Исправленный файл выглядит вот так:
— невооруженным глазом отличия незаметны, но второй вариант работает без артефактов.
Создаем новый проект
1. Создаем новый проект типа «Приложение».
По умолчанию IDE создает проект с названием «project1», в котором сразу создается один программный модуль с названием «unit1», описывающий класс с названием "«TForm1» и объявляющий его экземпляр с названием «Form1».
Вообще при создании новых объектов IDE присваивает им подобные имена, состоящие из названия типа объекта и порядкового номера. Я считаю хорошим стилем переименовывать все такие объекты, присваивая им осмысленные имена, отражающие роль или назначение объекта.
Так, наш проект будет называться не «project1», а «Phoenix» — по названию выбранного спрайта.
2. Сохраняем наш новый проект.
Желательно сохранять каждый проект в отдельный каталог с именем, совпадающим с названием проекта. В процессе сохранения мы указываем каталог для сохранения (при необходимости — тут же его создаем), затем имя файла проекта и имя файла программного модуля. Я создал папку «Phoenix» и сохранил туда файл проекта («Phoenix.lpi» вместо предложенного «project1.lpi») и файл программного модуля («UnitMain.pas» вместо предложенного «unit1.pas»).
Нюанс с регистром символов
Версия Lazarus для Windows приводит название файла программного модуля к нижнему регистру: «unitmain.pas», но программное название модуля сохраняет оригинальный регистр символов: «unit UnitMain;». С файлом проекта этого не происходит, имя файла сохраняет оригинальный регистр символов.
3. Переименовываем форму и меняем ее заголовок.
Только что созданная форма называется «Form1» (свойство Name), является экземпляром класса «TForm1» и содержит заголовок «Form1» (свойство Caption). Изменяем свойство Name формы на «FormMain», при этом название класса изменится на «TFormMain».
Свойство Caption меняем на «Phoenix», чтобы в заголовке окна отображалось название проекта.
4. В итоге у меня получился вот такой текст модуля «unitmain.pas»:
unit UnitMain;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs;
type
TFormMain = class(TForm)
private
public
end;
var
FormMain: TFormMain;
implementation
{$R *.lfm}
end.
5. Компилируем, запускаем проект (клавиша <F9>):
Помещаем спрайт на форму
Предполагая, что у вас уже установлен компонент TImageFragment, описанный в моей предыдущей статье Lazarus — пишем компонент для анимации спрайтов, выбираем на палитре компонентов вкладку «Game» и добавляем на форму компонент «TImageFragment».
Используя свойство Picture, загружаем в компонент изображение (исправленный вариант птицы Феникс). Кроме этого, меняем также следующие свойства нового объекта:
- свойства Height и Width устанавливаем в значение 96
- свойства Left и Top ставим в 0 (удобно для совпадения с моими скриншотами)
- свойство Name меняем с неудобного «ImageFragment1» на простое и понятное «Sprite»
Если все сделано правильно, компонент будет показывать первый кадр изображения:
В тексте модуля UnitMain при этом произойдут небольшие изменения:
— в раздел uses добавится модуль ImageFragment
uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs,
ImageFragment;
— в объявлении класса появится новый объект
TFormMain = class(TForm)
Sprite: TImageFragment;
private
public
end;
Добавляем анимацию — махи крыльями
1. Добавляем на форму новый компонент класса TTimer.
Этот компонент находится на вкладке «System» палитры компонентов. Разместить его можно в любом удобном месте формы, так как в работающем приложении он не отображается.
2. Переименовываем добавленный объект.
Новый объект автоматически получает имя «Timer1», но мы переименовываем его в «TimerLive». Часто удобно давать объектам такие имена, состоящие из двух частей: первая отражает класс объекта, а вторая — его назначение.
3. Меняем свойство Interval с 1000 на 100.
Пусть кадры этой анимации сменяют друг друга каждые 100 миллисекунд, то есть 10 раз в секунду. В дальнейшем это свойство можно менять для замедления или ускорения маха крыльев — на усмотрение программиста.
4. Добавляем обработчик события OnTimer.
Проще всего это сделать двойным щелчком мыши на значке нового объекта TimerLive. В результате этого действия IDE сама добавит в объявление класса формы новую процедуру, в свойства объекта — ссылку на эту процедуру, а в раздел implementation будет добавлено тело новой процедуры (и курсор будет помещен внутри этой новой процедуры, между ключевыми словами begin и end).
5. Добавляем в новую процедуру одну строку кода.
Sprite.OffsetX := (Sprite.OffsetX + 96) mod 384;
В результате этих действий объявление класса должно выглядеть примерно так:
TFormMain = class(TForm)
Sprite: TImageFragment;
TimerLive: TTimer;
procedure TimerLiveTimer(Sender: TObject);
private
public
end;
А новая процедура — обработчик события OnTimer должна выглядеть примерно так:
procedure TFormMain.TimerLiveTimer(Sender: TObject);
begin
Sprite.OffsetX := (Sprite.OffsetX + 96) mod 384;
end;
После компиляции и запуска приложения можно наблюдать, как птица Феникс машет крыльями.
Происходит это потому, что обработчик события таймера каждые 100 миллисекунд циклически меняет смещение отображаемого фрагмента, и выбранный кадр смещается по горизонтали, последовательно отображая 4 кадра верхней строки загруженного изображения. Операция mod — получение остатка от деления — предотвращает выход смещения за пределы размера изображения, и в результате за 4-м кадром снова следует 1-й.
Добавляем перемещение спрайта по окну
1. Добавляем в раздел uses модуль Math
uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls,
ImageFragment, Math;
2. Добавляем в объявление класса новую переменную и константу.
Для сохранения вектора перемещения спрайта по окну добавим переменную типа TPoint
private
FVector: TPoint;
Там же объявляем константу для задания модуля скорости перемещения
const
Speed = 10;
3. Добавляем на форму еще один компонент класса TTimer.
Напоминаю: этот компонент находится на вкладке «System» палитры компонентов.
Новый объект опять автоматически получает имя «Timer1», и мы переименовываем его — на этот раз в «TimerMove». Назначение второго таймера — управлять движением спрайта. Я не стал оба процесса (анимацию и перемещение) подвязывать на один и тот же таймер для того, чтобы каждый из таймеров можно было настраивать отдельно — например, замедлить частоту махов крыльями без замедления перемещения, и так далее.
4. Меняем свойство Interval с 1000 на 100.
Пусть этот таймер тоже срабатывает каждые 100 миллисекунд, то есть 10 раз в секунду. В дальнейшем это свойство также можно менять для замедления или ускорения частоты отрисовки факта перемещения спрайта.
5. Добавляем обработчик события OnTimer.
Для разнообразия на этот раз предлагаю сделать это двойным щелчком мыши напротив события OnTimer на вкладке «События» нового объекта TimerMove. Как и в прошлый раз, в результате этого действия IDE сама добавит в объявление класса формы новую процедуру, в свойства объекта — ссылку на эту процедуру, а в раздел implementation будет добавлено тело новой процедуры (и курсор будет помещен внутри этой новой процедуры, между ключевыми словами begin и end).
6. Добавляем в новую процедуру две строки кода.
Sprite.Left := Max(0, Min(Width - Sprite.Width, Sprite.Left + FVector.x));
Sprite.Top := Max(0, Min(Height - Sprite.Height, Sprite.Top + FVector.y));
Использование функций Max() и Min() предотвращает выход спрайта за пределы формы (главного окна приложения).
Именно для использования этих функций мы и подключили в раздел uses модуль Math.
7. Добавляем обработчик события OnKeyPress.
Выделяем форму (щелкаем мышью по серому прямоугольнику макета окна за пределами всех добавленных компонентов) и на вкладке «События» находим событие OnKeyPress. Двойным щелчком мыши по пустому значению обработчика события создаем и присваиваем новую процедуру — обработчик события.
8. Добавляем в новую процедуру несколько строк кода.
if Key = 'a' then
FVector := TPoint.Create(-Speed, 0)
else if Key = 'd' then
FVector := TPoint.Create(Speed, 0)
else if Key = 'w' then
FVector := TPoint.Create(0, -Speed)
else if Key = 's' then
FVector := TPoint.Create(0, Speed)
else if Key = ' ' then
FVector := TPoint.Create(0, 0);
В результате этих действий объявление класса должно выглядеть примерно так:
TFormMain = class(TForm)
Sprite: TImageFragment;
TimerMove: TTimer;
TimerLive: TTimer;
procedure FormKeyPress(Sender: TObject; var Key: char);
procedure TimerLiveTimer(Sender: TObject);
procedure TimerMoveTimer(Sender: TObject);
private
FVector: TPoint;
const
Speed = 10;
public
end;
А новые процедуры — обработчики событий OnTimer и OnKeyPress должны выглядеть примерно так:
procedure TFormMain.TimerMoveTimer(Sender: TObject);
begin
Sprite.Left := Max(0, Min(Width - Sprite.Width, Sprite.Left + FVector.x));
Sprite.Top := Max(0, Min(Height - Sprite.Height, Sprite.Top + FVector.y));
end;
procedure TFormMain.FormKeyPress(Sender: TObject; var Key: char);
begin
if Key = 'a' then
FVector := TPoint.Create(-Speed, 0)
else if Key = 'd' then
FVector := TPoint.Create(Speed, 0)
else if Key = 'w' then
FVector := TPoint.Create(0, -Speed)
else if Key = 's' then
FVector := TPoint.Create(0, Speed)
else if Key = ' ' then
FVector := TPoint.Create(0, 0);
end;
После компиляции и запуска приложения можно перемещать птицу Феникс по экрану при помощи клавиш «a», «w», «s», «d» и останавливать ее клавишей «пробел».
Используем разные проекции спрайта
Добавляем в конец процедуры TFormMain.FormKeyPress следующий код
if FVector.x < 0 then
Sprite.OffsetY := 96
else if FVector.x > 0 then
Sprite.OffsetY := 192
else if FVector.y < 0 then
Sprite.OffsetY := 288
else
Sprite.OffsetY := 0;
Изменение свойства OffsetY в зависимости от вектора перемещения приводит к тому, что изображение как бы поворачивается в сторону направления движения.
Весь текст модуля UnitMain
unit UnitMain;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, ExtCtrls,
ImageFragment, Math;
type
{ TFormMain }
TFormMain = class(TForm)
Sprite: TImageFragment;
TimerMove: TTimer;
TimerLive: TTimer;
procedure FormKeyPress(Sender: TObject; var Key: char);
procedure TimerLiveTimer(Sender: TObject);
procedure TimerMoveTimer(Sender: TObject);
private
FVector: TPoint;
const
Speed = 10;
public
end;
var
FormMain: TFormMain;
implementation
{$R *.lfm}
{ TFormMain }
procedure TFormMain.TimerLiveTimer(Sender: TObject);
begin
Sprite.OffsetX := (Sprite.OffsetX + 96) mod 384;
end;
procedure TFormMain.TimerMoveTimer(Sender: TObject);
begin
Sprite.Left := Max(0, Min(Width - Sprite.Width, Sprite.Left + FVector.x));
Sprite.Top := Max(0, Min(Height - Sprite.Height, Sprite.Top + FVector.y));
end;
procedure TFormMain.FormKeyPress(Sender: TObject; var Key: char);
begin
if Key = 'a' then
FVector := TPoint.Create(-Speed, 0)
else if Key = 'd' then
FVector := TPoint.Create(Speed, 0)
else if Key = 'w' then
FVector := TPoint.Create(0, -Speed)
else if Key = 's' then
FVector := TPoint.Create(0, Speed)
else if Key = ' ' then
FVector := TPoint.Create(0, 0);
if FVector.x < 0 then
Sprite.OffsetY := 96
else if FVector.x > 0 then
Sprite.OffsetY := 192
else if FVector.y < 0 then
Sprite.OffsetY := 288
else
Sprite.OffsetY := 0;
end;
end.
Вместо послесловия
Этот простой пример не претендует на высокие рейтинги по быстродействию или юзабилити. Если кто-то, как в прошлой статье, хочет рассказать в комментариях, что анимацию нужно делать не так — добро пожаловать, напишите свою статью. А тема этой статьи — как сделать анимацию в несколько строк кода, не используя никаких специальных библиотек, практически «на коленке». Этот метод проверен на практике, он реально работает, так что прежде чем критиковать и «минусовать», пожалуйста, еще раз перечитайте, о чем и для чего эта статья.