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).

Вот пример изображения, где спрайты расположены в виде таблицы, у которой каждая строка соответствует определенной проекции, а каждый столбец — определенной фазе анимации:
g1kgc-kfi7lnktzp3npiaed3vhy.png

Зачем так много картинок?

Для отображения такого спрайта достаточно поместить на экран простой компонент, отображающий не все изображение целиком, а только один его фрагмент; и затем, меняя смещение выделенного фрагмента по горизонтали и вертикали, можно заставить персонаж поворачиваться в разные стороны и совершать циклические движения (например, махи крыльями или шаги ногами). Такой прием часто используется при веб-разработке: даже простые наборы иконок для деловой графики часто размещают в одном файле и отображают в разных местах страницы с разным смещением, создавая впечатление разных изображений.

К сожалению, компонент TImage, входящий в стандартную поставку Lazarus (и Delphi), не позволяет показывать произвольный фрагмент изображения: изменяя его свойства, мы можем заставить его показывать только изображение целиком, левый верхний угол или центральную его часть. Для отображения произвольного фрагмента изображения, заданного смещением и размерами по обеим осям, нужен какой-то другой компонент. Но, как выяснилось, сделать его самостоятельно в Lazarus — совсем несложно! Создаем новый компонент
В качестве инструкции по созданию компонентов я воспользовался официальным руководством: wiki.freepascal.org/How_To_Write_Lazarus_Component.

Там все написано достаточно подробно, дублировать не имеет смысла. Я только остановлюсь на некоторых моментах.

1. Стандартный Project Wizard не предлагает нам создать package, и чтобы как-то получить доступ к редактору, выбираем «New Project» (в русской версии — «Новый проект»)
sf9mguxgv3kux3gf9jeika4aoew.png
и затем «Application» (в русской версии — «Приложение»)
se8yp1ohh6ipqartxklpqijmtg0.png

2. Действуя далее по инструкции, в меню «Package» (в русской версии — «Пакет») выбираем верхний пункт «New package…» (в русской версии — «Новый пакет…»), выбираем имя файла и путь для сохранения. Я назвал свой новый пакет «Game» и разместил его в отдельной папке с тем же названием:
-75cji5nhkxma0oabjnsinegqwo.png
Я создал отдельную папку Lazarus/Cmp в расчете на то, что у меня может появиться несколько разных пакетов с компонентами, и уже в этой папке создал папку «Game».
Если все сделано правильно, на экране должно появиться окно нового (пока пустого) пакета.

3. Действуя дальше опять же по инструкции, для создания нового компонента в окне пакета нажимаем кнопку «Add» (в русской версии — «Добавить) и в выпадающем списке выбираем «New Component» (в русской версии — «Новый компонент»):
1vrrhccbkjyh7jte6m0c4j6cfcw.png
В качестве класса-предка указываем TCustomImage — этот класс фактически используется для реализации компонента TImage, но отличается от него тем, что не содержит published properties и позволяет нам самим определить набор свойств, который будет доступен в дизайнере для нашего компонента.

Что такое 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 — соглашаемся:
7bxovpy3xzjrri9jkwyrvoeqwkm.png

3. После перезапуска на панели инструментов появится новая вкладка «Game», а на ней — иконка для нашего нового компонента.

Вместо послесловия
Если тема окажется интересной читателям, я могу дополнить статью разделом о том, как использовать такой компонент — за 5 минут создать окно, в котором анимированный персонаж будет двигаться в разные стороны и поворачиваться в сторону направления движения. А потратив чуть больше времени, можно сделать, к примеру, футбольное поле с парой футболистов, управляемых с клавиатуры. И уже для самых отчаянных — можно писать разные алгоритмы управления футболистами (боты) и устраивать между ними соревнования!

© Habrahabr.ru