Титульная страница DelphiGFX Сделать закладку Написать письмо автору сервера 

  Главная - Документация - 2D Графика

Мастера Delphi | Corba  

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

Copyright © 2000 Иван Дышленко 
Содержание
Стандартный Windows интерфейс - GDI
DirectX
Список ссылок
Стандартный Windows интерфейс - GDI

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

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

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

Способ 1

После создания спрайтов, те части спрайта, которые должны менять цвет, делаются серым, так чтобы составляющие цвета (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;

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

Способ 2

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

2d_10_1.gif (1941 bytes)

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

Способ 3

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

000000000000000011111111 это есть максимальная интенсивность чисто красного цвета, единички - это биты красного цвета, нули - биты других цветов. Соответственно существуют еще пять цветов.
000000001111111100000000 зеленый
111111110000000000000000 синий
000000001111111111111111 желтый
111111111111111100000000 морская волна
111111110000000011111111 темно-сиреневый

Вот такие цветовые маски могут быть использованы нами для создания кланов. В стандартном графическом интерфейсе Windows (GDI) им соответствуют цвета : clRed, clLime, clBlue, clYellow, clAqua, clFuchsia.

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

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

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

2d_10_3.gif (3802 bytes)


2d_10_2.gif (4033 bytes)

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

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

2d_10_5.gif (3113 bytes)

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

Вот так она должна выглядеть. Подобную операцию можно осуществить, практически, в любом графическом редакторе. Например: 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.

 

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

2d_10_6.jpg (38572 bytes)

2d_10_7.jpg (39048 bytes)

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

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

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. Примеры для данной статьи качать тут

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

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

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

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

Титульная страница DelphiGFX Сделать закладку Написать письмо автору сервера
Hosted by uCoz