Анимированные 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 DelphiXDelphiX - набор бесплатных компонент для 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 |