Работа со спрайтами
Содержание
- Введение
- Спрайты 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;
begin
...
Wizard := Tbitmap.Create;
Wizard.Loadfromfile('spr1.bmp');
WizardMask := Tbitmap.Create;
WizardMask.Loadfromfile('spr2.bmp');
// Копируем маску в буфер BitBlt(Buffer.Canvas.Handle,X,Y,50,50,
Buffer := Tbitmap.Create;
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
|
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. У вас может возникнуть вопрос - почему передвижение спрайта за мышкой. Ответ: наименьшие затраты на писанину тест программы, при неплохом разнообразии движения.
Использование внешних процедур для 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; - спрайт
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
Чем плохи рассмотренные выше методы вывода спрайтов - они медленные. Хочу подчеркнуть, что для каждой программы нужно выбирать свои методы написания. Конкретное задание требует своих средств исполнения. То что 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 ... очень удобно переносить код.
Список ссылок:
|