Программирование игр для Windows. Советы профессионала

         

Отрисовка фрагментов стен


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

top    = 100 - scale/2 // верхняя граница

bottom = top + scale   // нижняя

граница

где scale — окончательные вертикальные размеры фрагмента.



Отсечение лучей


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

Я написал маленькую демонстрационную программку, в основе которой лежит алгоритм отсечения лучей-. Она позволит вам «погулять» по трехмерному миру, состоящему из кубов. Весь этот мир на самом деле является двухмерной матрицей, считываемой из обычного ASCII-файла.

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

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

Поскольку здесь я использовал библиотеки поддержки математических операций с плавающей запятой, то программа работает довольно медленно. Когда вы ее оттранслируете и запустите, то увидите три окна вывода. Они показаны на рисунке 6.20. Изображение в левом углу экрана представляет собой плоскую карту Трехмерного мира, который является матрицей размером 64х64х64.




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



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

Как и в системах лазерного сканирования, мы также как бы сканируем область вокруг нас и строим образ на основе полученных результатов. То, что мы в итоге получим, будет зависеть от поля просмотра. Это поле является той «порцией» информации, которую мы можем увидеть за один раз. Если мы способны обозреть пространство под углом 45° вправо и влево по отношению к направлению просмотра (рис. 6.22), то наше поле просмотра составит 90°.





Вообще, поле просмотра - это одно из ключевых понятий в технологии отсечения лучей. Поэтому мы должны определить, какое поле просмотра нам необходимо. Большинство животных имеет очень большое поле просмотра - 90 и более градусов. Правда, для нашего случая я выбрал его равным 60°. Просто мне нравится то, что получается при этом на экране. Вы сами можете задать любой другой угол, но постарайтесь, чтобы он попадал в диапазон между 60 и 90 градусами.

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


Для примера посмотрим на рисунок 6.23.



На рисунке 6.23 игрок находится в мире размерностью 8х8. Поскольку мы установили угол зрения игрока в 60°, то нам надо начать отсекать все лучи до угла 30° и все лучи после 120°. Как видите, я изобразил результат отсечения лучей на рисунке 6.23.

Первым вопросом обычно бывает: «А сколько лучей нам необходимо отсечь?». Ответ прост; количество отсекаемых лучей численно равно горизонтальному разрешению экрана, на который мы собираемся спроецировать образ. В нашем случае это 320 лучей, поскольку мы работаем в режиме 13h. Интуиция должна подсказать вам, что угол в 60° требуется разделить на 320 частей и для каждой из них произвести отсечение.

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

Мы имеем набор лучей, распределенных в диапазоне от -30 до +30 градусов к лучу зрения. Как мы уже говорили, поле просмотра у нас равно 60°. Давайте смоделируем на экране поле просмотра. Для этого нам нужно:

1.       Отсечь 320 лучей (один для каждого вертикального столбца на экране) и вычислить пересечение каждого луча с блоками, из которых состоит наша двухмерная карта мира;

2.       Используя эту информацию, вычислим дистанцию между игроком и  точкой пересечения;

3.       Затем используем эту дистанцию для масштабирования вертикальной полосы. Горизонтальная позиция при этом соответствует координате текущего луча (0..319).

Алгоритм 6.1 показывает последовательность действий при отсечении лучей.

Алгоритм 6.1. Алгоритм отсечения лучей.

// Пусть игрок находится в позиции (хр,ур) и его взгляд

// Направлен под углом view_angle



// Инициализируем переменные

// Начальный угол -30 градусов относительно направления взгляда

стартовый угол = угол просмотра - 30;

// необходимо отсечь 320 лучей, по одному на каждый экранный столбец

for (ray=0; rау<320; rау++)

{

вычислить наклон текущего луча

while

(луч не отсечен)

{

// проверить на вертикальное пересечение

if (не пересекается с вертикальной стеной)

if (луч отсек блок по вертикали)

{

вычислить дистанцию от (хр,ур) до точки пересечения

сохранить дистанцию

} //конец проверки на вертикальное пересечение

if (не было пересечения с горизонтальной стеной)

if (луч отсек блок по горизонтали)

{

вычислить дистанцию от (хр,ур) до точки пересечения

сохранить дистанцию

} // конец проверки по горизонтали

} // конец цикла while

if (горизонтальное пересечение ближе вертикального)

{

вычислить масштаб по горизонтали

нарисовать полосу изображения

}

// конец оператора if

else // вертикальное пересечение ближе горизонтального

{

вычислить масштаб по вертикали нарисовать полосу изображения

} // конец оператора else

} // конец

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

Единственный вопрос, который может смутить: «А почему это всегда работает?». Просто мы смоделировали процесс прорисовки образа частицами света. Правда, проделали мы это в обратную сторону, но главное — такой метод работает. Он удаляет невидимые поверхности, создает перспективу и содержит всю необходимую информацию для создания теней, освещения и текстур. Именно поэтому алгоритм отсечения лучей является очень мощным средством для программиста. Мы можем создавать в играх окружение, которое было бы невозможно получить, используя стандартную технику работы с многоугольниками.                                              

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


Впоследствии вы сможете уменьшить размер блоков для создания более сложных сцен. Кроме того, вы научитесь изображать поверхности с углом наклона в 45°. Все в ваших руках.

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

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

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


Отсечение спрайтов в трехмерном пространстве


После построения аксонометрической проекции спрайта отсечение выполняется довольно легко. Алгоритм просто тестирует, не выходит ли проекция отмасштабированного спрайта за границы экрана, и, кроме того, проверяет, находится ли Z-координата спрайта внутри наблюдаемой области пространства. Следовательно, проблема отсечения сводится к проверке расположения прямоугольника относительно границ экрана. Решение этой проблемы мы уже рассмотрели раньше (в четвертой главе, «Механизмы двухмерной графики»).

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

При использовании объектно-пространственного алгоритма мы должны каким-то образом до прорисовки определить, какая часть спрайта будет нарисована и на основании этого вновь вычислить его проекцию. По существу, мы должны отсечь границами экрана прямоугольник, который получается в результате масштабирования спрайта. Это кажется несложным. Мы разбирали текст такой программы в предыдущей главе (Листинг 7.3), но я повторю этот алгоритм еще один раз! Такой уж я.

Алгоритм 8.1 предполагает, что:

§

Экран ограничивается точками (0,0) и (scrfeen_x, screen_y);

§          Верхняя левая точка спрайта (sprifce_x, sprite_y);

§          Спрайт имеет размеры width и height.

Предположим, отсечение пространства по оси Z уже было сделано и внешне образ выглядит вполне правдиво. Пусть также были сосчитаны масштаб объекта и координаты проекций по осям Х и Y.




Алгоритм 8.1. Масштабирование спрайта.

// Проверка полной невидимости спрайта

// ( то есть лежит ли он полностью за пределами экрана)

if ((sprite_x > SCRESN_X} or (sprite_y > SCREEN_Y)

or (sprite_x + Width < 0) or (sprite_y + Heigth < 0))

{

// ничего не делаем

return;

} // конец if

else

{

// Спрайт виден частично, следовательно,

// необходимо рассчитать рисуемую область

// Задаем область спрайта перед отсечением

start_x = sprite_x;

start_y = sprite_y;

end_x = sprite_x+ Width - 1;

end_y = sprite_y + Height - 1;

// Отсекаем область слева и сверху

if (sprite_x < 0) start x = 0;

if (sprite_y < 0) start_y = 0;

// Отсекаем область справа и снизу

if (sprite_x + Width > SCRESN_X) end_x = SCREEN_X;

if (sprite_y + Height > SCREEN_Y) end_y = SCREEN_Y;

// Теперь новый спрайт будет иметь координаты верхнего

// левого угла (start_x, start_y), а нижнего правого

// (end x, end у). Эти координаты будут использоваться

// при прорисовке спрайта

return;

} // конец else

Как и в любом алгоритме, массу деталей я опустил. Однако идею, я думаю, вы поняли.

Теперь перейдем к самой сложной части отсечения спрайтов: когда мы их масштабируем, то должны взять некоторое число пикселей и «перенести» их на место назначения (экран) столько раз, сколько это необходимо для получения необходимого размера изображения. Однако после масштабирования спрайта его экранный размер может измениться. Поэтому при изображении спрайта после масштабирования мы должны особенно тщательно следить за отсечением. Само по себе масштабирование будет производиться точно так же как и раньше, но рисоваться будет только видимая часть изображения. Другими словами, рассчитав коэффициент масштабирования (или, построив таблицу соответствия), мы будем выводить на экран только часть спрайта, поскольку если изображение будет рисоваться целиком, то оно может выйти за пределы экрана и оказаться не отсеченным.


Отсечения


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

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

ситуации.

Отсечения могут производится на двух; «уровнях»:

§

Уровень образа;

§          Уровень объекта.

Отсечение области образов основывается на проверке каждой точки, кото рая может быть нарисована на экране в отсекаемой области. Например, если мы имеем квадратную область, которая соприкасается с границами экрана в режиме 13п (320х200), то мы не будем рисовать точки, выходящие за границу. Точки, которые находятся внутри области и ограничены координатами 0-319 по оси Х и 0-199 по оси Y, будут нарисованы, остальные - нет.

Поскольку все объекты, которые могут появляться на экране компьютера состоят из точек, то этот метод наиболее прост в реализации. Какие геометри­ческие фигуры рисуются, в этом случае не имеет значения. Будь то линии, треугольники, битовые карты и еще что-нибудь - все они используют вызов функции рисования точки. Если функция, рисующая точку, определяет, что точка выходит за границы области, то данная точка не ставится.

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


Некоторые вычисления помогут определить, где линия будет разорвана. Это основа алгоритмов, позволяющих выполнять отсечения области объектов.

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

К счастью для нас, наиболее сложный образ, который нам придется разрезать, это битовый образ (мы узнаем о том, как с ним работать в главе седьмой, «Продвинутая битовая графика и специальные эффекты»). Мы будем стремиться использовать эти алгоритмы всегда, когда это только возможно, поскольку они дают максимальную скорость. Мы не можем себе позволить добавлять в наши функции вывода точек всевозможные проверки, пока это не является абсолютной необходимостью.

Чтобы уверенно чувствовать себя в двухмерном мире, давайте поговорим на тему, которую большинство людей почему-то не любит. Речь пойдет о матрицах.


Пакеты анимации


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

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

До сих пор существует одна компания - Electronic Art, которая создает высококачественную продукцию по низким ценам. Продукт разработан исключительно для режима 13h, может быть именно поэтому он такой недорогой! Он прост и имеет инструмент для выбора по сотням опубликованных игр. Он является невероятной системой рисования в режиме 13h, и поэтому я использую большей частью именно его.



Пакеты программ DIGPAK и MIDPAK


На подступах к новому тысячелетию разработчикам уже не надо беспокоиться о программировании на аппаратном уровне индивидуальных звуковых устройств. В Windows 3.1 уже существует механизм драйверов, позволяющий прикладным программам воспроизводить оцифрованный звук и музыку MIDI. Точно так же в реальном или защищенном режиме процессора существует ряд программных интерфейсов, освобождающих вас от этого тяжкого бремени, позволяя сфокусировать свои силы на написании самих звуков и музыки. Среди подобных коммерческих систем можно назвать Audio Interface Library фирмы Miles Design и Sound Operating System фирмы Human Machine Interfaces.

Одной из наиболее популярных среди разработчиков систем являются созданные фирмой Audio Solution пакеты программ DIGPAK и MIDPAK. Первый из них представляет собой универсальный DOS-интерфейс для испол­нения цифрового звука на практически любой звуковой карте. MIDPAK позво­ляет вам воспроизводить полнооркестровую MIDI-музыку практически на любой звуковой карте, включая прекрасную эмуляцию MIDI для карт, не обладающих подобной возможностью (например, Sound Blaster). С 1 января 1994 года пакеты программ DIGPAK и MIDPAK бесплатны для некоммерчес­кого использования. Небольшая лицензионная плата требуется для коммерчес­кого распространения драйверов. Эта плата идет на усовершенствование драй­веров и на содержание BBS поддержки — SoundBytes OnLine BBS.

Пакеты программ DIGPAK и MIDPAK были созданы для того, чтобы разработчик игр для DOS мог справиться с громадным количеством наводнив­ших рынок звуковых карт. Уже больше пяти лет мы с Джоном Майлсом из Miles Design (как и многие другие специалисты из фирм по производству звуковых плат) занимаемся разработкой, улучшением и обновлением этих драйверов. Пакеты программ DIGPAK и MIDPAK вы найдете на дискете, прилагаемой к этой книге, а чуть ниже в данной главе будет приведена докумен­тация на API этих пакетов. Обратите внимание, что на дискете есть дополни­тельная документация и многочисленные примеры использования драйверов.

Я хотел бы сказать спасибо всем, кто помогал создавать эти драйверы:

§

Джон Майлс, Miles Design

§          Скотт Синдров, Creativ Labs

§          Дуг Коди, MediaVision

§          Майк Лейбов, Forte

§          Майк Дабс, Simutronics

§          Керщен Хеллер, Sierra Semiconductor

§          Мило Стрит, Street Electronics

§          Брэд Крэг, Advanced Gravis

§          Ричард Мазерес, Turtle Beach



Память и обучение


Если бы мне было нужно определить термины «память» и «обучение», я вероятно сказал бы, что они являются способностью использовать прошлый опыт для решения новых проблем вместе со способностью накапливать и интерпретировать новую информацию.

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

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

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



PCX-графика


Для удобства все картинки, использованные в этой главе, сохранены на диске как 256-цветные PCX-файлы. Формат PCX был выбран главным образом потому, что его легко читать и данные изображения в нем содержатся в сжатом виде. Немаловажно и то, что формат PCX поддерживается большинством графических редакторов (для получения более полной информации по PCX-файлам смотрите пятую главу, "Секреты VGA-карт").



Передача параметров


Языки Си и ассемблер похожи на дальних родственников, живущих в одном доме - они вынуждены придерживаться сложных взаимных условностей. Однако ассемблер значительно более примитивен. Поэтому при передаче параметров ассемблерной процедуре нам приходится сочинять множество дополнительных строк кода, обеспечивающих доступ к ним. Вначале необходимо оформить фрейм стека, как показано в Листинге 2.1. Далее необходимо получить доступ к переданным параметрам, основываясь на новом значении регистра базы (ВР). Для обеспечения доступа к параметрам вы должны четко представлять себе, как именно передаваемые параметры размещаются в стеке. К примеру, вы хотите написать процедуру, вычисляющую сумму двух чисел и возвращающую результат в регистре АХ. На языке Си, описание этой функции выглядит так:

int Add_Int(int number_1, int number_2);

При выполнении этой процедуры компилятор языка Си создаст фрейм стека и поместит туда параметры. Иными словами, значения number_1 и number_2 будут расположены в стеке. Вы можете подумать, что сначала в стек будет помещено значение number 1, а затем - number_2. Однако компилятор Си думает несколько иначе. Он помещает параметры в стек в обратном порядке, что облегчает доступ к ним. За счет применения обратного порядка размещения параметров, адрес каждого из них будет задаваться некоторым положительным смещением относительно регистра ВР, что делает жизнь намного легче. В частности, именно благодаря такому механизму, некоторые функции (например, printf) могут получать переменное число параметров. Таким образом, при вызове функции Add_Int фрейм стека будет выглядеть, как показано па рисунке 2.1 или 2.2, в зависимости от используемой модели памяти. Причина, по которой вид фрейма стека зависит от модели памяти, состоит в следующем: при вызове процедуры в стек помещается адрес команды, следующей непосредственно за командой вызова. Если мы применили модель памяти SMALL, все процедуры по определению находятся внутри одного кодового сегмента. Следовательно, для доступа из программы к любой из них нам необходимо знать только смещение.


Как известно, значение смещения занимает два байта. Если же мы применяем модель памяти MEDIUM или LARGE, то должны сохранить как смещение, так и сегментную часть адреса. Вместе сегмент и смещение занимают уже целых четыре байта.                         

Как видно из рисунков 2.1 и 2.2, параметры помещаются в стек в том порядке, который обеспечивает их адресацию положительными смещениями относительно значения регистра базы (ВР). Следовательно, для доступа к параметру number 1 вы должны использовать [ВР+4] или [ВР+6], в зависимости от установленной модели памяти. В качестве примера рассмотрим полный текст функции Add_Int. Она вычисляет сумму двух передаваемых в качестве аргументов чисел. Результат возвращается в регистре АХ, который, в соответствии с соглашениями языка Си, используется для возврата 16-битных значений.

Листинг 2.2. Простая процедура сложения.

; Секция констант

integer_1 EQU [ВР+6]        ; задает адрес первого аргумента

integer_2 EQU [BP+8]        ; задает адрес второго аргумента

.MODEL medium                ; указываем компилятору, что он должен

; использовать модель памяти MEDIUM

.CODE                        ; начало кодового сегмента

PUBLIC _Add_Int              ; эта функция - общедоступна

_Add_Int PROC FAR            ; имя функции и ее тип (дальняя)

push BP                      ; эти две инструкции инициализируют

; фрейм стека

mov ВР, SP

mov AX,integer_1             ; помещаем первое слагаемое

; в аккумулятор (регистр АХ)

add AX,integer_2             ; добавляем второе, слагаемое

; к содержимому АХ

pop ВР                       ; ликвидируем фрейм стека

_Add_Int  ENDP               ; конец процедуры

END                          ; конец кодового сегмента

Единственное, что мы изменили по сравнению с Листингом 2.1, это добавили несколько строк кода и ввели определения для адресов параметров. Теперь давайте проанализируем то, что у нас получилось.

§          Как и в предыдущем листинге, здесь были использованы директивы ассемблера для указания модели памяти, способа вызова, начала и конца функции;

§          EQU — это простая директива, заменяющая одну строку на другую. Я прибег к ней потому, что мне не хотелось в тексте самой функций использовать синтаксические конструкции [ВР+6] и [BP+8]. Строки, задающие выражения, которые будут подставлены при компиляции, это:

integer_l EQU [ВР+6]

integer_2 EQU [BP+8]

В общем, использование таких подстановок позволяет сделать ассемблерную программу более читабельной. Единственной альтернативой такому подходу является написание команды индексирования относительно содержимого одного из регистров (типа [ВР+6]).


Передача указателей


Мы знаем, как передать значения таких параметров как BYTE или WORD, но как передать указатель? Указатели передаются как двойные слова, или DWORD. Для доступа к указателям в стеке нам придется воспользоваться старым приемом: разобьем двойное слово указателя на две переменные segment и offset, которые будут иметь тип WORD, и уже к ним будем обращаться в. нашей ассемблерной программе. К примеру, если мы вызываем ассемблерную функцию в модели MEDIUM, (скажем, это будет вызов типа FAR) в следующей строке:

pfoo(&x)

то получить адрес переменной Х можно будет с помощью следующих подстановок:

offset EQU [ВР+6] segment EQU [BP+8]

Если мы захотим изменить значение X, то нам придется сделать следующее:

mov DI,offset

mov AX,segment

mov ES,AX

mov ES:[DI],CX

Эта программа состоит из двух основных частей:

§          Во-первых, создается указатель на Х через регистры ES и DI;

§          Во-вторых, изменяется значение переменной X.

Ну вот и все о том, что связано с передачей параметров. Новые расширения директив PROC и USES просто великолепны, и вы можете всегда ими пользоваться, если чувствуете от их применения комфорт. Если вы предпочитаете все делать в стиле MASM 5.0, то это ваше право. С точки зрения быстродействия программы здесь нет никакой разницы.



Перемещение трехмерного объекта


Для перемещения точки (x.y.z) на расстояние (dx,dy,dz) необходимо выполнить следующие операции:

x=x+dx;

y=y+dy;

z=z+dz;

Если мы хотим использовать эту матрицу, то должны представить точку в виде четырех компонентов (x,y,z, 1). Матричное умножение будет выглядеть так:

где dx, dy и dz - это перемещения по осям координат, а х', у' и z' -  координаты точки после перемещения.



Перемещения, масштабирование и повороты в трехмерном пространстве


Все объекты в видеоиграх представлены точками, линиями и геометрическими фигурами. Потому мы должны всегда производить их разбиение на исходные составляющие — точки. Только проделав это, мы можем их преобразовывать. Поскольку точки — это вершины объектов, то они могут рассматриваться вообще отдельно от объекта, но при этом представлять его в целом. Например, если мы хотим повернуть или переместить куб, то, прежде чем выполнять данное преобразование, нам нужно разбить объект на многоугольники, а затем на точки.

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



Переопределение цветовой палитры


Таблица цветов организована в VGA-карте как регистровый файл. (Я использовал слово регистр, чтобы обозначить значение в таблице соответствия. Каждый регистр палитры — это 24 бита.) Для доступа к значению мы должны произвести некоторые действия. Мы не можем просто сказать; «Изменить компонент красного для значения 123». Мы должны модифицировать все три составляющих цвета, который хотим изменить.

Хотя каждое значение состоит из трех байтов (один для каждой из составляющих), только первые шесть битов каждого байта используются для обозначения цвета. Существует 64 оттенка для каждого цвета, или 2 в 18-й степени различных цветов (это и есть общее количество цветов — 262144). Таким образом, если вы поместите значение, превышающее размер в шесть битов (или 63), то можете нарушить все компоненты, но не изменить цвет.

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

#define PALETTE_MASK        0хЗС6

#define PALETTE_REGISTER_RD 0x3C7

#define PALETTE_REGISTER_WR 0хЗС8

#define PALETTE_DATA        0x3C9

Теперь посмотрим как это реализуется:

§

Порт 0хЗСб называется маской палитры и используется для маскирования битов нужного регистра палитры. Например, если вы поместите в этот порт число 0х00, то получите регистр 0, независимо от того, какой регистр запрашиваете. С другой стороны, если вы запишете в регистр маски значение 0xFF, то получите возможность доступа к любому регистру через индекс регистра палитры 0хЗС8 и 0x3C7 (первый из них используется для записи, а второй — для чтения);

§          Порт 0x3C7, называемый регистром чтения палитры, используется для выбора из таблицы цветов значения, которое вы хотите прочитать;

§          Порт 0хЗС8 называется регистром записи палитры и используется для выбора в таблице соответствия значения, которое вы хотите записать;

§          Наконец, данные красной, зеленой и синей составляющей вы можете записать или прочитать из порта по адресу 0хЗС9, называемого портом данных палитры.




Вы можете спросить: « А как мы прочитаем из одного порта три байта?» На самом деле вы можете прочитать их по очереди. После того как вы выберете необходимый регистр (значение таблицы цветов, к которому вам нужен доступ), то первый записанный в регистр палитры байт будет соответствовать значению красного цвета. Второй байт задаст значение зеленого цвета, ну а третий — синего. Когда вы будете читать, это правило будет так же верно, но. в отличие от записи трех байтов в каждый момент чтения вы будете получать следующий компонент считываемого значения выбранного регистра. Для записи в регистр палитры вы должны:             

§          Выбрать регистр, который хотите изменить;

§          Произвести три записи в порт регистра данных.

Когда вы читаете или пишете в регистр цвета, не забывайте каждый раз предварительно изменять значение маски на OxFF. Листинг 5.1 показывает код, содержащий эти шаги.

Листинг 5.1. Запись в регистр палитры.

void Set_Palette_Register(int index, RGB_color_ptr color)

{

// эта функция устанавливает один из элементов таблицы цветов.

// номер регистра задается параметром index, цвет - структурой color

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

_outp(PALETTE_MASK,Oxff) ;

// какой из регистров мы хотим обновить

_outp(PALETTE_REGISTER_WR, index);

// теперь обновляем RGB. Обратите внимание,

// что каждый раз используется один и тот же порт

_outp(PALETTE_DATA,color->red) ;

_outp(PALETTE_DATA,color->green) ;

_outp(PALETTE_DATA,color->blue) ;

} // конец функции

Помните, что я использую структуру данных RGB_color. Это структура с тремя полями, каждое из которых предназначено для определенного цвета. Она выглядит так:

typedef struct RGB_color_typ

{

unsigned char red;    // красный

компонент

0-63

unsigned char green; // зеленый

компонент

0-63

unsigned char blue;   // синий

компонент

0-63

} RGB_color, *RGB_color_ptr;

Все походит на то, что следующей операцией должно стать чтение из регистра.


Мы сделаем то же самое, что и в Set_Palette_Register, только вместо записи в порт палитры будем читать из него и пересылать полученные значения в структуру RGB color. Листинг 5.2 содержит необходимый для этого код.

Листинг 5.2 Чтение регистра палитры.

void Get_Palette_Register(int index, RGB_color__ptr color)

{

// функция читает регистры цвета и помещает полученные значения

// в поля структуры color

// установить маску регистра палитры

_outp(PALETTE_MASK,0xff);

// сообщаем VGA, какой из регистров мы будем читать

_outp(PALETTE_REGISTER_RD, index);

// читаем

данные

color->red   = _inp(PALETTE_DATA);

color->green = _inp(palette_data);

color->blue = _inp(PALETTE_DATA);

} конец функции

Теперь, когда мы знаем, как читать и писать в регистр палитры, почему бы нам не создать функцию для получения новой цветовой палитры? Неплохая идея! Напишем для этого функцию, которая строит палитру, имеющую 64 оттенка серого, красного и голубого. Листинг 5.3 содержит ее код.

Листинг 5.3. Создание новой цветовой палитры.

void Create_Cool_ Palette(void)

{

// эта функция создает новую палитру, содержащую по 64 оттенка

// серого, красного, зеленого и синего цветов

RGB_color color;

int index;

// в цикле последовательно создаем цвета и меняем значения регистров

for (index=0; index < 64; index++)

{ // это оттенки серого

color.red   = index;

color.green = index;

color.blue = index;

Set_Palette_Register(index, (RGB_color_ptr)&color);

// это оттенки красного

color.red   = index;

color.green = 0;

color.blue = 0;

Set_Palette_Register(index+64, (RGB_color_ptr)&color) ;

// это оттенки зеленого color.red   = 0;

color.green = index;

color.blue = 0;

Set_Palette_Register(index+128, (RGB_color_ptr)&color) ;

// это оттенки синего

color.red   = 0;

color.green = 0;

color.blue = index;

Set_Palette_Register(index+192, (RGB_color_ptr)&color);

} // конец цикла for

} // конец функции

Наличие возможности изменения цветовой палитры позволяет нам создавать различные интересные эффекты освещения и анимации в наших играх. (Например, как сделаны цветовые эффекты в DOOM'e? Часть цветовой палитры изменяется «на лету» во время игры.) Так достигаются эффекты освещения и стрельбы.Это особенно хорошо для реализации эффекта «отключения света».

Вы должны принять на веру работоспособность функции из Листинга 5,3, поскольку у нас нет возможности проверить ее. Прежде мы должны были бы разработать эффективные функции для рисования точки и линии, но сейчас я хочу поговорить о технике, применение которой мы вскоре найдем.


Первый этап: планирование


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

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



Первый шаг


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

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

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

Так как же получить на экране изображения и заставить его циклически возвращаться при достижении границ экрана? Один из простейших способов заключается в выводе битовой карты на экран двумя порциями. Начнем с тот что логически разделим наше изображение на две части.

Запомните положение логической границы, разделяющей изображение на две части. Полученное число будет соответствовать ширине левой части изображения. Теперь назовем это число LeftHalf и установим начальную ширину левой части равной одному пикселю.

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

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

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




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

§

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

§          Продолжайте рисование пикселей от левого края изображения до столбца LeftHalf.

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

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

// нарисовать левую половину изображения

memcpy(Screen+320-LeftHalf, Bitmap, LeftHalf) ;

// нарисовать „правую часть изображения

memcpy(Screen,Bitmap+LeftHalf,320-LeftHalf) ;

где Screen - указатель на видеопамять, LeftHalf - ширина логической левой части изображения, a Bitmap - указатель на битовую карту изображения, загруженную в память. Этот процесс повторяется для каждой строки развертки изображения.

Каждый раз, когда вы увеличиваете значение LeftHalf на единицу, вы должны убедиться, что оно не превышает общей ширины изображения:

§          Если LeftHalf больше ширины изображения, присвойте ей значение, равное единице;   

§          Если LeftHalf меньше единицы, присвойте ей значение, равное общей ширине изображения.

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

Листинг 17.1 - это файл заголовка PARAL.H, в котором содержатся объявления различных констант, структур данных, а также прототипы функций, используемых в демонстрационной программе из Листинга 17.2 (PARAL.C).

Листинг 17.1. Файл заголовка демонстрационной программы циклического скроллинга (PARAL.H).



//

//Paral.h - данный заголовок определяет константы и структуры

//данных, используемые в демонстрационной программе

//            параллакса

#define KEYBOARD 0х09 //

// Коды клавиатуры для прерывания INT 9h

#define RIGHT_ARROW_PRESSED   77

#define RIGHT_arrow_rel      205

#define LEFT_ARROW_PRESSED    75

#define LEFT_ARROW_REL       203

#define ESC_PRESSED          129

#define UP_ARROW_PRESSED      72

#define UP_ARROW_REL         200

#define DOWN_ARROW_PRESSED    80

#define down_arrow_rel       208

#define VIEW_WIDTH    320

#define VIEW_HEIGHT   150

#define MEMBLK        VIEW_WIDTH*VIEW HEIGHT

#define TRANSPARENT   0       // цветовые коды

#define TOTAL_SCROLL  320

enum (NORMAL, RLE},;

enum (FALSE,TRUE};

typedef struct

{

char manufacturer;   /* Всегда 0 */

char version;        /* Всегда 5 для 256-цветных файлов */

char encoding;       /* Всегда 1 */

char bits_per_pixel;

/* Должно быть равно 8 для 256-цветных файлов */

int  xmin, ymin;      /* Координаты левого верхнего угла */

int  xmax,ymax;      /* Высота и ширина образа */

int  hres;           /* Горизонтальное разрешение образа */

int  vres;           /* Вертикальное разрешение образа */

char palettel6[48];

/* палитра EGA; не используется для 256-цветных файлов */

char reserved;       /* зарезервировано */

char color planes;   /* цветовые планы */

int  bytes_per_line;

/* количество байт в каждой строке пикселей */

int  palette_type;

/* Должно быть равно 2 для цветовой палитры */

char filler[58];     /* Не используется */

} PcxHeader;

typedef struct

{

PcxHeader hdr;

char *bitmap;

char pal[768] ;

unsigned imagebytes,width,height;

} PcxFile;

#define PCX_MAX_SIZE 64000L enum {PCX_OK,PCX_NOMEM,PCX_TOOBIG,PCX_NOFILE};

#ifdef __cplusplus

extern "C" {

#endif                   

int ReadPcxFile(char *filename,PcxFile *pcx);

void _interrupt NewInt9(void) ;

void RestoreKeyboard(void);

void InitKeyboard(void);

void SetAllRgbPalette(char *pal);

void InitVideo (void);



void RestoreVideo(void);

int InitBitmaps(void); void FreeMem(void);

void DrawLayers(void);

void AnimLoop(void);

void Initialize(void);

void CleanUp (void) ;

void OpaqueBIt (char*, int, int, int) ;

void TransparentBit(char *,int,int,int) ;

#ifdef __cplusplus

} #endif

Программа из Листинга 17.2 (PARAL.C) демонстрирует повторяемое смещающееся изображение. Движущаяся картинка показывает облачное небо под солнцем. Хотя изображение и выглядит непрерывно меняющимся, но на самом деле оно неподвижно.

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

Запустив оттранслированную программу, используйте курсорные клавиши «влево» и «вправо» для изменения направления скроллинга. Для выхода из программы нажмите Esc. При этом она вычислит и покажет скорость анимации кадра. На машине с процессором 386SX/25 скорость выполнения составила около 35 кадров в секунду при размерах демонстрационного окна 320х100 Пикселей.

Листинг 17.2 Демонстрационная программа повторяемого смещения.

#include <stdio.h>

#include<stdlib.h>

#include<string.h>

#include <time.h>

#include<dos.h>

#include "paral.h"

char *MemBuf,            // указатель на буфер памяти

*BackGroundBmp,     // указатель на скрытую битовую карту

*VideoRam;          // указатель на память VGA

PcxFile pcx;             // структура PCX-файла

int volatile KeyScan;    // изменения клавиатурного обработчика

int frames=0,            // количество нарисованных кадров

PrevMode;            // сохраняет исходный видеорежим

int background;

void _interrupt (*OldInt9)(void); // указатель на клавиатурный

// обработчик BIOS

// Данная процедура загружает 256-цветный PCX-файл

int ReadPcxFile(char *filename,PcxFile *pcx)

{

long i;

int mode=NORMAL,nbytes;

char abyte,*p;

FILE *f;

f=fopen(filename,"rb") ;



if(f==NULL)

return PCX_NOFILE;

fread(&pcx->hdr,sizeof(PcxHeader),1,f) ;

pcx->width=1+pcx->hdr.xmax-pcx->hdr.xmin;

pcx->height=1+pcx->hdr.ymax-pcx->hdr.ymin;

pcx->imagebytes= (unsigned int) (pcx->width*pcx->height);

if(pcx->imagebytes > PCX_MAX_SIZE)

return PCX_TOOBIG;

pcx->bitmap= (char*)malloc (pcx->imagebytes);

if(pcx->bitmap == NULL)

return PCX_NOMEM;

p=pcx->bitmap;

for(i=0;i<pcx->imagebytes;i++)

{

if (mode == NORMAL)

{

abyte=fgetc(f);

if((unsigned char)abyte > Oxbf)

{

nbytes=abyte & Ox3f;

abyte=fgetc(f);

if(--nbytes > 0) mode=RLE;

}

}

else if(--nbytes == 0) mode=NORMAL;

*p++=abyte;

}

fseek(f,-768L,SEEK_END);      // извлечь палитру из PCX-файла

fread(pcx->pal,768,1,f) ;

p=pcx->pal;

for(i=0;i<768;i++)

*p++=*p >>2;

fclose(f) ;

return PCX_OK;                // успешное завершение

}

// Это новый обработчик прерывания 9h. Он позволяет осуществлять

// мягкий скроллинг. Если обработчик BIOS не будет запрещен,

// удержание клавиш управления курсором приведет к переполнению

// буфера клавиатуры и очень неприятному звуку из динамика.

void _interrupt NewInt9(void)

{

register char x;

KeyScan=inp(0х60);   // прочитать символ с клавиатуры

x=inp(0x61);      // сообщить клавиатуре, что символ обработан

outp(0x61,(х|0х80)) ;

outp(0х61,x);

outp (0х20,0х20} ;    // сообщить контроллеру

// прерываний о завершении прерывания

if(KeyScan == RIGHT ARROW REL ||  // проверить кнопки

KeyScan == LEFT_ARROW_REL)

KeyScan=0;

}

// Функция восстанавливает прежний обработчик прерываний клавиатуры

void RestoreKeyboard(void) {

_dos_setvect (KEYBOARD, OldInt9); // восстановить прежний вектор

}

// Эта функция сохраняет указатель вектора клавиатурного прерывания

// bios, а затем инсталлирует новый вектор прерывания, определенный

//в программе.

void InitKeyboard(void)

{

OldInt9=_dos_getvect(KEYBOARD); // сохранить вектор BIOS

_dos_setvect (KEYBOARD,NewInt9);// инсталлировать новый вектор



}

// Функция вызывает видео BIOS и заполняет все необходимые регистры

// для работы с палитрой, задаваемой массивом раl[]

void SetAllRgbPalette(char *pal)

{

struct SREGS s;

union REGS r;

segread(&s);                    // получить значение сегмента

s.es=FP_SEG((void far*)pal);    // ES указывает на pal

r.x.dx=FP_OFF((void far*)pal);  // получить смещение pal

r.x.ax=0xl012;                  // int l0h, функция 12h

// (установка регистров DAC)

r.x.bx=0;                      // первый регистр DAC

r.x.cx=256;                     // количество регистров DAC

int86x(0х10,&r,&r,&s);          // вызвать видео BIOS } 

// Функция устанавливает видеорежим BIOS 13h

// это MCGA-совместимый режим 320х200х256 цветов

void InitVideo()

{

union REGS r ;

r.h.ah=0x0f;          // функция BIOS Ofh

int86(0х10,&r,&r);         // вызывать видео BIOS

PrevMode=r.h.al;           // сохранить текущий видеорежим

r.x.ax=0xl3;               // установить режим 13h

int86(0х10,&r,&r);         // вызвать видео BIOS

VideoRam=MK_FP(0xa000,0) ;  // создать указатель на видеопамять

}

// Функция восстанавливает изначальный видеорежим

void RestoreVideo() {

union REGS r;

r.x.ax=PrevMode;          // восстановить начальный видеорежиы

int86(0xl0,&r,&r);        // вызвать видео BIOS

}

// Функция загружает битовые карты

int InitBitmaps()

{

int r;

background=l;

r=ReadPcxFile("backgrnd.pcx",&pcx); // прочитать битовую карту

if(r != РСХ_ОК)        // выход при возникновении ошибки return FALSE;

BackGroundBmp=pcx.bitmap;    // сохранить указатель битовой

//  карты

SetAllRgbPalette(pcx.pal);   // установить палитру VGA

MemBuf=malloct(MEMBLK);       // выделить память под буфер

if(MemBuf == NULL)           // проверить на ошибки при

//  выделении памяти

return FALSE;

memset(MemBuf,0,MEMBLK);     // очистить

return TRUE;                 // Порядок!

}

// Функция освобождает выделенную программой память

void FreeMem()



{

free(MemBuf);

free(BackGroundBmp);

}

// функция рисует прокручиваемую битовую карту, не содержащую

// прозрачных пикселей; для скорости используется функция memcpyO;

// аргумент ScrollSplit задает столбец по которому битовая карта

// разбивается на две части

void OpaqueBlt(char *bmp,int StartY,int Height,int ScrollSplit)

{

char *dest;

int i;

dest=MemBuf+StartY*320; // вычисляем начальную позицию буфера

for(i=0;i<Height;i++)

{

// нарисовать левую половину битовой карты в правой половине буфера menicpy(dest+ScrollSplit,bmp,VIEW_WIDTH-ScrollSplit) ;

// нарисовать правую половину битовой карты в левой половине буфера memcpy(dest,bmp+VIEW_WIDTH-ScrollSplit,ScrollSplit);

bmp+=VIEW_WIDTH;

dest+=VIEW_WIDTH;

} } // конец функции

// Функция рисует смещающиеся слои

void DrawLayers()

{

OpaqueBlt(BackGroundBmp,0,100,background);

}

// Функция, обеспечивающая анимацию изображения.

// Наиболее критичная по времени выполнения.

// Для оптимизации как эту функцию, так и процедуры,

// которые она вызывает, рекомендуется переписать на ассемблере

// (В среднем это увеличивает производительность на 30%)

void AnimLoop()

{

while(KeyScan != ESC_PRESSED)    // цикл, пока не нажата ЕSС

{

switch(KeyScan)                 // обработать нажатую клавишу

{

case RIGHT_ARROW_PRESSED:      // нажата правая стрелка

background-=1;      // скроллировать фон на 2

// пикселя влево

if(background < 1)           // еще не конец образа?

background+=VIEW_WIDTH;    // ...тогда, можно смещать

// фон дальше

break;

case LEFT ARROW_PRESSED:       // нажата левая стрелка

background+=1;               // скроллировать фон на 2

// пикселя вправо

if(background > VIEW_WIDTH-1) // еще не конец образа

background-=VIEW_WIDTH;     // ...тогда можно смещать

// фон дальше

break;

default:                        // обработать все остальные

// клавиши break;

} DrawLayers();

memcpy(VideoRam,MemBuf,MEMBLK); // копировать MemBuf в

// VGA-память

frames++;

} }

// Функция осуществляет полную инициализацию



void Initialize()

{

InitVideo();            // установить режим 13h

InitKeyboard();         // установить собственный обработчик

// прерываний клавиатуры

if(!InitBitmaps())      // прочитать битовые образы

Cleanup();            // освободить память

printf("\nError loading bitmaps\n");

exit(l);

} }

// функция восстанавливает исходное состояние системы

void Cleanup()

{

RestoreVideo();       // восстановить VGA

RestoreKeyboard();    // восстановить вектор клавиатуры

FreeMem();            // освободить память

}

// Начало основной программы

int main()

{

clock_t begin, fini;

Initialize();

begin=clock();         // получить "тики" часов при старте

AnimLoop();            // начать анимацию изображения

fini=clock();          // получить "тики" часов в конце

Cleanup();             // освободить память

printf("Frames: %d\nfps: %gf\n", frames,

(float)CLK_TCK*frames/(fini-begin));

return 0;

}


Планирование игровых объектов


Под клонированием игровых объектов я понимаю повторное использование кода для множественной инициализации объектов одного типа, где каждый тип объекта имеет свою собственную копию данных. Рисунок 11.7 иллюстрирует это определение.

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

Например, у нас есть текст программы полета мухи в игровом пространстве и не более того. Мы можем использовать функции, приведенные в Листинге 11.3.

Листинг 11.3. Функции полета мухи.

void Erase_Flies(void) // удаление

мух

for (каждой структуры данных полета мухи) do

{

удаление мухи

} // конец цикла

void Move_Flies(void) // перемещение

мух

for (каждой структуры данных полета мухи) do

{

current_fly.x+=current_fly.xv; // перемещение по оси Х

current_fly.y+=current_fly.yv; // перемещение по оси У

}

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



Побитовое копирование изображения (бит-блиттинг)


Термин бит-блиттинг (bit blitting) — означает процесс перемещения группы битов (образа) из одного места экрана в другое. В играх для ПК нас интересует перемещение образа из области хранения вне экрана в область видеобуфера. Давайте посмотрим на рисунок 5.9, чтобы уяснить сущность этой операции.

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

Чтобы понять суть перемещения, нам следовало бы написать несколько функций, которые бы брали битовую карту из PCX-файла и перемещали ее на экран. Но я хочу проявить некоторую «авторскую вольность» и поговорить о спрайтах и их анимации.



Почему?


Почему музыкальные и звуковые эффекты должны использоваться в компьютерных программах? По той же самой причине, по которой все окружающие пользуются пиктограммами, — потому что это следующий логический шаг в разработке компьютерных программ. Пиктограммы и графика избавляют пользователя от необходимости представлять себе, что же они должны увидеть, музыка же говорит им, что они должны почувствовать.

Я бы не хотел принижать интеллект среднего пользователя, но чтение требует определенной аналитической работы. Чтение медленный процесс, оно не соответствует нашему естественному восприятию мира, и пользователи не очень-то любят читать. Вот тут на помощь и приходят пиктограммы. Уже установлено, что они повышают производительность, предлагая нам образы из «реального мира» (что бы это ни значило) и позволяют нам связать свои представления, например, о книжном шкафе или волшебнике с тем, что мы видим на экране. Музыка и звук продвигают все это на следующую логическую ступень.

Графика у пользователя ассоциируется с чувствами, вызываемыми объектом реального мира. Музыка и звук действуют еще более непосредственно. Они вызывают ассоциативные чувства — пользователь теперь уже может представить себе злого волшебника или удобный шкаф. И эти ассоциации могут оказаться очень мощным инструментом. В PC Magazine, в обзорной статье о мультимедиа, ветеран интерактивного видео предположил, что эмоции, вызываемые информацией, могут являться основной причиной информационных перегpyзoк и таким образом оказаться даже более важными, чем сама информация.

Звук и музыка могут:

§

Повысить информативность;

§          Сделать общение с компьютером более приятным;

§          Повысить развлекательную ценность программы.



Подайте сюда врагов! (разработка персонажей)


Теперь, когда игровое пространство и объекты созданы, вы должны вдохнуть жизнь в выдуманных персонажей, чтобы с ними можно было как-то взаимодействовать. Разработка персонажей, невидимому, является наиболее утомительной частью создания игры. Для этого придется неоднократно рисовать, по крайней мере, четыре, а то и восемь кадров для представления различных фаз движения существа. Каждый из этих кадров должен быть тесно связан с остальными, чтобы движения не выглядели дерганными.

Существует ли легкий путь?

Проще всего создавать персонажей и просматривать полученный результат с помощью какой-нибудь трехмерной анимационной программы типа 3D Studio Моделирование, анимация и последующее редактирование персонажей иногда может быть выполнено и вручную, но перспектива и оттенки персонажей при использовании анимационной программы окажутся более реалистичны. Если у вас есть только двухмерный графический редактор, то ничего не поделаешь придется каждый кадр рисовать вручную. Однако существует несколько технических приемов, позволяющих немного упростить задачу, и сейчас мы их обсудим.

Пропорции

Пропорции человеческого тела различны в зависимости от возраста. Что касается тела взрослого человека, то оно делится на 8 частей:

§

Голова занимает одну восьмую часть от общей высоты тела. Этот раздел заканчивается в нижней точке подбородка;

§          Ко второму и третьему разделам относятся шея и торс, причем третья часть заканчивается на уровне талии;

§          Четвертый раздел — это область от талии до бедер;

§          Пятая и шестая части содержат верхний отдел ног. Сюда же относятся и колени, находящиеся чуть выше нижней границы шестого уровня;

§          Седьмой и восьмой разделы включают в себя нижнюю часть ног;

§          Что касается рук, то локти располагаются немного выше уровня талии, а плечо — на стыке третьего и четвертого разделов.


В целом рука начинается от плеча и заканчивается на уровне 4/5 пятого раздела.

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

Основополагающие принципы для оживления персонажа

Файл ЕХАМР15.РСХ, изображенный на рисунке 16.31, включен для того, чтобы помочь вам в разработке идущего человекоподобного персонажа, рассматриваемого с восьми точек. Эти кадры были созданы с помощью 3D Studio и точно показывают, как выглядит персонаж с различных точек зрения. Для каждого вида изготовлено по четыре кадра. После оживления картинок движения героя выглядят вполне плавными и убедительными.




Получение ASCII-кодов с клавиатуры


Давайте теперь посмотрим, как мы можем получить ASCII-символ, введенный с клавиатуры. Это может быть полезно, когда игрок вводит свое имя и нам нужны ASCII-коды. Мы можем получить скан-коды и транслировать их в ASCII, но к чему такие сложности, если сразу можно прочитать ASCII-коды?

Листинг 3.6 показывает функцию, которую мы будем часто использовать, работая с клавиатурой. Эта программа опрашивает клавиши и определяет их нажатие. Если символ введен, то функция возвращает его ASCII-код, в противном случае возвращается 0.

Листинг 3.6. Получение ASCII-кодов с клавиатуры.

unsigned char Get_Ascii_Key(void)

{

//если это нормальный ascii

код — возвращаем его, иначе 0

if (_bios_keybrd(_KEYBRD_READY))

return(_bios_keybrd(_KEYBRD_READ));

else

return(0);

}// конец

функции

Чтобы использовать функцию из Листинга 3.6, вы должны выполнить примерно следующие действия:

if (( c=Get_Ascii_Key()) > 0)

{

обработать_символ

} иначе

{

символов_нет

}



Получение скан-кодов с клавиатуры


Код, представленный в Листинге 3.5 напрямую считывает скан-код и возвращает его в вызывающую программу. Если ввода нет, то функция возвращает 0.

Листинг 3.5. Получение скан-кодов с клавиатуры.

unsigned char Get_Scan_Code(void)

{

// получить скан-код нажатой клавиши

// используется встроенный ассемблер

//клавиша нажата?

_asm

{

mov ah,01h    ;функция Olh - проверка на нажатие клавиш

int 16h       ;вызвать прерывание

jz empty      ;нет нажатых клавиш — выходим

mov ah,00h    ;функция 0 - получить скан-код

int 16h       ;вызвать прерывание

mov al,ah     ;результат поместить в AL

xor ah,ah     ;обнуляем АН

jmp done      ;в AX возвращается значение "все в порядке"

empty:

xor ax,ax     ;очистить AX

done:

} // конец ассемблерного блока

} // конец функции

Мы опять используем встроенный ассемблер. Можно, было, конечно, использовать функцию BIOS через вызов _int86() в Си, но на встроенном ассемблере это выглядит намного круче.



Последнее слово о трехмерных трансформациях


Подведем итог разговору о трансформациях трехмерных объектов. Реализовать эти трансформации не сложно. Это только часть возможных преобразований. Существует много вариаций и несколько совершенно новых типов — деформация и т. д. В общем, мы уже достаточно знаем, чтобы перейти к проекциям.



Последние дорисовки


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

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



Последовательный интерфейс ПК


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

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

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

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



Построение коммуникационной библиотеки


Нам не надо иметь слишком много функций в нашей библиотеке. Фактически, нам достаточно шести функций:

§          Для инициализации последовательного порта;

§          Для установки процедуры обработки прерывания;

§          Для чтения символа из последовательного порта;

§          Для записи символа в последовательный порт;

§          Для определения состояния порта;

§          Для закрытия последовательного порта.



Построение траекторий


Пусть мы имеем отмасштабированные и отсеченные нужным образом спрайты, и теперь перед нами стоит проблема их передвижения в мире игры. Это означает, что мы должны вычислить траектории движения наших спрайтов. Зная траектории, мы также сможем определить угол между направлением движения объекта и направлением взгляда наблюдателя и выбрать подходящие кадры для вывода на экран. Это будет создавать иллюзию объемности при перемещении спрайтов по экрану.

Траектория — это на самом деле вектор. Для построения вектора необходимо иметь начальную и конечную точки. Так как в нашем случае траектория — это вектор скорости, мы должны иметь исходную точку в начале координат, как показано на рисунке 8.4.

Конечная точка содержит информацию двух типов;

§          Во-первых, направление траектории или скорости;

§          Во-вторых, величину линейной или угловой скорости.

Построить вектор скорости просто. Пусть космический корабль движется в направлении 50 градусов в плоскости Х-2 (см. рис. 8.5).

Формула 8.3. Вычисление вектора скорости.

Чтобы сосчитать вектор скорости вы должны использовать следующую формулу:

x_vel = cos(50) * speed

z_vel = sin (50) * speed                          

Вектор скорости теперь можно записать как: V == <х_vel, z_vel>

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

Замечание

Отметим, что вектор V лежит в плоскости X-Z, или в плоскости основания по отношению к экрану. Вы, возможно, и не заметили, что в Wing Commander корабли перемещаются, в основном, в этой плоскости, а не в полном X-Y-Z объеме.

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

Если вы захотите, вектор скорости может генерироваться случайно на основании данных, введенных игроком или полученных из файла. Но в любом случае вам необходимо знать направление движения объекта (или его главный вид), чтобы точно определить соответствующий кадр для вывода на экран. Ведь объекты представляются с помощью спрайтов, которые на самом деле плоские!



Повороты объектов


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

Вращение битовых объектов до сих пор еще не до конца исследованная область. Инженеры и программисты и по сей день пытаются найти наиболее эффективные алгоритмы для этой операции. Проблема заключается в том, что битовый образ - это конгломерат сотни, если не тысячи пикселей и чтобы повернуть образ, нам надо правильно произвести поворот каждой из этих точек, Обычно это связано с довольно сложными математическими преобразованиями, которые выполняются на персональном компьютере не очень-то быстро. Вы можете спросить: «А как же это сделал Крис Роберте в игре Wing Commander?» Очень просто: он заранее получил все возможные повороты всех космических кораблей с помощью программы моделирования, а потом просто занес их в гигантскую таблицу. Единственная операция, которая могла бывыполняться долго ~ масштабирование, тоже была произведена заранее, а результаты также занесены в таблицу. Я всегда действую подобным же образом и вам советую заранее создать все варианты поворотов ваших изображений, используя один из двух пакетов, либо Deluxe Paint фирмы Electronic Arts или, если вам нравится тратить деньги, 3D Studio фирмы AutoDesk. Затем поместите их в таблицу, и пусть ваша программа извлекает изображения из этой таблицы, используя угол поворота, как индекс. Отрицательная сторона этого метода - не очень рациональное использование памяти. Хранение 32 или 64 битных образов для всех возможных объектов отъедает существенный кусок памяти. Однако можно использовать частичные а не полные таблицы. Например, вы можете поместить в таблицу только кадры для одного квадранта, а для других симметричных квадрантов генерировать изображения уже во время игры. Так как поворот вокруг осей Х и Y делается очень просто — такой метод используется очень часто.

Чтобы показать пример поворота растрового изображения с помощью таблиц, я написал программу, которая использует нашу функцию масштабирования для файла PCX, содержащего несколько кадров с летящим астероидом.


Эта программа, которую я назвал AFIELD.С, передвигает астероид по трехмерному звездному полю, масштабируя его по мере приближения или удаления оигрока. Листинг 7.11 содержит текст этой программы.

Листинг 7.11. Трехмерный астероид (AFIELD.С).

// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ////////////////////////////////////////

#include <io.h>

#include <conio.h>                                       

#include <stdio.h>

#include <stdlib.h>

#include <dos.h>

#include <bios.h>

#include <fcntl.h>

#include <memory.h>

#include <malloc.h>

#include <math.h>

#include <string.h>

#include "graph0.h" // включаем нашу графическую библиотеку

// ОПРЕДЕЛЕНИЯ/////////////////////////////////////////////

#define NUM_STARS 30

// СТРУКТУРЫ ///////////////////////////////////////////////

typedef struct star_typ

{

int x,y;    // позиция звезды

int vel;    // скорость звезды по координате х

int color; // цвет звезды

} star,*star_ptr;

// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ///////////////////////////////////

star stars[NUM_STARS]; // звездное поле

sprite object;

 pcx_picture ast_cells;

// функции ///////////////////////////////////////////

void Star_Field(void) {

static int star_first=1; // Эта функция создает трехмерное звездное поле

int index;

// проверяем, следует ли нам создать звездное поле,

//то есть первый ли раз вызвана функция

if (star_first)

{ // обнуляем флаг первого вызова

star_first=0;

// инициализируем все звезды

for (index=0; index<NUM STARS; index++)

{ // инициализируем для каждой звезды позицию, скорость и цвет

stars[index].х     = rand()%320;

stars[index].у     = rand()%180;

// определяем плоскость звезды

switch(rand()%3){

case 0: // плоскость 1 - самая далекая

{

// установка скорости и цвета

stars[index].vel = 2;

stars[index].color.= 8;

} break;

case 1: // плоскость 2 - среднее расстояние

{

stars[index].vel = 4;

stars[index].color = 7;

) break;

 case 2://плоскость 3 - самая близкая



{

stars[index].vel = 6;

stars[index].color = 15;

} break;

} // конец оператора switch

} // конец цикла

//конец оператора if else                                  

{ // это не первый вызов, поэтому делаем рутинную работу -

// стираем, двигаем, рисуем                        

for (index=0; index<NUM_STARS; index++)

{                                 

if ((stars[index].x+=stars[index].vel) >=320 ) stars[index].x = 0;

// рисуем

Plot_Pixel_Fast_D(stars[index].x,stars[index].y, stars[index].color);     

} // конец цикла

} // конец оператора else

} // конец Star_Field                         

////////////////////////////////////////////////////////////

void Scale_Sprite(sprite_ptr sprite,float scale)    

{

// эта функция масштабирует спрайт, рассчитывая число дублирования

// исходных пикселей, необходимое для получения требуемого размера

char far *work_sprite;

int work_offset=0,offset,x,у;

unsigned char data;          

float y_scale_index,x_scale_step,y_scale_step,x_scale_index;

// берем первый пиксель исходного изображения

y_scale_index = 0;

// рассчитываем

дробный шаг

y_scale_step = sprite_height/scale;

x_scale_step = sprite_width /scale;

// для простоты даем указателю на спрайт новое имя

work_sprite = sprite->frames[sprite->curr_frame];

// расчет смещения спрайта в видеобуфере

offset = (sprite->y << 8) + (sprite->y << 6) + sprite->x;

// построчно масштабируем спрайт

for (у=0; y<(int) (scale); у++)

{

// копируем следующую строчку в буфер экрана

x_scale_index=0;

for (x=0; x<(int)scale; x++)

{

// проверка на прозрачность пикселя

//(то есть равен ли он 0), если нет - прорисовываем пиксель

if ((data=work_sprite[work_offset+(int)x_scale_index]))

double_buffer[offset+x] = data;

x_scale_index+=(x_scale_step) ;

} // конец цикла по X

//используя дробный шаг приращения определяем

// следующий пиксель исходного изображения

у_scale_index+=y_scale_step;

// переходим к следующей строке видеобуфера



//и растрового буфера спрайта

offset     += SCREEN_WIDTH;

work_offset = sprite_width*(int)(y_scale_index) ;

} // конец цикла по Y

} // конец Scale Sprite ////////////////////////////////////////////////////////////

void Clear_Double_Buffer(void)

{ // эта функция очищает дублирующий буфер

// несколько грубо, зато работает

_fmemset(double_buffer, 0, SCREEN_WIDTH * SCREEN_HEIGHT + 1);

} // конец Clear_Double_Buffer

// ОСНОВНАЯ ПРОГРАММА ///////////////////////////////////////

void main(void)

{

int index,

done=0,dx=5,dy=4,ds=4;

float scale=5;

// установка видеорежима 320х200х256

Set_Mode(VGA256) ;

// установка размера спрайта

sprite_width

= sprite_height = 47;

// инициализация файла PCX, который содержит

// мультипликационные кадры

PCX_Init((pcx_picture_ptr)&ast_cells) ;

// загрузка файла PCX, который содержит мультипликационные кадры

PCX_Load("asteroid.рсх", (pcx_picture_ptr)&ast_cells,1) ;

// резервируем память под дублирующий буфер

Init_Double_Buffer() ;

Sprite_Init((sprite_ptr)&object,0,0,0,0,0,0) ;

// загрузка кадров вращающегося астероида

PCX_Grap_Bitmap((pcx_picture_ptr)&ast_cells,

(sprite_ptr)&object,0,0,0);

PCX_Grap_Bitmap ((pcx_picture_ptr) &ast_cells,

(sprite_ptr)&object,1,1,0} ;

PCX_Grap_Bitmap((pcx_picture_ptr)&ast_cells,

(sprite_ptr)&object,2,2,0) ;

PCX_Grap_Bitmap((pcx_picture_ptr)&ast_cells,

(sprite_ptr)&object,3,3,0) ;

PCX_Grap_Bitmap ((pcx_picture_ptr) &ast_cells,

(sprite_ptr)&object,4,4,0);

PCX_Grap_Bitmap((pcx_picture_ptr)&ast_cells,

(sprite_ptr)&object,5,5,0) ;

PCX_Grap_Bitmap((pcx_picture_ptr)&ast_cells, (sprite_ptr)&object,6,0,1);

PCX_Grap_Bitmap({pcx_picture_ptr)&ast_cells, (sprite_ptr)&object,1,1,1) ;

// позиционирование объекта в центре экрана

object.curr_frame =0;

object.x          = 160-(sprite width>>1);

object.у          = 100-(sprite_height>>1) ;

// очистка

дублирующего буфера

Clear_Double_Buffer();



// вывол

масштабированного спрайта

Scale_Sprite((sprite_ptr)&object,scale) ;

Show_Double_Buffer(double_buffer) ;

// главный цикл

while (!kbhit())

{ // масштабируем астероид

scale+=ds;

// не слишком ли велик или мал астероид?

if (scale>100 |1 scale < 5)

{

ds=-ds;

scale+=ds;

} // конец if

// перемещаем астероид

object.x+=dx;

object.y+=dy;

// коснулся ли астероид края экрана?

if ((object.x + scale) > 310 || object.x < 10)

{

dx=-dx;

object.x+=dx;

} // конец if

if ((object.у + scale) > 190 || object.у < 10) {

dy=-dy;

object.y+=dy;

} // конец if

// поворот астероида на 45 градусов

if (++object.curr_frame==8) object.curr_frame=0; // очистка дублирующего буфера

Clear_Double_Buffer();

// прорисовка звезд

Star_Field() ;

// масштабируем спрайт и выводим его в дублирующий буфер

Scale_Sprite((sprite_ptr)&object,scale) ;

// выводим дублирующий буфер на экран

Show_Double_Buffer (double_buffer);

} // конец оператора while

// удаляем файл PCX

PCX_Delete ((pcx_picture_ptr) &ast_cells);

// возврат в текстовый режим

Set_Mode (TEXT_MODE) ;

} // конец функции main


Позиционирование объекта


Теперь поговорим о строчке, которая определяет позицию объекта в структуре из Листинге 4.4. Координаты (хо,уо) описывают начальную позицию объекта на плоскости.

Многоугольник или объект рисуется относительно начальной позиции. Все это подводит нас к понятию относительной системы координат. Возможно, вы не знаете, что картезианская система называется еще мировой системой координат. Подразумевается, что она просто огромна. В то же самое время экран ПК имеет свою систему координат, называемую экранной. При этом все объекты на экране имеют свою, локальную систему координат. Это показано на рисунке 4.5.

Мы можем определить объект в локальной системе координат, затем преобразовать эти координаты в мировые и, наконец, нарисовать наш объект в экранной систем координат. Выглядит это достаточно долгим занятием, но на

деле все оказывается несколько проще. Давайте договоримся, что у нас на компьютере мировые и экранные системы координат совпадают. Это значит, что:

§          Точка (0,0) находится в левом верхнем углу экрана;

§          При движении вправо увеличивается значение Х-координаты;

§          При перемещении вниз увеличивается Y-координата.

Благодаря этим допущениям мы получаем экранные координаты, похожие на координаты 1-го квадранта (положительные значения осей Х и Y), но при этом надо всегда помнить, что ось Y у нас перевернута относительно экрана.

В принципе, в этом нет ничего страшного, хотя и несколько непривычно. Чтобы чувствовать себя уверенно, перевернем ось Y в нормальное положение. Тогда точка (0,0) будет находиться в левом нижнем углу экрана, как это показано на рисунке 4.6.

Теперь у нас есть все средства для представления объектов в компьютере. В следующем разделе мы обсудим, как перемещать объекты по экрану, поворачивать и масштабировать их.



Правильный расчет масштаба


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

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

Опять-таки не надо забывать о пространственных искажениях, возникающих при смешивании одной системы виртуальных объектов с другой. Поправочный коэффициент, сглаживающий эти искажения, легче всего подобрать экспериментально. Наша масштабирующая подсистема будет вычислять пиксельный размер, в соответствии с которым растровые текстуры должны будут соответственно сжиматься и растягиваться. Сам расчет масштаба на основании Z-координаты спрайта будет производиться по формуле 8.2.

Формула 8.2. Расчет масштабирования.

scale=scale_distance/sprite_z,

где scale_distance расстояние - визуально дающее хороший результат.

Мы знаем, что создание трехмерной графики не может быть легким делом с ней надо повоевать! И мне это даже нравится! Существует еще одна небольшая проблема - отсечение. И большая часть этой главы посвящена именно этой проблеме.



Преследование


Во-первых, нам необходимо знать расположение обоих объектов. У нас есть эти данные, так как мы знаем координаты игрока и игрового объекта, являющегося врагом.

Во-вторых, нам необходимо сконструировать алгоритм, который будет управлять поведением врага, преследующего игрока. Алгоритм 13.1 делает именно то, что мы от него и хотим:

Алгоритм 13.1. Алгоритм Преследования.

// Предположим, что рх,ру - координаты игрока,

// ех,еу - координаты противника

while (игра)

(

программный код

// Вначале рассматриваем перемещение по горизонтали (ось X)

if ех>рх then ex=ex+l if ex<px then ex=ex-l

 // Теперь рассматриваем вертикальный (Y) компонент

if ey>py then ey=ey+l if ey<py then ey=ey-l

программный код

}

§

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

§          Та же самая логика применима и к перемещению по вертикали.

Используя этот алгоритм, противник преследует игрока почти столь же неумолимо, как и Т1000 из Терминатора-2. Он не остановится до тех пор, пока не поймает игрока. Мы могли бы несколько облегчить его задачу путем добавления некоторой дополнительной логики, способствующей его движению к позиции нанесения удара. Однако, перед тем, как это делать, давайте посмотрим программу, моделирующую преследование. Листинг программы 13.1 рисует две точки: одну голубую (вы), а другую красную (противник). Что бы вы ни предпринимали, красная точка пытается настичь вас. Для движения (или, я бы сказал, бега!) нажмите клавишу U - перемещение вверх, N - вниз, Н -влево и J - вправо. Для выхода из программы нажмите Q.

Листинг 13.1. Программа Терминатор (TERM.С).

// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////

#include <stdio.h>

#include <graph.h>




// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ///////////////////////////////////

// указатель на переменную BIOS, содержащую текущее

// значение системного таймера. Это значение изменяется

// с частотой 18.2 раза в секунду

usigned int far *clock = (unsigned int far *)0x0000046C;

// функции /////////////////////////////////////////////////

void Timer(int clicks)

{ // Эта функция использует значение таймера для формирования

// задержки. Необходимое время задержки задается в "тиках"

// интервалах в 1/18.2 сек. Переменная, содержащая 32-битовое

// текущее значение системного таймера, расположена

// по

адресу 0000:046Ch

unsigned int now;

// получить текущее время

now = *clock;

// Ничего не делать до тех пор, пока значение таймера не

// увеличится на требуемое количество "тиков".

// Примечание: один "тик" соответствует примерно 55мс.

while(abs(*clock - now) < clicks){}

} // конец функции Timer

// ОСНОВНАЯ ПРОГРАММА ////////////////////////////////////

void main(void)

{

int рх=1б0,ру=100, // стартовая позиция игрока

ex=0,ey=0;     // стартовая позиция врага

done=0;        //флаг окончания работы программы

_setvideomode(_MRES256COLOR);

printf("     Terminator - Q to Quit");

//сновной игровой цикл

whilе(!done)

{

// удалить точки            

_setcolor(0);

_setpixel(px,py) ;

_setpixel(ex,ey) ;

// передвинуть игрока

if (kbhit())

{

// куда движется игрок?

switch(getch())

{

case 'u' : // вверх

{

py-=2;

} break;

case 'n': // вниз

{

py+=2;

} break;

case 'j': // вправо

{

px+=2;

} break;

case 'h': // влево

{

px-=2;

} break;

case 'q':

{

done=1;

}break;

} // конец оператора switch

} // конец обработки нажатия клавиши

// переместить врага

// начало работы "мозга"

if (рх>ех) ех++;

if (рх<ех) ех--;

if

(рy>еу) еу++;

if (рy<еу) еу--;

// конец работы "мозга"

// нарисовать точки

_setcolor(9);                                

_setpixel(px,py);

_setcolor(12);

_setpixel(ex,ey);

// немного подождать

Timer(2);

} // конец основного игрового цикла while

_setvideoroode(_DEFAULTMODE) ;

} // конец функции main

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


Придание «кирпичикам» стен глубины трехмерного пространства


Созданные вами «кирпичики» могут выглядеть на мониторе абсолютно безупречно, но когда вы помещаете их в трехмерное пространство игры, они вдруг становятся выглядящими как-то не так. В чем же дело?

Как правило, когда «кирпичики?» в трехмерном пространстве рассматриваются под углом, участки, имеющие темный оттенок, кажутся утопленными внутрь, и наоборот, светлые фрагменты выдвигаются на передний план. Помните это, когда создаете изображения и старайтесь правильно использовать. Чтобы придать вашим «кирпичикам» глубину и форму, воспользуйтесь приемом, продемонстрированным на рисунке 16.1. Постепенно изменяйте оттенки цветов от светлого к темному, располагая светлые ближе к поверхности, а темные - в глубине. Иногда, чтобы придать определенным частям более реалистичный вид, используйте резкий переход между оттенками светлого и темного,



У вас может быть более


У вас может быть более поздняя версия WarEdit'a. Однако база данных, которую создает программа, описанная в этой книге, будет почти полностью совместима с любой более новой версией WarEdit'a. Я гарантирую, что оформление и текстуры будут другими, цвета тоже могут измениться. Поэтому, если вы обнаружите, что ваши объекты в новой версии редактора выглядят как-то иначе, не обращайте на это внимания — новый WarEdit будет способен загрузить ваши уровни. Я мог бы Дать вам последний вариант программы прямо сейчас, однако, прилагаемая версия более надежна в работе. В следующей главе вы увидите, какие изменения в редактор нужно ввести и зачем. Если я их реализую, то вы о них обязательно прочтете в аннотации к программе.
Теперь, когда мы поговорили о WarEdit с точки зрения пользователя, давайте обсудим его структуру и формат базы данных.

Примечание по поводу демонстрационной программы


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

Внимание

Все программы в этой главе написаны на Borland C++ 3.1 и Турбо ассемблере 3.1. Однако все примеры на Си были написаны с максимальной осторожностью, без привлечения особенностей Borland С. Так что они должны легко компилироваться любыми трансляторами C/C++ без внесения значительных изменений. Программы на ассемблере писались с использованием ключа IDEAL. Обратите внимание, что они не используются в демонстрационном примере, приведенном в этой главе, поскольку здесь же приведены их аналоги на Си.



Примечания по выполнении


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

Это происходит потому, что время доступа к системной памяти значительно выше, чем к области видеобуфера. Однако, это не всегда справедливо. Некоторые адаптеры, такие как LOCAL BUS, работают значительно быстрее.

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

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

Если вам никак не обойтись без цикла FOR, попробуйте его развернуть. Некоторые оптимизирующие компиляторы будут пытаться развернуть циклы, но гораздо практичнее это сделать вручную (мы подробно рассмотрим методику разворачивания циклов в восемнадцатой главе «Техника оптимизации»).

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

Ваша стратегия должна выглядеть так:




§
При разработке новых алгоритмов пишите функции на Си;
§          Занимайтесь их отладкой до тех пор, пока не убедитесь, что они правильно работают и соответствуют тем задачам, для которых предназначены;
§          Найдите наиболее критичные по быстродействию части программы и перепишите их на ассемблере.
Примечание
Вам понравилась графика, использованная в этой главе в демонстрационных целях? Я уверен, что понравилась. Не будет преувеличением сказать, что качественные рисунки облагораживают игру в целом. Вся графика, использованная в этой главе, была пожертвована автором шестнадцатой главы Дениз Тайлер. Спасибо Дениз!
ИТОГ
Существует множество различных способов моделирования параллакса. Методы, представленные в этой главе, не являются самыми быстрыми и наиболее элегантными. Однако они обладают тем достоинством, что они относительно просты в реализации и для понимания.
Уверен, что применение технических приемов из этой И других глав при разработке своих собственных игр доставит вам немало удовольствия. Не бойтесь экспериментировать! Это истинное наслаждение.

Применение логических операций


Для того чтобы переместить образ (процесс блиттинга) на экран или даже вынести его за пределы рабочей области экрана, можно выполнять логические операции с исходными и результирующими пикселями. Если вы внимательно посмотрите на функцию DrawSprite () в Листинге 5.13 пятой главы, «Секреты VGA-карт», то увидите, что она на самом деле не просто замещает данные, а предварительно проверяет их тип:                                       

§

Если пиксели «непрозрачные (то есть они не черного цвета), то на экране происходит замена исходных данных соответствующими пикселями;

§          Если пиксели «прозрачны», то экранные данные остаются без изменений.

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

if ((data=work_sprite[work_offset+x]))

video_screen[offset+x]=data;

Оператор IF выполняется много раз. Если быть точными, то число повторов этой операции равно произведению высоты спрайта на его ширину. Так для образа 64х64 это составит 4096 раз. Не слишком ли много?!        

Однако в некоторых случаях можно отказаться от использования оператора IF и выводить спрайт без проверки пикселей на «прозрачность». Если нарисованный нами спрайт имеет точно прямоугольную форму и целиком занимает объем 64х64 пикселя, исходные данные без ущерба можно замещать на реэультирующие (конечно, если внутри самого изображения нет «прозрачных» областей). Увы, так бывает довольно редко! Конечно, в подобных случаях мы можем применять и такой вариант блиттинга (например, при текстурировании стен), но, тем не менее, попробуем найти «золотую середину» между использованием оператора IF и функции memсру ().

Один из способов размещения данных на экране состоит в употреблении поразрядных логических операций типа OR, XOR, AND и NOT. Таким образом, мы можем воспользоваться ими вместо оператора IF, поскольку они быстрее обрабатываются центральным процессором.


Осталось только выяснить совсем немного — какой же из логических операторов применить и что он будет делать?  Вспомним, как представлены данные в режиме 13h. Каждый пиксель задается одним байтом, значение которого используется в качестве индекса в таблице выбора цвета. Если мы начнем производить логические операции с исходными и результирующими пикселями, то изменим индекс цвета, а, следовательно, и сам цвет, чего нам совсем не нужно!

Рассмотрим несколько примеров. Допустим, мы отображаем пиксель красного цвета (со значением 10) на экран в точку (100,100), в которой уже находится розовый пиксель (56). В результате отображения мы хотели бы увидеть на экране наш красный пиксель (10) в положении (100,100). Однако при использовании имеющихся в нашем распоряжении логических операций, вместо этого мы получим значения, приведенные в таблице 7.1.

Таблица 7.1. Результаты логических операций.

Дано: 56 - 00111000b и 10 = 00001010b:

Источник(битовый образ)

Пример обработчика прерывания № 1 - Там полно звезд...


Чтобы продемонстрировать вам, насколько полезными могут оказаться многозадачность и прерывания, я написал в виде обработчика прерывания программу, которая рисует трехмерное звездное небо. Как вам уже известно, при каждом приращении счетчика внутреннего таймера, генерируется прерывание. При помощи вектора 0х1С я сопоставил этому прерыванию свою процедуру обслуживания, которая и создает трехмерное звездное небо. Поскольку обращение к моему обработчику происходит 18.2 раза в секунду, значит и картина звездного неба обновляется с этой же частотой. Нечто подобное мы могли бы использовать при написании игры, работающей на одном экране (игры типа «перестреляй их всех»). Наше звездное небо будет изменяться независимо от того, что будет происходить на переднем плане.

Обратите внимание на то, что процедура обслуживания прерывания сделана автономной. Это означает, что она инициализирует свои данные при первом обращении к ней (в этот момент она создает базу данных с информацией о звездах), а при последующих вызовах просто видоизменяет картинку.

И напоследок еще одно маленькое замечание: завершая программу, вы можете оставить ее резидентной в памяти, нажав клавишу Е. Сделав это, вы увидите, что звездное небо присутствует в строке приглашения DOS, что выглядит несколько странно! Причина такого поведения программы кроется в том, что когда вы завершаете ее нажатием клавиши Е, она не восстанавливает прежний обработчик прерывания. Компьютер продолжает вызывать по прерыванию от таймера наш обработчик. Вы наверняка даже и представить себе не могли, что это сработает! И, тем не менее, завершаясь, программа оставляет в оперативной памяти большую часть своего кода неизменным, в результате чего обработчику прерывания удается пережить окончание работы породившей его программы. Если вы попытаетесь запустить еще какую-нибудь программу, компьютер, скорее всего, «зависнет». Поэтому для написания настоящих резидентных программ такой метод применять не стоит. (На самом деле для этих целей предназначена специальная функция DOS, которая называется dos keep, но сейчас мы не будем подробно рассматривать резидентные программы.


Мы просто случайно создали одну из них). Если система «зависнет», вам придется перезагрузиться.
Текст программы, изображающей трехмерное звездное небо, представлен в Листинге 12.6.
Листинг 12.6. Трехмерное звездное небо (STARS.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ /////////////////////////////////////
#include <dos.h>
#include <bios.h>
#include <stdio.h>
#include <math.h>
#include <conio.h>
#include <graph.h>
// ОПРЕДЕЛЕНИЯ //////////////////////////////////////////
#define TIME_KEEPER_INT 0x1C
#define NUM_STARS 50
// структуры ////////////////////////////////////////////
typedef struct star_typ
int x,y;    // координаты звезды
int vel;    // проекция скорости звезды на ось Х
int color; // цвет звезды
} star, *star_ptr;
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ /////////////////////////////////////////
void (_interrupt _far *01d_Isr)(void);
// хранит старый обработчик прерывания
unsigned char far *video_buffer = (char far *)0xA0000000L;
// указатель на видеобуфер
int star_first=1; // флаг первого вызова автономной функции
// звездного неба
star starstNUM_STARS]; //звездное небо
// функции ///////////////////////////////////////////////////////
void Plot_Pixel_Fast(int x,int y,unsigned char color)
{
// эта функция рисует точку заданного цвета несколько быстрее,
// чем обычно за счет применения операции сдвига вместо операции
// умножения
// используем известный факт, что 320*у = 256*у + 64*у = у<<8 + у<<6
video_buffer[((у<<8) + (у<<6)) + х] = color;
} // конец Plot Pixel_Fast
/////////////////////////////////////////////////////////////
void _interrupt _far Star_Int(void)
{
// эта функция создает иллюзию трехмерного звездного неба,
// наблюдаемого из иллюминатора космического корабля Enterprise
// Замечание: эту функцию следует исполнять не реже одного раза
// в 55.4мс, иначе не избежать перезагрузки!
int index;
// проверка, надо ли нам инициализировать звездное поле
// (то есть первый ли раз вызывается функция)         


if (star_first)
{
// сброс флага первого вызова
star_first=0;
// инициализация всех звезд
for(index=0; index<NUM_STARS; index++)
{
// инициализация цвета, скорости и координаты каждой звезды
stars[index].х     = rand()%320;
stars[index].у     = rand()%180;
// выбор плоскости для звезды switch(rand()%3)
{ case 0:
// плоскость 1 - наиболее удаленная плоскость
{
// установка скорости и цвета звезды
stars[index].vel = 2;
stars[index].color = 8;
} break;
case 1: // плоскость 2 - плоскость, расположенная
// посередине
{
stars[index].vel = 4;
stars [index] .color =7;
} break;
case 2: // плоскость 3 -самая ближняя плоскость
{
stars[index].vel = 6;
stars[index].color = 15;
} break;
} // конец оператора switch
} // конец цикла
} // конец оператора if
else
{ // не первый вызов функции, поэтому производим рутинные
// действия: стирание, перемещение, рисование
for (index=0; index<NUM_STARS; index++)
{ // стирание
Plot_Pixel_Fast(stars[index].х,stars[index]-у,0);
// перемещение
if ((stars[index].x+=stars[index].vel) >=320 ) stars[index].х = 0;
// рисование
Plot_Pixel_Fast(stars[index],x,stars[index],y, stars[index].color);
} // конец цикла
} // конец else
} // конец Star_Int
// ОСНОВНАЯ ПРОГРАММА ///////////////////////////////////
void main(void)
{ int num1, num2,с
;
_setvideomode(_MRES256COLOR) ;
// установка обработчика прерывания
Old_Isr = _dos_getvect(TIME_KEEPER_INT) ;
_dos_setvect(TIME_KEEPER_INT, Star_Int);
// ожидание нажатия клавиши пользователем
_settextposition(23,0);
printf("Hit Q - to quit.");
printf("\nHit E - to see something wonderful...");
// чтение символа
с = getch();
// хочет ли пользователь рискнуть?
if (c=='e')
{
printf("\nLook stars in DOS, how can this be ?") ;
exit(0);
// выход без восстановления старого обработчика прерывания
} // конец оператора if
/ восстановление старого обработчика прерывания
_dos_setvect(TIME_KEEPER_INT, 01d_Isr);
_setvideomode(_DEFAULTMODE) ;
}// конец
функции main

Пример обработчика прерывания № 2 - Ловим нажатия клавиш!


Как мы узнали в третьей главе, «Основы работы с устройствами ввода», мы можем использовать BIOS для чтения нажатия клавиши путем проверки значения скан-кода. Это отличный способ, и он вполне применим для большинства случаев, но что делать, если вы хотите одновременно нажать две клавиши? (Примером такой ситуации может служить момент, когда вы одновременно нажимаете клавиши «стрелка вверх» или «стрелка вниз» и «стрелка вправо» чтобы изменить направление движения на диагональное). Единственный способ обработать подобную ситуацию в программе требует более тонкой работы с клавиатурой. BIOS по сравнению со стандартными функциями Си просто дает нам еще один уровень управления, но если мы хотим добиться необходимой для профессионально написанных компьютерных игр функциональности, нам следует глубже, глубже и еще глубже разобраться с клавиатурой.

На самом деле клавиатура персонального компьютера представляет собой отдельный микрокомпьютер, который называется 8048. При нажатии или отпускании клавиши клавиатура посылает последовательность сигналов персональному компьютеру на обработку. BIOS сообщает нам о том, какая клавиша нажата, но умалчивает об отпущенных клавишах. Такой половинчатой информации не достаточно для того, чтобы организовать обработку одновременного нажатия нескольких клавиш. Нам нужно знать и когда была нажата, и когда была отпущена та или иная клавиша, чтобы написать алгоритм, способный отслеживать состояния некоторого набора клавиш. При нажатии клавиши мы выставляем соответствующий ей флаг в еостояние «нажато», а при отпускании — сбрасываем значение этого флага. Применяя такой подход, мы регистрируем нажатия различных клавиш и храним эту информацию в нашей структуре данных до тех пор, пока клавиша не будет отпущена.

При нажатии клавиши клавиатурой посылается код нажатия, а при отпускании — соответствующий код отпускания. Эти коды похожи на уже известный вам скан-код. Различие состоит в том, что при отпускании клавиши, каким бы ни было значение кода нажатия, к нему всегда прибавляется 128.




Чтобы наша программа смогла взять контроль за клавиатурой на себя, мы выгрузим обработчик клавиатуры операционной системы DOS и установим наш собственный драйвер. Он будет получать коды клавиш независимо от того, что произошло — нажатие или отпускание, а затем сохранять эту информацию в глобальной переменной, к которой имеет доступ наша Си-программа. При таком подходе функция нашей программы сможет использовать текущее значение этой переменной для выяснения того, какие клавиши в данный момент нажаты, а какие отпущены. Разобравшись что к чему, функция сведет эту информацию в таблицу. Клавиатурное прерывание имеет номер 0х09. Все, что нам требуется сделать, это написать и установить соответствующую процедуру обработки данного прерывания.
Прежде чем мы этим займемся, вспомним адреса клавиатурных портов ввода/вывода и их функции. Собственно клавиатурный порт ввода/вывода находится по адресу 60h, а регистр, управляющий клавиатурой — по адресу 61lh Эти порты находятся на микросхеме PIA (Peripheral Interface Adapter). Кроме всего прочего, нам еще потребуется выполнить определенные процедуры перед вызовом нашего обработчика прерывания и после его завершения. Общий порядок всех действий таков:
1.
Войти в процедуру обслуживания прерывания. Это происходит при каяждом нажатии клавиши.
2.       Прочитать из порта ввода/вывода 60h код клавиши и поместить его в глобальную переменную для последующей обработки программой или обновления содержимого таблицы, в которой хранится информация о нажатых клавишах.
3.       Прочитать содержимое управляющего регистра из порта ввода/вывода 61h и выполнить над ним логическую операцию OR с числом 82h.
4.       Записать полученный результат в порт регистра управления 61h.
5.       Выполнить над содержимым управляющего регистра логическую операцию AND с числом 7Fh. Это сбрасывает состояние клавиатуры, давая ей понять что нажатие на клавишу обработано и мы готовы к считыванию информации о нажатии других клавиш.


6.       Сбросить состояние контроллера прерываний 8259. (Без этого можно и обойтись, однако лучше подстраховаться). Для этого следует записать в порт 20h число 20h. Забавное совпадение, не правда ли?
7.       Выйти из обработчика прерывания.
Листинг 12.7 содержит текст программы обработчика клавиатурного прерывания, позволяющего отслеживать состояние клавиш со стрелками. При работе этой программы вы можете использовать курсорные клавиши или их комбинации для перемещения маленькой точки в любом направлении по экрану.
Листинг 12.7. Киберточка (CYBER.C)._____________________
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ///////////////////////////////////////
#include
<dos.h>
#include <bios.h>
#include <stdio.h>
#include <math.h>
#include <conio.h>
#include <graph.h>
// ОПРЕДЕЛЕНИЯ //////////////////////////////////////////
#define KEYBOARD_INT    0х09
#define KEY_BUFFER      0х60
#define KEY_CONTROL     0х61
#define INT_CONTROL     0х20
// коды нажатия и отпускания для клавиш со стрелками
#define MAKE_RIGHT      77
#define MAKE_LEFT       75
#define MAKE_UP         72                                        
#define MAKE_DOWN       80
#define BREAK__RIGHT     205
#define BREAK_LEFT     203
#define BREAK_UP        200
#define BREAK_DOWN      208
// индексы в таблице состояния клавиш со стрелками
#define INDEX_UP        0
#define INDEX_DOWN      1
#define INDEX_RIGHT     2
#define INDEX_LEFT      3
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ////////////////////////////////
void (_interrupt _far *01d_Isr)(void);
// хранит старый обработчик прерывания
unsigned char far *video_buffer = (char far *)0xA0000000L;
// указатель на видеобуфер
int raw_key;    // необработанные данные от клавиатуры
int key_table[4] = {0,0,0,0};
// таблица состояний клавиш со стрелками
// ФУНКЦИИ //////////////////////////////////////////////
void Plot_Pixel_Fast(int x,int y,unsigned char color)
{
// эта функция рисует точку заданного цвета несколько быстрее,


// чем обычно за счет применения операции сдвига вместо операции
// умножения
// используем известный факт, что 320*у = 256*у + 64*у = у<<8 + у<<6
video_buffer[((y<<8) + (у<<6) ) + х] = color;
} // конец Plot_Pixel_Fast
/////////////////////////////////////////////
void Fun_Back(void)
{
int index;
// несомненно  запоминающийся рисунок фона
_setcolor(1) ;
_rectangle(_GFILLINTERIOR, 0,0,320,200);
_setcolor{15) ;
for (index=0; index<10; index++)
{
_moveto(16+index*32,0);
_lineto(16+index*32,199);
} // конец
цикла
for (index=0; index<10; index++)
{
_moveto(0,10+index*20) ;
_lineto(319,10+index*20);
} // конец цикла
} // конец Fun Back
///////////////////////////////////////
void _interrupt _far New_Key_Int(void)
{
// я в настроении немного попрограммировать на ассемблере!
_asm{
sti                ; разрешаем прерывания
in al,KEY BUFFER   ; получаем нажатую клавишу
xor ah,ah          ; обнуляем старшие 8 бит регистра АХ
mov raw_key, ax  ; сохраняем код клавиши
in al,KEY_CONTROL ; читаем управляющий регистр
or al,82h       ; устанавливаем необходимые биты для сброса FF
out KEY_CONTROL,al ; посылаем новые данные в управляющий регистр
and al,7fh
out KEY_CONTROL,al ; завершаем
сброс
mov al,20h
out INT CONTROL,al; завершаем
прерывание
} // конец ассемблерной вставки
// теперь вернемся К Си, чтобы изменить данные
// в таблице состояния клавиш со стрелками
// обработка нажатой клавиши и изменение таблицы
switch(raw_key)
{
case MAKE_UP:    // нажатие стрелки вверх
{
key_table[INDEX_UP]    = 1;
} break;
case MAKE_DOWN:  // нажатие стрелки вниз
{
key_table[INDEX_DOWN]  = 1;
) break;
case MAKE_RIGHT: // нажатие' стрелки вправо
{
key_table[INDEX_RIGHT] = 1;
} break;
case MAKE_LEFT:  // нажатие стрелки влево
{
key__table[INDEX_LEFT]  = 1;
} break;
case BREAK_UP:    // отпускание стрелки вверх
{
key_table[INDEX_UP]    = 0; 
} break;
case BREAK DOWN:  // отпускание стрелки вниз
{
key_table[INDEX_DOWN]  = 0;


} break;
case BREAK_RIGHT; // отпускание стрелки вправо
{
key_table[INDEX_RIGHT] = 0;
} break;
case BREAK_LEFT:  // отпускание стрелки влево
{
key_table[INDEX_LEFT]  = 0;
} break;
default: break;
} // конец оператора switch
} // конец New_Key_Int
//  ОСНОВНАЯ ПРОГРАММА ////////////////////////////////
void main(void)
{
int dопе=0, x=160, y=100;// флаг выхода и координаты точки
//установка видеорежима 320x200x256
_setvideomode(_MRES256COLOR) ;
Fun_Back(); // оригинальная картинка, не так ли?
printf("\nPress ESC to Exit.");
// установка нового обработчика прерывания
Old_Isr = _dos_getvect(KEYBOARD_INT) ;
_dos_setvect(KEYBOARD_INT, New_Key_Int);
// основной цикл
while(!done)
{
_settextposition(24,2) ;
printf("raw key=%d   ",raw_key);
// смотрим в таблицу и перемещаем маленькую точку
if (key_table[INDEX_RIGHT]) х++;
if (key_table[INDEX_LEFT]) х--;
if (key_table[INDEX_UP]) y--;
if (key_table[INDEX_DOWN]) y++;
// рисуем
киберточку
Plot_Pixel_Fast(x,y,10);
// Наша клавиша завершения. Значение кода нажатия ESC равно 1
if (raw_key==1) done=l;
} // конец while
// восстановление старого обработчика прерывания
_dos_setvect(KEYBOARD_INT, Old_Isr) ;
_setvideomode (_DEFAULTMODE) ;
} // конец
функции main
Уфф... Вот и все, ребята!

Пробовали ли вы текстурировать?


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

Вы будете приятно удивлены, узнав что достаточно внимательно посмотреть на всевозможные здания и сооружения, а затем посвятить некоторое время программе Deluxe Paint. Взгляните на рис. 7.8.

Эти несколько грубых текстур я сделал с помощью этой программы за несколько минут. Эти текстуры не так уж плохи, к тому же они показывают, что это можно сделать и не будучи профессионалом. Однако если вы рисуете совсем как кура лапой, то можете воспользоваться тысячей уже готовых текстур с одного из многочисленных дисков CD-ROM.

(Однако, пожалуйста, не размещайте водопроводные краны и ванные на стенах, как это сделано в некоторых играх, названия которых вы сами легко вспомните.)



Проекции


Сейчас мы знаем, как изобразить трехмерный объект и как произвести операции перемещения, масштабирования и поворота этого объекта. Возникает вопрос: «А как мы можем рисовать трехмерные объекты на плоском экране?» ответ прост: мы «проецируем» их на поверхность экрана.

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

Замечание

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

Тем не менее, мы будем использовать монитор для проецирования трехмерного образа на экран. Я хотел бы обсудить типы проекции, которые могут применяться с этой целью: параллельная, или ортогональная проекция и перспективная проекция. Рисунок 6.5 показывает диаграммы каждого типа проекций.

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

С другой стороны, перспективная проекция дает большее приближение и выглядит почти «трехмерно». Она имеет качество «длинной дороги». На рисунке 6.6 изображена такая «дорога». Перспективная проекция принимает во внимание 2-компонент и соответст­венно изменяет компоненты Х и Y.

Элементы, которые подвергаются «перспективному» преобразованию, должны:

§          Просто делиться или умножаться на Z-компонент;

§          Обладать дистанцией просмотра.

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



Программа Астероиды с использованием матриц


Я уже устал от разговоров — давайте что-нибудь напишем. К примеру, перепишем нашу программу из Листинга 4.8. Вы должны ее помнить. Мы ее переделаем и используем в ней матрицы. Листинг 4.11 показывает эту программу. Она называется «Супер Астероиды».

Листинг 4.11. Супер Астероиды (FIELD.C).

// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////////////////

#include <stdio.h>

#include <graph.h>

#include <math.h>

//ОПРЕДЕЛЕНИЯ ///////////////////////////////////////////////

#define NUM__ASTEROIDS 10

#define ERASE 0

#define DRAW 1

#defineX_COMP 0

#define Y_COMP 1

#define N_COMP 2

// СТРУКТУРЫ ДАННЫХ ////////////////////////////////////

// новая структура, описывающая вершину

typedef struct vertex_typ

{

float p[3]; // единичная точка в двумерном пространстве

//и фактор нормализации

} vertex, *vertex_ptr;

// общая структура матрицы

typedef struct matrix_typ

{

float elem[3] [3]; // массив для хранения элементов

// матрицы

} matrix, *matrix_ptr;

// определение структуры "объект",.

typedef struct object_typ

{    

int num_vertices;     // количество вершин в объекте

int color;            // цвет объекта

float xo,yo;          // позиция объекта

float x_velocity;     // скорости пермещения по осям Х float y_velocity;

// и Y matrix scale;         // матрица масштабирования

matrix rotation;      // матрицы поворота и перемещения

vertex vertices[16];  // 16 вершин

} object, *object_ptr;

// ГЛОБАЛЬНЫЕ.ПЕРЕМЕННЫЕ /////////////////////////////////

object asteroids [NUM_ASTEROIDS] ;

// ФУНКЦИИ ////////////////////////////////////////////

void Delay(int t)

{

// функция выполняет некоторую задержку

float x = 1;

while(t—>0)

x=cos(x) ;

} // конец функции

///////////////////////////////////////////////////////

void Make_Identity(matrix_ptr i)

{

// функция формирует единичную матрицу

i->elem[0][0] = i->elem[l][1] = i->elem[2][2] = 1;

i->elem[0][1] = i->elem[l][0] = i->elem[l][2] = 0;

i->elem[2][0] = i->elem[01[2] = i->eiem[2][1] = 0;




} // конец функции ///////////////////////////////////////////////////////

void Clear_Matrix(matrix_ptr m) {

// функция формирует нулевую матрицу

m->е1еm[0][0] = m->elem[l][1] = m->е1еm[2][2] = 0;

m->elem[0][1] = m->elem[l] [0] = m->е1еm[1] [2] = 0;

m->elem[2][0] = m->elem[0] [2] = m->elem[2][1] = 0;

} // конец

функции

///////////////////////////////////////////////////////

void Mat_Mul (vertex_ptr v, matrix_ptr m)

{

 // функция выполняет умножение матрицы 1х3 на матрицу 3х3

// элемента. Для скорости каждое действие определяется "вручную",

// без использования циклов. Результат операции - матрица 1х3

float x new, у

new;

x_new=v->p[0]*m->elem[0][0] + v->p[1]*m->elem[1][0] + m->elem[2][0];

y_new=v->p[0]*m->elem[0][1] + v->p[1]*m->elem[1][1] + m->elem[2][1];

// N_COMP - всегда единица

v->p[X_COMP] = x_new;

v->p[Y_COMP] = y_new;

} // конец

функции

///////////////////////////////////////////////////////

void Scale_Object_Mat(object_ptr obj)

{

// функция выполняет масштабирование объекта путем умножения на

// матрицу масштабирования

int index;

for (index=0; index<obj->num_vertices; index++)

{

Mat_Mul ((vertex_ptr) &obj->vertices [index],

(matrix_ptr)&obj->scale) ;

} // конец цикла for

} // конец функции

///////////////////////////////////////////////////////

Rotate_Object_Mat(object_ptr obj)

{

// функция выполняет.поворот объекта путем умножения на

// матрицу поворота

int index;

for (index=0; index<obj->num_yertices; index++)

{

Mat_Mul((vertex_ptr)&obj->vertices[index],(matrix_ptr)&obj->rotation) ;

} // конец цикла for

} // конец функции ///////////////////////////////////////////////////////

void Create_Field(void)

{      

int index;

float angle,c,s;

// эта функция создает поле астероидов

for (index=0; index<NUM_ASTEROIDS; index++)

{

asteroids[index].num vertices = 6;

asteroids [index] .color        = 1 + rand() % 14; //астероиды



// всегда

видимы

asteroids[index].xo           = 41 + rand() % 599;

asteroids[index].yo           = 41 + rand() % 439;

asteroids [index].,x_velocity   = -10 +rand() % 20;

asteroids[index].y_velocity   = -10 + rand() % 20;

// очистить

матрицу

Make_Identity((matrix_ptr)&asteroids[index].rotation) ;

// инициализировать матрицу вращений

angle = (float) (- 50 + (float) (rand ()\ % 100)) / 100;

c=cos(angle);

s=sin(angle);

asteroids[index].rotation.elem[0][0] = с;

asteroids[index].rotation.elem[0][1] = -s;

asteroids[index].rotation.elem[l][0] = s;

asteroids[index].rotation.elem[l][1] = с;

// формируем матрицу масштабирования

// очистить матрицу и установить значения коэффициентов

Make_Identity((matrix ptr)&asteroids[index].scale);

asteroids[index].scale.elem[0][0] = (float) (rand() % 30) / 10;

asteroids[index].scale.elem[1][1] = asteroids[index].scale.elem[0][0];

asteroids[index].vertices[0].p[X_COMP] = 4.0;

asteroids[index].vertices[0].p[Y_COMP] = 3.5;

asteroids[index].vertices[0].p[N_COMP] = l;

asteroids[index].vertices[1].p[X_COMP] = 8.5;

asteroids[index].vertices[l].p[Y_COMP) = -3.0;

asteroids[index].vertices[1].p[N_COMP] = l;

asteroids[index].vertices[2].p[X_COMP] = 6;

asteroids[index].vertices[2].p[Y_COMP] = -5;

asteroids[index].vertices[2].p[N_COMP] = l;

asteroids[index].vertices[3].p[X_COMP] = 2;

asteroids[index].vertices[3].p[Y_COMP] = -3;

asteroids[index].vertices[3].p[N_COMP] = l;

asteroids[index].vertices[4].p[X_COMP] = -4;

asteroids[index].vertices[4].p[Y_COMP] = -6;

asteroids[index].vertices[4].p[N_COMP] = 1;

asteroids[index].vertices[5].p[X_COMP] = -3.5;

asteroids[index].vertices[5].p[Y_COMP] = 5.5;

asteroids[index],vertices[5].p[N_COMP] = 1;

// теперь масштабировать астероиды

Scale_Object_Mat((object_ptr)&asteroids[index]);

} // конец цикла for

} // конец функции

///////////////////////////////////////////////////////

void Draw Asteroids (int erase)

{

int index,vertex;

float xo,yo;

// эта функция в зависимости от переданного флага рисует или стирает



// астероиды

for (index=0; index<NUM_ASTEROIDS; index++)

{

// нарисовать астероид

if (erase==ERASE)

_setcolor(0);

else

_setcolor(asteroids[index].color);

// получаем позицию объекта

xo = asteroids[index].xo;

yo = asteroids [index].yo;

// перемещаемся в позицию первой вершины астероида

_moveto((int)(xo+asteroids[index]-vertices[О].p[X_COMP]), (int)(yo+asteroids[indexl.vertices[0].p[Y_COMP])) ;

for (vertex=l; vertex<asteroids[index].num_vertices; vertex++)

{

_lineto((int)(xo+asteroids[index].vertices[vertex].p[X_COMP]), (int)(yo+asteroids[index].vertices[vertex].p[Y_COMP])) ;

} // конец цикла по вершинам

_lineto((int)(xo+asteroids[index].vertices[0],p[X_COMP]), (int)(yo+asteroids[index].vertices[0].p[Y_COMP]));

} // замкнуть контур объекта

} // конец функции

///////////////////////////////////////////////////////

void Translate_Asteroids(void)

{

// функция перемещает астероиды

int index;

for (index=0; index<NUM_ASTEROIDS; index++)

{

// перемещать текущий астероид

asteroids[index].xo += asteroids[index].x velocity;

asteroids[index].yo += asteroids[index].y_velocity;

// проверка на выход за границы экрана

if (asteroids[index].xo > 600 || asteroids[index].xo < 40)

{

asteroids[index].x_velocity = -asteroids[index].x_velocity;

asteroids[index].xo += asteroids[index].x_velocity;

      }        

if (asteroids[index].yo > 440 1) asteroids[index].yo < 40)

{

asteroids[index].y_velocity = -asteroids[index].y_velocity;

asteroids[index].yo += asteroids[index].у_velocity;

)

} // конец цикла for

} // конец функции

////////////////////////////////////////////////////////

void Rotate__Asteroids()

{

int index;

for (index=0; index<NUM_ASTEROIDS; index++)

{

// вращать

текущий астероид

Rotate_Object_Mat((object_ptr)&asteroids[index]);

} // конец цикла for

} // конец функции                       

///////////////////////////////////////////////////////

void main(void)

{

// перевести компьютер в графический режим



_setvideomode(_VRES16COLOR); // 640х480, 16 цветов

// инициализация

Create_Field();

while(!kbhit())

{

// стереть

поле

Draw_Asteroids(ERASE) ;

// преобразовать

поле

Rotate_Asteroids();

Translate_Asteroids() ;

// нарисовать

поле

Draw_Asteroids(DRAW);

// немного подождем...

Delay(500);

)

// перевести компьютер в текстовый режим

_setvideomode(_DEFAULTMODE);

} // конец функции

Вам потребуется время, чтобы изучить эту программу. Уделите внимание способу, которым астероиды масштабируются и поворачиваются. Если вы сравните время исполнения программ из Листингов 4.8 и 4.11, то не найдете никакой разницы. Если же вы потратите время и поместите все повороты, масштабирование и перемещение в одну матрицу, то программа из Листинга 4,11 будет работать значительно быстрее.

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


Программируем системные часы


Внутренние часы компьютера, следящие за временем, на самом деле вовсе никакие не часы. Это микросхема типа таймер/счетчик, которая просто в, зависимости от начальных настроек с определенной частотой изменяет значение счетчика. Эта микросхема называется микросхемой таймера 8253. В ней содержится три шестнадцатиразрядных счетчика. Эти счетчики можно программировать самыми разнообразными способами. В таблице 12.2 показано назначение каждого из них.

Таблица 12.2. Счетчики микросхемы таймера 8253.

Порт ввода/вывода

Номер счетчика

Назначение

40h

Счетчик 0

Таймер/Диск

41h

Счетчик 1

Обновление памяти

42h

Счетчик 2

Накопитель на магнитной ленте

43h

Управляющий регистр

Управление таймером

Доступ к микросхеме таймера осуществляется через порты 40h-43h. Как видите, мы можем использовать только счетчики 0 и 3. Связываться со счетчиком 1 нам определенно не стоит! Счетчик 0 уже используется DOS для системных часов. Так почему бы нам не использовать его, перепрограммировав на нужную частоту?

Это вполне можно сделать. И этот метод не вызовет неполадок в компьютере. Единственный недостаток такого подхода состоит в том, что изменится системное время. Мы можем обойти это, сохранив время, за которое изменяется значение частоты таймера и по ходу игры отслеживать время с учетом этого изменения. Затем, перед окончанием работы программы, следует восстановить прежнее значение частоты и изменить системное время. Для сохранения и восстановления системного времени можно использовать функции Си.



Произведение операций над матрицами


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

Для примера, рассмотрим сложение двух матриц размерностью 2х3 - матрицы А и матрицы С:

При сложении матриц А и С нужно складывать каждый из элементов m,n. Суммы элементов займут в результирующей матрице сответствующие места:

Мы также можем умножить матрицу на скаляр k. Например, чтобы умножить матрицу А на 3, мы должны умножить на 3 каждый ее элемент:

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

§

Количество столбцов в первой матрице (n) должно быть равно количеству строк во второй (также n). Это значит, что если размерность первой матрицы (mxn), то размерность второй матрицы должна быть (nхr). Два остальных измерения m и r могут быть любыми;

§          Произведение матриц не коммутативно, то есть А х В не равно В х А.

Умножение матрицы mxn на матрицу nхr может быть описано алгоритмически следующим образом:

1.       Для каждой строки первой матрицы:

§         Умножить строку на столбец другой матрицы поэлементно.

§         Сложить полученный результат;

2.       Поместить результат в позицию [i,j] результирующей матрицы, где i - это строка первой матрицы, a j - столбец второй матрицы.

Для простоты посмотрим на рисунок 4.9:

Мы можем это сделать намного проще, написав программу на Си. Давайте определим матрицу 3х3 и напишем функцию, умножающую матрицы. В Листинге 4.9 показана соответствующая программа.

Листинг 4.9. Определение и умножение двух матриц.

// общая структура матрицы

typedef sruct matrix_typ

{

float elem[3][3]; // место для хранения матрицы

} matrix, *matrix_ptr;

void Mat_Mult3X3 (matrix_ptr matrix_1, matrix_ptr matrix_2, matrix_ptr result)

{

index i,j,k;

for(i=0; i<3; i++)

{

for (j=0; j<3; j++)

{

result[i][j] = 0; // Инициализация элемента

for(k=0; k<3; k++)

{

result->elem[i][j]+=matrix_1->elem[i][k] * matrix_2->elem[k][j];

} // конец цикла по k

} // конец цикла по j

} // конец цикла по i

} // конец функции

Перед выходом из этой функции мы имеем результат, сохраненный в переменной result.



Производство кинофильма


Новой тенденцией в производстве видеоигр является использование оцифрованных образов реальных актеров и декораций. То, что я чувствую, глядя на такие игры, можно выразить так: «Ни за что!» Это же видеоигра! В ней только предполагается, что персонаж выглядит так же, как и в кино! Вообще- то оцифрованные декорации не так уж и плохи, если программа делает похожими на обычные "компьютерные" картинки и они не конфликтуют с игровыми объектами. Интересным техническим приемом, который может даже наихудшему из артистов придать великолепную внешность, является использование моделей игровых созданий совместно с камерой и устройством ввода и регистрации кадров изображения (фреймграббером). Рисунок 15.1 показывает, как можно было бы использовать реальные модели и видеоаппаратуру для этих целей.

В восьмой главе, «Высокоскоростные трехмерные спрайты», мы углубились в детали построения небольшой студии и использования видео для этого рода работы. То же самое вкратце:

§

Модель некоего создания помещена на платформу с голубым, черным или белым фоном, который может быть удален позднее;

§          Далее вы оцифровываете изображение, используя ПК, видеокамеру и фреймграббер.                          

В частности, так создавались изображения DOOM. Модельер изготавливает модели созданий, потом помещает их на платформу и оцифровывает. Затем фиксирует их цвета и отдает программистам, чтобы те использовали их в игре. Следовательно, получение различных ракурсов трехмерных объектов является таким же легким делом, что и поворот или передвижение модели. Потому  если у вас есть немного денег, и ваши художественные таланты не зачахли стоит попробовать пойти этим путем.



«Прозрачные» пиксели


«Прозрачными» будем называть такие пиксели, которые при выводе на экран пропускаются и не перекрывают имеющееся изображение. Один из методов получения такого результата заключается в проверке значения цвета каждого пикселя перед тем, как он будет нарисован. Если цвет пикселя совпадает с «прозрачным», мы пропускаем данный пиксель и переходим к следующему. Такое дополнение к алгоритму ложится тяжелым бременем на нашу борьбу за скорость работы программы в процессе выполнения, а тем более — при выводе на экран. Ведь теперь мы не можем воспользоваться функцией memcpy() для вывода целой строки пикселей на экран, а должны применить цикл for() для изображения каждой точки отдельно.

Листинг 17.3 содержит новую функцию, называемую TransparentBlt(). Она заменит нам OpaqueBIt(). Разница между ними состоит только в том, что TransparentBlt() пропускает «прозрачные» пиксели (и это тоже тормозит работу программы).

Но как же TransparentBlt() отличает «прозрачные» пиксели от «непрозрачных»? Я решил, что любой пиксель со значением цвета, равным 0 (обычно, это черный) будет «прозрачным», но вы можете назначить для этого другой цвет. Функция пропускает любой пиксель, у которого значение цвета равно объявленной константе TRANSPARENT. Программа из Листинга 17.3 (PARAL1.C) является демонстрацией смещения двух повторяющихся слоев. Дальний слой сплошной, в то время как ближний включает в себя «прозрачные» пиксели. Для вывода изображений используются функции OpaqueBIt() и TransparentBit() соответственно. Несмотря на то, что у нас имеется всего два движущихся слоя, эффект получается довольно реалистичным. Как и в программе из Листинга 17.2, курсорные клавиши «влево» и «вправо» перемещают изображение по горизонтали, а для завершения программы нужно нажать Esc.

Обратите внимание, что скорость смены кадров в этой программе значительно ниже, чем в предыдущей. Это происходит из-за использования функции для работы с «прозрачными» пикселями. На компьютере с процессором 386SX/25 я получил примерно 10 кадров в секунду.


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

Листинг 17.3. Простой двойной параллакс (PARAL1.C).

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <time.h>

#include <dos.h>

#include "paral.h"

char *MemBuf,            // указатель на дублирующий буфер

*BackGroundBmp,     // указатель на битовую карту фона

*ForeGroundBmp,     // указатель на битовую карту

// ближнего плана

*VideoRam;          // указатель на видеобуфер

PcxFile pcx;             // структура данных

// для чтения PCX-файла

int volatile KeyScan;    // заполняется обработчиком

// прерывания клавиатуры

int frames=0,            // количество нарисованных кадров

PrevMode;            // исходный видеорежим

int background, // позиция прокрутки фона

foreground, //позиция прокрутки битовой карты

// ближнего плана position;    // общее расстояние прокрутки

void _interrupt (*OldInt9)(void); // указатель на обработчик

// прерывания клавиатуры BIOS

// Функция загружает 256 - цветный PCX-файл

int ReadPcxFile(char *filename,PcxFile *pcx)

{

long i;

int mode=NORMAL,nbytes;

char abyte,*p;

FILE *f;

f=fopen(filename,"rb");

if(f==NULL)

return FCX_NOFILE;

fread(&pcx->hdr,sizeof(PcxHeader),1, f);

pcx_width=1+pcx->hdr.xmax-pcx->hdr.xmin;

pcx->height=1+pcx->hdr.ymax-pcx->hdr.ymin;

pcx->imagebytes=(unsigned int) (pcx->width*pcx->height);

if(pcx->imagebytes > PCX_MAX_SIZE) return PCX_TOOBIG;

pcx->bitmap= (char*)malloc (pcx->imagebytes);

if(pcx->bitmap == NULL) return PCX_NOMEM;

p=pcx->bitmap;

for(i=0;i<pcx->imagebytes;i++)

{

if(mode == NORMAL)

{

abyte=fgetc(f);

if((unsigned char)abyte > 0xbf)

{ nbytes=abyte & 0x3f;

abyte=fgetc(f);

if(--nbytes > 0)

mode=RLE;

}

}

else if(-—nbytes == 0) mode=NORMAL;

*p++=abyte;

}

fseek(f,-768L,SEEK_END);      // получить палитру,из PCX-файла

fread(pcx->pal,768,1,f);

p=pcx->pal;



for(i=0;i<768;i++) // битовый сдвиг цветов в палитре

*р++=*р >>2;

fclose(f) ;

return PCX_OK;

}

// Новый обработчик прерывания клавиатуры для программы прокрутки

// Он используется для интерактивной прокрутки изображения.

// если стандартный обработчик прерывания 9h не будет заблокирован

// длительное нажатие на клавиши управления курсором приведет

// к переполнению буфера клавиатуры и появлению крайне неприятного

// звука из динамика.

void _interrupt Newlnt9(void)

{

register char x;

KeyScan=inp(0х60);// прочитать код клавиши

x=inp(0x61);      // сообщить клавиатуре, что символ обработан

outp(0x61, (х|0х80));

outp(0х61,х);

outp(0х20,0х20);  // сообщить о завершении прерывания

if(KeyScan == RIGHT_ARROW_REL ||// проверка кода клавиши

KeyScan == LEFT_ARROW_REL) KeyScan=0;

}

// Функция восстанавливает исходный обработчик прерываний клавиатуры

void RestoreKeyboard(void)

{

_dos_setvect(KEYBOARD,OldInt9);   // восстанавливаем

// обработчик BIOS

}

// Эта функция сохраняет прежнее значение вектора прерывания // клавиатуры и устанавливает новый обработчик нашей программы.

void InitKeyboard(void)

{

OldInt9= _dos_getvect(KEYBOARD);   // сохраняем адрес

//  обработчика BIOS

_dos_setvect(KEYBOARD,NewInt9);   // устанавливаем новый

//  обработчик прерывания 9h

}

// Эта функция использует функции BIOS для установки в регистрах

// видеоконтроллера значений, необходимых для работы с цветами,

// определяемыми массивом раl[]

void SetAllRgbPalette(char *pal)

{

struct SREGS s;

union REGS r;

segread(&s);  // читаем текущее значение сегментных регистров

s.es=FP_SEG((void far*)pal);  // в ES загружаем сегмент ра1[]

r.x.dx=FP OFF((void far*}pal);// в DX загружаем смещение pal[]

r.x.ax=0xl012;         // готовимся к.вызову подфункции // 12h функции BIOS 10h

r.x.bx=0;            /;/ номер начального регистра палитры

r.х.сх=256;           // номер последнего изменяемого регистра

int86x(0xl0,&r,&r,&s);// вызов видео BIOS

}



// Функция устанавливает режим 13h

// Это MCGA-совместимыЙ режим 320х200х256 цветов

void InitVideo()

{

union REGS r;

r.h.ah=0x0f;       // функция Ofh - установка видеорежима

int86(0xl0,&r,&r); // вызов видео BIOS

PrevMode=r.h.al;   // сохраняем старое значение режима

r.x.ax=0xl3;       // устанавливаем режим 13h

int86(0х10,&r,sr); // вызов видео BIOS

VideoRam=MK_FP(0xa000,0); // создаем указатель на видеопамять

}

//Эта функция восстанавливает исходный видеорежим

void RestoreVideo()

{

union REGS r;

r.x,ax=PrevMode;   //исходный видеорежим

int86(0х10,&r,&r); // вызов видео BIOS

}

// Функция загрузки битовых карт слоев

int InitBitmaps()

{

int r;

// начальное положение линии деления

background=foreground=1;

// читаем битовую карту фона

r=ReadPcxFile("backgrnd.pcx",&pcx);

// проверка на ошибки чтения if(r != РСХ_ОК)

return FALSE;

// запоминаем указатель на битовую карту

BackGroundBmp=pcx.bitmap;

// устанавливаем палитру

SetAllRgbPalette(pcx.pal) ;

// читаем битовую карту переднего слоя

r=ReadPcxFile("foregrnd.pcx",&pcx);

// проверка на ошибки чтения

if (r != РСХ_ОК) return FALSE;

//запоминаем указатель на битовую карту

ForeGroundBmp=pcx.bitmap;

// создаем буфер в памяти

MemBuf=malloc(MEMBLK);

// проверка на ошибки распределения памяти

if(MemBuf == NULL) return FALSE;

memset(MemBuf,0,MEMBLK); // очистка буфера

return TRUE;

// все в порядке!

}

// функция освобождает выделенную память

void FreeMem()

(

free(MemBuf);

free(BackGroundBmp);

free(ForeGroundBmp);

}

// Функция рисует слои параллакса.

// Порядок отрисовки определяется координатой слоя по оси Z.

void DrawLayers()

{

OpaqueBlt(BackGroundBmp,0,100,background);

TransparentBlt(ForeGroundBmp,50,100,foreground);

}

// Эта функция осуществляет анимацию. Учтите, что это наиболее

// критичная по времени часть программы. Для оптимизации отрисовки

// как сама функция, так и те функции, которые она вызывает,

// следует переписать на ассемблере.


Как правило, это увеличивает

// быстродействие на 100 процентов.

void AnimLoop()

{

while(KeyScan != ESC_PRESSED)    // пока не нажата клавиша ESC

(

switch(KeyScan)     // определяем, какая клавиша была нажата

{

case RIGHT_ARROW_PRESSED:      //нажата "стрелка вправо"

position--;                  // изменяем позицию

if(position < 0)             // останавливаем прокрутку,

//  если дошли до конца

{

position=0;

break;

} backgrpund —=1;      // прокручиваем фон влево на 2 пикселя

if(background < 1)        // дошли до конца?

background+=VIEW_WIDTH; // ...если да - возврат к началу

foreground-=2;            // прокручиваем верхний

// слой влево на 4 пикселя

if(foreground < 1)        // дошли до конца?

foreground+=VIEW_WIDTH; // ...если да - возврат к началу

break;

case LEFT_ARROW_PRESSED:      // нажата "стрелка влево"

position++;          // изменяем текущую позицию прокрутки

if(position > TOTAL_SCROLL) // останавливаем прокрутку,

// если дошли до конца

{

position=TOTAL_SCROLL;

break;

}

background+=l;     // прокручиваем фон вправо на 2 пикселя

if(background > VIEW_WIDTH-1) // дошли до конца?

background-=VIEW_WIDTH; // ...если да - возврат к началу

foreground+=2;            // прокручиваем верхний слой

// вправо на 4 пикселя

if(foreground > VIEW_WIDTH-1) // дошли до конца?

foreground-=VIEW_WIDTH; // ...если да - возврат к началу

break;

default:                   // игнорируем остальные клавиши

break;

}

DrawLayers();                // рисуем слои в буфере в        

// оперативной памяти

memcpy(VideoRam,MemBuf,MEMBLK); // копируем буфер в

// видеопамять

frames++;                   // увеличиваем счетчик кадров

) }

//эта функция осуществляет необходимую инициализацию

void Initialize()

{

position=0;

InitVideo();            // устанавливаем видеорежим 13h

InitKeyboard();         // устанавливаем наш обработчик

// прерывания клавиатуры

if(!InitBitmaps())      // загружаем битовые карты



{

CleanUp();        //освобождаем память

printf("\nError loading bitmaps\n");

exit(1);

} }

// функция выполняет всю необходимую очистку

void Cleanup()

{

RestoreVideo();       // восстанавливаем исходный видеорежим

RestoreKeyboard();    // восстанавливаем обработчик

// прерывания клавиатуры BIOS

FreeMem();            // освобождаем всю выделенную память



// Это начало программы. Функция вызывает процедуры инициализации.

// Затем читает текущее значение системного таймера и запускает

// анимацию. Потом вновь читается значение системного таймера.

// Разница между исходным и конечным значениями таймера

// используется для вычисления скорости анимации.

int main()

{

clock_t begin,fini;

Initialize(};          // проводим инициализацию

begin=clock();         // получаем исходное значение таймера

AnimLoop();            // выполняем анимацию

fini=clock();          // получаем значение таймера

CleanUp();             // восстанавливаем измененные параметры

printf("Frames: %d\nfps: %f\n",frames,

(float)CLK_TCK*frames/(fini-begin));

return 0;

}


Рассеянное освещение


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

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



Раздел 1: Инициализация


В этой части программы мы загружаем все файлы со звуковыми эффектами И графикой для игры. Элементы изображения для танков берутся из загруженный файлов и размещаются в предварительно выделенной под буфер области памяти. Net-Tank использует технику дублирующей буферизации для исключения мерцания изображения. Напомню, это означает, что изображение вначале формируется в оперативной памяти и в уже полностью подготовленном виде копируется в видеопамять. Кроме того, во время инициализации структуры данных, описывающие все игровые объекты, обнуляются или устанавливаются в исходное положение.

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



Раздел 2: Игровой цикл


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

Заметьте, что игра различает, когда она находится в состоянии соединения, а когда - нет.



Раздел 3: Удаление объектов


Как вы узнали из предыдущих глав, посвященных графике, прежде чем рисовать спрайты на новом месте, мы должны удалить их из прежней позиции. Эта часть программы убирает с игрового поля все движущиеся объекты путем восстановления ранее сохраненного фона под ними.



Раздел 4: Получение входных данных и передача состояния дистанционно управляемой системе


Здесь начинается самое приятное. Эта часть программы подразделена на два фрагмента:

§

Первый из них принимает входные данные от локального игрока;

§          Другой принимает входные данные от удаленного игрока.

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

§          Можно передавать состояние игры в целом;

§          Вы можете посылать статус устройств ввода и трактовать это как прием данных от другого джойстика или клавиатуры.

В Net-Tank я применил второй метод. Один раз в течение цикла второй машине передаются все манипуляции игрока с клавиатурой. В это же время другой компьютер интерпретирует полученные по сети данные как действия со своей собственной клавиатурой.

Помните, чтобы этот технический прием работал, обе игры должны быть полностью детерминированы. Никаких случайностей быть не должно. В Net-Tank я полностью следовал этому правилу всюду, кроме фрагмента, изображающего взрыв. Обычно этого не сложно избежать, но иногда, после уничтожения одного из танков, игры теряют синхронизацию.



Раздел 5: Перемещение объектов


Следующая часть программы занимается перемещением объектов. Мы просто используем информацию, поступающую от локального и дистанционного ввода для передвижения и разворота танков. Интересен способ перемещения танков. Они могут двигаться в 16 различных направлениях, угол между которыми составляет 22.5 градуса. Вообще-то, чтобы переместить танк в выбранном направлении нам потребовалось бы, прежде всего, найти угол, а затем вычислить его синус и косинус для нахождения параметров передвижения.

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

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

Во всяком случае, как только танки двинутся, сразу же можно открывать огонь.



Раздел 6: Распознавание столкновений


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

Что касается столкновений танков друг с другом, то пока позволим им это делать беспрепятственно (предлагаю вам доработать программу в этой части самостоятельно). Кроме этого нам нужно позаботиться о том, чтобы танки не могли проходить сквозь стены. Если вы помните, игровое поле представляет собой матрицу элементов, имеющую определенную размерность. В Net-Tank размер игрового поля составляет 20х11 ячеек, каждая из которых имеет площадь 16х16 пикселей. Следовательно, чтобы увидеть, не столкнулся ли танк со стенкой, то есть попал в занятую ячейку, необходимо:

§

Разделить обе координаты танка на 16;                      

§          Округлить результат до целого;

§          Использовать полученное значение как индекс ячейки игрового поля, чтобы увидеть, есть ли там блок. Если столкновение произошло, вернуть танк в прежнюю позицию.



Раздел 7: Рисование объектов


Теперь мы готовы нарисовать все объекты. Чтобы сделать это, мы должны сохранить фон в тех местах, где мы планируем разместить объекты. После этого их можно нарисовать. На этом этапе игровой экран полностью построен в дублирующем бу4эере и теперь можно посмотреть, что же у нас получилось.



Раздел 8: Дублирующий буфер


В Net-Tank используется техника дублирующей буферизации для исключения мерцания изображения. В этой части игры крайне быстрый цикл ассемблера копирует дублирующий буфер на экран. Однако мы перемещаем только первые 176 строк из буфера, .так как нижняя часть экрана неизменна и ее не надо перерисовывать.



Раздел 9: Всякая всячина


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