Трассировка лучей
Трассировка лучей - это метод, применяемый для создания реалистичных образов на компьютере, используя полные модели трехмерного мира. Трассировка лучей решает множество проблем. Этот алгоритм может выполнять следующие действия:
§
Удаление невидимых поверхностей;
§ Перемещение;
§ Отражение;
§ Рассеяние;
§ Окружающее освещение;
§ Точечное освещение;
§ Наложение теней.
Изначально этот алгоритм разрабатывался для решения проблемы удаления невидимых поверхностей. Трассировка лучей создает образ, исходя из тех же законов, что и наше зрение. На рисунке 6.19 изображено некоторое пространство, которое может быть просчитано с помощью алгоритма трассировки лучей. Вы видите несколько объектов: источник света, наблюдателя и план наблюдения.
Чтобы воспользоваться трассировкой лучей для создания натуральных образов, нам придется использовать миллиарды световых лучей из источника света, и затем рассматривать каждый из них, надеясь, что он попадет в план наблюдения и примет участие в создании образа. Возникает вопрос - а зачем трассировать каждый возможный луч? На самом деле, нас интересуют только те лучи, которые достигают плана просмотра.
Запомнив это, давайте попробуем трассировать лучи в обратном направлении. Проследим движение лучей для каждого из пикселей на экране, а затем посмотрим, где эти лучи пересекаются с планом просмотра. Отметив пересечение, мы останавливаемся и окрашиваем соответствующий пиксель в нужный цвет. Это называется первичной трассировкой лучей.
Данная техника позволяет создавать трехмерные образы, но при этом не видны такие эффекты, как тени, рефракция и рефлексия. Чтобы воссоздать перечисленные эффекты, мы должны принять в рассмотрение специальные вторичные лучи, которые исходят из точек пересечения.
Это все делается рекурсивно до достижения некоторого уровня детализации. Затем полученные по всем лучам результаты складываются и соответствующему пикселю присваивается вычисленный цвет.
Трассировка лучей - это один из наиболее насыщенных вычислениями методов расчета трехмерных изображений, но зато и результаты получаются впечатляющими. Есть только одна проблема: для решения этой задачи в реальном времени не хватает мощности даже самого быстродействующего компьютера. Потому нам придется учесть данное обстоятельство и применить идею трассировки лучей для создания другого метода. Он будет более ограничен, но позволит нормально работать с трехмерными мирами на обычном ПК. Исходя из этого, мы попробуем реализовать упрощенный вариант трассировки лучей, используя только первичные лучи для генерации изображения. С последующими оптимизациями возможно достижение достаточно высокой производительности. Если вам интересно узнать, как это можно сделать, то стоит продолжить чтение нашей книги.
Трехмерное звездное небо
Техника трехмерных спрайтов, которую мы обсуждаем, используется не только в играх типа Wing Commander, но также и в DOOM, и в Wolfenstein. Если вы поиграете в DOOM, то заметите, что гуляющие или преследующие вас монстры (спрайты) изображаются в разных видах. В шестой главе, «Третье измерение», мы обсуждали метод трассировки лучей и метод представления объектов с помощью многоугольников. Теперь я хотел бы снова вернуться к этому и показать, как можно создать трехмерное космическое пространство внутри которого и будут происходить наши звездные войны.
Теоретически создание трехмерного звездного неба тривиально. Мы могли бы просто задать несколько миллионов случайных точек в трехмерном пространстве, использовать аксонометрическую проекцию для их визуализации и, нарисовать их по пикселям. Этот метод волне применим и обычно используется в симуляторах. Однако у него есть ряд недостатков: он работает медленно, требует много памяти и получаемое в результате изображение быстро надоедает из-за своего однообразия. Но поскольку мы с вами сейчас занимаемся оформлением игры, то должны сделать так, чтобы экран действительно стал трехмерным звездным небом. Но мало сказать, надо еще и сделать! Если вы найдете время и посмотрите Star Trek или подобный симулятор, то увидите звездное небо, созданное с помощью одиночных пикселей (звезд). Эти звезды расположены на экране случайным образом и передвигаются к периферии вначале медленно, а потом все быстрее и быстрее. Это происходит до тех пор, пока звезды не будут отсечены, выйдя за пределы обозримого пространства.
Удачный угол зрения на спрайты
Эта тема наиболее сложная из всех, которые мы до сих пор рассматривали. При перемещении трехмерных спрайтов по экрану, с точки зрения наблюдателя все должно выглядеть так, словно объекты и в самом деле имеют три измерения. Наблюдатель в нашем случае — это игрок, и его позиция фиксирована. Обычно она имеет координаты (0,0,0) или слегка сдвинута по оси Z. В любом случае, когда спрайт, к примеру, делает поворот, и мы хотим, чтобы он выглядел как настоящий трехмерный объект, нам следует менять кадры этого спрайта на экране в последовательности, соответствующей реальной смене ракурсов «живого» объекта. (Такую фразу и не выговоришь на одном дыхании!)
Полное и элегантное решение этой проблемы слишком... масштабно для нас. Нам всего лишь требуется, чтобы наши программы, модели и алгоритмы. давали реалистичные результаты. Поэтому, алгоритм, который мы обсудим в этом разделе, едва ли решит поставленную задачу в полном объеме, и в будущем, вы наверняка внесете в него свои дополнения. Однако он является хорошей стартовой площадкой, а я всегда предпочитаю иметь хороший фундамент для последующего строительства, а не слабое его подобие, на котором после ничего и не соорудишь.
Как и при написании любой программы для любого компьютера, мы должны вначале четко определить задачу, а затем обдумать возможные пути ее решения. • Итак, проблема: как выбрать кадр для изображения объекта на основании угла между лучом зрения игрока и направлением «взгляда» самого объекта.
Замечание
В общем случае объект может передвигаться и не в том направлении, куда обращена его лицевая сторона, Мы не будем сейчас заострять внимание на этом варианте движения, чтобы чрезмерно не усложнять нашу задачу. Пока будем считать, что объект всегда движется в ту сторону, куда он обращен лицом. Это предположение вполне обосновано, так как в программе, которую мы впоследствии напишем, будут участвовать космические корабли с кормовыми дюзами. Точно так же и в играх типа Wolfenstein или DOOM игровые персонажи обычно движутся в том направлении, куда они смотрят (или в обратном, если они пятятся).
Попытаемся вначале проанализировать проблему. Нам необходимо определить вид объекта, который зависит от направления взгляда игрока и траектории объекта или направления его движения. Как мы уже говорили, луч зрения игрока можно зафиксировать и считать, что он всегда перпендикулярен экрану. Тогда нам нужно будет побеспокоиться только о векторе траектории объекта, выводимого на экран. На рисунке 8.7 изображена взаимосвязь между вектором направления взгляда игрока и некоторой траекторией передвижения объекта.
Теперь мы должны сделать вот что: возьмем игрушечную машинку или что-нибудь подобное и будем передвигать ее перед собой (шум мотора имитировать при этом не обязательно, можно все делать тихо). Проделав это, вы быстро придете к выводу, что рисуемое на экране изображение космического корабля, движущегося прямолинейно, практически одинаково для всех параллельных траекторий независимо от местоположения объекта. Конечно, это справедливо только частично, зато мы получили хорошую отправную точку для нашего первого алгоритма выбора правильного кадра.
Что же мы должны сделать:
§ Вычислить угол между траекторией движения объекта и лучом зрения игрока (который всегда направлен прямо в экран);
§ Разделить полученный угол на квадранты. Затем на основании полученного индекса выбрать наиболее подходящее изображение среди предварительно подготовленных оцифровкой фотографий модели или нарисованных в графическом редакторе. (Более подробно это обсуждается в разделе «Оцифровка объектов и моделирование».)
§ Вывести на экран подходящий кадр, используя аксонометрическую проекцию и масштабируя объект до необходимого размера.
В результате на экране получается реалистичная картина.
Каким же образом находится угол между траекторией объекта и лучом зрения наблюдателя? Ответ может быть получен с помощью скалярного произведения векторов.
Мы знаем, что угол между двумя векторами можно найти с помощью скалярного произведения векторов, как это показано на рисунке 8.8.
Формула 8.4. Вычисление угла между наблюдателем и объектом.
Если мы зададим вектор направления взгляда, как V, а вектор скорости, как О, тогда угол между ними можно будет найти по следующей формуле:
Пусть V = (vx,vy,vz) и О = (ox,oy,oz), тогда
Если бы мы хотели сформулировать это действие словами, то могли бы сказать так: «Угол между V и О равен арккосинусу скалярного произведения этих векторов, разделенного на произведение длин векторов».
Угол между V и О, рассчитанный по этой формуле, имеет одну особенность: он всегда внутренний, то есть больше 0, но меньше 180 градусов. Следовательно, один и тот же результат, полученный по этой формуле, может соответствовать двум разным углам. Это происходит потому, что скалярное произведение не дает информации о направлении вектора (или о направлении, в котором вы отсчитываете положительный угол). Другими словами, эта формула всегда выдает наименьший из углов между двумя векторами. Если вы будете помнить об этом, то такое поведение данной формулы не будет большой проблемой. (Это напоминает бутерброд, который всегда падает маслом вниз. Если вы не знаете об этом, то такой результат может свести вас с ума. А кто предупрежден, тот вооружен.)
Рисунок 8.9 иллюстрирует указанную проблему графически. На этом рисунке показан вектор направления взгляда, три возможных положения вектора траектории и полученный в результате расчетов по формуле 8.4 угол.
Кстати, формулу 8.4 можно значительно упростить, вспомнив, что нас интересует только плоскость X-Z, так как луч зрения всегда перпендикулярен плоскости просмотра.
Но как же, в конце концов, определить действительный угол? Конечно, вы Могли бы воспользоваться еще и векторным произведением, чтобы решить, корректен ли угол, полученный в результате расчетов по формуле 8.4 или необходимо увеличить его еще на 180 градусов. Однако я слишком не люблю математику (возможно, именно поэтому я и доктор математических наук) и предпочитаю вместо грубой силы использовать тонкую интуицию.
Если мы сообразим, что вектор траектории объекта имеет ту же исходную точку, что и вектор направления взгляда, а затем проверим, в какой из полуплоскостей относительно луча зрения расположен Х-компонент вектора траектории, то мы сможем определить, больше или меньше 180° искомый угол. Это наглядно изображено на рисунке 8.10.
Применяя метод проверки Х-компонента, мы можем написать простую функцию, которая вначале рассчитывает угол, используя скалярное произведение, а затем проверяет, находится ли координата Х справа (положительная) или слева (отрицательная) от вектора направления взгляда. Если координата Х положительная, мы вычитаем угол, полученный с помощью формулы 8.4 из 360 градусов (это все равно, что прибавить 180). Затем мы можем взять рассчитанный угол и разбить его на 12 квадрантов (либо взять его модуль по основанию 12). Полученное число затем можно использовать как индекс для нахождения кадров спрайта. (Конечно, кадры должны быть расположены в правильном порядке, то есть кадрам, полученным при вращении объекта против часовой стрелки с шагом в 30 градусов, должны соответствовать индексы от 0 до 11. При этом нулевой индекс должен указывать на кадр объекта, повернутого тыльной стороной к наблюдателю.)
Если значение координаты Х отрицательное, происходит то же самое за исключением того, что будет использован другой банк изображений, и оперировать потребуется с абсолютным значением X.
Кадры, которые я создал для демонстрации этого алгоритма, расположены в файле VRYENTXT.PCX. Они расположены слева направо и сверху вниз. Каждая картинка содержит изображение, повернутое на 30° против часовой стрелки, а в исходной позиции нос корабля направлен прямо в экран (или, с точки зрения игрока, корабль обращен к нему тыльной стороной). Этот же файл мы использовали и в предыдущем примере.
Демонстрационная программа будет использовать рассчитываемые углы для выбора кадров. Но мы же не можем поместить корабль просто в пустоту. Это будет скучно! Нам надо добавить что-нибудь для оживления картинки.Я предлагаю создать трехмерное звездное небо. Под трехмерностью я здесь понимаю то, что звезды будут перемещаться к вам или от вас, а не влево или вправо, как это мы делали раньше, Надо отметить, что космический корабль, летящий в звездном пространстве, выглядит превосходно. Однако следует еще поговорить о том, как же создается такое трехмерное звездное небо.
Удаление невидимых поверхностей
Среди программистов, работающих в области компьютерной графики, техника удаления невидимых поверхностей считается «высшим пилотажем». Известно, что удаление невидимых поверхностей является сложной математической задачей. Существует довольно много алгоритмов для ее решения, но все они очень сложны в реализации и редко работают с приемлемой для видеоигр скоростью.
Удаление невидимых поверхностей можно разделить на две фазы:
Фаза 1. Прежде всего удаляются поверхности, которые никогда не будут видны с точки зрения наблюдателя. Для этого мы должны использовать точку пересечения между вектором наблюдения и вектором нормали к каждой из рассматриваемых плоскостей. Мы вычисляем это значение. Если значение угла меньше 90°, то поверхность видна, если больше 90° - нет и она удаляется.
Фаза 2. После того как скрытые поверхности удалены, видимые плоскости должны быть окрашены. Вы должны быть уверены, что в течение этой фазы, объекты будут выглядеть правильно.
Существует популярный Алгоритм Художника, который это хорошо умеет делать. Он работает, выполняя пять тестов для каждой видимой пары многоугольников (поверхностей) и затем создает последовательность их окраски от дальней части изображения к ближней.
Другая техника создания этой последовательности носит название Алгоритма Z-буфера. Он работает в пространстве образа на уровне пикселей. Он прост в реализации, но медленно работает и требует много памяти.
Настоящие разработчики видеоигр никогда не используют эти алгоритмы в чистом виде. Мы должны создать свою систему», которая будет сочетать оба метода. Для этого детально рассмотрим каждый из них.
Удобные детали и инструменты
На что вам в первую очередь необходимо обратить внимание при выборе графической программы? Существует не так уж и много возможностей графических редакторов, которые я использую наиболее часто. Я с удовольствием помогу вам и укажу на них:
§
Посмотрите, есть ли в программе возможность импорта и экспорта различных графических форматов. Существует целое семейство битовых графических форматов, и у вас должна иметься возможность отконвертировать любое изображение в тот формат, который вы в конечном счете используете. К примеру, вы можете решить, что ваши окончательные картинки должны сохраняться в PCX-файлах, но нашли несколько интересных изображений в GIF-, BMP- или TIF-формате. Было бы прекрасно иметь возможность преобразовать их в выбранный формат, при необходимости отредактировать и сохранить в новом PCX-файле. Эти четыре формата наиболее часто употребляются графическими редакторами, поддерживающими 256-цветный режим;
§ Обратите внимание на способ управления цветами и их расположение в палитре. У вас может возникнуть необходимость собрать окончательное изображение из нескольких исходных картинок, и вам потребуется создать палитру, которая будет обеспечивать наилучшие результаты с различными типами изображений. Вы можете обнаружить, что комбинирование этих изображений приводит к перемешиванию палитры. Возможность перестановки цветов по яркости оттенков является удобной деталью. Мы еще вернемся к этой возможности и рассмотрим ее чуть позже в данной главе;
§ Очень хорошо, если программа позволяет уменьшить количество цветов в изображении. Вы быстро обнаружите, что 256 цветов вам хватит ненадолго, если вы работаете с несколькими фотографиями или набором текстур. Вам может потребоваться уменьшить количество цветов, используемое каждым изображением, до 64 или даже до 32;
§ Очень хорошей особенностью некоторых программ является способность комбинировать палитры двух и более изображений и подбирать лучшие цвета для объединенного изображения.
После сокращения палитр отдельных изображений (как это было описано в предыдущем пункте) вы сможете собрать несколько, меньших палитр в одной. Если вы не найдете такую программу, воспользуйтесь редактором, поддерживающим 24-разрядную графику (реальные цвета). Этот тип программ, позволит вам отрезать, и приклеивать различные изображения в одном файле, а затем преобразовать собранное изображение в вашу лучшую 256-цветную палитру;
§ Вам также может потребоваться возможность создания плавных цветовых переходов. Существует множество таких программ и некоторые из них работают с двумя цветами, плавно изменяя окраску от одного оттенка к другому. Иные графические программы позволяют выбрать диапазон цветов в палитре и произвольно смонтировать их. Я предпочитаю последний метод, поскольку он дает наиболее хороший результат и особенно удобен для раскраски битовых образов. Одна из программ, обладающих таким достоинством, называется Electronic Art's Deluxe Paint;
§ Наконец, вы будете очень довольны, если сможете выполнять сглаживание. Это такой технический прием, который позволяет убирать неровности внешнего вида скошенных или закругленных областей путем смешивания промежуточных цветов в шероховатых местах. Если вы используете эту особенность осторожно, то можете сгладить цветовой переход в наиболее контрастных участках и придать изображению более натуральный внешний вид.
Однако существует ряд важных «инструментальных средств», которые невозможно купить ни за какие деньги. Это желание, терпение и хороший художественный вкус. Доведение до совершенства любой задачи требует практики, будь то программирование, кулинария или рисование. Не отчаивайтесь, если ваше первое творение окажется совсем не таким, каким вы его себе представляли. Считайте, что, сделав это, вы уже чему-то научились и двигайтесь вперед.
Теперь давайте рассмотрим основы создания игровой графики.
Уклонение
Пока мы еще не ушли слишком далеко в наших рассуждениях, продолжим нашу Дискуссию разговором о явлении прямо противоположном преследованию — об уклонении. Чтобы сконструировать создание, уклоняющееся от игрока, нам нужно сделать в точности противоположное предыдущим действиям. Алгоритм 13.2 в общих чертах показывает это.
Алгоритм 13.2. Алгоритм Уклонения.
//пусть (рх,ру) - позиция игрока и (ех,еу) - позиция противника
whilе(игра) {
.....// код программы
// Вначале - горизонтальная составляющая перемещения
if ex>px then ex=ex-1
if ex<px then ex=ex+1
//Теперь - вертикальная составляющая
if ey>py then ey=ey-1
if ey<py then ey=ey+1
.... // код программы
В этом месте я еще раз хочу заострить ваше внимание на том, что игры не думают. Персонажи в них только совершают несложные эволюции в пространстве и времени, да определенным образом реагируют на окружающую обстановку и действия игрока. (Некоторые ученые считают, что люди - это не что иное, как набор действий и реакций.)
А теперь перейдем к следующей теме и обсудим такое понятие как «шаблонные мысли».
Улучшения WarEdit
WarEdit уже будет работать вполне удовлетворительно, но на полноценный редактор он все же не тянет. В нем недостает многих деталей, необходимых для настоящей работы. Сейчас я хочу сказать о некоторых моментах, которые вы могли бы доработать сами:
§ Использование разноцветных точек для представления игровых объектов слишком примитивно. Более серьезным и удобным решением было бы использование окна редактирования с возможностью прокрутки, в котором каждый элемент игрового пространства представлен собственной уменьшенной копией;
§ В нашем редакторе любой объект занимает всю ячейку игрового пространства целиком. Но что если мы хотим поместить два мелких предмета в одно и то же место? Здесь могло бы пригодиться окно детализации ячейки;
§ А как быть насчет спецэффектов типа освещения? Неплохо было бы иметь возможность изменять уровни освещения в целом;
§ Еще не помешало бы добавить возможность указания начальных и конечных точек перемещения (телепортации) игрока;
§ Важный технический прием, не примененный в WarEdit, называется полиморфизмом. Эта методика позволяет, используя одну и ту же ссылку, обращаться к различным типам данных. Все структуры данных и определения обычно жестко закодированы в программе и их необходимо импортировать в игру в виде включаемого файла. Но гораздо лучше иметь некоторое подобие синтаксического анализатора, который будет считывать все игровые объекты и значения из файла инициализации.
Можно было бы и дальше продолжить список усовершенствований редактора. Обычно разработчики так и поступают, пока не получат окончательную версию. Фактически, мы могли бы потратить несколько дней и улучшить редактор до уровня Wolfenstein 3-D. (Добавление в него деталей из DOOM отняло бы месяцы). В любом случае, я думаю, у вас уже есть множество идей на этот счет, и если ваш первый редактор окажется вдвое лучше WarEdit'a, то с ним, вероятно, вы создадите и вдвое лучшую игру.
Уменьшение проекционных искажений
Проблема, о которой нам надо поговорить, заключается в проекционном искажении. Как вы знаете, для реализации отсечения лучей мы нарушили правило и использовали одновременно полярные и декартовы системы координат. Это привело к эффекту «рыбьего глаза» (то же самое возникает,когда вы смотрите сквозь сильную линзу).
Это сферическое искажение возникает вследствие использования нами при трассировке лучей радиального метода. Мы рассчитываем все лучи, выходящие из одной точки (позиции игрока). Сферические искажения возникают потому, что все объекты, с которыми пересекаются лучи, определены в «прямоугольном» пространстве. Расчет же лучей проводится в «сферическом» или «полярном» пространстве. Пример такого пространства представлен на рисунке 6.27.
Теперь посмотрим на рисунок 6.28. Мы увидим, что наблюдает игрок, когда смотрит прямо на стену. Он видит прямоугольник. Но так как расстояния до точек пересечения различны, изображение получается искаженным. Рисунок 6.29 показывает два результата отсечения лучей. Первый построен с учетом компенсационных искажений, а второй — без их учета. Все это очень интересно, но как это реализовать? Ответ прост: нужно умножить функцию масштаба на инверсную функцию. Синусоидальное искажение может быть компенсировано умножением масштаба на cos-1
текущего угла по отношению к полю наблюдателя (60 градусов). То есть мы должны умножить каждое значение угла от -30 до +30 на cos-1
того же угла. Это исключит искажение.
Универсальный асинхронный приемопередатчик
ПК оборудованы универсальным асинхронным приемопередатчиком (UART) - чипом, который принимает и передает последовательные данные. Существуют Два наиболее популярных UART для ПК:
§
Модель 8250;
§ Модель 16550.
Можете считать, что они полностью совместимы друг с другом и нам не нужно выяснять, какой из них используется. Единственным их важным отличием является только то, что модель 16550 имеет внутренний FIFO (First In, First Out - "первый вошел - первый вышел") буфер, который располагает входящие - данные так, что они не могут потеряться вследствие задержки обработки. Теперь взглянем на каждый из регистров UART и на то, как получить к ним доступ. После того как мы обязались написать полную библиотеку для связи, необходимо уяснить, как открыть последовательный порт, а также как осуществлять чтение и запись. Написав однажды,соответствующие функции, мы можем сконцентрироваться на целях игры.
Управление приоритетным состоянием
Можно еще усовершенствовать наш КА, если ввести управление сменой состояний с помощью некоторых переменных и функций. На практике это означает возможность изменения текущего режима, не дожидаясь полного завершения программы, отвечающей за его выполнение.
В имитаторе движения «Мухи» каждое состояние выполнялось до полного завершения. Но если включить в программу проверку выполнения или невыполнения некоторых условий, это позволило бы КА «выпрыгнуть» из состояния, не дожидаясь окончания его «отработки».
Этим заканчивается наш разговор о конечных автоматах, которые могут быть использованы в наших играх для моделирования поведения существ и придания им видимости наличия интеллекта. Теперь к имеющейся у нас системе высокоуровневого управления не помешает добавить низкоуровневое функционирование.
Уравнение плоскости
Я говорил, что мы можем использовать уравнение плоскости для многоугольника, для нахождения значения Z-компонента каждого из пикселей внутри преобразуемого прямоугольника. Вот это уравнение,
Дано: Точка (х,у) и вектор нормали к многоугольнику <Nx,Ny,Nz>
|
Ускорение процесса двоичного кодового преооразовзния (бит-блиттинга)
Если подумать о типах разрабатываемых нами компьютерных игр, то станет очевидно, что основной упор нам придется делать на графику.
При работе с такими играми (трехмерные игры, подобные Wolfenstein 3D или DOOM) компьютер большую часть времени тратит на прорисовку стен в трехмерном пространстве или на изображение двухмерных образов спрайтов, представляющих игровые объекты. Мы обсудили изображение в трехмерном пространстве в шестой главе, «Третье измерение». Поэтому теперь я хотел бы остановиться на вопросе оптимизации вывода спрайтов, поскольку это нам особенно понадобится при создании игр данного типа.
В предыдущих главах мы разобрались с понятием бит-блиттинга (глава пятая, «Секреты VGA-карт») и рассмотрели, как передается растровое изображение из одной области памяти в другую. Держу пари, что 90 процентов времени при создании игр уходит на то, чтобы придумать, как ускорить эти процессы. В восемнадцатой главе, «Техника оптимизации», мы обсудим общую теорию оптимизации и разберем несколько примеров использования соответствующей техники после завершения работы над игрой. (Не забывайте о том, что все оптимизационные трюки следует применять на заключительной стадии разработки игры!) Вы должны четко представлять, что для каждой новой игры необходимо заново создавать и реализовывать подходящие алгоритмы битового замещения; более того, вы можете исгюльзовать от двух до пяти различных способов бит-блиттинга в одной и той же игре. При этом каждый из них будет предназначаться для вполне конкретного случая. Например, вы можете обнаружить, что можно оптимизировать функцию бит-блиттинга для объектов, которые на протяжении всей игры остаются неподвижными. Если это так, создайте два преобразования: одно — для движущихся объектов, другое — для стационарных. Код для каждого процесса бит-блиттинга занимает примерно один-два килобайта ияи даже меньше.
Рассмотрим еще один пример возможной оптимизации. Естественно, что чтение данных вашей функцией бит-блиттинга определенным образом зависит от способа их хранения.
И возможно, преобразование будет происходить быстрее, если представить данные в другом виде. Например, вы можете потерять эффективность, сохраняя значения пикселей построчно (в виде двухмерной матрицы), а не по столбцам (по 16 бит). Если иная форма хранения данных дает выигрыш в производительности для некоторых из ваших алгоритмов бит-блиттипга, напишите для них процедуру предварительного преобразования этих данных.
В любом случае, думайте о том, как максимально ускорить процесс бит-блиттинга. Ведь в компьютерной игре на экране присутствуют в среднем от 2 до 20 объектов размером примерно 32х32 пикселя, а это достаточно много для работы в реальном времени.
Примечание
Как вы знаете, я привожу в этой книге некоторые алгоритмы бит-блиттинга. Пожалуйста, не используйте их в ваших компьютерных играх! Они написаны исключительно в демонстрационных целях. Вы должны для каждого конкретного случая найти свой вариант.
Рассмотрим еще один важный аспект бит-блиттинга объектов, который вы сможете с выгодой использовать. Если вы твердо знаете, что размер ваших объектов будет фиксированным (например, 16х16 или 32х32), то можете написать отдельный алгоритм бит-блиттинга, специально оптимизированный для каждого из этих размеров. Помните: не существует общих правил быстрого написания компьютерных игр. Нет уже готовых алгоритмов и написанных кем-то библиотек. Вы должны будете использовать по максимуму весь свой творческий потенциал. Только, не пугайтесь. Даже самая замысловатая игра использует алгебру и геометрию не более чем на уровне высшей школы. Это, конечно, не значит, что вы не можете использовать сверхсложные алгоритмы и забираться в дебри, высшей математики. Однако в результате может оказаться, что двенадцатилетний подросток найдет путь в 100 раз более быстрый и эффективный только потому, что он не такой умный как вы!
Установка обработчика прерывания
Регистрация (или установка) подпрограммы обработки прерываний заключается том, что мы помещаем ее начальный адрес в соответствующее поле таблицы векторов прерываний. Это можно сделать двумя способами:
Мы можем просто изменить адрес прерывания таким образом, чтобы он Указывал на наш собственный обработчик и так все и оставить. Однако при этом, старый обработчик прерывания никогда больше вызываться не будет.
Кроме того, а что произойдет, если прерывание будет сгенерировано именно в тот момент, когда мы будем заниматься заменой адреса в таблице векторов? Р-р-раз и «зависли» — вот что случится! Именно поэтому пусть это действие за вас выполнит DOS при помощи функции _dos_setvect(). Для сохранения старого вектора прерывания следует использовать функцию _dos_getvect(). Обе функции гарантировано изменяют вектора без ущерба для операционной системы;
Очень часто нам требуется расширить функциональные возможности системы без полной замены уже работающих функций. Тогда мы используем цепочку прерываний. Под цепочкой прерывания понимается следующее: мы запоминаем старый адрес процедуры обслуживания прерывания и заменяем его на собственный, затем, по окончании нашей процедуры обработки прерывания, передаем управление старому обработчику. Таким образом, нам удается сохранить чужие обработчики прерываний или резидентные программы - если, конечно, нам это надо!
Взгляните на рисунок 12.3, на котором представлены два способа установки обработчиков прерываний.
Листинг 12.1 демонстрирует установку нашей процедуры обслуживания прерывания timer () (которая по-прежнему ничего не делает).
Листинг 12.1. Установка процедуры обслуживания прерывания.
void (_interrupt _far old_isr) (); // Указатель для сохранения
// вектора старого
// обработчика прерываний
// Сохранить вектор старого обработчика прерывания таймера
old_isr = _dos_getvect( 0х1C);
// Установить новую процедуру обработки прерывания
_dos_setvect(0x1C, Timer);
Ну как, трудно? Да вовсе нет! Для восстановления старого обработчика нам потребуется сделать только:
_dos_setvect(0x1C, old_isr);
Вот и все. Теперь, для примера, заставим наш обработчик прерывания делать что-нибудь полезное, например, обновлять значения глобальной переменной. В Листинге 12.1 показан текст программы SPY.С, которая представляет собой бесконечный цикл, печатающий значение переменной. Единственный способ изменить значение этой переменной - присвоить ей значение из другой программы, например, из обработчика прерывания.
Листинг 12.2. Шпионим за часами (SPY.C)._________________
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////////////////////
#include <dos.h>
#include <bios.h>
#nclude <stdio.h>
#include <math.h>
#include <conio.h>
#include <graph.h>
// ОПРЕДЕЛЕНИЯ//////////////////////////////////
#define TIME_KEEPER_INT 0x1C
// ГЛОБАЛЬНЫЕ
ПЕРЕМЕННЫЕ
///////////////////////////////
void (_interrupt _far *old_Isr)();
// хранит старый обработчик прерывания
long time=0;
// функции ////////////////////////////////////
void _interrupt _far Timer()
{
// Увеличивает глобальную переменную.
// Еще раз отметим, что мы можем это делать, так как при входе
// в процедуру обработки прерывания регистр DS
указывает на сегмент
// глобальных данных нашей программы,
time++;
} // конец Timer
// ОСНОВНАЯ ПРОГРАММА ////////////////////////////////
void main(void)
{
// установка процедуры обработки прерывания
Old_Isr = _dos_getvect(TIME_KEEPER_INT);
_dos_setvect(TIME_KEEPER_INT, Timer) ;
// ожидание нажатия клавиши пользователем
while(!kbhit())
{
// вывод переменной. Примечание: сама по себе функция main
// значение этой переменной не изменяет...
_settextposition(0,0);
printf("\nThe timer reads:%ld ",time);
} // конец while
// восстановление старого обработчика прерывания
_dos_setvect(TIME_KEEPER_INT, Old_Isr) ;
} // конец функции main
Запустив программу, приведенную в Листинге 12.2, вы увидите, что приращение счетчика происходит очень быстро. Фактически, значение этой переменной увеличивается каждую 1/18.2 секунды — так настроены по умолчанию внутренние системные часы. (Не переживайте, очень скоро мы научимся управлять и ими).Главное, что сама программа не увеличивает значенй переменной time. Этим занимается исключительно подпрограмма обслужива ния прерывания.
Итак, мы создали процедуру обслуживания прерывания, установили ее и убедились в том, что она работает - и все это мы сделали, написав лишь несколько строк кода. Восхитительно, не правда ли? Более сложными обработчиками прерываний мы займемся чуть позже. Сейчас же давайте сменим тему и поговорим об игровом цикле.
Установка прерывания
Однажды пройдя через все эти фокусы, чтобы установить простое прерывание, можем окончательно инсталлировать наш собственный вектор ISR, зависящий от СОМ-порта. Запомните, что порты 3 и 4 используют те же самые прерывания, что и порты 1 и 2 соответственно.
Таблица 14.4. Векторы прерывания последовательного порта.
Установка видеорежимов
В играх есть немало технических хитростей: работа со звуком, «искусственный интеллект» и многое другое. Но перед тем как начать этим заниматься, давайте попробуем инициализировать наш дисплей.
Конечно, мы сразу можем набрать гору документации с подробным описанием устройства дисплея, регистров и установок, но все это весьма опасно, и вот почему. То, что будет работать на одной видеокарте, может оказаться абсолютно неработоспособным на другой. Таким образом, чтобы избежать возможной несовместимости, для установки видеорежима мы будем использовать базовую систему ввода/вывода (BIOS).
Можно смело сказать, что основная графическая мощь ПК сосредоточена в прерывании с номером 10h. Использование этого прерывания весьма просто — необходимо правильно установить нужные регистры процессора в зависимости от выполняемой функции. В этой книге мы будем пользоваться режимом 13h (это графический режим с разрешением 320х200 точек, при 256 цветах). Теперь нам нужно найти, как перенести компьютер в этот режим. Для этого давайте напишем программу на ассемблере для установки режима 13h и программу на Си для проверки. Соответствующие фрагменты показаны в Листингах 2.8 и 2.9.
Листинг 2.8. Ассемблерная процедура, устанавливающая видеорежим (SETMODEA.ASM).
.MODEL MEDIUM, С
;модель памяти - MEDIUM, соглашения языка Си
.CODE ;начало кодового сегмента
PUBLIC Set_Mode ;объявляем функцию как общедоступную
Set_Mode PROC FAR С vmode:WORD ;функция получает один параметр
mov АН,0 ;функция 0 прерывания 10h - установка режима
mov AL,BYTE PTR vmode ;номер режима, который вы хотите установить
int 10h ; используем BIOS для установки режима
ret ; возврат из процедуры
Set_Mode ENDP ; конец процедуры
END ;конец кодового сегмента
Листинг 2.9. Си-функция, тестирующая видеорежим (SETMOPEC.C).
#include <stdio.h>
#define VGA256 0х13
#define TEXT_MODE 0х03
extern Set_Mode(int mode);
void main(void)
{
// устанавливаем режим 320х200 точек, 256 цветов
Set_Mode(VGA256);
// ждем нажатия любой клавиши
while (kbhit()) {}
// возвращаем компьютер в текстовый режим
Set_Mode(TEXT_MODE);
} // конец функции main
Теперь если вы наберете и запустите эти программы, то, скорее всего, увидите пустой экран. Правда, в данном случае это не означает «зависание» компьютера, а свидетельствует о том, что VGA-карта переключилась в режим 13h. Стоит только нажать любую клавишу, и вы вновь окажетесь в привычном текстовом режиме 80х25. Конечно, можно было бы использовать функцию _setvideomode() из графической библиотеки Microsoft С, но наша функция работает в сотни раз быстрее.
Теперь, когда мы научились переключать экран в графический режим, неплохо бы попробовать его очистить.
Установки и статус UART
Установки и статус UART управляются через набор внутренних регистров доступных как порты ввода/вывода, адреса которых начинаются от некоторого базового адреса. Базовый адрес определяется номером последовательного порта через который вы хотите связаться. Рассмотрим таблицу 14.1, в которой указаны базовые адреса управляющих регистров UART.
Таблица 14.1. Базовые адреса управляющего регистра UART.
Последовательный порт Базовый адрес порта
СОМ1 3F8h
COM2 2F8h
COM3 3E8h
COM4 2E8h
Как видите, если мы хотим играть через последовательный порт СОМ1, нам необходимо использовать порт 3F8h в качестве базового адреса ввода/вывода. Каждый порт имеет девять регистров, в которые можно писать или из которых можно считывать информацию в зависимости от их типа. Следовательно, для доступа к регистру 1 порта СОМ1 необходимо использовать адрес ввода/вывода 3F8h+1, то есть 3F9n.
Теперь мы знаем, где расположены регистры. А что каждый из них делает?
Это регистр поддержки передачи, куда помещается следующий символ для передачи. Если это одиночный байт и вы используете схему передачи, имеющую менее 8 бит, то данные игнорируются, и не передаются вообще.
Регистр 0 также выполняет функции буферного регистра приема. В зависимости от того, пишете вы в него или читаете из этого регистра, буфер передает или принимает символы с другого компьютера соответственно. В любом случае, при чтении из этого регистра он содержит последний переданный ему символ.
Этот регистр используется, чтобы задействовать тот тип прерываний, который может сгенерировать UART. Он доступен как для чтения, так и для записи. После установки серийного порта было бы неудобно постоянно опрашивать его, поэтому для получения входных данных лучше написать процедуру обслуживания прерывания (ISR), которая будет вызываться каждый раз при получении символа.
Этот регистр позволяет нам сообщить UART'y, какие именно события Должны вызывать прерывание. Для нас представляет интерес только прерывание RxRDY, которое генерируется при получении символа UART'ом.
Регистр идентификации прерывания используется для определения причины по которой UART выдал прерывание. Это может показаться избыточным однако если вы предварительно установили UART для получения прерывания по двум или более различным событиям, то, поскольку этот регистр определяет тип произошедшего прерывания, он поможет вам выяснить, что именно произошло.
Регистр управления линией используется для изменения некоторых характеристик последовательного порта, таких как количество передаваемых битов данных, тип четности. Этот регистр также выполняет функции управления загрузкой старшего и младшего байтов делителя, задающего скорость передачи и имеющего WORD. Этот регистр также доступен и для записи, и для чтения.
Данный регистр оказывает влияние на некоторые выходные данные линий управления модема. Нас больше всего в нем интересует бит GP02. Когда он установлен, появляется возможность прихода прерываний.
Регистр состояния линии используется, чтобы обрисовать состояние коммуникационного порта. В этом регистре нас интересует пятый бит (ТВЕ), который используется для определения возможности продолжения передачи символов (THR).
Регистр состояния модема используется, чтобы показать состояние линий управления модемом. Для наших целей этот регистр едва ли понадобится. Однако вы могли бы найти применение индикатору звонка (RI). Вы можете написать программу, которая будет перехватывать звонок и когда вызывается ваш номер, например, сообщать об этом соответствующей надписью на экране и звуком.
Регистр 7: Регистр временного заполнения (Scratch-Pad Register)
Не используется
Регистр 8: Менее значимый ключ делителя скорости передачи (Baud-Rate Divisor Latch Least-Significant Byte - DLL)
Предназначен для хранения младшего байта делителя, используемого при вычислении действительной скорости передачи через порт.Окончательная скорость вычисляется так: берут младший и старший банты и используют их как делитель числа 115200. В результате получится скорость передачи. Этот регистр доступен через регистр 0 при установленном 7-м бите (DLAB) регистра З (LCR).
Регистр 9: Регистр более значимого байта ключа делителя скорости пepeдaчи,(Baud-Rate Divisor Latch Most-Significant Byte-DLM)
Этот регистр используется для поддержки старшего байта делителя, использумого для вычисления действительной скорости передачи через последовательный порт. Окончательная скорость передачи вычисляется следующим образом: берут старший и младший байты и используют их как делитель, на который нужно разделить число 115200. Это дает скорость передачи. Данный регистр доступен через регистр 1 при установленном 7-м бите (DLAB) регистра 3 (LCR).
Устранение эффекта сдвига кадра
На медленных машинах или на машинах с медленными видеокартами можно заметить некий сдвиг изображения, как будто оно копируется на экран. Из-за эффекта сдвига изображение выглядит как бы разорванным. Этот интересный но нежелательный эффект появляется оттого, что адаптер сканирует видеобуфер и рисует изображение на дисплее примерно 60 раз в секунду. Этот процесс называется регенерацией экрана. Если программа в момент начала регенерации дисплея находится в процессе рисования кадра, вы заметите эффект сдвига изображения.
К счастью, существуют методы проверки статуса регенерации экрана. На VGA-карте есть регистр, сообщающий, регенерируется ли экран в настоящее время- Все, что требуется для устранения эффекта сдвига кадра, это подождать, пока регенерация экрана завершится. Затем можно начать рисовать изображение.
В Листинге 17.7 содержится фрагмент программы, ожидающей завершения цикла регенерации экрана. Это дает вам примерно 1/60 секунды, чтобы нарисовать следующий кадр. Данный фрагмент можно поместить непосредственно перед функцией, перемещающей кадр из системной памяти в видеобуфер. Выполняйте такую проверку каждый раз перед копированием буфера на экран. Только на очень быстрых машинах или при использовании небольшого окна вывода, одной шестидесятой секунды будет достаточно для изображения нескольких планов и их копирования на экран. Это главный недостаток режима 13h. Единственная альтернатива проверке на регенерацию экрана — это использование видеорежимов, поддерживающих несколько видеостраниц, и переключение между ними.
Листинг 17.7. Проверка вертикальной трассировки.
asm mov dx,0x3da
NoRetrace:
asm in al,dx
asm and al,8
аsm jz NoRetrace // ждать, пока трассировка завершится
Retrace:
asm in al,dx
asm and al,8
asm jnz Retrace // ждать начала трассировки
Программа в этой главе не выполняет проверку вертикальной трассировки. Это было сделано для того, чтобы свести к минимуму использование ассемблера. Настоятельно рекомендую использовать этот фрагмент во всех программах вывода графики.
Устройство и архитектура звуковой карты Sound Blaster
Sound Blaster — сложная звуковая карта с большими возможностями. Управлять ею не так уж и просто, и мы наверняка не сможем рассмотреть все ее впечатляющие способности в одном-единственном разделе. Поэтому мы приведем здесь только основные характеристики современных моделей Sound Blaster и кратко остановимся на их функциональных различиях.
На данный момент существует четыре различных модификации Sound Blaster. Их характеристики приведены в таблице 9.1.
Sound Blaster может создавать два типа звуков:
§ Синтезированный звук;
§ Оцифрованный звук.
Синтезированный звук создается искусственно с помощью электронной аналоговой или цифровой аппаратуры. В Sound Blaster применяется современный подход к синтезу звука — метод цифровой частотной модуляции (FM synthesis). В этом методе для создания звука используется та самая частотная модуляция, которую используют и музыкальные радиостанции, работающие в УКВ-диапазоне. Мы поговорим об этом подробнее позже в этой главе.
Sound Blaster также имеет процессор цифровых сигналов (digital signal processor, DSP), который помогает в синтезе и воспроизведении MIDI-музыки. По-существу, MIDI (musical instrument digital interface, цифровой интерфейс электромузыкальных инструментов) — это стандарт для оцифровки голоса и инструментальной музыки таким образом, чтобы они могли быть воспроизведены с помощью компьютера или такого синтезатора, как Yamaha.
Кроме синтеза звука Sound Blaster позволяет оцифровывать; а затем воспроизводить такие звуковые фрагменты, как речь или различные эффекты. Это очень полезное свойство, так как некоторые звуки очень сложно или даже невозможно создать только с помощью частотного синтезатора и DSP. В наших играх мы как раз и будем использовать оцифрованные звуковые эффекты вместе с MIDI-музыкой.
В следующем разделе мы рассмотрим как Sound Blaster работает с оцифрованным звуком.
В каком формате должны быть представлены данные MIDI?
В случае настоящего MIDI-устройства, такого как MPU401 или SoundCanvas, ему будут передаваться данные MIDI без каких-либо изменений (исключая сообщения SysEx).
В случае Adiib или Sound Blaster, MIDPAK будет эмулировать устройство MIDI и эта эмуляция имеет определенные ограничения;
§
Каналы 2-9 используются для мелодических инструментов;
§ Канал 10 используется для ударных инструментов;
§ Загружаемые алгоритмы должны быть в формате обобщенного MIDI.
Вектор Номер Адресная функция
0х0В 0x002C-0x002F RS-232 порт 1
0х0С 0х0030-0х0033 RS-232 порт 2
Все что нам нужно сделать для установки нового ISR, это использовать функцию Си _dos_getvect(), чтобы запомнить прежнее значение вектора, и _dos_setvect(), чтобы инсталлировать наш собственный ISR на место старого. Далее, с приходом прерывания (то есть когда получен символ), будет вызываться наша процедура. Звучит это великолепно, но что она будет делать?
Наш ISR должен выполнять только одну задачу — получить символ из регистра приемного буфера (RBR) и поместить его в программный буфер. Чтобы основная программа могла брать поступающие символы по мере надобности, мы должны буферизировать ввод. С этой мыслью создадим буфер с перезаписью и установим его размер равным 128 байтам, хотя, вообще-то, его длина может быть любой.
Алгоритм буферизации работает так. Полученный из RBR следующие символ помещается в буфер в текущую позицию. Далее текущий индекс буфера инкрементируется. Когда позиция записи в буфере доходит до конца, она перемещается к началу. Как вы понимаете, при этом данные, которые были записаны ранее, окажутся перекрыты. Надеюсь, что до того, как это произойдет основная программа успеет прочитать символы из буфера и обработать полученные данные. Рисунок 14.3 поясняет принцип работы буфера с перезаписью.
Мы должны обсудить еще одну тонкость, прежде чем закончим разговор об ISR. Непосредственно перед выходом из процедуры обработки прерывания необходимо сообщить PIC'y о ее завершении. Для этого в конец процедуры нужно вставить команду записи в порт 20h значения 20h. Если этого не сделать, произойдет сбой системы. Но это — между прочим, ибо пока вы используете функции Си, об этом не стоит беспокоиться. Вот если бы вы решили писать программы исключительно на ассемблере, то вопрос правильного завершения прерываний оказался бы весьма актуален и мы обсудили бы его более подробно. Но давайте пока остановимся на Си.
Листинг 14.1 показывает операции с ISR.
Листинг 14.1. Операция ISR.
void _interrupt _far Serial_Isr(void)
{
// Это процедура обработки прерывания СОМ-порта. Она очень проста.
// При вызове она читает полученный символ из регистра 0 порта
//и помещает его в буфер программы. Примечание: язык Си сам
// заботится о сохранении регистров и восстановлении состояния
// запрещаем работу всех других функций
//во избежание изменения буфера
serial_lock = 1;
// записываем символ в следующую позицию буфера
ser_ch = _inp(open_port + SER_RBF);
// устанавливаем новую текущую позицию буфера
if (++ser_end > SERIAL_BUFF_SIZE-1) ser_end = 0;
// помещаем символ в буфер
ser_buffer[ser_end] = ser_ch;
++char_ready;
/ / восстанавливаем состояние контроллера прерываний
_outp(PIC_ICR,0x20);
// разрешаем работу с буфером
serial_lock = 0;
} // конец функции
Программа из Листинга 14.1 выполняет все то, о чем мы говорили. Однако стоит обратить внимание на одну маленькую деталь. В программу включена переменная serial_lock, которая оберегает основную программу от конфликт тов связанных с обращением к буферу, пока ISR обновляет его. Такой прием называется «блокировкой» или «семафором». В DOS'e подобной проблемы никогда не возникает по ряду причин, о которых говорить слишком долго. Необходимость регулирования доступа к общим данным возникает только для полностью многозадачных систем. Тем не менее, введение «семафоров» - хорошая практика, даже если на данном этапе такая техника и не нужна. Все, мы почти у цели!
Вероятностные автоматы
Наверное, вы уже поняли, как вероятность и случайные числа могут быть использованы для выбора направлений и состояний. Мы научились использовать случайные последовательности для конструирования «характера» персонажей. Я имею в виду, что «Муха» в нашем предыдущем примере могла самостоятельно выбирать различные состояния, основываясь на окружающей обстановке. Если несколько изменить метод выбора состояний, основанны на генерации случайных чисел (то есть, создать условия, при которых вход в опредёленное состояние стал бы легче или тяжелее), то, в конечном счете, нам удалось бы изменить "характер" «Мухи».
Скажем, нам захотелось иметь в игре две «мухи». Если одну и ту же программу использовать для создания траектории движения каждой «мухи», они бы действовали одинаково. Во многих случаях большего и не требуется. Однако гораздо интересней иметь много «мух» с небольшими различиями в поведении. Это можно было бы реализовать изменением диапазона случайных чисел-во всех строках программы, где выбираются состояния «мухи». Но такой подход будет очень грубым. Мы пойдем другим путем — путем создания общего метода управления характером персонажей, основанного на вероятности.
В искусственном интеллекте «индивидуальность» означает возможность существ по-разному выполнять определенные действия при одних и тех же обстоятельствах. Например, у меня есть несколько достаточно активных друзей, которые захотели бы слегка проучить плута, попытавшегося их надуть. Но у меня также есть друзья, которые более спокойны и предпочитают сначала думать, а потом действовать. Скорее всего, мошеннику удалось бы как-то с ними договориться. То, что мы видим на данном примере и является «индивидуальностью». Каким именно способом это будет достигнуто, не принципиально, важен конечный результат.
В видеоиграх мы могли бы иметь несколько противников, которые постоянно преследуют нас, пока другие в это время неподвижны и стреляют. Третьи трусливы и предпочитают убегать, а не сражаться. Анализируя ситуацию, мы видим, что имеется все тот же набор состояний, но вероятности перехода в них различны для каждого создания.
Попытаемся вначале проанализировать проблему. Нам необходимо определить вид объекта, который зависит от направления взгляда игрока и траектории объекта или направления его движения. Как мы уже говорили, луч зрения игрока можно зафиксировать и считать, что он всегда перпендикулярен экрану. Тогда нам нужно будет побеспокоиться только о векторе траектории объекта, выводимого на экран. На рисунке 8.7 изображена взаимосвязь между вектором направления взгляда игрока и некоторой траекторией передвижения объекта.
Теперь мы должны сделать вот что: возьмем игрушечную машинку или что-нибудь подобное и будем передвигать ее перед собой (шум мотора имитировать при этом не обязательно, можно все делать тихо). Проделав это, вы быстро придете к выводу, что рисуемое на экране изображение космического корабля, движущегося прямолинейно, практически одинаково для всех параллельных траекторий независимо от местоположения объекта. Конечно, это справедливо только частично, зато мы получили хорошую отправную точку для нашего первого алгоритма выбора правильного кадра.
Что же мы должны сделать:
§ Вычислить угол между траекторией движения объекта и лучом зрения игрока (который всегда направлен прямо в экран);
§ Разделить полученный угол на квадранты. Затем на основании полученного индекса выбрать наиболее подходящее изображение среди предварительно подготовленных оцифровкой фотографий модели или нарисованных в графическом редакторе. (Более подробно это обсуждается в разделе «Оцифровка объектов и моделирование».)
§ Вывести на экран подходящий кадр, используя аксонометрическую проекцию и масштабируя объект до необходимого размера.
В результате на экране получается реалистичная картина.
Каким же образом находится угол между траекторией объекта и лучом зрения наблюдателя? Ответ может быть получен с помощью скалярного произведения векторов.
Мы знаем, что угол между двумя векторами можно найти с помощью скалярного произведения векторов, как это показано на рисунке 8.8.
Формула 8.4. Вычисление угла между наблюдателем и объектом.
Если мы зададим вектор направления взгляда, как V, а вектор скорости, как О, тогда угол между ними можно будет найти по следующей формуле:
Пусть V = (vx,vy,vz) и О = (ox,oy,oz), тогда
Если бы мы хотели сформулировать это действие словами, то могли бы сказать так: «Угол между V и О равен арккосинусу скалярного произведения этих векторов, разделенного на произведение длин векторов».
Угол между V и О, рассчитанный по этой формуле, имеет одну особенность: он всегда внутренний, то есть больше 0, но меньше 180 градусов. Следовательно, один и тот же результат, полученный по этой формуле, может соответствовать двум разным углам. Это происходит потому, что скалярное произведение не дает информации о направлении вектора (или о направлении, в котором вы отсчитываете положительный угол). Другими словами, эта формула всегда выдает наименьший из углов между двумя векторами. Если вы будете помнить об этом, то такое поведение данной формулы не будет большой проблемой. (Это напоминает бутерброд, который всегда падает маслом вниз. Если вы не знаете об этом, то такой результат может свести вас с ума. А кто предупрежден, тот вооружен.)
Рисунок 8.9 иллюстрирует указанную проблему графически. На этом рисунке показан вектор направления взгляда, три возможных положения вектора траектории и полученный в результате расчетов по формуле 8.4 угол.
Кстати, формулу 8.4 можно значительно упростить, вспомнив, что нас интересует только плоскость X-Z, так как луч зрения всегда перпендикулярен плоскости просмотра.
Но как же, в конце концов, определить действительный угол? Конечно, вы Могли бы воспользоваться еще и векторным произведением, чтобы решить, корректен ли угол, полученный в результате расчетов по формуле 8.4 или необходимо увеличить его еще на 180 градусов. Однако я слишком не люблю математику (возможно, именно поэтому я и доктор математических наук) и предпочитаю вместо грубой силы использовать тонкую интуицию.
Если мы сообразим, что вектор траектории объекта имеет ту же исходную точку, что и вектор направления взгляда, а затем проверим, в какой из полуплоскостей относительно луча зрения расположен Х-компонент вектора траектории, то мы сможем определить, больше или меньше 180° искомый угол. Это наглядно изображено на рисунке 8.10.
Применяя метод проверки Х-компонента, мы можем написать простую функцию, которая вначале рассчитывает угол, используя скалярное произведение, а затем проверяет, находится ли координата Х справа (положительная) или слева (отрицательная) от вектора направления взгляда. Если координата Х положительная, мы вычитаем угол, полученный с помощью формулы 8.4 из 360 градусов (это все равно, что прибавить 180). Затем мы можем взять рассчитанный угол и разбить его на 12 квадрантов (либо взять его модуль по основанию 12). Полученное число затем можно использовать как индекс для нахождения кадров спрайта. (Конечно, кадры должны быть расположены в правильном порядке, то есть кадрам, полученным при вращении объекта против часовой стрелки с шагом в 30 градусов, должны соответствовать индексы от 0 до 11. При этом нулевой индекс должен указывать на кадр объекта, повернутого тыльной стороной к наблюдателю.)
Если значение координаты Х отрицательное, происходит то же самое за исключением того, что будет использован другой банк изображений, и оперировать потребуется с абсолютным значением X.
Кадры, которые я создал для демонстрации этого алгоритма, расположены в файле VRYENTXT.PCX. Они расположены слева направо и сверху вниз. Каждая картинка содержит изображение, повернутое на 30° против часовой стрелки, а в исходной позиции нос корабля направлен прямо в экран (или, с точки зрения игрока, корабль обращен к нему тыльной стороной). Этот же файл мы использовали и в предыдущем примере.
Демонстрационная программа будет использовать рассчитываемые углы для выбора кадров. Но мы же не можем поместить корабль просто в пустоту. Это будет скучно! Нам надо добавить что-нибудь для оживления картинки.Я предлагаю создать трехмерное звездное небо. Под трехмерностью я здесь понимаю то, что звезды будут перемещаться к вам или от вас, а не влево или вправо, как это мы делали раньше, Надо отметить, что космический корабль, летящий в звездном пространстве, выглядит превосходно. Однако следует еще поговорить о том, как же создается такое трехмерное звездное небо.
Вертикальный обратный ход луча
Образ, рисуемый на экране ЭЛТ (электронно-лучевой трубки) и управляемый картой VGA, образуется в результате взаимодействия следующих факторов:
§ Луч электронов движется, по экрану слева направо и сверху вниз, рисуя картинку;
§ Когда он достигает нижней границы, он вновь возвращается вверх и все начинается сначала.
Рисунок 5.16 показывает это.
§ Чтобы вернуться в исходную позицию, лучу требуется примерно 1/60 секунды. Это идеальное время для обновления видеобуфера. В течение этого периода видеобуфер недоступен VGA-карте. Таким образом, 1/60 секунды — это аппаратно-зависимый параметр.
В седьмой главе, «Продвинутая битовая графика и специальные эффекты», мы узнаем, как «синхронизировать» наши игры с этим сигналом и создать чистые, свободные от мерцания изображения.
Video for Windows
Когда все ваши технические проблемы решены и вы получили приличное видеоизображение вашего макета, настало время использовать компьютера программное обеспечение (или, скорее, я бы сказал, его отсутствие) для того, чтобы перевести его в цифровую последовательность. Я сам не люблю понапрасну тратить много денег, и поэтому не буду и вас убеждать приобрести оборудование и программное обеспечение на сотни или тысячи долларов. Следовательно, нам придется использовать то, что поставляется с платой ввода графической информации. Я имею в виду Microsoft Video for Windows, которая, в общем-то, предназначена, скорее, для оцифровки сюжетов, а не для моментальных снимков вроде наших.
Так как почти все платы ввода графической информации продаются вместе с Microsoft Video for Windows, я хочу рассказать, как использовать Video for Windows для обработки образов ваших игровых объектов.
§
При создании каждого кадра используйте опцию single-frame (одиночный кадр);
§ Создав кадр, сохраняйте его в формате BMP-файла, а затем с помощью другой программы преобразуйте его в PCX-формат.
§ При создании кадров с вращающимся объектом расположите под ним круглый транспортир, чтобы точно повернуть объект на нужное количество градусов. Затем, когда вы оцифруете все ваши образы, запишите их в файлы с именами nameххх.bmp, где ххх - угол поворота. Я считаю, что для получения вполне реалистичной картины движения, перед очередным кадром можно поворачивать объект на 30°.
§ Завершив оцифровку всех кадров, вы должны преобразовать их в формат PCX с помощью какого-нибудь графического редактора или специализированной программы. (Большинство графических редакторов для персональных компьютеров поддерживают PCX-файлы в режиме 320х200х256.)
§ После того как все ваши файлы сохранены в формате PCX, нужно тщательно вычистить из них фон и попавшую в кадр платформу.
§ Затем возьмите все кадры и разместите их в одном файле PCX с общей палитрой. Я пришел к выводу, что для получения реалистичного изображения не требуется изготавливать картинки размерами более чем 128х128 пикселей.
Описанная здесь последовательность действий в виде алгоритма представлена на рисунке 8.13.
ВИДЕОИГРЫ. ПЕРВЫЕ ШАГИ...
С чего начать? Хочется так много сказать, что невольно придется посвятить этому несколько страниц. То путешествие, которое мы собираемся предпринять в мир разработки видеоигр, можно смело назвать захватывающим приключением. Создание видеоигр можно сравнить с написанием стихов или рисованием картины. Для этого нужно вдохновение, ведь создатель хочет поделиться с окружающим миром частичкой своего воображения. Один великий скульптор сказал однажды; «Статуя была здесь всегда, Я просто освободил ее из камня». Это высказывание вполне применимо и к видеоиграм.
Компыотер — это просто хранилище битов информации и, устанавливая их в 1 или 0, вы создаете образ. В этом заключается искусство. Я хочу, чтобы вы настроились на созидательную работу. Нам потребуется полное взаимопонимание. В этой главе я расскажу о том, как создаются видеоигры. Вы узнаете вот о чем:
§
Кто пишет видеоигры;
§ Откуда берутся идеи;
§ Фазы создания видеоигры;
§ Что вы узнаете из этой книги.
В следующих главах вы узнаете, как писать игры.
Видимый объем
Как мы узнали в главах, посвященных трехмерной графике, базирующейся на многоугольниках (глава шестая, «Третье измерение» и седьмая, «Улучшенная битовая графика и специальные эффекты»), объекты должны быть отсечены в пределах видимого объема (или усеченной пирамиды просмотра). Это достигается путем определения ребер каждого многоугольника и отсечения их шестью гранями видимого объема. Возникающая при этом проблема состоит в том, что объем просмотра представляет собой трехмерный трапецоид, состоящий из шести интересующих нас плоскостей, так как это показано на рисунке 8.3.
Мы, конечно, не хотим рассчитывать пересечения спрайтов с произвольными плоскостями. Это было бы уж слишком! Но если мы вначале спроецируем каждый спрайт в аксонометрии и рассчитаем их масштаб, то сможем превратить объем просмотра в прямоугольник. Теперь, так как мы выполняем обратную операцию, мы сможем отсечь спрайты куда как более легким образом, чем при использовании уравнений произвольных плоскостей.
Этот прием основывается на том факте, что объем просмотра уже является аксонометрической проекцией. Если мы отсечем края трехмерного объекта? видимым объемом прежде, чем спроецируем этот объект, мы будем обязаны использовать трапецеидальные формы объема просмотра. Однако, если мы вначале спроецируем объект с учетом перспективы, а затем
отсечем его прямоугольными границами видимого объема, то.результат, полученный таким образом, будет полностью совпадать с результатом, достигаемым первым способом. Отсечение же в плоскостях, параллельных плоскости просмотра, сводится к простому условному оператору, в котором проверяется, является ли спрайт слишком далеким или слишком близким по отношению к вам.
В случае видеорежима l3h мы можем отсекать все спрайты по размерам экрана или четырехугольника, границы которого определяют точки (0,0) и (319,199). Отсечение в плоскостях, параллельных плоскости просмотра, осуществляется с помощью простого теста на выполнение условия: если спрайт находится внутри наблюдаемого Z-пространства, то визуализируйте объект, а в противном случае игнорируйте его. Отсечение в этих плоскостях выглядит так просто оттого, что в действительности спрайты — это прямоугольные многогранники, расположенные перпендикулярно линии взгляда, или параллельно плоскости просмотра (вы можете думать, как вам больше нравится, но на самом деле это одно и то же).
Я полагаю, что, поэкспериментировав с отсечением дальней плоскостью, вы сделаете так, чтобы объект размером меньше, чем в один пиксель по любому измерению (после масштабирования) вообще не рисовался. Выбирая ближнюю плоскость отсечения, старайтесь сделать так, чтобы приближающийся объект не выглядел как под микроскопом с многократно увеличенными пикселями.
Вопросы и ответы
В этом разделе приведены ответы на наиболее часто задаваемые вопросы, касающиеся озвучивания игр.
При исполнении MIDI-музыки с помощью пакета программ MIDPAK мне кажется, что часть музыкальной композиции теряется
Проверьте назначение каналов. Эмуляция MIDI на Sound Blaster и других картах происходит на каналах 2-9 для мелодических инструментов и на 10 канале для ударных. Многие программы записывают последовательности MIDI, начиная с канала 1. При эмуляции MIDI пакетом MIDPAK канал 1 игнорируется. Назначения каналов MIDPAK были разработаны для эмуляции Roland МТ-32. Несмотря на ряд усовершенствований для поддержки обобщенного MIDI, каналы все еще ограничены номерами со второго по девятый и десятым.
При исполнении MIDI-музыки с помощью пакета программ MIDPAK, она звучит иначе, чем при использовании моего собственного музыкального контроллера. Почему?
Ваш контроллер использует другие алгоритмы, нежели MIDPAK. Чтобы избежать этого, необходимо обратиться к профессиональному композитору, который сможет скорректировать вашу композицию для исполнения с помощью MIDPAK. Однако эти услуги вряд ли будут бесплатными.
Мне не удается изменить громкость с помощью MIDPAK. Почему?
Уровень громкости в MIDPAK изменяется относительно базового уровня данного канала. MIDPAK не может изменить громкость, если вы не определили базовый уровень для каждого канала в вашем файле MIDI. Вы можете определить базовую громкость для MIDI-канала, используя Контроллер 7.
Могу ли я использовать один и тот же MIDI-файл для всех звуковых карт?
Нет, но вы можете сделать нечто похожее. Во-первых, сделайте запись в стандарте обобщенного MIDI. Затем ее нужно скорректировать для Sound Canvas, MIDI, OPL2/OPL3 и МТ-32. Базовые уровни громкости и качество звучания разных алгоритмов для этих устройств немного различаются. Исходные тексты программы SETM (программа конфигурации MIDPAK) входят в поставку (файл SETUP.ZIP) и, в зависимости от звукового драйвера выбранного пользователем, вы можете копировать различные версии вашей музыки.
Восприятие игры
Человеку, который собрался поиграть в компьютерную игру, хочется чтобы она была интерактивной. В данном случае под словом интерактивная я подразу меваю то, что графическое изображение должно быть четким и быстро сменяться. При этом музыка должна звучать в соответствующем темпе и игра обязана мгновенно реагировать на действия игрока (по крайней мере, с точки зрения играющего). Игроку должно казаться, что все (музыка, графика, звуковые эффекты и т. д.) происходит одновременно. Теперь взглянем на это с точки зрения программиста.
Вообще-то, сделать так, чтобы разные события в игре происходили одновременно, сложно. Персональный компьютер — это не многозадачная система (во всяком случае, не для игр, работающих под управлением DOS). Более того, у персонального компьютера только один процессор. Следовательно, иллюзия реальности или "реального времени" должна создаваться как-то иначе. Этот иной способ опирается исключительно на скорость работы компьютера. Быстродействие компьютера настолько превышает скорость человеческого восприятия, что машина успевает выполнять все операции последовательно, а человеку кажется, что все происходит одновременно.
На самом деле, в компьютерной игре все происходит примерно следующим образом: мы получаем команды пользователя, реагируем на них в соответствии с логикой игры, выводим на экран изображения объектов и озвучиваем происходящие события, затем повторяем все снова и снова. Благодаря тому, что это происходит десятки, если не сотни, раз в секунду, нам удается заставить игрока поверить в существование созданного нами виртуального мира.
Воспроизведение оцифрованного звука
Теперь поговорим о воспроизведении оцифрованных звуков. Я не буду показывать вам, как оцифровывать звуки. Вы сами можете воспользоваться одним из десятков программных пакетов, предназначенных для этой цели. Тем более, что в самой компьютерной игре звук не надо записывать, его надо воспроизводить!
Из этих соображений я снабдил эту главу условно-бесплатной программой Blaster Master. Эта программа работает в среде MS-DOS и позволяет делать с оцифрованным звуком все что угодно. Она может записывать звук в файлы различных форматов и применять к звуку специальные эффекты (эхо, реверберацию, смену тональности и т. д.)
Примечание
Оцифрованный звук, как и любая другая информация, должен иметь определенный формат для хранения данных. На персональных компьютерах наиболее распространены форматы WAV и VOC. Оба из них, кроме собственно звуковых данных, имеют специальные заголовки. Формат WAV был предложен в Windows, а формат VOC является стандартом «де-факто».
Blaster Master способен преобразовывать данные из формата VOC в формат WAV и обратно. Демонстрационные игры, которые мы будем рассматривать в этой книге, используют исключительно формат VOC. Поэтому, прежде чем использовать свои звуковые эффекты с нашими примерами программ, вы должны записать их как VOC-файлы или преобразовать их в этот формат, иначе ничего не выйдет! Мы выбрали этот формат из-за того, что он проще для понимания. Кроме того, WAV-файлы могут быть записаны только с частотой 11,22 или 44КГц, а это приводит к большому расходу памяти.
С чего мы начнем? Неплохой вопрос. Начнем-ка мы с драйвера. Чтобы проигрывать звуки, нам понадобится драйвер CT-VOICE.DRV, поставляемый фирмой Creative Labs. Этот драйвер позволяет нам вызывать функции работы со звуковой картой, точно так же, как мы вызываем системные функции BIOS.
Однако этот драйвер не является расширением BIOS и этим отличается от драйверов джойстика и мыши. Он использует другую технологию, называемую загружаемыми драйверами устройств. При таком подходе, драйвер загружается в память и реализованные в нем функции исполняются посредством передачи управления по определенному смещению относительно начала кода драйвера.
Передача параметров функциям драйвера осуществляется загрузкой их значений в определенные регистры процессора.
(Впервые я столкнулся с этой технологией около семи лет назад. И хотя я уже долгое время занимался программированием, не сразу сообразил, что мне надо самостоятельно загрузить драйвер в память, а затем передать ему управление.)
CT-VOICE.DRV имеет множество команд, и я не буду детально обсуждать каждую из них. Поговорим только о тех, которые потребуются нам для загрузки и воспроизведения оцифрованных звуков. В таблице 9.2 приведены необходи мые нам функции драйвера.
Таблица 9.2. Подмножество функций драйвера CT-VOICE.DRV.