Delphi GFX

         

Анимированные cпрайты в TrueSpace


Copyright © 2000 Мироводин Дмитрий

Как создать высокохудожественное изображение не имея хороших навыков рисования? Раньше все спрайты приходилось рисовать по точкам или в простеньких редакторах а-ля MS Paint ( хотя году в 92-93 такой редактор считался вполне нормальным :). С появлением таких пакетов как 3D Studio, Ray Dream Studio и т.д. положение изменилось. Весь процесс создания заключается в изготовлении единичной модели, которую потом можно отрендерить под любым углом и придать необходимые движения.

Но вернемся к практической части. Первое: нужно достать непосредственно сам пакет 3D графики. Я выбрал TrueSpace 4 по нескольким причинам:

Он не так требователен к ресурсам : вполне нормально работает на P-233 с 32Mb, a 3D Studio нужно 48Mb минимум Более визуализирован: все операции можно производить прямо на объекте. Экспортирует в формат Direct3D - *.x На мой взгляд его проще понять без книжки. Интуитивный интерфейс.

Демо версию TrueSpace 4.3 можно скачать на сервере фирмы-разработчика: Caligari, или Вы можете поискать программу на многочисленых FTP архивах. На данный момент доступна версия 5.X, но честно говоря все необходимые функции есть и в старой версии.



DirectX


Почему, уже рассказав в первой части статьи, о принципах создания кланов, я взялся за написание второй части статьи? Половина, а то и больше, игр пишется под DirectX, поэтому я просто счел своим долгом осветить технические особенности все тех же операций из первой части статьи, но уже под DirectX. Ну, давайте начнем….

Практически любое игровое приложение связано с понятиями видеорежимов, разрешения экрана, глубины цвета и другими. В нашем случае, мы тесно связанны с машинным представлением цвета, поэтому сперва я расскажу об используемых в большинстве игр видеорежимах. Обычно в своих опусах я пишу тот материал, который Вы не найдете в книгах, поскольку программисты почему-то предпочитают не освещать подобные проблемы, но следующий материал взят (вернее не взят, т.к. я излагаю его своим языком) из книги Стена Трухильо "Графика для Windows средствами DirectDraw" - настоятельно рекомендую.

В DirectX предусмотренно 4 видеорежима, соответственно 8 бит, 16 бит, 24 бита и 32 бита.

Итак, рассмотрим восьмибитный режим. В общем-то, на нем не стоило останавливаться, потому что современные игровые приложения его уже не используют, но поскольку в заголовке статьи указана игра, которая была написана именно в этом режиме, я решил все же немного рассказать и о нем.

Восмибитный режим кое в чем удобен - для определения цвета в нем используется один байт. Максимальное количество цветов соответственно - 256. Само значение, заносимое в этот байт, не является цветом, а лишь ссылкой на палитру цветов, где хранятся RGB цветовые компоненты ( индексом в массиве цветов ). Так вот, когда писали игру Warcraft, скорее всего пошли следующим путем: Часть палитры определили под изменяющиеся цвета, например: У Вас есть два клана - синий и зеленый, выделяем на синие и зеленые цвета по 16 элементов палитры с номерами 0-15 и 16-31 соответственно. Рисуем все спрайты, делая изменяющиеся их части одним из этих цветов, скажем синим. При выводе спрайта на экран, смотрим какого цвета его клан должен быть. Допустим он должн быть зеленым. Затем попиксельно просматриваем спрайт. Если находим пиксели с номерами 0-15 (синий цвет), меняем их на соответствующие с номерами (16-31). К примеру это может выглядеть так:

Color : Byte; // Цвет пикселя
Const Blue = 0;
Green = 1;
If (Color div 16) = Blue then Color = Green*16 + ( Color mod 16 );

Эта информация не сочетается с той, что я выдал в первой части статьи, но это обусловлено особенностью именно восьмибитного режима.

Теперь, перепрыгнув через 16-битный режим, рассмотрим режим 24-бита. О 16-битном режиме разговор особый. В 24-битном режиме цвет представляется тремя байтами, каждый из каторых содержит одну цветовую компоненту - RGB соотоветственно. С этим режимом работать достаточно легко. Создание кланов происходит так же, как я писал в первой части статьи, за тем лишь исключением, что обращаться за пикселями приходится не к Tbitmap, а к TdirectDrawSurface, поскольку мы имеем дело с DirectX. Что такое TdirectDrawSurface я объяснять не буду, для этого Вам придется прочесть специальную литературу (ниже указан список), а как с этим работать поясню в примере для 16-битных поверхностей.

Теперь мы подошли к самому распространенному и самому сложному режиму - 16 бит. Как Вы уже поняли, цвет в этом режиме представлен двумя байтами. Это 64 кб цветов. Этого достаточно для самой взыскательной игры и занимает на одну треть меньше памяти, чем 24-битный цвет. Но за все надо платить - возни с ним побольше.



Во-первых, есть два типа этих режимов: в одном из них RGB компоненты в цвете занимают 15 бит, в другом 16 бит. Это иллюстрирует следующий рисунок:

Верхний вариант, который не использует один бит, иногда обозначают 555, нижний - где у зеленой компоненты на один бит больше, иногда обозначают 565.

Но это еще не все, помимо этих режимов еще существуют два таких же, но, с переставленными красной и синей компонентами, то есть вместо RGB - BGR. Причем Вы никогда не угадаете, какой из этих режимов будет у компьютера активизирован, это зависит от конкретного видеоустройства. Иногда не требуется знать, как именно представлен цвет в компьютере, но не в нашем случае, мы ведь выполняем побитовые операции с цветом. К счастью в DirectX реализованна возможность узнать с каким режимом мы работаем в данный момент.

Кроме этой проблемы, существует еще проблемы ширины картинки. В DirectX картинки хранятся на поверхностях TdirectDrawSurface. Так вот, оказывается, что ширина поверхности не всегда соответствует ширине картинки. Это тоже зависит от конкретного видеоустройства. Иными словами иногда DirectDraw выделяет памяти немного больше, чем надо - некоторые видеоустройства требуют чтобы ширина поверхности была кратна 12, если у вашей картинки ширина не кратна 12, то все равно под поверхность будет выделен участок памяти с шириной кратной 12. Это, при попиксельном доступе к поверхности, тоже необходимо учитывать, если Вы не хотите видеть Ваш спрайт перекошенным. К счастью DirectX также дает возможность узнать шаг (ширину поверхности) в байтах.

Ну, вот, в кратце, я рассказал про видеорежимы, теперь рассмотрим исходный текст демонстрационной прграммы.

Примечание: Прграмма написана на Delphi 5 с использованием компонент DelphiX, скачать можно тут.

Картинка со спрайтом помещается в компонент DXImageList под именем Sprite, картинка с изображением серой маски помещается под именем Mask.

Для начала нам понадобятся битовые цветовые маски красного, синего, зеленого, желтого, сиреневого, голубого цветов.

var
...
RMask,GMask,BMask, YMask,FMask,AMask : Word;
... // Получить их можно следующим образом:
RMask := DXDraw1.Surface.SurfaceDesc.ddpfPixelFormat.dwRBitMask;
GMask := DXDraw1.Surface.SurfaceDesc.ddpfPixelFormat.dwGBitMask;
BMask := DXDraw1.Surface.SurfaceDesc.ddpfPixelFormat.dwBBitMask;
// Маска желтого цвета получается сложением по системе OR зеленой и красной маски:
YMask := DXDraw1.Surface.SurfaceDesc.ddpfPixelFormat.dwRBitMask or DXDraw1.Surface.SurfaceDesc.ddpfPixelFormat.dwGBitMask;
// Маска сиреневого цвета получается сложением по системе OR синей и красной маски:
FMask := DXDraw1.Surface.SurfaceDesc.ddpfPixelFormat.dwRBitMask or DXDraw1.Surface.SurfaceDesc.ddpfPixelFormat.dwBBitMask;
// Маска голубого цвета получается сложением по системе OR зеленой и синей маски:
AMask := DXDraw1.Surface.SurfaceDesc.ddpfPixelFormat.dwGBitMask or DXDraw1.Surface.SurfaceDesc.ddpfPixelFormat.dwBBitMask;

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

procedure TForm1.CloneSprite( ColorMask : Word);
var
  //Объект - поверхность DirectDraw SurfaceDescSprite
  SpriteSurface, MaskSurface : TDirectDrawSurface;  
  SurfaceDescMask : TDDSurfaceDesc; // Структура описывающая поверхность
  pBitsSprite,
  pBitsMask : PWordArray; // Указатель на начало области памяти   поверхности
  SurfaceHeight: Integer;
  SurfaceWidth: Integer; // Размеры поверхности
  i,j : Integer; // Циклические переменные
  MaskColor : Word; // Цвет пикселя на серой маске (временная переменная)
begin
  DXTimer.Enabled := False;
  // Отключить таймер ответственный за перерисовку
  // Здесь происходит присваивание ссылок на поверхность временным переменным

  SpriteSurface := DXImageList.Items.Find('Sprite').PatternSurfaces[0];
  MaskSurface := DXImageList.Items.Find('Mask').PatternSurfaces[0];

  // Для получения прямого доступа к поверхности ее надо заблокировать,
  // в параметрах передается прямоугольник на поверхности к которому
  // требуется   получить доступ и структура с информационными полями

  SpriteSurface.Lock(SpriteSurface.ClientRect,SurfaceDescSprite);
  MaskSurface.Lock(MaskSurface.ClientRect,SurfaceDescMask);

  // После блокировки поля структуры будут содержать необходимую нам информацию   
  // Получить высоту поверхности
  SurfaceHeight := SurfaceDescSprite.dwHeight;

  // Получить ширину поверхности в байтах, напомню, что она может отличаться от
  // ширины нашей картинки (спрайта), этот параметр надо разделить на 2, т.к. у
  // нас цвет кодируется двумя байтами
  SurfaceWidth := SurfaceDescSprite.lPitch div 2;

  // Получить указатели на поверхности спрайта и серой маски
  pBitsSprite := SurfaceDescSprite.lpSurface;
  pBitsMask := SurfaceDescMask.lpSurface;

  // В цикле по строкам и столбцам изображения производим сложение пикселей
  // серой маски с цветовой маской и присваиваем полученное пикселям спрайта
  for j := 0 to SurfaceHeight - 1 do
    for i := 0 to SurfaceWidth - 1 do
    begin
      // Получить пиксель серой маски
      MaskColor := pBitsMask[j*SurfaceWidth + i];
      // Если он не черный, то
      if MaskColor <> 0 then
      // Сложить с цветовой маской и присвоить пикселю спрайта
      pBitsSprite[j*SurfaceWidth + i] := MaskColor AND ColorMask;
    end;
  // Не забыть разблокировать поверхности иначе компьютер зависнет в мертвую
  SpriteSurface.UnLock;
  MaskSurface.UnLock;  
  DXTimer.Enabled := True; // Включить таймер перерисовки
end;

Вывод спрайта на экран осуществляется в обработчике события OnTimer, компонента TDXTimer:

procedure TForm1.DXTimerTimer(Sender: TObject; LagCount: Integer);
begin
  DXDraw1.Surface.Fill(0);// Очистить буфер
  // Нарисовать спрайт
  DXImageList.Items.Find('Sprite').Draw(DXDraw1.Surface,0,0,0);
  // Вывести информацию о частоте кадров
  with DXDraw1.Surface.Canvas do
  begin
    Brush.Style := bsClear;
    Font.Color := clWhite;
    Font.Size := 12;
    Textout(0, 0, 'FPS: '+inttostr(DXTimer.FrameRate));
    Release;
  end;
  // Переключить поверхности
  DXDraw1.Flip;
end;

Вот, собственно и все. Для полного понимания смотрите тексты примера. Если возникнут какие-нибудь вопросы пишите мне на email, который указан в Copyright. Примеры для данной статьи качать тут



Добавление в программу изображений


Работа с битмапами в DelphiX обычно трудностей не вызывает. Все, что вам нужно сделать - создать картинку и затем добавить компоненту

TDXDib или
TDXImageList. Для нашего примера перетащим на форму TDXImageList (и назовем ее DXImageList). В инспекторе объектов вы увидите 2 свтойства: DXDraw и Items.

DXDraw определяет поверхность DirectDraw, на которой будет рисоваться эта картинка (или серия картинок). Возможно использование нескольких поверхностей. В нашем случае просто выберите уже созданную поверхность DXDraw.

Items содержит все изображения в серии. Для добавления новых изображений нажмите на кнопку "..." и добавьте свои картинки.



Изучаем таймер


Следующий шаг в нашем простом примере - добавление таймера

(назовем его DXTimer). Как вы наверное знаете, SystemTimer, входящий в стандартный набор компонент Delphi, не очень точен для использования в играх. DXTimer имеет разрешение, близкое к миллисекунде, что вполне достаточно для наших (и более серьезных) целей. Установите его свойства следующим образом:

ActiveOnly = true (таймер всегда активен) Enabled=false (мы сами будем управлять им - запукать и останавливать) Interval=0 (максимальная частота срабатывания таймера)

Еще одна очень приятная фича DXTimer'а - свойство DXTimer.FrameRate. Оно позволяет получить значение FPS в любой момент времени. Событие таймера обычно используется для методов типа DXDraw.Flip, DXDraw.Render, DXDraw.Update и др. В нашем примере используется метод DXDraw.Flip.



Этап 1 - сделать нужную модель


Здесь можно поступить двумя путями - скачать готовые модели со специальных серверов, либо делать что-то самому. Быстрее всего достать уже готовую и изменить ее для своих целей. Но можно делать с нуля, применяя различные модификаторы к стандартным примитивам. В примере я взял готовую модель самолета B-25:

Выглядит она впечатляюще. Маленький совет на : стадии работы не заливайте модель текстурами - это сильно тормозит работу компьютера. Текстуры наложите только в последнюю очередь.



Этап 2 - задание движения


Допустим наш самолет летит только вперед и делает поворот вправо и влево. Для этого модель надо повернуть по ходу его полета. Вызываем меню объекта ( правый клик на значке курсора ) и вводим параметры поворота модели: X : 0, Y : 0, Z : -90.

Далее приводим модель в начальное положение - крайне правое. Для этого установим поворот Y : 45. Далее инициализация анимации : нажмите кнопку 2 на рисунке ( Record ) и запускаем анимацию - кнопка 3. Программа запомнила начальное положение объекта.

Теперь введем количество кадров для анимации ( на рис цифра 1 ) 30 кадров и развернем самолет в его конечное положение ( крайне левое ). Для этого введем Y : -45 градусов. Все - теперь нажав кнопку Play вы сможете увидеть поворот самолета. Поворачивая камеру Вы можете создать анимацию под любыми углами. Все зависит от выбора вида в игре.



Этап 3 - заключительный этап рендеринг


Ддля некоторых он может быть головной болью из-за нехватки быстродействия. Каждый кадр анимации записывается в отдельный файл или в видео ролик. Тут все просто. Главное в свойствах рендеринга поставить цвет фона ( BackGround : Color ) и сглаживание ( AntiAlias : None ).

BackGround - нужно выбрать, токой какого цвета нет на сомой модели, иначе не возможно будет выводит спрайты с прозрачным цветом.

Что из этого получилось, можно увидеть тут.

Единственный минус всех пакетов, и TrueSpace в частности - он создает на каждый кадр свой отдельный файл. И в конце рендеринга у Вас получится огромное количество файлов с которыми очень неудобно работать. Надо склеить каждое движение в один файл и для этого я написал небольшую программку BMPCreator.

Пользоваться ей очень просто: Вы задаете каталог, где лежат BMP файлы. Задаете ( если понадобится ) отсечение сверху, снизу, справа, слева и отступ между спрайтами.

Далее, задав выходное имя файла, нажимаете 'Создать' и все отдельные спрайты склеиваются в один файл.Программа создает временный файл и Вы сразу можете посмотреть полученную анимацию, нажав "загрузить". Если Вас все устраивает, то сохраняйте полученный файл - "Сохранить в файл".

Потом его очень удобно грузить в ImageList или в DirectDrawSurface. На каждое законченное движение лучше создавать свой файл. Для компиляции потребуется DelphiX и RXLib. Да, совсем забыл сказать - скомпилированную программу я не высылаю, если вы не можете откомпилировать готовый пример - вам не чего заниматься созданием игр :)



Краткий обзор DirectX


Говоря техническим языком, DirectX - набор объектов COM (Component Object Model), которые реализуют интерфейсы для облегчения работы с видеоаппаратурой, звуком, межкомпьютерными соединениями и некоторыми системными сервисами.

DirectX был создан для решения проблемы совместимости аппаратуры, пополняющейся все новыми образцами с новыми возможностями и функциями, и программ, этой аппаратурой управляющих. Также применение DirectX с аппаратурой, имеющей функции аппаратного ускорения (3Dfx, NVidia и подобные) позволяет разгрузить основной процессор.

DirectX состоит из 7 основных компонент:

DirectDraw - позволяет напрямую работать с видеопамятью и аппаратными функциями оборудования, при этом сохраняя совместимость с Windows-приложениями. DirectInput - интерфейс для устройств ввода (мышь, клавиатура, джойстик и т.д.) DirectPlay - интерфейс для многопользовательских приложений (TCP/IP, Direct Dial, локальное подключение) DirectSound - интерфейс для звуковой аппаратуры (WAV, MIDI и др.) DirectSound3D - позвляет позиционировать звуковые источники в любой точке трехмерного пространства, создавая таким образом реальный объемный звук. Direct3D - интерфейс к 3D - аппаратуре

Все эти компоненты спроектированы таким образом, чтобы дать програмисту прямой доступ к аппаратуре.



Обзор DelphiX


Для работы с DelphiX необходимы следующие компоненты:

DirectX Run-time Delphi 3, 4, 5, 6 DelphiX

DelphiX - набор бесплатных компонент для Delphi для упрощения использования DirectX. Компоненты и их назначение представлены ниже:

Название
компоненты
Описание
компоненты
TDXDraw
Дает доступ к поверхностям DirectDraw и включает весь код, необходимый для работы с DirectDraw и DirectDraw.
TDXDib
Позволяет хранить DIB (Device Independent Bitmap) подробне
TDXImageList
Позволяет хранить серии DIB-файлов, что очень удобно для программ, содержащих спрайты. Позволяет загружать серию с диска во время выполнения программы.
TDX3D
Оставлен для совместимости с предыдущими версиями DelphiX, используйте TDXDraw.
TDXSound
Позволяет легко проигрывать wav-файлы.
TDXWave
"Хранилище" для wav-файла.
TDXWaveList
"Хранилище" для серии wav-файлов.
TDXInput
Позволяет получить доступ к объекту DirectInput и, соответственно, к мыши, клавиатуре и т.д.
TDXPlay
Позволяет разработчику легко подсоединить данные, находящиеся на другом компьютере, в том числе через Internet или LAN.
TDXSpriteEngine
Облегчает и автоматизирует работу со спрайтами. Поддержка методов Move, Kill и т.д.
TDXTimer
Дает более высокую точность, чем при использовании обычного таймера (TTimer). Используются потоки, синхронизация.
TDXPaintBox
DIB-версия стандартной компоненты TImage


Подготовительные действия


Перед тем как собственно что-нибудь нарисовать, нужно подготовить кое-какую информацию для нормальной работы приложения.

Во-первых, в закрытую секцию необъодимо добавить некоторые переменные и процедуры:

SineMove : array[0..255] of integer; { Таблица синусов для движения }
CosineMove : array[0..255] of integer; { Таблица косинусов }
SineTable : array[0..449] of integer; { Таблица синусов }
CenterX, CenterY : Integer; { Для координат центра черной дыры, которую мы будем рисовать }

procedure CalculateTables; { Заполнение таблиц синусов и косинусов }
procedure PlotPoint( XCenter, YCenter, Radius, Angle : Word); { Рисование точки на бэк-буфере }

Таблицы содержат заранее рассчитываемые значения синусов и косинусов, используемые для моделирования волн при движении. Зачем эти значения рассчитываются заранее? Вообще, в программировании игр всегда существует компромисс между скоростью и объемом. Если приложение занимает немного памяти, то оно все просчитывает само, следовательно, отнимается какое-то время. Либо можно просчитать все до цикла рисования, сохранить результаты и затем использовать их, выиграв в скорости. Процедура заполнения таблиц может выглядеть следующим образом (хотя ваша реализация, несомненно, будет работать намного лучше):

procedure TMainForm.CalculateTables;
var
wCount : Word;
begin
{ Precalculted Values for movement }
   for wCount := 0 to 255 do
   begin
      SineMove[wCount] := round( sin( pi*wCount/128 ) * 45 );
      CosineMove[wCount] := round( cos( pi*wCount/128 ) * 60 );
   end;
{ Precalculated Sine table. Only One table because cos(i) = sin(i + 90) }
   for wCount := 0 to 449 do
   begin
      SineTable[wCount] := round( sin( pi*wCount/180 ) * 128);
   end;
end;

К процедуре построения точки мы вернемся позже. Следующее, что нужно сделать - это добавить кое-какой код в событие OnCreate формы:

procedure TMainForm.FormCreate(Sender: TObject);
begin
   CenterX := Width div 2;
   CenterY := Height div 2;
   CalculateTables;
end;

И для того, чтобы форма завершилась по нажатию на клавишу ESC, нужно добавить код в обработчик события OnKeyDown:

procedure TMainForm.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
   if Key=VK_ESCAPE then Close;
end;

Теперь выбираем компоненту DXDraw, которую мы поместили на форму и создаем обработчик события OnFinalize:

DXTimer.Enabled := False;

Этот кусок кода останавливает таймер. Создайте обработчик события OnInitialize и добавьте в него строчку:

DXTimer.Enabled := True;

что запускает таймер, т.е. начинает отображение картинки (конечно, если поверхность готова к этому)



Самое главное - это время


Как видно, для подготовки к работе с DirectX нужно совсем немного. Теперь перейдем собственно к рисованию. Создайте обработчик события OnTimer таймера и добавьте в него следующий код:

procedure TMainForm.DXTimerTimer(Sender: TObject; LagCount: Integer);
const
  x : Word = 0;
  y : Word = 0;
  IncAngle = 12;
  XMove = 7;
  YMove = 8;
var
  CountAngle : Word;
  CountLong : Word;
  IncLong :Word;
begin
  if not DXDraw.CanDraw then exit;
  IncLong := 2;
  CountLong := 20;
  DXDraw.Surface.Fill( 0 );
  repeat
    CountAngle := 0;
    repeat
      PlotPoint(CosineMove[( x + ( 200 - CountLong )) mod 255],
      SineMove[( y + ( 200 - CountLong )) mod 255], CountLong, CountAngle);
      inc(CountAngle, IncAngle);
    until CountAngle >= 360;
    inc(CountLong, IncLong);
    if ( CountLong mod 3 ) = 0 then inc(IncLong);
  until CountLong >= 270;
  x := XMove + x mod 255;
  y := YMove + y mod 255;   
  with DXDraw.Surface.Canvas do
  begin
    Brush.Style := bsClear;
    Font.Color := clWhite;
    Font.Size := 12;
    Textout( 0, 0, 'FPS: '+inttostr( DXTimer.FrameRate ) );
    Release;
  end;
DXDraw.Flip;
end;

Это основной код нашего приложения. Рассмотрим некторые важные моменты:

Вызов процедуры Fill поверхности DXDraw.Surface (DXDraw.Surface.Fill(0);) заполняет буфер цветом, который передается ей в качестве параметра (в нашем случае - черный).

Рассмотрим теперь процедуру PlotPoint. Все таблицы для ее работы были заполнены на подготовительном этапе, так что ничто не мешает нам нарисовать все, что мы хотим. Итак,

procedure TMainForm.PlotPoint(XCenter, YCenter, Radius, Angle: Word);
var
   X, Y : Word;
begin
   X := ( Radius * SineTable[90 + Angle]);
   asm
      sar x,7
   end;
   X := CenterX + XCenter + X;
   Y := ( Radius * SineTable[Angle] );
   asm
      sar y,7
   end;
   Y := CenterY + YCenter + Y;
   if (X < Width ) and ( Y < Height ) then
   begin
      DXDraw.Surface.Canvas.Pixels[X, Y] := clBlue;
      DXImageList.Items[0].Draw( DXDraw.Surface, X, Y, 0 );
   end;
end;

Основная строка в этой процедуре:

DXImageList.Items[0].Draw( DXDraw.Surface, X, Y, 0 );

Как было сказано выше DXImageList содержит массив картинок, с которыми мы работаем. Доступ к элементам осуществляется через индекс, начинающийся с 0. Т.е. указывая DXImageList.Items[0], мы получаем первую картинку, и т.д. Свойство Items имеет метод Draw, в который нужно передать 4 параметра. Первый параметр определяет поверхность, на которой будет рисоваться эта картинка. Второй и третий параметры - X и Y - определяют позицию, в которую будет выведено изображение. Последний параметр - флаг, определяющий прозрачность выводимой картинки. Так что строка кода, приведенная выше может прочитаться как "Вывести картинку с индексом 0 на поверхность DXDraw.Surface в позицию X, Y со значением прозрачности 0".

Также можно использовать свойство Pixels объекта Canvas для указания цвета определенной точки на экране (эта строка закоментирована, так как в нашем случае используется картинка. Эксперименты со свойством Pixels даются вам в качестве домашнего задания).

После того, как мы нарисовали нашу картинку, мы выводим значение FPS используя свойство FrameRate таймера. Вывод производится с помощью свойства Canvas объекта DXDraw.Surface.

Наконец вызывается метод DXDraw.Flip для отображения картинки на экране. При этом основная поверхность становится буфером.

Все, компилируйте и запускайте ваше приложение. На P120 получается порядка 12-15FPS (в зависимости от полноэкранного/оконного режима).



Delphi GFX


Стандартный Windows интерфейс - GDI
DirectX
Список ссылок
Краткий обзор DirectX
Терминология
Обзор DelphiX
Замечания по инсталляции
Начало программирования
Создание поверхности
Добавление в программу изображений
Изучаем таймер
Подготовительные действия
Самое главное - это время
Список ссылок


Создание поверхности


Для использования DirectDraw нужно создать поверхность, на которой мы бдем рисовать. Просто перетащите компоненту

TDXDraw на вашу форму. Дайте ей имя DXDraw. В инспекторе объектов вы увидите 4 свойства, которые нас интересуют. Это Align, Autoinitialize, Display и Options.

Установите свойство Align в alClient, т.к. мы хотим, чтобы весь экран стал поверхностью DirectDraw.

Autoinitialize всегда должно быть установлено в true, только если мы не хотим инициализировать поверхность вручную, для чего, наверное, нужно использовать метод DXDraw.Initialize в обработчике OnCreate формы.

Свойство Display поможет вам выбрать размер области рисования. Допустимые видеорежимы показаны в выпадающем списке. Для нашего примера установите свойство в 640x480x8.

Свойство Options дает доступ к 18 атрибутам. Таблица объясняет их назначение.

Атрибут Описание
doFullScreen Запускает приложение в полноэкранном режиме. Видеорежим может быть указан в свойстве Display.
doNoWindowChange Если выбрана эта опция и doFullScreen, приложение сначала максимизирует свое окно, а затем устанавливает режим, указанный в свойстве Display.
doAllowReboot Определяет, можно ли в программе использовать комбинацию Alt+Ctrl+Del. Это полезно во время отладки.
doWaitForBlank Определяет, будет ли ожидаться вертикальная развертка при выполнении операции флиппинга. Опция немного уменьшает FPS.
doAllowPalette256 Будет ли использоваться 256-цветная палитра
doSystemMemory Определяет, использовать ли системную память вместо видеопамяти. Опция немного уменьшает FPS.
doStretch Если ваша игра использует область отображения большую (или меньшую), чем указано в свойстве Display, с помощью этой опции можно сжать (растянуть) изображение на весь экран.
doCenter Поверхность отобразится в центре экрана.
doFlip Применяется только для полноэкранных режимов. Если используется двойная буферизация и требуется отобразить буфер, то в случае установленной опции это происходит очень быстро (применяется операция флиппинга).
Замечание: размер буфера должен равняться размеру основной поверхности.
do3D Позволено ли использовать 3D акселерацию
doHadrware Если видеоадаптер поддерживает аппаратное ускорение типа 3D или 2D, то полезно установить опцию в true.
Замечание: Если опция установлена в true, а видеокарта не поддерживает акселерацию, опция будет установлена в false. это можно использовать для определения поддержки аппаратного ускорения.
doRetainedMode Опция имеет эффект только если установлена опция do3D. Если опция равна true, используется режим Direct3D Retained, иначе - Immediate.
doSelectDriver В полноэкранном режиме определяет будет ли использоваться драйвер DirectDraw. Для Voodoo и подобных видеоадаптеров опция должна быть установлена в True.
doDrawPrimitive Использовать рисование примитивов.
doZBuffer Использовать ли Z-буфер. Эта опция может устранить некоторые проблемы с пропаданием объектов или наоборот, с появлением объектов, которые должны находиться на заднем плане. Требует часть процессорного времени. Некоторые карты поддерживают эту функцию аппаратно.
doTexture Будем ли мы использовать текстуры на 3D объектах?
doRGB Определяет, станет ли использоваться цветовая модель RGB. Может улучшить внешний вид 3D объектов, но отнимает процессорное время. Если карта аппаратно поддерживает эту функцию, опция не влияет на работу.
doMono Использовать ли черно-белую цветовую модель.
doDither Определяет будет ли подбираться ближайший цвет из палитры, если в ней не окажется запрашиваемого нами цвета. В основном используется с атрибутом doAllowPalette256.

Наши установки буду выглядеть следующим образом:

doFullScreen=False (программа будет стартовать в обычном окне) doAllowReboot=True (Возможно, возникнет ситуация, когда нам нужно будет снять задачу из-за ошибок) doWaitVBlank=True/False (Попробуйте оба значения. Возможно, вы получите приемлемое качество при установке False, при этом возрастет FPS) do3D=False (Наше приложение будет использовать только 2D) doHardware=True (Нам нужно определить, поддерживает ли аппаратура акселерацию.)

Остальные атрибуты оставлены как есть.



Создание редактора карт в стратегиях типа WarCraft


Copyright © 2001 Иван Дышленко

Довелось мне как-то озадачиться идеей написать редактор карт для моей новой игры. Скажу сразу, что задача эта не из простых. Приступим сразу к делу. Как правило, в двумерных стратегических играх типа Warcraft, Heroes of Might and Magic, Z и т. д. карты строятся из ячеек. Иными словами, карта - это матрица с некоторыми числовыми значениями внутри ячеек. Эти значения есть номера текстур (растровых картинок с изображениями земли, воды, камней и т. д., из которых и будет склеиваться Ваш уникальный ландшафт).

Рисунок 1

На рисунке изображена ну очень маленькая карта с размером матрицы 3х3. Для создания подобной карты задается двумерный массив ( Map : Array[3,3] of Byte ), записываются, каким-либо образом, в каждую ячейку порядковые номера текстур и при выводе карты на экран эти номера читаются из массива. Ну например:

...
For i := 0 to 2 do
  For j := 0 to 2 do
  Begin
    Number := Map[i,j];
    X := J * TextureWidth;
    Y := i * TextureHeight;
    DrawTexture(X,Y,Number);
  End;
... Number - номер текстуры,
Х - координата текстуры на экране,
Y - то же самое,
DrawTexture - некая процедура вывода текстуры на экран.

Совет!!!
Если Вам заранее не известно из какого количества ячеек будет состоять Ваша карта, не используйте Tlist в Tlist'e для ее создания. Советую воспользоваться PByteArray.

GetMem(PbyteArray,MapWidth*MapHeight*SizeOf(Тип ячейки));

Тип ячейки в нашем случае - Byte. Обращение в этом случае будет таким:

Number := PbyteArray[Y*MapWidth + X];Где X,Y - координаты нужной ячейки в матрице.

Все что мы рассмотрели выше подходит для карт на основе только лишь одного типа земли. Взгляните на рисунок расположенный выше. Вы увидите, что поскольку все текстуры разные - карта как-бы состоит из квадратиков. Кому она такая нужна? Хочется чтобы эти текстуры плавно перетекали друг в друга. Отсюда есть три выхода:

Создавать карту из текстур мало отличающихся друг от друга и при рисовании карты выбирать их случайным образом. Налепить целю кучу "пересекающихся" между собой текстур и класть их на карту вручную. Так же налепить ту же кучу текстур и написать программу позволяющую автоматически распределять их на карте. Первый способ не очень интересен. Он скорее подходит для создания ролевых игр. Где, как правило, присутствует базовый тип земли, а все остальное, такое как вода, камни, травка представляется объектами. Второй способ легок по реализации, но очень утомительно будет потом создавать карты в таком редакторе. Посмотрите на рисунок.

Рисунок 2

Если у Вас вся карта состоит из текстур с травой, а Вам надо добавить участок воды, то мы видим, что для того чтобы добиться плавного перетекания Вам придется добавить еще 8 промежуточных текстур окружающих текстуру с водой. Если делать это вручную( по второму способу ), то это займет слишком много времени и сил. Поэтому нам второй способ тоже не подходит. Мы остановимся на третьем способе и будем создавать карту подобно тому, как это происходит в WarCraft'e. При добавлении текстуры на карту( фактически - записи номера текстуры в определенную ячейку матрицы ), окружающие ее текстуры будут рассчитываться автоматически. Как этого добиться?

Я достаточно долго ломал голову над этой проблемой. Я пытался найти какой-нибудь способ позволяющий не утруждать компьютер громоздкими вычислениями и работать максимально быстро и эффективно. Один раз я даже вывел формулу, по которой рассчитывались новые значения ячеек, но она увы имела ограниченное действие( только 2 типа земли ) и плохо подходила для создания карт, где требуется максимальное разнообразие. Но достаточно лирики, давайте вернемся к нашим баранам.

Прежде всего необходимо выяснить - какое количество переходных текстур нам понадобится для обеспечения плавного перетекания между двумя типами земель. Здесь есть свои тонкости.

Представим, что у нас имеется два типа земли: ВОДА и ЗЕМЛЯ, тогда: Во-первых нам понадобятся две базовых текстуры , это текстуры полностью заполненные водой или землей.

Рисунок 3

Во вторых нам понадобятся промежуточные текстуры. Сколько их нужно мы сейчас посчитаем.

Рисунок 4

Оказалось, что для плавного перетекания двух земель друг в друга надо 14 промежуточных текстур, плюс две базовых. Итого 16. Всякий программист знает, что это хорошая цифра.

Возможно кто-то спросит: А зачем так много? Не достаточно ли 8 текстур, как на рисунке 2 - где трава пересекается с водой? Нет не достаточно. Ведь ситуации бывают разные. Окружающие ячейки могут быть не полностью забиты травой ( в данном случае землей ), и тогда понадобятся дополнительные текстуры.

Тогда может последовать другой вопрос: Почему так мало текстур? Где например текстуры когда вода с трех сторон окружена землей, и с четырех, и другие? Не следует ли предусмотреть все случаи?

И это правильный вопрос, но здесь все зависит от конкретной реализации алгоритма автоматического вычисления необходимой текстуры. В моем примере он реализован так, что остальные текстуры не нужны. Объясню наглядно:

1. Текстуры воды окруженные землей с двух противоположных сторон превращаются в базовую текстуру земли ( в текстуру заполненную только землей ). Соответственно то же самое происходит когда вода окружена с трех или четырех сторон.

Рисунок 5

2. Текстуры воды окруженные с двух уголков на одной стороне превращаются в текстуры полностью окруженные землей с одной стороны.( если уголки с трех сторон, то вода оказывается окружена полностью с двух сторон, если уголков 4, то вода превращается в землю совсем).

Теперь, я надеюсь, все ясно. С помощью применения подобной техники количество промежуточных текстур удалось уменьшить ровно в два раза! Это существенная экономия памяти, особенно если учесть, что типов земель будет больше. Кстати в WarCraft'e, если я не ошибаюсь, используется такой же набор текстур.

Ну хорошо, теперь давайте еще посчитаем. Для "слияния" двух земель нам понадобилось 16 текстур. Но если к земле и воде добавить еще траву, то придется создавать также переходные текстуры для трава-земля и трава-вода. Это еще 32 текстуры. Добавим еще каменистую почву( надо же сделать карту разнообразнее). Еще 48 текстур. И так далее и так далее. А если мы хотим сделать несколько видов одной и той же текстуры( опять таки для разнообразия )? Количество текстур растет как на дрожжах. Что делать?

Но тут на помощь пришел опять-таки старый, добрый, затертый до дыр мышкой WarCraft. Никогда не замечали, что если в WarCraft'e, вернее в War Editor'e, "кладешь" воду на траву, то между травой и водой появляется прослойка земли? Вот и я заметил.

Рисунок 6а Рисунок 6б

Посмотрите на эти два рисунка. Из них видно, что вода граничит только с землей, трава тоже граничит только с землей. Земля в данном случае является "переходным" типом земли. Достаточно создать текстуры вода-земля, трава-земля, камни-земля, песок-земля и т. д. По 16 штук на каждую землю и все. Можно больше не беспокоится. Земли будут соединяться между собой через "переходный" тип земли. Спасибо WarCraft'у.

Итак, с количеством текстур и тем какими они должны быть мы разобрались, и вот наконец-то мы приступаем к самой реализации данной задачи.

Условимся, что:

1. Ячейку с номером 12 я буду называть активной или текущей.
2. Землю которой мы рисуем я также буду называть активной или текущей.
3. Землю которая была прежде была в ячейке 12 я буду называть прежней.
4. Ячейки под номерами 6,7,8,11,13,16,17,18 я буду называть первым кругом.
5. Ячейки под номером 0,1,2,3,4,5,9,10,14,15,19,20,21,22,23,24 я буду называть вторым кругом.
6. Все текстуры имеющие в себе участок некоторого типа кроме переходного есть эта земля. То есть, к примеру, ячейки в первом круге - это вода.(см. Рисунок 6б)

Пусть для данного примера у нас будет три типа земли: ВОДА, ТРАВА, КАМНИ. Плюс переходный тип - ЗЕМЛЯ. Нам понадобится 48 текстур. Почему 48, а не 64? - спросите вы, - ведь типов-то 4. Потому, что переходный тип и так есть в каждом из трех первых типов, в промежуточных текстурах.

Допустим, что текстуры у Вас будут храниться в компоненте ImageList, для нашего случая это удобнее всего. Разместим мы их следующим образом: за номером 0 будет располагаться цельная текстура воды, номера 1 - 14 займут промежуточные текстуры ВОДА-ЗЕМЛЯ (как на Рисунке 4), номер 15 займет цельная текстура ЗЕМЛИ. Следующий элемент ТРАВА займет номера 16 - 31 по тому же принципу, элемент КАМНИ займет номера с 32 - 47. Как Вы наверное заметили, номера 15,31,47 оказываются заняты одинаковыми цельными текстурами земли. Их можно сделать немного отличающимися друг от друга для обеспечения большего разнообразия, а затем выбирать случайным образом.

Введем базовые индексы типов земель. Пусть базовый индекс воды равен 0, базовый индекс травы равен 1, камней - 2. Тогда, узнав порядковый номер текстуры, мы можем выяснить какому типу земли она принадлежит, достаточно разделить целочисленным делением (Div) порядковый номер текстуры на 16. Если же мы разделим этот номер делением по остатку (Mod) на 16, то узнаем смещение или номер промежуточной текстуры внутри интервала номеров принадлежащего данному типу земли. Например, мы обратились к ячейке и получили номер 23. Поделив этот номер целочисленным делением на 16 получим 1. Это тип земли - ТРАВА. Поделив делением по модулю остатка на 16 получим 7. Это номер промежуточной текстуры.(См. Рисунок 4, только в данном случае была бы трава с землей) Заметьте, если бы вместо 7 мы получили 0, это означало бы цельную текстуру данной земли, 15 означало бы цельную текстуру переходного типа - ЗЕМЛЯ.

Теперь давайте немного попишем:

PMap : PbyteArray; // указатель на матрицу содержащую нашу карту
WorldWidth, WorldHeight : Integer; // Ширина и высота карты в ячейках

Procedure CreateNewMap(WorldWidth,WorldHeigth : Integer);
Begin
  // Выделение памяти под матрицу
  GetMem(pMap,WodrldWidth*WorldHeight);
  // Заполнение этого участка нулями
  FillChar(pMap,WorldWidth*WorldHeight,0);
End;

funcion GetElement(x,y : Integer):byte;
Begin
  // Получить значение ячейки
  Result := pMap[y*WorldWidth + x];
End;

Procedure PutElement(x,y : Integer; Index : Byte);
Begin
  // Записать значение в ячейку
  PMap[y*WorldWidth + x] := Index;
End;

Function GetBaseIndex(Index : byte): byte;
Begin
  // Получить тип земли в виде номера(индекса)
  Result := Index div 16;
End;

Function GetAdditionalIndex(Index : byte):byte;
Begin
  // Получить номер переходной текстуры
  Result := Index mod 16;
End;

Вот. Вспомогательные функции мы написали, перейдем к рассмотрению технологии.

Посмотрите на Рисунок 6(б). Видно, что когда мы заменяем значение одной ячейки, эти изменения влияют, как на первый так и на второй круги ячеек. Возникает резонный вопрос: не случится ли такой ситуации, когда помещение на карту новой текстуры потребует перерисовки всей карты, так, словно кто-то бросил камень в воду? Если следовать принципам изложенным в этой статье, то не случится. Я проверял все варианты. Изменения касаются лишь первого и второго круга. Кто не верит, может проверить, посчитать, прикинуть, но это займет много времени. Теперь мы подходим к главному - по какому принципу рассчитывать новые значения изменяемых текстур. Возможно я Вас немного удивлю, но рассчитывать нам больше ничего не придется. Нам понадобится создать три массива (таблицы)16 на 25 элементов, записать в них заранее расчитанные значения, а затем их считывать в ходе выполнения программы. Сейчас поясню.

Поскольку в общей сумме у нас по максимуму может измениться 25 элементов на карте (Рисунок 6(б)), мы создадим вспомогательную матрицу 5х5, куда будем считывать с карты значения соответствующих ячеек. Затем мы изменим значения в этой матрице и поместим ее снова на карту откуда взяли.

В каждой ячейке может быть следующее значение:

Index + GroundIndex*16 , где

Index - число от 0 до 15 указывающее на номер переходной текстуры. GroundIndex - число от 0 до 2 указывающее на тип земли - ВОДА, ТРАВА, КАМНИ

Итак мы знаем номер лежащей в ячейке переходной текстуры (GetAdditionalIndex), мы также знаем номер этой ячейки в матрице 5х5. Этого вполне достаточно. Мы создадим массив-таблицу ширина которого равна количеству возможных переходных текстур 16, а высота равна количеству ячеек в матрице 5х5=25. Дальше мы действуем следующим образом: Считываем в матрицу 5х5 участок карты центром которого является ячейка в которую мы "кладем" новую землю, в ячейку 12 кладем цельную текстуру той земли которой мы рисуем. Затем для всех ячеек матрицы 5х5 кроме 12-ой делаем следующее: Поучаем номер переходной текстуры (GetAdditionalIndex) и обращаемся к таблице 16х25. Где номер переходной текстуры это положение ячейки таблицы 16х25 по горизонтали, а номер ячейки в матрице 5х5 это положение ячейки таблицы 16х25 по вертикали.

Рисунок 7

На рисунке 7 , цифра 6 по горизонтали это GetAdditionalIndex от текстуры, которая прячется в матрице 5х5 в ячейке номер 17, а "Х" в красной клетке это тот самый новый номер для этой текстуры. Фактически смысл сводится к следующему: посмотрели какая была текстура - заглянув в таблицу, узнали какая стала.

Вы наверное спросите - а как узнать какие значения должны быть в таблице 16х25? Никак. Они рассчитываются в уме и записываются в таблицу ручками. Но вы можете не задумываться над этим, я уже рассчитал и записал их в своем примере. Смотрите в исходниках.

Кстати в тексте статьи я упоминал о том, что нам придется создать три таблицы 16х25. Я не оговорился. Дело в том, что у нас возможны три варианта, когда значения одной и той же ячейки в таблице должны быть разными:

1. Активная земля равняется прежней земле. Например, мы рисуем ТРАВОЙ, а в рассчитываемой ячейке тоже ТРАВА или ТРАВА с ЗЕМЛЕЙ.
2. Активная земля не равна прежней земле. Например, мы рисуем ТРАВОЙ, а в рассчитываемой ячейке ВОДА или ВОДА с ЗЕМЛЕЙ.
3. Рисуем переходным типом земли - ЗЕМЛЯ.
Если кому-нибудь еще что-то не понятно, то надеюсь после рассмотрения исходных текстов программы все встанет на свои места.

Пример написан на Delphi 3 Professional, с использованием компонент библиотеки DelphiX для DirectX 6.0

Модуль MapDat

// Определение класса Matrix5
Type TMatrix5 = class(TObject)
private
  Matrix : array[0..4,0..4] of byte;
  Vector : array[0..24] of byte;
public
  function GetBaseIndex( ElementIndex : Integer ): Integer;
  Function GetAdditionalIndex( ElementIndex : Integer ): Integer;
  procedure Fill(X,Y : Integer);
  procedure Place(X,Y : Integer);
  procedure Culculate(X,Y : Integer; BrushIndex : Integer );
  procedure Draw(X,Y : Integer; BrushIndex : Integer );
end;

Внутри класса определены переменные в виде матрицы 5х5 и вектора. Некогда я думал, что это упростит написание программы, сейчас я думаю, что можно воспользоваться только вектором. Методы GetBaseIndex и GetAdditionalIndex мы уже рассматривали, рассмотрим остальные:

Метод Fill(X,Y : Integer); Заполняет матрицу и вектор 25-ю элементами карты. Х,Y - указывает на центральный элемент.

procedure TMatrix5.Fill(X,Y : Integer);
var
  i,j : Integer;
begin
  for j := 0 to 4 do
    for i := 0 to 4 do
      Matrix[i,j] := MainForm.GetElement(X - 2 + i,Y - 2 + j);
  for j :=0 to 4 do
    for i := 0 to 4 do
      Vector[j*5 + i] := Matrix[i,j];
end;

Метод Place(x,y : Integer); Выполняет процедуру обратную методу Fill. То есть кладет матрицу 5х5 на карту.

procedure TMatrix5.Place(X,Y : Integer);
var
  i,j : Integer;
begin
  for j := 0 to 4 do
    for i := 0 to 4 do
      Matrix[i,j] := Vector[j*5 + i];
  for j := 0 to 4 do
    for i := 0 to 4 do
      MainForm.PutElement(X - 2 + i,Y - 2 + j, Matrix[i,j] );
end;

Метод Draw(X,Y : Integer; BrushIndex : Integer);

procedure TMatrix5.Draw(X,Y : Integer; BrushIndex : Integer);
begin
  Self.Culculate(X,Y,BrushIndex);
  Self.Place(X,Y);
end;

Выполняет методы Culculate , а затем Place. X,Y - указывают центральный элемент в матрице 5х5, BrushIndex - индекс активной земли. (0-вода,1-трава,2-камни,3- переходный тип - земля).

Прежде чем перейти к основному методу данного модуля - Culculate, покажу вам созданные таблицы.

const BasicTable : array[0..24,0..15] of byte = (
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
( 9, 1, 6, 8, 4, 5, 6,15, 8, 9, 1,14, 4, 5,14,16),
( 1, 1, 6,15, 5, 5, 6,15,15, 1, 1, 6, 5, 5, 6,16),
(10, 1, 2, 7,15, 5, 6, 7,15, 1,10, 2, 7,13, 6,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
( 4, 5,15, 8, 4, 5,15,15, 8, 4, 5, 8, 4, 5, 8,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
( 2, 6, 2, 7,15,15, 6, 7,15, 6, 2, 2, 7, 7, 6,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(12, 5, 7, 3, 4, 5,15, 7, 8, 4,13, 3,12,13, 8,16),
( 3,15, 7, 3, 8,15,15, 7, 8, 8, 7, 3, 3, 7, 8,16),
(11, 6, 2, 3, 8,15, 6, 7, 8,14, 2,11, 3, 7,14,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16));

EqualTable : array[0..24,0..15] of byte = (
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,10,16,16,12,13, 2,16, 3, 0,16,16,16,16,11, 7),
(16, 0,11,16,12,12,11, 3, 3, 0, 0,16,16,12,11, 3),
(16, 9,11,16,16, 4,14, 3,16,16, 0,16,16,12,16, 8),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,10,16,11, 0,10, 2, 2,11, 0,16,16, 0,10,11, 2),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16, 9, 0,12,16, 4, 9,12, 4,16, 0, 0,16,12, 9, 4),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,11, 9, 1,16, 2,14,16,16,16, 0,10,16, 6),
(16,16,10, 0, 9, 1, 1,10, 9,16,16, 0, 0,10, 9, 1),
(16,16,10,12,16,16, 1,13, 4,16,16, 0,16,16, 9, 5),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16),
(16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16));

NotEqualTable : array[0..24,0..15] of byte = (
( 9, 1, 6, 8, 4, 5, 6,15, 8, 9, 1,14, 4, 5,14,15),
( 1, 1, 6,15, 5, 5, 6,15,15, 1, 1, 6, 5, 5, 6,15),
( 1, 1, 6,15, 5, 5, 6,15,15, 1, 1, 6, 5, 5, 6,15),
( 1, 1, 6,15, 5, 5, 6,15,15, 1, 1, 6, 5, 5, 6,15),
(10, 1, 2, 7, 5, 5, 6, 7,15, 1,10, 2,13,13, 6,15),
( 4, 5,15, 8, 4, 5,15,15, 8, 4, 5, 8, 4, 5, 8,15), (23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23), (19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19), (24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24),
( 2, 6, 2, 7,15,15, 6, 7,15, 6, 2, 2, 7, 7, 6,15),
( 4, 5,15, 8, 4, 5,15,15, 8, 4, 5, 8, 4, 5, 8,15), (18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18), (16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16), (20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20),
( 2, 6, 2, 7,15,15, 6, 7,15, 6, 2, 2, 7, 7, 6,15),
( 4, 5,15, 8, 4, 5,15,15, 8, 4, 5, 8, 4, 5, 8,15), (22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22), (17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17), (21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21),
( 2, 6, 2, 7,15,15, 6, 7,15, 6, 2, 2, 7, 7, 6,15),
(12, 5, 7, 3, 4, 5,15, 7, 8, 4,15,13,12,13, 8,15),
( 3,15, 7, 3, 8,15,15, 7, 8, 8, 7, 3, 3, 7, 8,15),
( 3,15, 7, 3, 8,15,15, 7, 8, 8, 7, 3, 3, 7, 8,15),
( 3,15, 7, 3, 8,15,15, 7, 8, 8, 7, 3, 3, 7, 8,15),
(11, 6, 2, 3,15,15, 6, 7, 8,14, 2,11, 3, 7,14,15));

BasicTable - используется, когда мы рисуем переходным типом земли.
EqualTable - испльзуется, когда прежняя земля в ячейке равна активной.
NotEqualTable - испльзуется, когда прежняя земля в ячейке не равна активной.

Заметьте, что в таблицах иногда используется число 16, а в таблице NotEqualTable и больше. Число 16 указывает, что текстура не изменится в результате наших воздействий. Честно говоря, я просто не помню зачем я вводил числа больше 16-ти, я написал эту программу год назад. В дальнейшем в теле модуля Culculate я от этих чисел отнимаю 16, а зачем - Бог его знает. Кому охота - можете исправить, но программа работает.

Да, на первый взгляд таблицы выглядят немного устрашающе. Кто-то может спросить: Зачем громоздить такие кошмары? Неужели не найти формулу для расчета? Ведь так будет намного компактнее. Но я отвечу, что программы на ассемблере выглядят тоже страшновато, зато работают намного быстрее, чем на других языках. Может и есть формула, но я уверен, что она непростая, а стало быть работать будет намного медленнее чем простое обращение к массиву.

Модуль Culculate(X,Y : Integer; BrushIndex : Integer);

procedure TMatrix5.Culculate(X,Y : Integer ; BrushIndex : Integer );
var
  i:Integer;
  BaseIndex, AdditionalIndex : Integer;
Begin
  // Заполнить матрицу считав значения с карты
  Self.Fill(X,Y);
  if BrushIndex = 3 then // Если рисуем переходной землей
  begin
    Vector[12] := 15;// Заносим центральный элемент
    for i := 0 to 24 do
    begin
      // Получить тип земли в виде индекса(0,1,2)
      BaseIndex := GetBaseIndex(Vector[i]);
      // и прежний номер переходной текстуры
      AdditionalIndex := GetAdditionalIndex(Vector[i]);
      // Если число в таблице BasicTable не равно 16 то,
      // к индексу типа земли умноженному на 16
      // прибавляем новое смещение
      // и заносим в Vector
      // ,иначе ничего не меняется
      if BasicTable[i,AdditionalIndex] <> 16 then Vector[i] := BaseIndex*16 +       BasicTable[i,AdditionalIndex];
    end;
  end { Конец обработки варианта "Переходная земля"}
  else // Иначе, если рисуем не переходной землей
    begin
    Vector[12] := BrushIndex*16;// Заносим центральный элемент
    for i := 0 to 24 do
      begin // Получить тип земли в виде индекса(0,1,2)
      BaseIndex := GetBaseIndex(Vector[i]);
      // и прежний номер переходной текстуры
      AdditionalIndex := GetAdditionalIndex(Vector[i]);
      // Если прежняя земля имеет тот же тип, что и активная
      if BaseIndex = BrushIndex then
      begin
        // Если число в таблице EqualTable не равно 16 то,
        // к индексу типа земли умноженному на 16
        // прибавляем новое смещение
        // и заносим в Vector
        // ,иначе ничего не меняется
        if EqualTable[i,AdditionalIndex] <> 16 then Vector[i] := BaseIndex*16 +         EqualTable[i,AdditionalIndex];
      end
      else // Если заменяемая и замещающая земля имеют разные типы
      begin
       // Если число в таблице NotEqualTable не равно 16 то,
       // к индексу типа земли умноженному на 16
       // прибавляем новое смещение
       // и заносим в Vector
       // ,иначе ничего не меняется
       if NotEqualTable[i,AdditionalIndex] < 16 then Vector[i] := BaseIndex*16 + NotEqualTable[i,AdditionalIndex] else
       if NotEqualTable[i,AdditionalIndex] > 16 then Vector[i] := BrushIndex*16+ NotEqualTable[i,AdditionalIndex] - 16;
     end;
   end;
end;

Разберем все по полочкам: Первая строчка Self.Fill(X,Y); заполняет матрицу 5х5 значениями считанными с карты. Дальше следует такой кусок кода:

if BrushIndex = 3 then
begin
  Vector[12] := 15;
  for i := 0 to 24 do
  begin
    BaseIndex := GetBaseIndex(Vector[i]);
    AdditionalIndex := GetAdditionalIndex(Vector[i]);
    if BasicTable[i,AdditionalIndex] <> 16 then Vector[i] := BaseIndex*16 +     BasicTable[i,AdditionalIndex];
  end;
end;

В нем мы рассчитываем случай, когда рисуем переходным типом земли - ЗЕМЛЯ(if BrushIndex = 3 then). Строка Vector[12] := 15; заносит в центральный элемент №12 цельную текстуру активной земли, для нашего случая это могут быть числа 15,31,47. Как мы помним именно под этими номерами в нашем ImageListe находятся цельные текстуры ЗЕМЛИ. Далее в цикле, для каждого элемента взятого с карты и положенного в матрицу ( в данном виде - в вектор, для упрощения организации цикла) получаем индекс типа земли(BaseIndex := GetBaseIndex(Vector[i]);) , получаем номер переходной текстуры (AdditionalIndex := GetAdditionalIndex(Vector[i]);), и лезем в соответсвующую таблицу ( входные параметры которой это номер ячейки i и номер переходной текстуры AdditionalIndex). Если на выходе получим число 16, то ничего не меняем, если другое число, то индекс типа земли умножаем на 16 - это номер цельной текстуры данного типа земли, и прибавляем число полученное из таблицы - это новый номер переходной текстуры.

Рисунок 8

Как видно из рисунка 8, если в матрице 5х5 лежит в некоторой ячейке число 20, то индекс переходной текстуры будет равен 4 ( 20 mod 16), индекс типа земли равен 1 (20 div 16), а индекс цельной текстуры земли равен 16 ( Индекс типа земли * 16 ). Номер ячейки, где лежит число 20, и индекс переходной текстуры (4) - входные параметры в таблицу BaseTable. Если мы на выходе получим, к примеру число 8, то нужно к индексу цельной текстуры прибавить 8, чтобы получить индекс новой переходной текстуры. ( Индекс типа земли * 16 + 8 = 24 ) Это будет новое число, которое мы поместим на карту.

Следующий кусок кода:

else
   begin
     Vector[12] := BrushIndex*16;
     for i := 0 to 24 do
     begin
       BaseIndex := GetBaseIndex(Vector[i]);
       AdditionalIndex := GetAdditionalIndex(Vector[i]);
       if BaseIndex = BrushIndex then
       begin
         if EqualTable[i,AdditionalIndex] <> 16 then Vector[i] := BaseIndex*16 + EqualTable[i,AdditionalIndex];
       end
       else
       begin
         if NotEqualTable[i,AdditionalIndex] < 16 then Vector[i] := BaseIndex*16 + NotEqualTable[i,AdditionalIndex]
         else
           if NotEqualTable[i,AdditionalIndex] > 16 then Vector[i] := BrushIndex*16+ NotEqualTable[i,AdditionalIndex] - 16;
      end;
    end;
  end;
end;

Делает все то же самое, для двух оставшихся случаев. Голубым выделены те строчки, которые по моему мнению можно удалить, но при этом исправить в таблице NotEqualTable числа больше 16 на эти же числа минус 16. Все, с технологией покончено!!!

Следующие страницы я посвящу некоторым особенностям вывода карты на экран в моем примере. Кого интересовала только технология расчета плавных перетеканий текстур, дальше, если нет желания, могут не читать.

Как я уже говорил, в примере я использовал компоненты для DirectX, написанные каким-то хорошим китайцем. Имя у него соответственно самое что ни на есть китайское, по этому я его не помню.

Конкретно для вывода карты на экран использовались компоненты TDXDraw, TDXImageList и TDXTimer.

TDXDraw - в основном используется для переключения страниц видеопамяти. Что это такое объяснять не буду.

TDXImageList - хранит в качестве элементов файлы со спрайтами выстроенными в одну цепочку. Соответственно к конкретному спрайту можно обратится по имени файла и номеру спрайта в нем. Также в этом компоненте есть две переменные PatternWidth, PatternHeight для указания ширины и высоты спрайтов, и переменная TransparentColor для указание прозрачного цвета.

TDXTimer - используется для генерации события DXTimerTimer с частотой заданной или рассчитанной в ходе выполнения программы.

Итак, текстуры выполнены в виде одного файла внутри которого выстроены в цепочку в соответствии с принципами изложенными выше и помещены в TDXImageList под именем "West". ( TDXImageList позволяет находить файлы внутри себя по их имени)

Нам нужно вывести на экран некоторую часть карты, причем карта наша состоит из кусочков и нам нужно вывести только те кусочки, которые видны в данный момент.

Можно сделать окно вывода кратным размеру текстур, а скроллинг организовать потекстурно с шагом равным ширине/высоте текстуры, тогда нет проблем, но это смотрится не очень красиво. Наша задача состоит в том, чтобы организовать скроллинг попиксельно и дать возможность задать окно вывода любого размера. Для того, чтобы это сделать нужно рассчитать сколько текстур по горизонтали и сколько текстур по вертикали мы должны отрисовать в окне вывода, включая и те текстуры которые в данный момент времени видны только частично.

Рисунок 9

На рисунке 9 клеточками изображена карта. Черным контуром показано окно вывода. Как видно - не все ячейки карты целиком влезли в окно, но их тоже надо отрисовать. Положение окна вывода на карте определяется координатами его левого верхнего угла относительно карты.( TopLeftCorner.x, TopLeftCorner.y) Их величины в пикселях(Нам же надо сделать попиксельный скроллинг) При создании новой карты они приравниваются нулям, и в дальнейшем определяются положением полос прокрутки. Вот часть кода:

procedure TMainForm.RedrawMap;
Var
  OffsPoint : TPoint;
  TopLeftElem : TPoint;
  ElemCount : TPoint;
  HelpVar1 : Integer;
  HelpVar2 : Integer;
  i,j : Integer;
  x,y : Integer;
  Index : Integer;
begin
  OffsPoint.x := TopLeftCorner.x mod ElemWidth;
  OffsPoint.y := TopLeftCorner.y mod ElemHeight;

Данные две строчки позволяют получить смешение левого верхнего угла экрана внутри левой верхней ячейки(См. рисунок 9). Глобальные переменные ElemWidth,ElemHeight это высота и ширина ячейки(текстуры). Теперь нам необходимо получить номер строки и столбца ячейки где находится левый верхний угол окна вывода:

TopLeftElem.x := TopLeftCorner.x div ElemWidth;
TopLeftElem.y := TopLeftCorner.y div ElemHeight;

Далее необходимо рассчитать сколько у нас целых текстур влезает в окно вывода по вертикали и горизонтали:

HelpVar1 := DXDraw.Width - (ElemWidth - OffsPoint.x );
HelpVar2 := DXDraw.Height - (ElemHeight - OffsPoint.y );
ElemCount.x := HelpVar1 div ElemWidth;
ElemCount.y := HelpVar2 div Elemheight;

Где DXDraw.Width, DXDraw.Height - это ширина и высота окна вывода. Если у нас есть нецелые текстуры снизу и справа окна вывода, то добавляем к ElemCount.x, ElemCount.y по единице:

if (HelpVar1 mod ElemWidth) > 0 Then Inc( ElemCount.x );
if (HelpVar2 mod ElemHeight) > 0 Then Inc( ElemCount.y );

Далее следует вывод на экран:

For j := 0 to ElemCount.y do
  For i := 0 to ElemCount.x do
  Begin
    // Вычислить координаты куда выводить
    X := i * ElemWidth - OffsPoint.x;
    Y := j * ElemHeight - OffsPoint.y;
    // Вычислить номер текстуры
    Index := GetElement(TopLeftElem.X + i,TopLeftElem.Y + j);
    // Вывести текстуру на экран
    // Учтите что LandType это не тип земли, а тип мира
    // Snow,West и т.д.
    ImageList.Items.Find(LandType).Draw(DXDraw.Surface,x,y,Index);
  end;

Строка :

Index := GetElement(TopLeftElem.X + i,TopLeftElem.Y + j);

обращается к матрице карты и считывает оттуда номер текстуры, следующая строка выводит ее на экран.

Возможно вы спросите: А как же нецелые текстуры слева и сверху окна вывода? Их-то ты не учел? Посмотрите на кусок кода отвечающий за вывод на экран. Циклическая переменная инициализируется от 0 до ElemCount.(x,y). Это значит, что всегда выводится на одну текстуру больше, чем в ElemCount, а если слева и сверху нет нецелых текстур, то переменная OffsPoint.(x,y) будет равна размерам ячейки. Переменные HelpVar(1,2) станут на размер ячейки меньше, и следовательно переменные ElemCount.(x,y) станут на единицу меньше. Все. Смотрите исходники в модуле Main.pas.

В программе не отловлены все баги. Например определен только один тип мира "West", да и текстуры нарисованы чисто схематически.

Если данный материал оказался чем-нибудь полезен для Вас, то благодарности отсылайте Дмитрию Мироводину. Если бы не он я бы никогда не написал эту статью. Если возникнут какие- нибудь вопросы пишите мне по адресу, указанному в copyright.

Да, кстати. Миша, незабудь, что эту хрень, типа квадратиков карт ещё нужно развернуть!

Исходные тексты Вы можете скачать тут, а библиотеку DelphiX найдете в разделе Lib



Список литературы


1. Стен Трухильо "Графика для Windows средствами DirectDraw"
2. Клейт Уолнам "Секркты программирования игр для Windows 95"



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


Адрес автора
Официальный сервер True Space
Пример полученных спрайтов
Исходный код BMP Creator
Модели 3D Cafe
Просмоторщик и конвертер 3D форматов 3D Exploration

Адрес автора
Исходный код примера
Компоненты DelphiX

Эффекты с палитрой. Маленький "радактор" палитры. Пример показывает как можно изменять цвета палитры и отслеживать изменения изображения в real time режиме. Палитру можно сохранять и загружать из файла. В общем если Вы работали с Photo Shop, это прямой его аналог, но на Delphi. Более простой пример, но пригоден только для 256 цветов! Для компиляции потребуется DXDib из комплекта DelphiX
 
 



Адрес автора
Исходный код примера
Компоненты DelphiX
 
 




Скачать пример
Скачать библиотеку DelphiX
Официальный сайт DelphiX
Не официальный сайт поддержки DelphiX
DelphiX FAQ
 
 



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


Адрес автора
Исходный код примера
Компоненты DelphiX
Эффекты с палитрой. Маленький "радактор" палитры. Пример показывает как можно изменять цвета палитры и отслеживать изменения изображения в real time режиме. Палитру можно сохранять и загружать из файла. В общем если Вы работали с Photo Shop, это прямой его аналог, но на Delphi. Более простой пример, но пригоден только для 256 цветов! Для компиляции потребуется DXDib из комплекта DelphiX
 
 


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


Адрес автора
Исходный код примера
Компоненты DelphiX
 
 



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


Скачать пример
Скачать библиотеку DelphiX
Официальный сайт DelphiX
Не официальный сайт поддержки DelphiX
DelphiX FAQ
 
 



После создания спрайтов, те части


После создания спрайтов, те части спрайта, которые должны менять цвет, делаются серым, так чтобы составляющие цвета (R,G,B) в каждом сером пикселе были равны между собой. Вывод спрайта на экран осуществить попиксельно, проверяя в цикле каждый пиксель на принадлежность серому цвету. Если цвет не является серым вывести его на экран без изменений, если является - изменить и вывести. Например, мы хотим вывести на экран спрайт, принадлежащий красному клану:

For i := 0 to Sprite.Width-1 do
For j := 0 to Sprite.Height-1 do
        Begin
            If Sprite.Pixels[i,j]=GrayColor then
                Canvas.Pixels[i,j]:=RedColor
            Else
                Canvas.Pixels[i,j]:=Sprite.Pixels[i,j];
        End;
При этом интенсивность красного цвета должна быть пропорциональна интенсивности серого в каждом конкретном пикселе. Вы можете написать несколько строчек кода для расчета интенсивности красного на основе серого цвета, но можете и не писать, т. к. этот способ далеко не лучший. Он накладывает ряд ограничений. Во-первых ни один находящийся в здравом рассудке человек не будет совершать попиксельный вывод на экран, т. к. быстродействие такого приложения сможет вывести из себя даже слона, во-вторых мы не сможем использовать оттенки серого цвета для изображения частей спрайта не участвующих в смене цветов( А, простите, каким цветом мы будем рисовать рыцарские доспехи и амуницию?) и в-третьих процедура расчета интенсивности нового цвета на основе интенсивности серого тоже займет порядочно времени и вызовет торможение программы.


Второй способ основан на цветовой


Второй способ основан на цветовой ротации.
О цветовой ротации рассказано много. В основном, когда используют этот термин, имеют в виду изменение палитры в восьмибитном режиме. Иногда этот способ называют Pallete Animation. Но как бы красиво это не называлось, нам это не походит. Во-первых, я надеюсь, мы не собираемся делать игру в восьмибитном режиме, во-вторых, мы работать будем не с палитрой, а с отдельными битами цвета.



На рисунке изображено представление цвета в 24 - битном режиме. Палитра, как таковая, отсутствует, так как она не нужна. Каждый оттенок представлен одним байтом ( восемь бит). Общее количество цветов зашкаливает аж за 16 миллионов. Чем нам это может быть полезно? Вот если мы сделаем наши спрайты таким образом, чтобы те части спрайта, которые должны менять цвет в зависимости от клана, были нарисованы только оттенками одного цвета ( например только красным - первые восемь бит), то получим возможность получить другие цвета. Как это работает? Есть такая ассемблерная операция, называется циклический сдвиг. Это когда берется какое-нибудь число и биты в нем циклически переставляются, первый становится последним, второй становится первым и так далее. Иногда в обратную сторону. Так вот если у нас изменяющиеся цвета выполнены в одном только красном оттенке, то у этих пикселей биты с 1 по 8 (вернее с 0 по 7) могут быть как единицей, так и нулем. Все остальные биты заведомо будут нулями. Теперь, если мы выполним циклический сдвиг вправо на 8, то все биты красного цвета переместятся туда где расположен синий цвет, в результате чего цвет станет оттенком синего. Если сдвинем вправо на 16 или влево на 8, то биты красного цвета займут места битов зеленого цвета - цвет станет зеленым. Так, получается, чтобы получить новый цвет, требуется узнать какой цвет имеем на данный момент, вычислить, на сколько сдвигать, сдвинуть прямо на изображении в памяти и вывести картинку на экран. Все. Это очень хороший способ, основное преимущество которого состоит в том, что не требуется никаких дополнительных спрайтов или временных буферов и достаточно высока скорость выполнения, но основной его недостаток сводит на нет его преимущества - мы может получить только три клана ( синий, зеленый, красный ). Бывают ситуации, когда этого вполне достаточно ( игра Z ), но в заголовке статьи упоминается игра WarCraft, а там кланов намного больше. Есть способ лучше - РОНДО!, то есть я хотел сказать - МАСКИ!


Серые Маски. Этот способ основан


Серые Маски. Этот способ основан на сложении цветов по логическим схемам И, ИЛИ ( AND,OR ). Давайте посмотрим, что получается, когда мы складываем по логическим схемам И, ИЛИ черный и серый цвета с цветовыми масками. Ну во-первых, что такое цветовая маска? Грубо говоря это цвет и есть, а на самом деле это некая битовая последовательность, где единичные биты указывают на биты принадлежащие конкретному цвету.
Ну например для красного цвета цветовая маска будет выглядеть следующим образом :

000000000000000011111111 это есть максимальная интенсивность чисто красного цвета, единички - это биты красного цвета, нули - биты других цветов. Соответственно существуют еще пять цветов.
000000001111111100000000 зеленый
111111110000000000000000 синий
000000001111111111111111 желтый
111111111111111100000000 морская волна
111111110000000011111111 темно-сиреневый
Вот такие цветовые маски могут быть использованы нами для создания кланов. В стандартном графическом интерфейсе Windows (GDI) им соответствуют цвета : clRed, clLime, clBlue, clYellow, clAqua, clFuchsia.

Теперь, если мы сложим черный цвет с какой-нибудь из маской, по системе ИЛИ, то получим ту же самую маску в качестве результата:

000000000000000000000000 OR 000000000000000011111111 = 000000000000000011111111, это неинтересно.

Гораздо интереснее если мы сложим таким же образом маску с серым цветом, в этом случае мы получим оттенок цвета маски, но немного светлее, и чем светлее серый цвет, тем светлее будет результирующий цвет вплоть до белого. Стало быть, если у нас к примеру картинка выполнена в оттенках серого цвета, то сложив каждый пиксель с маской красного цвета по системе OR, мы сделаем картинку розового цвета не исказив ее содержания. Сложим с маской синего цвета - получим голубоватую и так далее. Есть одна неприятность - черный цвет становится цветом маски, а это не всегда приемлемо. Однако в этом случае можно складывать по схеме И, при этом черный цвет как был черным так им и останется, с чем бы его ни складывали, а вот серый приобретет оттенок маски.





Эти рисунки иллюстрируют изменеие серого цвета после сложения его с маской красного цвета по системе ИЛИ и И. При этом заметно что, при сложении по системе OR результирующий цвет светлее по тону, чем исходный, а при сложении по системе AND наоборот - темнее. Как с этим справлятся я поясню дальше, а сейчас расскажу о том, каким образом данный материал может нам помочь.

Итак, предполжим у нас есть спрайт ( Рисунок слева )

Некоторые части этого спрайта должны изменять свой цвет так, чтобы определять его принадлежность к некоторому клану. Делаем серую маску тех частей, которые подвержены изменению ( Рисунок справа )

Вот так она должна выглядеть. Подобную операцию можно осуществить, практически, в любом графическом редакторе. Например: Photoshop'e. Теперь, если мы сложим данную маску по системе И (AND) с маской нужного нам цвета, маска станет не серой, а именно того цвета, который нам нужен. После этого мы можем вывести маску на спрайт, а спрайт в свою очередь вывести на экран.

Код демонстрационной программы:

type
...
ColorBox: TComboBox;// Список выбора цвета
...
end;

var
MainForm: TMainForm;
  RoboSprite : TBitmap; // Картинка спрайта
  RoboMask : TBitmap; // Картинка серой маски
  MaskColor : TColor; // Цветовая маска

...

procedure TMainForm.FormCreate(Sender: TObject);
begin
  RoboSprite := TBitmap.Create; // Загружаем картинки
  RoboMask := TBitmap.Create;
  RoboSprite.LoadFromFile('Sprite.bmp');
  RoboMask.LoadFromFile('Maska.bmp');

  // Устанавливаем прзрачный цвет спрайта в черный
  RoboSprite.Transparent := True;
  RoboSprite.TransparentColor := clBlack;
end;

procedure TMainForm.ColorBoxChange(Sender: TObject);
Var
  W,H : Integer;
  X,Y : Integer;
begin
  W := RoboSprite.Width;
  H := RoboMask.Width;

  // Получаем цвет маски из ComboBox'a выбора цвета
  MaskColor := StringToColor(ColorBox.Text);

  // В цикле осуществляем сложение по системе И
  for Y := 0 to H-1 do
    for X := 0 to W-1 do
      begin
        // Если пиксель в спрайте не равен черному
        if RoboMask.Canvas.Pixels[X,Y] <> clBlack Then
        // Складываем пиксель маски с цветовой маской и результат кладем на спрайт
        RoboSprite.Canvas.Pixels[x,y] := RoboMask.Canvas.Pixels[X,Y] AND MaskColor;
      end;
  Self.Canvas.Draw(0,0,RoboSprite);// Отрисовываем спрайт
end;

end.
Вот собственно и вся премудрость. Выглядит это следующим образом:

Да, как я уже говорил, при сложении по системе И результирующий цвет выглядит немного темнее, чем исходный цвет. Проблема решается просто: в том же PhotoShop'e просто повысьте яркость маски. На сколько ее повысить следует определит путем подбора. Все.

Это была первая часть статьи. Во второй части я опишу, как провернуть то же самое работая с DirectX, и под различными режимами.


Стандартный Windows интерфейс - GDI


Наверное, любой геймер играл в такие хиты, как WarCraft и StarCraft. Известно, что население той или иной миссии не ограничивается количеством рас в игре. Так, например, в WarCraft'e существуют всего две расы: Люди и Орки, в StarCraft'e таких рас три: Люди, Зерги, Протосы. Однако, помимо рас существуют еще и кланы, которые принадлежат одной и той же расе, но различаются между собой цветом. Вот о том, как клонировать спрайты, но сделать их различными по цвету и пойдет разговор.

Можно сделать все очень просто - наделать столько спрайтов, сколько кланов полагается в игре и вся проблема решена, но представьте себе, что в игре, которую Вы пишите, полагается сделать три расы в каждой спрайтов по 400. Если к каждой расе сделать 6 кланов, то итоговое количество спрайтов в игре станет равным: 3*400*6 = 7200. Не правда ли многовато? И хотя этот способ самый простой и, скорее всего, самый быстродейственный по результатам работы получившейся потом игры, но слишком большой расход оперативной памяти не даст Вам покоя, он будет мучить Вас и днем и ночью.

Когда у меня возникла данная проблема, я озадачился всерьез и надолго. Дело в том, что, как оказалось, у этой задачи есть великое множество решений, и выбирать один из них дело не простое, все зависит от Вашей конкретной программы, а на этапе разработки игры, трудно сказать, какой из этих способов станет Вашим лучшим и возможно вообще придется изобретать свой. Вот, что я наизобретал:



Терминология


Перед началом работы с DirectX необходимо остановиться на терминологии.

Поверхность (surface) - участок видеопамяти, который используется для хранения различных картинок. Все видеобуферы ссылаются на поверхности. Поверхность, которая отображается на экране в текущий момент называется основной (primary) поверхностью. Эта поверхность занимает столько памяти, сколько нужно для текущего разрешения и глубины цвета. Так, если установлен видеорежим 640 x 480 x 256 цветов (8 bpp), тогда основная поверхность будет занимать 307200 байт видеопамяти. Обычно вам нужна еще одна поверхность такого же размера, что и основная, используемая для флиппинга (что это такое будет объяснено чуть позже). Это значит, что нужно 614400 байт видеопамяти просто чтобы начать работать, не загружая никаких картинок. Если количества видеопамяти не хватает, поверхности будут создаваться в системной памяти, теряя преимущества аппаратного ускорения. В настоящий момент вам необходима видеокарта с 2MB видеопамяти - это абсолютный минимум для простых игр.

Двойная буферизация (double buffering) - техника, применяемая для получения быстрой, плавной анимации. Обычно используется дополнительный буфер, где создается сцена, которая затем отображается на экране.

Анимация с флиппингом страниц (page flipping animation) - для понимания этой техники вам достаточно нарисовать что-нибудь в блокноте и быстро пролистать страницы. При этом получится изменяющаяся картинка. В нашем случае сцена создается копированием картинок и спрайтов на поверхность (буфер), которая затем отображается на экране. Флиппинг - очень быстрая операция из-за того, что реально не происходит копирования больших объемов информации. При флиппинге происходит изменение одного регистра видеокарты, который содержит адрес участка памяти, отображающегося в текущий момент. В основном флиппинг состоит из 3-х шагов:
1. Создание сцены в буфере.
2. Переключение буфера для отображения, при этом поверхность, которая отображалась до этого становится буфером.
3. Повторяем с шага 1.

Кадры в секунду (frames per second) - обычно записывается как FPS, и обозначает количество кадров анимации, необходимое для получения плавного реального движения. Обычно для игр достаточно 24-25 FPS для получения приемлемых результатов (хотя для некоторых игр это значение может быть еще меньше).

Эффект разрывания экрана (tearing) - Большинство мониторов обновляют экран с частотой примерно 70 раз/сек (70 Гц) сверху вниз. Проблема возникает когда вы пытаетесь отобразить новую картинку где-то в середине процесса обновления экрана. При этом верхняя половина экрана отображает старую картинку, а нижняя - новую. Во время существования DOS для предотвращения этого эффекта вам нужно было бы синхронизироваться с вертикальной разверткой. DirectX освобождает вас от этой процедуры.

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

Отметим, что каждый фрейм отображает определенный угол поворота объекта. Если показывать эти картинки одна за другой, то получится что-то тип этого:

Также отметим, что прозрачный цвет спрайта - черный, поэтому фон получающегося спрайта - белый.

Клиппинг (clipping) - эта техника применяется для того, чтобы приложение смогло рисовать только в отведенный ему прямоугольник на экране. Если клиппинг не применяется, приложение может рисовать на всем экране. Это прекрасно работает если ваше приложение полноэкранное, но если оно оконное - клиппинг должен применяться обязательно.

Теория цвета - цвет в Windows обычно представляется моделью RGB (красный, зеленый, синий). Используя эти три основных цвета позволяет получить все мыслимые оттенки комбинацией основных. Обычно цвет хранится в виде 3-х байтов - каждый байт представляет относительную интенсивность основного цвета (от 0 до 255 включительно).

В Delphi цвет представляется в виде класса TColor, объявленном в модуле Graphics. Вы можете определить цвет используя консттанты типа clBlue, clLime и т.п. или определяя TColor как 4-хбайтовое число, где 3 младших байта представляют RGB цвет. Так, 0x00FF0000 - синий, 0x0000FF00 - зеленый, 0х000000FF - красный, 0x00000000 - черный, 0х00FFFFFF - белый и т.д. По поводу старшего байта в помощи по VCL сказано: "Если старший байт равен $00, получаемый цвет - ближайший подходящий цвет системной палитры, если он равен $01 - берется ближайший цвет из текущей палитры, если $02 - подбирается ближайший цвет, соответствующий контексту устройства".



Замечания по инсталляции


Предположим, вы уже распаковали DelphiX на жесткий диск и готовы инсталлировать ее на палитру компонент. Если библиотека пришла в виде исходников, желательно перекомпилировать ее. В любом случае для инсталляции выберите из меню File пункт Open и найдите каталог, в который вы распаковали библиотеку. Там должны быть файлы Delphi_for3.dpk, Delphi_for4.dpk и Delphi_for5.dpk, для соответствующей версии Delphi. Появится диалог подобный следующему:

Нажмите кнопку Compile и после компиляции нажмите кнопку Install. Если все прошло успешно, появится диалог, сообщающий об этом. Все готово для экспериментов с DirectX.

Если у Вас имеется каталог DelphiX\Bin то для подключения компонент проще использовать утилитки Install_for3.exe, Install_for4.exe и Install_for5.exe - они не только установят DelphiX, но и подключат файл помощи.