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

Вид материалаДокументы

Содержание


Секреты vga-карт
Подобный материал:
1   2   3   4   5   6   7   8   9   ...   37
^ СЕКРЕТЫ VGA-КАРТ

Когда мы пишем видеоигры, то используем компьютер для создания миров и VGA-карту для рисования разнообразных объектов. В этом случае VGA-карта является «связующей нитью» между компьютером и экраном дисплея. В данной главе мы поговорим о следующем:
  • Обзор VGA-карт;
  • 256-цветный режим;
  • Конфигурация видеопамяти;
  • Таблицы цветов;
  • Переопределение цветовой палитры;
  • Цветовое вращение;
  • Рисование точек;
  • Рисование линий;
  • Файлы PCX;
  • Вывод битовых образов (бит-блиттинг);
  • Спрайты;
  • Вывод текста
  • Дублирующее буферизирование;
  • Синхронизация с вертикальной разверткой;
  • Игра Tombstone

Обзор VGA-карт

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

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

Как вы знаете, VGA-карта поддерживает много различных графических режимов и может быть перепрограммирована для получения совершенно фантастических разрешений (например, я создал режим 320х400, 256 цветов путем прямого перепрограммирования временных регистров VGA-карты). Такая функциональность стала возможной благодаря архитектуре VGA-карты. Карта VGA имеет ряд регистров, которые можно по-разному программировать для решения различных задач. В любом случае, программирование регистров VGA — это тема для отдельной книги. Если вы этим огорчены, не отчаивайтесь, существует множество книг на эту тему. В моей маленькой домашней библиотеке более 30 книг посвящены VGA-картам. Если вы хотите узнать больше про секреты VGA, то, я уверен, у вас не возникнет проблем с источниками информации.

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

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

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

Мы будем использовать VGA в режиме 13h. Но как этот режим работает? Прочитав следующий раздел, вы найдете ответ на этот вопрос.

256-цветный режим

В режиме 13h (или 19 в десятичной нотации) экран состоит из 320 пикселей по горизонтали и 200 — по вертикали. В нем поддерживается одновременное отображение до 256 цветов.

Теперь мы приближаемся к пониманию истинной ценности режима 13h. Он является наиболее простым в программировании. Как вы заметили, все графические режимы (CGA, EGA, VGA, SVGA, XGA и другие) используют некоторую область памяти для представления битовой карты, выводимой на экран. Этот раздел памяти называется видеобуфером. Для режима 13h и VGA-карт видеобуфер начинается с адреса А000:0000 и простирается до адреса AOOO;F9FF. Если вы владеете шестнадцатеричной арифметикой, то легко увидите, что размер буфера равен 64000 байт. Умножив 320 на 200, мы получим тот же результат. Это значит, что каждый пиксель в режиме 13h представлен одним байтом. Этот замечательный факт заставляет меня любить VGA-карту. На рисунке 5.1 показано строение видеобуфера VGA.



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

Конфигурация видеопамяти

Давайте поговорим о записи в видеобуфер. Как вы можете видеть из рисунка 5.1, видеобуфер начинается с адреса А000:0000 и заканчивается по адресу AOOO:FF9F. Поскольку каждый пиксель представлен одним байтом, то адресация видеобуфера довольно проста. Все, что нам нужно сделать, это определить указатель на видеобуфер, после чего можно записывать пиксели, используя этот указатель. И все!

Видеобуфер — это одна большая, непрерывная область памяти или массив байтов. Мы знаем, что он состоит из 320 байтов в каждой строке и таких строк насчитывается 200. Таким образом, чтобы нарисовать пиксель с координатами (х,у), нам надо:
  • Умножить координату Y на 320;
  • Прибавить Х-координату к результату умножения;
  • Использовать рассчитанное значение как смещение от базового указателя на видеобуфер;
  • По полученному адресу записать значение от 0 до 255.

Таблицы цветов

Как я уже говорил, мы можем записать по рассчитанному адресу число от 0 до 255. А что означает это число? Ответ прост - это цвет пикселя, который мы хотим отобразить.

VGA-карта способна одновременно отобразить на экране до 256 цветов. Цвет, который мы хотим получить, должен быть представлен числом от 0 до 255. Это здорово, но какая связь между числом и действительным цветом? Число используется как индекс в таблице цветов, хранящей действительные значения цвета, который мы увидим на экране.

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

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

Таблица цветов состоит из 768 байт (3х256). Например, когда карта считывает из видеобуфера число 72, то она проверяет адрес 72 в таблице цветов. Адрес 72 находится по смещению 72х3 от начала таблицы, поскольку каждое из значений занимает три байта. Затем значения зеленого, красного и синего считываются из таблицы и используются как значения сигналов. Давайте рассмотрим рисунок 5.2 для более подробного ознакомления.



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

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

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

Цветовая ротация

Когда я купил свой первый компьютер в 1978 году (хотя я до сих пор уверен, что это был божий дар) я был глубоко поражен компьютерной графикой и видеоиграми. Компьютер назывался Atari 800. В то время это было пределом возможностей. Один из интересных эффектов, который он поддерживал, назывался цветовой ротацией. Цветовая ротация может быть выполнена только на компьютерах, имеющих таблицу преобразования цветов. Как известно, изображение рисуется на экране компьютера. Например, пусть это будет водопад. Изображение водопада состоит (в нашем случае) из 16 оттенков синего цвета, каждый из которых — это число, ссылающееся в таблице преобразования на соответствующее значение цвета. Мы получаем, что водопад содержит 16 оттенков синего, находящихся в регистрах цвета с адресами от 100 до 115.

Теперь представьте, что мы берем одно из значений и сдвигаем его в следующий регистр и так далее до 115-го, содержимое которого переносим в 100-й регистр. Что произойдет? Правильно, возникает ощущение движения. Уже в 70-х годах это было возможно на процессорах 6502 с тактовой частотой 1.79МГц.

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

Рисование точки

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

Как мы узнали чуть раньше, для рисования точки нам достаточно вычислить адрес смещения относительно начала видеобуфера (А000:0000) и записать байт, отображающий ее цвет. Больше ничего делать не надо. Листинг 5.4 содержит фрагмент кода, который рисует точку определенного цвета с координатами Х и Y.

Листинг 5.4. Рисование точки в позиции (х,у).

void Plot_Pixel(int x, int у, unsigned char color)

{

// эта функция отображает точку выбранного цвета. Каждая строка

// занимает 320 байт, поэтому для вычисления адреса надо умножить Y

// на 320 и прибавить значение Х

video_buffer[y*320+x]=color;

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

Итак, рисование точки довольно просто. Я думал, все окажется сложнее, и поэтому так много написал об этом, но то, что мы использовали режим 13h, значительно упростило дело. Функция Plot_Pixel получилась простой, это всего одна строка кода. Тем не менее, давайте попробуем ее оптимизировать.

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

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

Рисунок 5.3 поясняет это.



Поскольку операция сдвига выполняется примерно в 2-10 раз быстрее, чем умножение, то мы получим быстрые функции для рисования. Единственная сложность состоит в том, что число 320 — это не степень двух, но чтобы выйти из положения, мы применим маленькую хитрость. Представим выражение 320 х у, как 256 х, у + 64 х у. Листинг 5.5 показывает код для быстрого рисования точки.

Листинг 5.5. Программа быстрого рисования точки.

void Plot_Pixel_Fast ( int x, int y, unsigned char color )

{

// эта функция рисует точку несколько быстрее за счет замены

// операции умножения на сдвиг

// учитываем, что 320*у=256*у+64*у=у<<8+у<<б

video_buffer[((у<<8) +(у<<6)) + х ] = color;

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

Эта функция работает примерно в два раза быстрее — вот что значит оптимизация. Позже мы научимся оптимизировать программы так, что парни из Microsoft перестанут нам верить.

Теперь, когда у нас есть функция рисования точки, надо дополнить нашу графическую библиотеку функцией рисования линии.

Рисование линий

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

Мы напишем две функции. Одна из них рисует горизонтальные линии слева направо, а другая — вертикальные сверху вниз. Рисунок 5.4 показывает, как они выглядят в видеобуфере.

Поскольку горизонтальные линии рисовать легче, то с них мы и начнем. Как видно из рисунка 5.4, горизонтальную линию можно получить, заполняя ряд пикселей в матрице 320х200. Чтобы это проделать, мы должны найти начальный адрес строки и заполнить ее значениями пикселей от начальной до конечной позиции. Для этого стоит использовать функцию rnemset. Это один из самых быстрых способов. Листинг 5,6 содержит код такой функции.

Листинг 5.6. Рисование горизонтальной линии.

void H_Line (int х1, int x2, int y, unsigned int color)

{

// функция рисует горизонтальную линию, используя memset()

// x2 должно быть больше х1

_fmemset ((char far *) (video_buffer + ((у << 8) + (у <<6)) + х1), color, x2 - х1 + 1);

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

Следует кое-что запомнить:
  • Мы используем функцию _fmemset, поскольку она, в отличие от memset, корректно работает с дальними (FAR) указателями;


  • Мы вычисляем начальный адрес линии, и функция заполняет определенное количество байтов заданным значением цвета;
  • Количество байтов равно длине строки, которую мы вычисляем как разность между правым и левым концами линии. При этом нужно прибавить единицу, чтобы не потерять последнюю точку.

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