Delphi World - Работа со спрайтами
Delphi World - это проект, являющийся сборником статей и малодокументированных возможностей  по программированию в среде Delphi. Здесь вы найдёте работы по следующим категориям: delphi, delfi, borland, bds, дельфи, делфи, дэльфи, дэлфи, programming, example, программирование, исходные коды, code, исходники, source, sources, сорцы, сорсы, soft, programs, программы, and, how, delphiworld, базы данных, графика, игры, интернет, сети, компоненты, классы, мультимедиа, ос, железо, программа, интерфейс, рабочий стол, синтаксис, технологии, файловая система...
Работа со спрайтами

Содержание

  • Введение
  • Спрайты 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 ... очень удобно переносить код.


Список ссылок:

Проект Delphi World © Выпуск 2002 - 2017
Автор проекта: Эксклюзивные курсы программирования