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