Спрайты. Часть первая
Дата создания: 2009-12-21 02:59:09
Последний раз редактировалось: 2012-02-08 11:13:09
- Предварительные уроки:
- Файловый ввод/вывод. Перейти.
- Формат bmp. Перейти.
- Камера. Часть вторая. Перейти.
В сегодняшней программе мы познакомимся со спрайтами.
В архиве с исходным кодом программы к сегодняшнему уроку (скачать можно из раздела Листинги и программы) лежат пять картинок .bmp. Эти картинки мы будем использовать для создания анимации. Если вы собираетесь запускать программу не через отладчик, то скопируйте эти файлы в одну папку с исполняемым файлом.
Размер всех картинок: 72 (в ширину) на 102 (в высоту) пикселя.
Наши задачи в сегодняшнем уроке:
1. Научиться выводить картинки на экран.
2. Научиться анимировать движения двухмерных персонажей.
Для загрузки картинок используется пять разных потоков. Для каждой картинки выделяется массив: toTheRight_Phase0, toTheRight_Phase1, toTheRight_Phase2, toTheRight_Phase3, toTheRight_Phase4.
Для загрузки заголовков используются один и те же переменные. В вычислениях будут использоваться только высота и ширина изображения - dimensions[1], dimensions[0]. Загрузку заголовков файла и изображения мы рассматривать не будем, мы уже делали это в уроке о формате bmp. Сразу приступим к загрузке изображения:
char* toTheRight_Phase0 = new сhar[dimensions[1]*dimensions[2]*bpp*4/32]; for (unsigned int i = dimensions[1]; i > 0; --i) { for (unsigned int j = 0; j < dimensions[0]; ++j) { is0.read(reinterpret_cast<char*>(&b),sizeof(b)); is0.read(reinterpret_cast<char*>(&g),sizeof(g)); is0.read(reinterpret_cast<char*>(&r),sizeof(r)); is0.read(reinterpret_cast<char*>(&x),sizeof(x)); toTheRight_Phase0[(dimensions[0]*i+j)*4] = b; toTheRight_Phase0[(dimensions[0]*i+j)*4+1] = g; toTheRight_Phase0[(dimensions[0]*i+j)*4+2] = r; toTheRight_Phase0[(dimensions[0]*i+j)*4+3] = 0; } }
Обратите внимание: так как в bmp картинка хранится в перевёрнутом виде, мы загружаем её в память снизу вверх (внешний цикл).
Картинку можно было бы загружать не по байту, а хотя бы по пикселю. В данном случае нужно было бы создать указатель на массив типа int. Но вариант, который используется сейчас, позволяет легко получить доступ к каждому каналу цвета в пикселе.
Фоновый буфер
Как вы помните, для вывода изображения на экран в DirectX используется две поверхности: основная поверхность и фоновая. В фоновой поверхности рисуется изображение (на последних стадиях графического конвейера), а затем фоновый буфер и основной меняются местами.
Свойства основной и фоновой поверхностей задаются в структурной переменной D3DPRESENT_PARAMETERS.
Доступ к основной поверхности получить нельзя. А вот к фоновой можно. При этом можно воспользоваться различными способами. В данном уроке мы будем получать доступ к фоновой поверхности напрямую. Т.е. мы будем самостоятельно копировать изображение в поверхность бит за битом.
Width (ширина) vs. Pitch (шаг, поле)
Pitch - одно из самых неприятных понятий в компьютерной графике для русскоговорящих, так как очень сложно подобрать перевод. Мы будем использовать: шаг поверхности или поле поверхности.
Под основную поверхность (а значит и под фоновую) выделяется больше памяти чем нужно. Дополнительная память называется шагом (Pitch). Т.е. если у вас есть поверхность шириной в 500 пикселей по четыре байта, то шаг скорее всего будет равен 2048 байт (шаг всегда измеряется в байтах). В данном случае выделятся на 48 байт больше чем нужно. Делается это для выравнивания данных в памяти.
Для наглядности скопируем столбик высотой в десять пикселей из картинки в поверхность размером 500 на 500. Будем считать, что шаг равен 2048. surface - поверхность в которую будет скопировано изображение.
for (unsigned int line = 0; line < 10; ++line) { memcpy(static_cast<char*>(surface)+surface.Pitch*line, static_cast<char*>(image)+width*line*4, width*4); }
Разберём аргументы memcpy. surface - указатель на поверхность. Так как мы будем работать с байтами, мы приводим указатель к типу char*. line - текущая строка. Нам нужно скопировать столбик пикселей, поэтому к указателю на поверхность мы прибавляем номер текущей строки умноженной на шаг. Вот это важный момент, мы умножаем не на ширину поверхности, а на шаг!
Второй аргумент - указатель на изображение из которого будут копироваться биты. width - ширина изображения, которую необходимо скопировать. Задаётся в пикселях. Мы копируем столбик пикселей, поэтому width = 1. Так как ширина задаётся в пикселях, то нужно не забыть домножить на 4.
Третий аргумент - количество копируемых байт. Нам нужно скопировать один пиксель. Поэтому width мы умножаем на 4 (мы работаем с пикселями размером в 4 байта).
Всегда помните: ширина поверхности (width) измеряется в пикселях, а шаг поверхности (pitch) - в байтах. Когда работаете с поверхностями учитывайте не только их ширину, но и шаг. Как узнать шаг поверхности мы разберём ниже.
Вернёмся к фоновому буферу. Когда мы создаём устройство Direct3D, основная и фоновая поверхности создаются неявно. Чтобы получить доступ к фоновой поверхности можно воспользоваться методом IDirect3DDevice9::GetBackBuffer:
HRESULT IDirect3DDevice9::GetBackBuffer ( UINT iSwapChain, UINT BackBuffer, D3DBACKBUFFER_TYPE Type, IDirect3DSurface9 ** ppBackBuffer );
iSwapChain
(swap - смена, chain - цепочка):
Данный параметр говорит какую цепочку обмена поверхностей использовать. Пока что будем передавать ноль.
BackBuffer
:
Номер фонового буфера. Так как у нас пока только один фоновый буфер, то мы будем получать доступ как раз к нему. Т.е. нам нужен первый буфер. Нумерация буферов начинается с нуля.
Type
:
Тип фонового буфера. Здесь по идее можно получить доступ к буферу со стерео изображением. Но в Direct3D стерео изображения не поддерживаются, поэтому единственное возможное значение данного аргумента - D3DBACKBUFFER_TYPE_MONO.
ppBackBuffer
:
Указатель на фоновый буфер. Фоновый буфер - это обычная поверхность IDirect3DSurface9.
Чтобы иметь возможность работать с данными из фонового буфера напрямую, в поле Flags структурной переменной D3DPRESENT_PARAMETERS нужно указать D3DPRESENTFLAG_LOCKABLE_BACKBUFFER (замыкаемый буфер).
Ну а теперь код:
D3DPRESENT_PARAMETERS pp; ZeroMemory(&pp,sizeof(pp)); pp.BackBufferWidth = 500; pp.BackBufferHeight = 500; pp.BackBufferFormat = D3DFMT_X8R8G8B8; pp.BackBufferCount = 1; pp.MultiSampleType = D3DMULTISAMPLE_NONE; pp.SwapEffect = D3DSWAPEFFECT_DISCARD; pp.hDeviceWindow = hWnd; pp.Windowed = true; pp.Flags = D3DPRESENTFLAG_LOCKABLE_BACKBUFFER; d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &pp, &videocard); IDirect3DSurface9* backBuffer = NULL; videocard->GetBackBuffer(0,0,D3DBACKBUFFER_TYPE_MONO,&backBuffer);
Теперь в переменной backBuffer содержится указатель на фоновую поверхность.
В одном из уроков я уже упоминал о методе IDirect3DSurface9::LockRect. Этот метод замыкает часть поверхности (или всю) для того, чтобы можно было работать с данными поверхности напрямую. Разберём его более подробно.
HRESULT LockRect( D3DLOCKED_RECT * pLockedRect, CONST RECT * pRect, DWORD Flags );
pLockedRect
:
Указатель на замыкаемый прямоугольник.
pRect
:
Указатель на структурную переменную RECT. Данный аргумент задаёт координаты и размер прямоугольника, который нужно замкнуть в поверхности.
Flags
:
Флаги.
Рассмотрим тип первых двух аргументов метода LockRect более подробно. Сначала разберёмся со структурой WinAPI - RECT:
typedef struct tagRECT { LONG left; // левая сторона LONG top; // верхняя сторона LONG right; // правая сторона LONG bottom; // нижняя сторона } RECT;
Здесь всё просто: четыре переменные, задающие координаты и размер прямоугольника. Мы будем копировать наши картинки примерно в середину окна:
RECT rectSize; rectSize.left = 200; rectSize.top = 200; rectSize.right = 272; rectSize.bottom = 302;
Изображение будет отступать на 200 пикселей от верхней и левой границы окна (первые два поля). Размер всех картинок - 72x102, поэтому правая и нижняя границы будут равны 272 и 302.
Замечание: Мда... Неудачное имя я для переменной подобрал - rectSize. Но пусть остаётся.
Теперь посмотрим на структуру D3DLOCKED_RECT:
typedef struct D3DLOCKED_RECT { INT Pitch; void * pBits; } D3DLOCKED_RECT, *LPD3DLOCKED_RECT;
Здесь всего два поля. Первое - шаг поверхности. Второе - указатель непосредственно на данные замкнутого прямоугольника.
Переменную типа D3DLOCKED_RECT мы назовём rectangle (прямоугольник):
D3DLOCKED_RECT rectangle;
До основного цикла нам осталось объявить несколько переменных:
DWORD timer, dt, timer1 = 0, timer2 = 0; bool flag = 0; int phase = 0;
Напоминаю, что DWORD - это переопределение типа unsigned int. Здесь я использовал именно DWORD, чтобы избавиться от предупреждений компилятора.
В программе используется четыре таймера. timer1 используется для поворота направо, timer2 - используется для поворота налево, timer используется в обоих случаях как вспомогательная переменная, dt - переменная, хранящая разницу времени. Таймеры - это обычные переменные с помощью которых отслеживается время.
Для получения значения времени мы воспользуемся функцией timeGetTime. Для её использования нужно добавить библиотеку winmm.lib и включить файл mmsystem.h. Данная функция возвращает системное время. Системное время - время прошедшее с момента запуска компьютера. Измеряется оно в миллисекундах. В одной секунде - 1000 миллисекунд.
И последние переменные: flag - булева переменная, которая отслеживает, в какую сторону происходит поворот; phase - переменная хранит текущую фазу анимации, т.е. номер картинки, которую сейчас нужно выводить на экран.
Весь остальной код расположен в основном цикле:
Основной цикл
Заметьте, в этой программе у нас нет никакой трёхмерной графики.
Сначала мы осуществляем ввод с клавиатуры:
hr = keyboard->GetDeviceState(sizeof(buffer),buffer); if (hr != DI_OK) keyboard->Acquire(); if (buffer[DIK_RIGHT] & 0x80) { flag = 1; } if (buffer[DIK_LEFT] & 0x80) { flag = 0; }
Если пользователь нажал на стрелочку вправо, переменной flag присваиваем 1. Была нажата стрелочка влево, flag присваивается 0. Теперь нужно очистить фоновую поверхность:
videocard->Clear(0,NULL,D3DCLEAR_TARGET, D3DCOLOR_XRGB(255,255,255),1.0f,0);
Далее я приведу код проверки поворота направо. Поворот налево проверяется почти также.
if (flag == 1) { if (timer1 == 0) timer1 = timeGetTime(); timer2 = 0; timer = timeGetTime(); dt = timer - timer1; if (dt <= 50) phase = 0; if (dt > 50 && dt <= 100) phase = 1; if (dt > 100 && dt <= 150) phase = 2; if (dt > 150 && dt <= 200) phase = 3; if (dt > 200) phase = 4; }
Мы проверяем переменную flag - нажал ли пользователь стрелочку вправо. В теле ветвления идёт работа с таймерами. Сначала проверяется timer1. Если он равен нулю, то этому таймеру присваивается текущее время. Если таймер не равен нулю, это значит, что текущее ветвление уже выполнялось по крайней мере один раз и в timer1 содержится корректное время. timer1 содержит время начала анимации поворота направо. Далее обнуляется timer2, а переменной timer присваивается текущее время. timer получает текущее время при каждой итерации цикла (если пользователь нажал стрелочку вправо).
Следующая строка - самая важная. В ней вычисляется количество времени прошедшее между timer и timer1. Если прошло 0,05 секунды - это первый кадр анимации (фаза), если прошло от 0,05с. до 0,1с. - это второй кадр анимации.
Как видно, вся анимация произойдёт за 0,2 секунды. В анимации поворота пять фаз. Для каждой фазы я нарисовал отдельные картинки. Художник из меня никудышный, ну и заметно, что пять кадров слишком мало. Но принцип создания двухмерной анимации, думаю, понятен.
Поворот в обратную сторону реализован аналогично, только фазы меняются в обратном порядке и используется timer2.
В предыдущем отрывке кода мы определили только текущую фазу анимации. Теперь нужно вывести соотвествующий этой фазе кадр на экран. Рассмотрим только вывод первой фазы. Остальные фазы выводятся аналогично:
if (phase == 0) { backBuffer->LockRect(&rectangle,&rectSize,0); for (unsigned int i = 0; i < dimensions[1]; ++i) // i - номер строки memcpy(static_cast<char*>(rectangle.pBits)+rectangle.Pitch*i, toTheRight_Phase0+dimensions[0]*i*4, dimensions[0]*4); backBuffer->UnlockRect(); }
Первая строка - замыкание буфера. Далее мы с помощью цикла копируем каждую строку изображения в фоновый буфер. Напоминаю, что dimensions[1] - высота изображения (102), dimensions[0] - ширина (72).
Обратите внимание на использование поля Pitch переменной rectangle.
После того как все строки изображения скопированы, нужно разомкнуть буфер. Всё! Теперь в фоновой поверхности нарисовано изображение. Осталось вывести содержимое фонового буфера на экран:
videocard->Present(NULL,NULL,NULL,NULL);
После завершения основного цикла нужно не забыть освободить память:
delete[] toTheRight_Phase0; delete[] toTheRight_Phase1; delete[] toTheRight_Phase2; delete[] toTheRight_Phase3; delete[] toTheRight_Phase4;
Заключение
В программе две серьёзные проблемы:
Первая - код загрузки изображений просто отвратительный! Мы пять раз загружали заголовки файлов! Решением данной проблемы будет использование дополнительных средств по работе с файловыми потоками. Как только найду время, постараюсь дописать урок по файловому вводу/выводу.
Вторая - управление анимацией персонажа. Переменная flag - полный отстой! Решением данной проблемы станет более правильная организация кода, в частности - конечные автоматы.
Написав нормальный загрузчик картинок и укротив анимацию, можно создать полноценную двухмерную игру.
Данный урок называется Спрайты. Часть первая. Других частей скорее всего не будет. Причина - я медленно и плохо рисую, да и тема мне не слишком интересна. Вторая и следующие части появятся только в одном случае: если вам интересна эта тема и вы хотите узнать больше. В данном случае напишите мне об этом на e-mail. Если будет много писем, то мы продолжим изучать спрайты.
На сегодня всё.