Lazarus — пишем компонент для анимации спрайтов
Вместо предисловия
В одесской школе ученики 8-го класса на уроках информатики используют бесплатную кроссплатформенную среду разработки Lazarus (официальный сайт: www.lazarus-ide.org), внешне и внутренне очень напоминающую любимый многими Delphi, использующую версию Object Pascal под названием Free Pascal и в действительности сильно упрощающую процесс вхождения в программирование.
Но детям неинтересно писать программу для вычисления силы тяжести по непонятной им пока формуле F=mg. Практически все дети, которых я пытался учить программированию, с первого занятия хотят написать игру. К счастью, Lazarus прекрасно подходит и для написания несложных игр.
Правда, для создания анимированных спрайтов мне понадобился компонент, отображающий произвольный фрагмент изображения (на котором изображены несколько разных проекций одного и того же персонажа в разных фазах движения), а такого компонента в стандартной поставке нет. Написать его самому оказалось совсем несложно, и об этой технологии я и хочу рассказать в этой статье.
Для отображения веселого графического контента вместо сухого делового набора стандартных компонентов в Lazarus (как и в Delphi) есть 3 компонента на вкладке Additional:
— TImage (отображение картинки из произвольного файла);
— TShape (отображение одного из нескольких заранее заданных графических примитивов);
— TPaintBox (отображение холста, на котором можно рисовать программно).
Самое эффектное для школьника — загрузить небольшой спрайт в TImage и написать программу для перемещения его по экрану — по событиям мыши/клавиатуры, автоматически в цикле или автоматически по событию от таймера.
Как только это начинает работать, у школьника возникает следующий законный вопрос:, а нельзя ли сделать так, чтобы персонаж двигался? И можно ли сделать сделать так, чтобы он смотрел не постоянно на нас, а поворачивался в сторону, совпадающую с направлением движения?
В Сети можно найти большое количество готовых изображений для использования при разработке игр. И многие персонажи заранее разработаны в несколько проекций и несколько кадров анимации (как, например, вот на этом сайте: untamed.wild-refuge.net/rmxpresources.php? characters).
Вот пример изображения, где спрайты расположены в виде таблицы, у которой каждая строка соответствует определенной проекции, а каждый столбец — определенной фазе анимации:
Для отображения такого спрайта достаточно поместить на экран простой компонент, отображающий не все изображение целиком, а только один его фрагмент; и затем, меняя смещение выделенного фрагмента по горизонтали и вертикали, можно заставить персонаж поворачиваться в разные стороны и совершать циклические движения (например, махи крыльями или шаги ногами). Такой прием часто используется при веб-разработке: даже простые наборы иконок для деловой графики часто размещают в одном файле и отображают в разных местах страницы с разным смещением, создавая впечатление разных изображений.
К сожалению, компонент TImage, входящий в стандартную поставку Lazarus (и Delphi), не позволяет показывать произвольный фрагмент изображения: изменяя его свойства, мы можем заставить его показывать только изображение целиком, левый верхний угол или центральную его часть. Для отображения произвольного фрагмента изображения, заданного смещением и размерами по обеим осям, нужен какой-то другой компонент. Но, как выяснилось, сделать его самостоятельно в Lazarus — совсем несложно! Создаем новый компонент
В качестве инструкции по созданию компонентов я воспользовался официальным руководством: wiki.freepascal.org/How_To_Write_Lazarus_Component.
Там все написано достаточно подробно, дублировать не имеет смысла. Я только остановлюсь на некоторых моментах.
1. Стандартный Project Wizard не предлагает нам создать package, и чтобы как-то получить доступ к редактору, выбираем «New Project» (в русской версии — «Новый проект»)
и затем «Application» (в русской версии — «Приложение»)
2. Действуя далее по инструкции, в меню «Package» (в русской версии — «Пакет») выбираем верхний пункт «New package…» (в русской версии — «Новый пакет…»), выбираем имя файла и путь для сохранения. Я назвал свой новый пакет «Game» и разместил его в отдельной папке с тем же названием:
Я создал отдельную папку Lazarus/Cmp в расчете на то, что у меня может появиться несколько разных пакетов с компонентами, и уже в этой папке создал папку «Game».
Если все сделано правильно, на экране должно появиться окно нового (пока пустого) пакета.
3. Действуя дальше опять же по инструкции, для создания нового компонента в окне пакета нажимаем кнопку «Add» (в русской версии — «Добавить) и в выпадающем списке выбираем «New Component» (в русской версии — «Новый компонент»):
В качестве класса-предка указываем TCustomImage — этот класс фактически используется для реализации компонента TImage, но отличается от него тем, что не содержит published properties и позволяет нам самим определить набор свойств, который будет доступен в дизайнере для нашего компонента.
Для тех, кто этого не знает, уточню, что published — это раздел класса (наподобие public), в котором описываются новые или просто указываются унаследованные свойства, которые должны быть доступны в визуальном редакторе свойств на этапе разработки программы. Промежуточные классы не объявляют ничего в этой секции, оставляя возможность программисту самому вынести туда то, что он сочтет нужным. Так, класс TImage не добавляет никакой функциональности, а только помещает в раздел published ряд свойств, унаследованных от родителя TCustomImage. Часть из этих свойств нам нужно спрятать, поэтому мы также унаследуем наш новый компонент от TCustomImage и выведем в published только то, что не противоречит логике нашего компонента.
Хорошим стилем было бы рисовать персональную иконку для каждого нового компонента, но так как наша задача — показать, как это все просто, мы оставим это поле пустым, что приведет к отображению на панели инструментов стандартной иконки, используемой в Lazarus/Delphi для всех самодельных компонентов.
Кстати, упомянутая выше инструкция содержит отдельный раздел, посвященный созданию иконок для компонентов — это для тех, кого не устраивает «дефолтная» иконка.
Заполнив все поля, нажимаем кнопку «Create New Component» (в русской версии — «Создать новый компонент»).Добавляем код в новый компонент
Сразу после создания нового компонента его исходный код получается примерно таким:
unit ImageFragment;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs;
type
TImageFragment = class(TCustomImage)
private
protected
public
published
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Game', [TImageFragment]);
end;
end.
Как и следовало ожидать, объявление класса абсолютно пусто, а имплементация вообще отсутствует. Все, что есть — функция регистрации компонента на вкладке «Game».
Нам нужно добавить несколько унаследованных published properties, создать два своих и переопределить одну виртуальную функцию. Приступим!
0. В секции импорта нам понадобятся два дополнительных модуля: ExtCtrls и LCLProc — добавляем их в раздел uses:
uses
Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, ExtCtrls, LCLProc;
1. Добавляем список published properties, полностью аналогичный компоненту TImage, за исключением нескольких properties, позволяющих изменить масштаб и позицию изображения:
published
property AntialiasingMode;
property Align;
property Anchors;
//property AutoSize;
property BorderSpacing;
//property Center;
//property KeepOriginXWhenClipped;
//property KeepOriginYWhenClipped;
property Constraints;
property DragCursor;
property DragMode;
property Enabled;
property OnChangeBounds;
property OnClick;
property OnDblClick;
property OnDragDrop;
property OnDragOver;
property OnEndDrag;
property OnMouseDown;
property OnMouseEnter;
property OnMouseLeave;
property OnMouseMove;
property OnMouseUp;
property OnMouseWheel;
property OnMouseWheelDown;
property OnMouseWheelUp;
property OnPaint;
property OnPictureChanged;
property OnPaintBackground;
property OnResize;
property OnStartDrag;
property ParentShowHint;
property Picture;
property PopupMenu;
//property Proportional;
property ShowHint;
//property Stretch;
//property StretchOutEnabled;
//property StretchInEnabled;
property Transparent;
property Visible;
end;
Для пущей убедительности я не удалил, а закомментировал те properties, которые есть в компоненте TImage, но будут мешать в нашем новом компоненте TImageFragment.
2. Добавляем в объявление класса два новых properties для задания смещения изображения по горизонтали и по вертикали:
private
FOffsetX: Integer;
FOffsetY: Integer;
procedure SetOffsetX(AValue: Integer);
procedure SetOffsetY(AValue: Integer);
published
property OffsetX: Integer read FOffsetX write SetOffsetX default 0;
property OffsetY: Integer read FOffsetY write SetOffsetY default 0;
и не забываем добавить в имплементацию класса две объявленных процедуры:
implementation
procedure TImageFragment.SetOffsetX(AValue: Integer);
begin
if FOffsetX = AValue then exit;
FOffsetX := AValue;
PictureChanged(Self);
end;
procedure TImageFragment.SetOffsetY(AValue: Integer);
begin
if FOffsetY = AValue then exit;
FOffsetY := AValue;
PictureChanged(Self);
end;
3. Переопределяем виртуальную функцию DestRect:
public
function DestRect: TRect; override;
и добавляем ее реализацию в имплементацию класса:
function TImageFragment.DestRect: TRect;
begin
Result := inherited DestRect();
if (FOffsetX <> 0) or (FOffsetY <> 0) then
LCLProc.OffsetRect(Result, -FOffsetX, -FOffsetY);
end;
Компилируем пакет и пересобираем Lazarus
1. В окне пакета нажимаем кнопку «Compile» (в русской версии — «Компилировать»). Если все сделано правильно, в окне сообщений появится зеленая надпись об успешной компиляции, если нет — надпись будет желтой или красной.
2. В том же окне нажимаем на кнопку «Use» (в русской версии — «Использовать») и в выпадающем меню выбираем второй пункт «Install» (в русской версии — «Установить»). Программа предложит пересобрать и перезапустить IDE — соглашаемся:
3. После перезапуска на панели инструментов появится новая вкладка «Game», а на ней — иконка для нашего нового компонента.
Вместо послесловия
Если тема окажется интересной читателям, я могу дополнить статью разделом о том, как использовать такой компонент — за 5 минут создать окно, в котором анимированный персонаж будет двигаться в разные стороны и поворачиваться в сторону направления движения. А потратив чуть больше времени, можно сделать, к примеру, футбольное поле с парой футболистов, управляемых с клавиатуры. И уже для самых отчаянных — можно писать разные алгоритмы управления футболистами (боты) и устраивать между ними соревнования!