Данный материал взят с сайта old.shatalov.su и является его зеркалом

Создаём компьютерную игру. Создание игр на C++/DirectX

Есть вопросы?
Ошибка на сайте?
рус eng esp
Внимание! Данный сайт не обновляется. Новая версия: shatalov.su

Спрайты. Часть первая

Дата создания: 2009-12-21 02:59:09
Последний раз редактировалось: 2012-02-08 11:13:09


В сегодняшней программе мы познакомимся со спрайтами.

В архиве с исходным кодом программы к сегодняшнему уроку (скачать можно из раздела Листинги и программы) лежат пять картинок .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. Если будет много писем, то мы продолжим изучать спрайты.

На сегодня всё.