Работа со спрайтами
Содержание
Введение
Спрайты c готовой маской
Cпрайты c программной маской Transparent
Использование TImageList
Использование Direct X
Список ссылок
Введение
Для начала нужно разобраться, что же такое спрайт. Вот такое описание я нашел
в книге Андрэ Ла Мота:
" Знаете, есть такой газированный напиток... Снова шучу. На самом деле спрайты
- это маленькие объектики, которые находятся на игровом поле и могут двигаться.
Этот термин прижился с легкой руки программистов фирмы 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" или что-то такого плана - количество
кадров в секунду. Делается это так : заводится переменная:
При каждом вызове потока 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. У вас может возникнуть вопрос - почему передвижение спрайта
за мышкой. Ответ: наименьшие затраты на писанину тест программы, при неплохом
разнообразии движения.
Использование внешних процедур для Transparent вывода спрайтов,
хорошо но есть несколько минусов данного способа:во первых эти процедуры не слишком
оптимизированы - их основное предназначение вывод простеньких элементов приложения,
таких как иконок, картинок кнопок и т.д. Хотя это не относится к некоторым библиотекам,
код которых на 90% состоит из ассемблера.во вторых хранить выводимое изображение
нужно в bmp файле, хотя подойдет и любой другой, не применяющий компрессию с потерей
( Jpeg) . Если картинок более 1-й, а при нормальной анимации их набирается порядка
150-200 на один юнит, то сложно получать именно нужный участок файла.Приведу пример.В
bmp файле содержатся 8 картинок - 64x64 пикселя. Нужно получить доступ к 6-й картинке
( на рисунке помечена розовым квадратом)- ее координаты будут 128,64
Чтобы получить следующий кадр анимации, нужно снова ко номеру
кадра считать координаты … Не совсем удобно. Все эти проблемы можно решить используя
TImageList.
Использование TImageList
Используя этот компонент можно не думать о координатах картинки,
цвете прозрачности - он решает сразу две проблемы. Разберем что нужно сделать,
для вывода спрайта с использованием TImageList. Во первых нужно загрузить набор
спрайтов TImageList, для этого лучше всего использовать команду:
TImageList.AddMasked(Image: TBitmap; MaskColor: TColor): Integer; |
Первый параметр - это Bitmap, второй Transparent Color - цвет прозрачности.
Если Вам не нужно использовать цвет прозрачности, то нужно использовать процедуру
Add. После загрузки всех картинок, можно приступать к их выводу на экран. Для
этого существует процедура:
procedure TImageList.Draw(Canvas: TCanvas; X, Y, Index: Integer); |
Первый параметр Canvas на который будет произведена отрисовка, второй и третий
координаты для вывода X и Y а четвертый индекс или порядковый номер выводимого
изображения. Для примера:
ImageList1.Draw(Canvas,0,0,6); // Тот же самое, но
с использованием BitBlt:BitBlt(Canvas.Handle,0,0,64,64,Bitmap_Mask.Canvas.Handle,128,64,SrcPaint);
- маска BitBlt(Canvas.Handle,0,0,64,64,Bitmap.Canvas.Handle,128,64,SrcAnd;
- спрайт |
Думаю пояснять нет нужды, что использовать TImageList лучше, и проще. Пример
работы с TImageList описан в файле. Там показана анимация персонажа из игры WarCraft
и Warlord III. Я так и не разобрался как работает механизм отрисовки в TImageList.
Мои раскопки привели к такой функции :
function ImageList_Draw(ImageList: HImageList; Index: Integer; Dest:
HDC; X, Y: Integer; Style: UINT): Bool; stdcall; и
function ImageList_DrawEx(ImageList: HImageList; Index: Integer; Dest: HDC;
X, Y, DX, DY: Integer; Bk, Fg: TColorRef; Style: Cardinal): Bool; stdcall;
HImageList - Handle на TImageList. |
Так как вызывается экспортируемая процедура, находящаяся в библиотеке Comctl32.dll
то остается не понятным, какие алгоритмы используются при выводе изображения.
Могу только сказать, что при добавлении нового изображения, добавляется как изображение
так и маска.
Я заинтересовался данным вопросом и продолжал копать стандартные библиотеки
Windows и компоненты. Возможно информация по данным вопросам содержится во многочисленных
SDK, выпускаемых Microsoft. В компоненте TFastDIB я наткнулся на процедуру Draw:
procedure TFastDIB.MaskDraw(fDC,x,y:Integer;c:TFColor);
begin
TransBlt(fDC,x,y,Width,Height,hDC,0,0,Width,Height,PDWord(@c)^);
end; |
Естественно меня заинтересовала процедура TransBlt и вот что я нашел:
function TransBlt(p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11:DWord):BOOL; stdcall;
...
function CreateDIB; external 'gdi32.dll' name 'CreateDIBSection';
function TransBlt; external 'msimg32.dll' name 'TransparentBlt';
function AlphaBlt; external 'msimg32.dll' name 'AlphaBlend'; |
Мне захотелось посмотреть, а что еще может библиотека 'msimg32.dll' и вот полный
список:
AlphaBlend
GradientFill
TransparentBlt
DllInitialize
vSetDdrawflag
Все, хватит, а то некоторые читатели и так ничего не поняли. Но для интересующихся
скажу - не все процедуры и функции описаны в Delphi, многое не документировано.
Дальнейшие раскопки показывают, что данная библиотека используется воспроизведения
видео, в такой программе как Медиа плайр. Причем, что замечательно, по возможности
она использует DirectX функции. Более подробно работа с данной библиотекой описана
в MSDN.
Использование Direct X
Чем плохи рассмотренные выше методы вывода спрайтов - они медленные. Хочу подчеркнуть,
что для каждой программы нужно выбирать свои методы написания. Конкретное задание
требует своих средств исполнения. То что Microsoft написал библиотеку Direct X
не значит что тут же нужно писать всю графику используя ее.
Приведу пример. Самая популярная игра для Windows - Quake II, Warcraft, Diablo
- нет САПЕР и ПАСЬЯНС. Можете не верить, но это факт. В первую категорию играют
ограниченный контингент людей в последнюю играли ВСЕ. Я это говорю к тому, что
если вы пишите графическое приложение, то нужно ориентироваться на его потребности
и выбирать соответствующие технологию зависимости от них. Какие это потребности:
необходимость вывода большого количества часто сменяющихся изображений
большой объем графической информации
аппаратная поддержка
максимальное быстродействие.
Используя Direct X можно получит все вышеперечисленное. Набор этих библиотек,
изначально разрабатывался как средство для работы с графикой. Что было, когда
писали под DOS: строили в участке памяти ( back buffer ) какое то изображение
или копировали туда спрайты, а потом перемещали этот back buffer в область "экранной"
памяти. Сразу отрисовывался весь экран. С приходом Windows, переместить участок
памяти в экранную область не возможно. Приходится использовать Canvas, Handle.
DirectX позволяет решить все эти проблемы. Вы можете подготавливать изображение
на так называемой поверхности, и потом FLIP и вся поверхность становится видимой
- копируется в экранную область видеопамяти. Должен заметить, что алгоритм работы
ничуть не меняется.
С появлением DirectX появились и аппаратные поддержки таких необходимых вещей
как: Trancparent Color и Bit blitting.
Термин бит-блиттинг означает процесс перемещения группы битов (образа) из одного
места экрана в другое или памяти. В играх на ПК нас интересует перемещение образа
из области хранения вне экрана в область видеобуфера. Кто интересуется аппаратными
возможностями своей видео карты, то их можно узнать достав Microsoft DirectX CPL.
В ней можно просмотреть, какие функции в видео карте реализуются аппаратно, а
какие програмно.
Итак процесс работы таков, загружаете спрайты на поверхность (ISurface) затем
нужно вызвать процедуру BLT или BLTFAST, потом поменять буферную и видимую поверхность
командой FLIP и все.В начале раздела я написал Direct X, но я несколько обманул
Вас. Я расскажу как выводить спрайты с помощью Direct X, но с использованием набора
VCL компонентов DelphiX . Я это делаю по той простой причине, что если я напишу
пример используя стандартные модули DirectX то их некоторые не поймут, отчаяться
и бросят программировать вообще :) Согласитесь не все сразу поймут, что делает
данная процедура, хотя она всего лишь меняет поверхности.
var
hRet : HRESULT;
begin
Result := False;
while True do
begin
hRet := FDDSPrimary.Flip(nil, 0);
if hRet = DD_OK then Break else
if hRet = DDERR_SURFACELOST then
begin
hRet := RestoreAll;
if hRet <> DD_OK then
Exit;
end
else if hRet <> DDERR_WASSTILLDRAWING then
Exit;
end;
Result := True;
end; |
По этому я и решил использовать DelphiX. Писать
с помошью него очень просто. Нам потребуется всего два компонента. Первый TDXDraw
- если объяснить коротко, то это аналог TCanvas. Еще один компонент это TDXImageList
- прямой аналог TImageList, единственно все элементы являются TDIB и не содержат
ни каких масок. Что нужно сделать чтобы успешно создать и анимировать спрайт.
Как и с TImageList нужно загрузить BMP файл в элемент TDXImageList. Элемент TImageList
предварительно нужно создать в программе или создать из Object Inspector.
DXImageList1.Items[0].picture.LoadFromFile('golem_start.bmp');
// Для вывода нужно использовать процедуру: DXImageList1.Items[0].draw(DXDraw1.Surface,0,0,6);
|
Вот и все. Прямая аналогия с TImageList ... очень удобно переносить код. Пример
использования можно посмотреть тут.
Список ссылок
Адрес автора |
|
Исходный код примера с готовыми масками |
|
Исходный код примера с програмными масками |
|
Исходный код примера c использованием DirectX |
|
Библиотека Media Library Component Version 1.93 |
|
Библиотека DelphiX |
|
Библиотека FastDib / есть процедуры TransparentDraw / |
|
Статья "Обзор формата DIB и компонентов для работы с
ним" |
|
Пример
работы со спрайтами: перемещение, создание, уничтожение ( смерть юнита ), перемещение
в заданную точку, атака противника... Спрайты взяты из игры Diablo. Для компиляции
потребуется DelphiX |
|
|