Работа со спрайтами

Введение

Для начала нужно разобраться, что же такое спрайт. Вот такое описание я нашел в книге Андрэ Ла Мота:

" Знаете, есть такой газированный напиток... Снова шучу. На самом деле спрайты - это маленькие объектики, которые находятся на игровом поле и могут двигаться. Этот термин прижился с легкой руки программистов фирмы Atari и Apple в середине 70-х годов. Спрайты - это персонажи в играх для ПК, которые могут без труда перемещаться по экрану, изменять цвет и размер "

И так, спрайт это персонаж игры. Не углубляясь в дебри программирования, могу сказать что спрайт это массив из цветов - для простоты представим его как BMP файл или TBitmap, тем более что, этот формат поддерживаемый windows и не содержащий компрессии.

Что нам нужно от спрайта - заставить его появляться на экране и образовывать анимацию. Анимация это не только смена координаты спрайта, но и изменение самой картинки. Следовательно спрайт может иметь не одно изображение, а несколько. Смена их и приводит к анимации.

Как я уже говорил спрайт это матрица. При вписывании в кравдрат ( прямоугольник ) сложного объекта, например волшебника из рисунка ниже, остается свободное пространство. Его заполняют цветом, которого нет в изображении самого объекта. При простом копировании этой матрицы ( или для простоты BMP или TBitmap ) на экран выводится и волшебник и фон под ним. Но нам это не всегда, подчеркну не всегда, нужно. Если спрайт выводится на фон, то он затирает все квадратную область. Как я уже говорил спрайт это матрица. При вписывании в кравдрат ( прямоугольник ) сложного объекта, например волшебника из рисунка ниже, остается свободное пространство. Его заполняют цветом, которого нет в изображении самого объекта. При простом копировании этой матрицы ( или для простоты BMP или TBitmap ) на экран выводится и волшебник и фон под ним. Но нам это не всегда, подчеркну не всегда, нужно. Если спрайт выводится на фон, то он затирает все квадратную область.

Не правда ли есть разница, и довольно заметная. При выводе на экран использовался один и тот же рисунок, но все зависит от способа выведения спрайта.1-й способ ( маг в белом квадрате ) основан на простом копировании одной области памяти в другую. 2-й способ ( маг на фоне ) то же копирование, но интеллектуальное. Копирование происходит по следующему алгоритму: Если цвет копируемого элементы матрицы ( область памяти ) соответствует значению цвета Transparent Color, то копирования не происходит, переходим к следующему элементу. 3-й способ так же основан на копирование области памяти, но с применением логических операций - маски.
Спрайты c готовой маской

Способов вывести спрайт на поверхность экрана много. Рассмотрим один из них. Это способ, когда отдельно рисуется спрайт и отдельно маска. Для этого нам понадобится сам спрайт, его маска и буфер. Спрайт
Маска спрайта

И спрайт и маска должны иметь одинаковый размер, в данном примере 50x50. Для чего нужна маска? Она нужна для того, чтобы при выводе спрайта не затиралось изображение, которое находится под ним. Маску можно заготовить отдельно в BMP файле - более быстрый способ, а можно рассчитать программно.Спрайт и маску помещаем в TBitmap. Wizard:=Tbitmap.Create;
Wizard.Loadfromfile('spr1.bmp'); // Bitmap для спрайта
WizardMask:=Tbitmap.Create;
WizardMask.Loadfromfile('spr2.bmp'); // Bitmap для маски

Ну вот, у нас есть спрайт, маска и нам это вывести его на экран. Для этого существует функция Win32Api: BitBlt (param_1, X1, Y1, dX1, dY1, param_2, X2, Y2, param_3);

Param_1 - Handle на поверхность куда выводить.
X1, Y1 - Смещение от начала координат.
dX1, dY1 - Размер выводимого изображения.
Param_2 - Handle откуда брать.
X2, Y2 - Размер выводимого изображения.
Param_3 - Параметры копирования.

Для нашего случая: BitBlt(Buffer.Canvas.Handle, X, Y, 50, 50, WizardMask.Canvas.Handle, 0, 0, SrcPaint); BitBlt(Buffer.Canvas.Handle, X, Y, 50, 50, Wizard.Canvas.Handle, 0, 0, SrcAnd);

SrcPaint - Копировать только белое.
SrcAnd - Копировать все кроме белого.

Сначала выводим маску с параметром SrcPaint, а затем в тоже место ( координаты X, Y) сам спрайт с параметром SrcAnd. Осталось рассмотреть зачем же нужен буфер. При выводе одного спрайта вы не почувствуете мелькания изображения, но когда их будет 100-200 это будет заметно. По этому все спрайты копируются в буфер - это Tbitmap размером с экран или окно, короче изменяемой области. Вот как окончательно будет выглядеть фрагмент программы : ...
var
Wizard, WizardMask, Buffer:Tbitmap;
X, Y:integer;
...
Wizard:=Tbitmap.Create;
Wizard.Loadfromfile('spr1.bmp');
WizardMask:=Tbitmap.Create;
WizardMask.Loadfromfile('spr2.bmp');
Buffer:=Tbitmap.Create; // Копируем маску в буфер
BitBlt(Buffer.Canvas.Handle, X, Y, 50, 50, WizardMask.Canvas.Handle, 0, 0, SrcPaint);
// Копируем спрайт в буфер
BitBlt(Buffer.Canvas.Handle, X, Y, 50, 50, Wizard.Canvas.Handle, 0, 0, SrcAnd);
...
// Перемещаем буфер на форму BitBlt(Form1.Canvas.Handle, 0, 0, 320, 240, Buffer.Canvas.Handle, 0, 0, SrcCopy);

Флаг SrcCopy означает копирование без изменения, аналогичен простому перемещению одного участка памяти в другой.Не нужно думать, что готовая маска это прошлое компьютерных игр. В любом случае, маска создается, только иногда это делается программно, а иногда заготавливается в виде отдельного файла. Какой вариант лучше, нужно смотреть по конкретному примеру.Я не буду расписывать все параметры BitBlt, если интересно смотрите сами в Delphi Help. Ну вот и все.

Напоследок исходники и картина творчества

.Cпрайты c программной маской - Transparent

Другой метод вывода спрайтов - методом программной маски. Этот способ, немного медленнее, но не требует возни с изготовлением масок. Это не значит, что маски вообще нет. Маска присутствует и создается в памяти. Для счастливых обладателей Windows NT подойдет способ, который используется в самой ОС. Это функция MaskBlt. Судя по ее названию, она позволяет выводить растры используя битовые маски.Привиду пример на спрайтах из игры Эпоха Империй I. Наша задача, как и во всех предыдущих примерах, вывести спрайт с Transparent Color (по русски плохо звучит). В игре он черный.
Начальный вариант спрайта Это уже полученная маска Вызвали MaskBLT MaskBlt + BitBlt
Рис 1 Рис 2 Рис 3 Рис 4

var
Sprite, Mask:TBitmap;
begin
Sprite:=TBitmap.Create;
Sprite.LoadFromFile('G0100219.bmp');
Mask:=TBitmap.Create;
Mask.LoadFromFile('G0100219.bmp');
Mask.Mask(clBlack); // Создание маски
// Преобразование в маску, после этого получится Bitmap, представленный на Рис 2
MaskBlt(Form1.Canvas.Handle, 10, 10, Sprite.Width, Sprite.Height, Sprite.Canvas.Handle, 0, 0, Mask.MaskHandle, 0, 0, SRCPAINT);
// После вызова этой функции, экран выглядит как на рисунке 3.
BitBlt(Form1.Canvas.Handle, 10, 10, Sprite.Width, Sprite.Height, Sprite.Canvas.Handle, 0, 0, SRCPAINT);
end;

С Windows NT все понятно, но как быть в других ОС? ( Хотя возможно, эта функция появится(-лась) в Windows 2000 и Windows Me). Использовать библиотеки сторонних разработчиков. Если они поставляются с исходным кодом, то вы можете перенести необходимые вам процедуры в собственный модуль. Я нашел самую быструю библиотеку для работы с графикой - Media Library Component Version 1.93. В примере используется только часть ее. Нам понадобится только одна процедура: DrawBitmapTransparent(param_1, X, Y, param_2, param_3);

param_1 - Canvas, куда копировать
X, Y - Смещение
param_2 - TBitmap, что копировать.
param_3 - TColor, цвет Transparent - этот цвет не будет копироваться

Применение только данной библиотеки не принципиально. Практически в любом наборе VCL компонентов от сторониих производителей есть процедуры или функции для вывода Bitmap с использованием цвета прозрачности. Такие процедуры есть в библиотеке RXLib, LMD Tools, Cool Control и многих других.Для нашего примера: DrawBitmapTransparent(Buffer.Canvas, WizardX, WizardY, Wizard, clRed); Спрайт должен выглядеть так:

Небольшое замечание по поводу Transparent. Цвет надо выбирать такой, которого нет на самом спрайте, иначе неизбежны "дырки" в изображении. Лучше всего такой : #00FF00 - ярко зеленый, но можно использовать черный или белый.В предыдущей главе "Работа спрайта c готовой маской" я подвесил передвижение спрайта на таймер: procedure TForm1.Timer1Timer(Sender: TObject);
begin
... // тело цикла
end.

Да cпособ хорош, но не так быстродейственен. Есть еще пара вариантов :

1. Создать поток TThread - в примере разобран именно он.
2. "Подвесить" на IDLРассмотрим сначала второй способ т.к. он наименее прогрессивен:) Пишем такую процедуру: procedure TForm1.Tic(Sender: TObject; var Done: Boolean);
begin
...
// Сюда заносим, что надо исполнять.
...
Done := false;
end;

.... и еще немного: procedure TForm1.FormCreate(Sender: TObject);
begin
...
Application.OnIdle := Tic;
end;

Способ быстрее в 1-2 раз чем таймер, но не лишен недостатков. Не буду объяснять почему. Первый способ самый оптимальный для игры, как самой сложной так и простой. Реализуется он с помощью потоков. В игре их можно создать несколько - один для обработки графики, другой для AI, третий для музыки и т.д. У каждого потока свой приоритет, но высший только у одного. При работе с несколькими потоками не забывайте их "прибивать" при выходе из программы.Сначала заводим новый класс: TGameRead=class(TThread) // класс для таймера игры
protected
procedure Execute;override; // Запуск
procedure Tic; // Один тик программы
end;

Потом переменную : var
...
T1:TGameRead;
...

Описываем процедуры класса : procedure TGameRead.execute;
begin
repeat
synchronize(Tic);
until Terminated
end;procedure TGameRead.Tic;
begin
...
// Тут пишем все как в TTimer - OnTime
...
end;

В событии Form1.Create инициализируем поток, и задаем приоритет. Расписывать все приоритеты не буду, читайте Delphi Help ...и не забываем убрать за собой: ...
T1:=TGameRead.Create(false); // Создаем поток
T1.Priority:=TpHighest; // Ставим приоритет

...
procedure TForm1.FormDestroy(Sender: TObject);
begin
T1.Suspend;// Приостановим и прибьем
T1.Free;
end;

Ну вот и все. Ах да, вас наверное заинтересовала строчка FPS. Так это тоже самое, что выдает Quake на запрос "showframerate" или что-то такого плана - количество кадров в секунду. Делается это так : заводится переменная: var
G:integer;
...

При каждом вызове потока Tic, она увеличивается на единицу: procedure TGameRead.Tic;
begin
...
Inc(G); // Увеличиваем значение G
end;

Создаем таймер с интервалом 1000 - это 1 секунда, и в событии OnTime выводим значение G в метку. В значении G будет количество вызовов процедуры DoSome за 1 секунду: procedure TForm1.Timer1Timer(Sender: TObject);
begin
label1.caption:='FPS :'+IntToStr(G);
G:=0; // Обнуляем G
end;

На моем средненьком Pentium AMD 233 c Intel 740 8M - выдает 90-100 кадров в секунду, при окне 360X360. Для начала неплохо! Исходники тут, картинка перед вами.P.S. У вас может возникнуть вопрос - почему передвижение спрайта за мышкой. Ответ: наименьшие затраты на писанину тест программы, при неплохом разнообразии движения.



Источник: http://www.delphigfx.narod.ru