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

Спрайты


Вы можете спросить: «Что такое спрайт?». Знаете, есть такой газированный напиток... Снова шучу. На самом деле спрайты - это такие маленькие объектики, которые находятся на игровом поле и могут двигаться. Этот термин

прижился с легкой руки программистов фирмы Atari и Apple в середине, 70-х годов. Теперь поговорим о спрайтах и их анимации. В будущем мы еще вернемся к этой теме в седьмой главе, «Продвинутая битовая графика и специальные эффекты. Именно с этой мыслью я создал несколько небольших спрайтов, которые мы будем использовать в дальнейшем.

Спрайты - это персонажи в играх для ПК, которые могут без труда перемещаться по экрану, изменять цвет и размер. Все это звучит как мечта программиста. Но надо помнить, что в IBM-совместимых компьютерах нет спрайтов!  Во нормальных компьютерах существует аппаратная поддержка спрайтов. Такие машины как Atari, Amiga, Commodore и последние модели Apple имеют эту возможность, а вот ПК - нет. Поэтому мы вынуждены делать это самостоятельно.

М-да. Нам будет чем заняться.

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

Вот что нам надо:

§

Мы должны уметь извлекать матрицу пикселей из загруженного РСХ-образа и сохранять ее в буфере, связанном со спрайтом;

§          Более того, хотелось бы считывать сразу несколько образов из PCX-файла и загружать их в массив, связанный с одним спрайтом. Это позволит нам оптимизировать программу по скорости выполнения.

Рисунок 5.10 показывает последовательность кадров, которые приводят в движение ковбоя. Мы воспользуемся ею позже.

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




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

Давайте на этом Месте остановимся и поговорим чуть-чуть об анимации. В играх для ПК применяется два способа обновления экрана:

§          Мы можем перерисовывать весь экран целиком, как это сделано; в игре Wolfenstein 3D;                                         

§          Можно перерисовывать лишь участки экрана.

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

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



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

Листинг 5.11. Структура спрайта с полями для анимации.

typedef struct sprite_typ

{

int x,y;            // текущая позиция спрайта

int x_old, y_old;    // предыдущая позиция спрайта

int width,height;   // размеры спрайта

int anim_clock;     // время анимации

int anim_speed;     // скорость анимации

int motion_speed;   // скорость движения

int motion_clock;   // время

движения

char far *frames [MAX_SPRITE_FRAMES] ; // массив

указателей



//на кадры

int curr_frame;                      // отображаемый кадр

int num_frames;                      // общее число кадров

int state;                           // статус спрайта

char far *background;                // фон

под спрайтом

}sprite, *sprite_ptr;

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

Прежде всего мы должны извлечь битовую карту из PCX-файла и поместить ее в массив, хранящий образы спрайта. Если вы помните, я создал файл в формате PCX (CHARPLATE.PCX), в который вы можете дорисовать свои картинки и героев. Функция, извлекающая битовые карты из РСХ-образа подразумевает, что вы создали свои образы с помощью этого файла. Программа из Листинга 5.12 позволяет перемещать спрайт, который вы хотите изменить, в указанные координаты.

Листинг 5.12. Функция извлечения спрайта из загруженного PCXфайла.

void PCX_Grap_Bitmap(pcx_picture_ptr image, sprite_ptr sprite, int sprite_franie, int grab_x, int grab_y)

{

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

// загружен PCX-файл

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

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

// размером 24х24 пикселя

int x_off,y_off, х,у, index;

char far *sprite_data;

// вначале выделяем память для хранения спрайта в структуре спрайта

sprite->frames[sprite_frame] = (char far *)malloc(SPRITE_WIDTH * SPRITE_HEIGHT);

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

// для

ускорения доступа

sprite_data = sprite->frames[sprite_frame];

// теперь перемещаем битовый образ спрайта из области PCX-файла

// в выделенную память

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

// помните, что в действительности файл представляет собой массив.

// 12х8 элементов, каждый из которых имеет размер 24х24 пикселя.

// Индекс (0,0) соответствует верхнему левому углу спрайта,

// (11,7) - нижнему правому



х_off = 25 * grab_x

+ 1;

у_off = 25 * grab_y + 1;

// вычисляем начальный адрес

y_off

=y_off * 320;

for (y=0; y<SPRITE_HEIGHT; y++)

{

for (x=0; x<SPRITE_WIDTH; X++)

{

// получить очередной байт текущей строки и поместить его

//в следующую позицию буфера спрайта

sprite_data[у*24 + х] = image->buffer[y_off + x_off + х];

} // конец копирования строки

// перейти к следующей строке

y_off+=320;

} // конец копирования

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

sprite->num_frames++;

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

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

§          Вычислить начальную позицию спрайта согласно его координатам (х,у);

§          Преобразовать полученные координаты в адрес видеобуфера;

§          Переместить байты изображения в видеобуфер.

Для рисования спрайта мы должны выполнить все операции с текущим кадром анимации. Код в Листинге 5.13 делает все перечисленное.

Листинг 5.13. Функция рисования спрайта.

void DrawSprite (sprite_ptr sprite)

{

// функция, рисующая спрайт на экране строка за строкой,

// очень быстро. Вместо умножения используется сдвиг

char far *work_sprite;

int work_offset=0,offset,x,у;

unsigned char data;

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

work_sprite = sprite->frames[sprite->curr frame];

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

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

for (y=0; y<SPRITE_HEIGHT; y++)

{

for (x=0; x<SPRITE_WIDTH; x++)

{

// Проверка на "прозрачный" пиксель (с кодом 0).


Если пиксель

// "непрозрачный" - выводим его на экран.

if ((data=work_sprite[work_offset+x])) video_buffer[offset+xj = data;

} // конец вывода строки

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

//в буфере

спрайта

offset      += SCREEN_WIDTH;

work_offset += SPRITE_WIDTH;

} // коней вывода спрайта

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

Эта функция работает примерно так же, как и Plot_Pixel_Fast из Листинга 5.5. Сначала вычисляется стартовый адрес расположения спрайта, а затем все его байты строка за строкой переписываются в видеобуфер.

Следует учесть, что здесь потребуется некоторая оптимизация. Почему бы не использовать функцию memcpy, чтобы копировать всю строку разом (а всего 24 строки)? Однако этого сделать нельзя, так как при выводе спрайта нам необходимо применить технику, использующую "прозрачные" пиксели. Что это дает? Взгляните на рисунок 5.12.
 

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

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

Листинг 5.14. Сохранение фона под спрайтом.

void Behind_Sprite(sprite_ptr sprite)

{ // функция сохраняет область видеопамяти, в которую будет

// выводиться

спрайт

char far *work_back;

in work_offset=0,offset,y;

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

work_back = sprite->background;

// вычисляем смещение в видеобуфере

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

for (y=0; y<SPRITE_HEIGHT; y++)

{

// копируем строку видеопамяти в буфер

_fmemcpy((char far *)&work_back[work_offset], (char far *)&video_buffer[offset], SPRITE_WIDTH);

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

offset    += SCREEN_WIDTH;

work_offset += SPRITE_WIDTH;

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

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

Функция Behind_Sprite считывает матрицу размером 24х24, где должен находится спрайт. Данные после считывания находятся в поле background структуры спрайта. Это поле является указателем на область памяти, где находится спрайт.

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


Содержание раздела