Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Создание компьютерных игр


Информационный Канал Subscribe.Ru

Cоздание компьютерных игр.
Выпускается еженедельно по пятницам.
Автор рассылки - Евгений Казеко.

Выпуск 17. (от 5 декабря 2003 года)
Графические функции Win32.

--------------------------------------------------------------

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

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

Мы можем создавать перья и кисти, делать их текущими (выбирать их), и удалять. Думаю, что следует сразу перейти к тексту программы, чтобы рассмотреть, как создаются, выбираются и удаляются кисти и перья, а также узнать о некоторых функциях GDI. Я вновь решил не изобретать велосипед, а воспользоваться программой-обучалкой с моего сайта "Школа создателей компьютерных игр" (www.gamecoder.nm.ru), немного подредактировав ее комментарии. Программа выводит в окне различные фигуры при нажатии функциональных клавиш.


#include <windows.h>
#include <stdlib.h>

// Ширина и высота окна
#define WIN_WIDTH 320
#define WIN_HEIGHT 240

// Объявим некоторые переменные глобальными, чтобы они были
// доступны в любом месте программы

HDC hdc; // контекст устройства 

// Две переменные для хранения перьев 
// Как видите, для перьев и кистей (да и пожалуй для всего в Windows)
// зарезервированы специальные типы данных
HPEN hpen;
HPEN old_pen;

// Кисти
HBRUSH hbrush;
HBRUSH old_brush;



// Стандартная callback функция
LRESULT CALLBACK WinProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);


// Эта функция будет очищать наше окно, закрашивая его белым
void ClearScreen();


int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hprev, PSTR cmdline, int ishow)
{
    char class_name[] = "class1";
    
    HWND hwnd;
    MSG msg;
    WNDCLASS wndclass = {0};
 
    wndclass.style="CS_HREDRAW" | CS_VREDRAW;
    wndclass.lpfnWndProc = WinProc;
    wndclass.hInstance = hinstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);

    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
/*  Интересный момент, который мы раньше обходили стороной.
    В этой строке мы указываем фон окна. Но как мы это делаем?
    Мы используем стандартную белую кисть. В Windows есть несколько
    стандартных кистей и перьев. (В том числе и NULL_BRUSH и HOLLOW_BRUSH, т.е.
    "пустые" кисти. В письме мне задавался вопрос, можно ли сделать окно с
    прозрачным фоном. Я ответил, что вряд ли это удастся сделать "в лоб" при
    создании окна. И в самом деле, хотя указанные выше стандартные кисти и
    создают окно "без фона", при перемещении окна копируется фон под ним,
    что убивает весь эффект прозрачности. )
    Для выбора стандартного объекта, например кисти, существует функция
    GetStockObject, которая этот стандартный объект и возвращает. Только
    она не знает, какой тип имеет этот объект, поэтому нужно явно указывать
    его, используя операцию приведения к типу - (HBRUSH)GetStockObject приводит
    возвращаемые данные к типу HBRUSH, т.е кисти.
*/


    wndclass.lpszClassName = class_name;

    RegisterClass(&wndclass); // Регистрируем окно

  
    hwnd = CreateWindow(class_name,
   "A Shapely Figure",
   WS_OVERLAPPEDWINDOW,
   CW_USEDEFAULT,   
   CW_USEDEFAULT,   
   WIN_WIDTH,
   WIN_HEIGHT,
   NULL,
   NULL,
   hinstance,
   NULL);

   // Проверка на ошибку
 if(!hwnd)
  return EXIT_FAILURE; // Случилось что-то плохое

 srand( GetTickCount() ); // Сбрасываем генератор случайных чисел

 hdc = GetDC(hwnd); // Получаем контекст устройства окна
  
 // Проверка на ошибку
 if(!hdc)
  return EXIT_FAILURE; // Не смогли получить hdc окна



// Вот так мы создаем перья и кисти

 // Создаем красное перо
 hpen = CreatePen(PS_SOLID,2,RGB(200,20,2));

 /* Функция CreatePen возвращает нужное нам перо.
    PS_SOLID - мы указываем ей, что перо сплошное (а не пунктирное).
           Далее мы устанавливаем толщину пера 2 и цвет в формате RGB.
 */


 // Создаем синюю кисть
 hbrush = CreateSolidBrush(RGB(15,15,200));

 // Что интересно - для создания кистей есть три функции -
        // CreateSolidBrush для создания сплошной кисти
        // CreateHatchBrush для кисти со штриховкой
        // CreatePatternBrush для кисти с заданной текстурой

// А здесь мы видим еще один важный момент - выбор объектов или
// точнее, установка текущих пера и кисти.

     old_pen = (HPEN)SelectObject(hdc,hpen);
     old_brush = (HBRUSH)SelectObject(hdc,hbrush);

     /* Как это работает? Функция SelectObject делает текущими
        заданный объект в заданном контесте устройства.
        А возвращает она объект, который был текущим до вызова
        функции. То есть одним вызовом мы выполняем сразу два
        действия - делаем текущим нужный объект и сохраняем
        старый (опять же, используя операцию приведения к типу).
        "Старые текущие объекты" пригодятся нам при завершении
        программы - мы вновь сделаем их текущими и вернем все
        "как и было". (Кстати, очень важный момент в программировании,
        потому что глючной бывает не только операционная система
        WindowsMustDie, но и программы горе-программистов, после которых
        не обойтись без перезагрузки компьютера. Всегда старайтесь
        по мере возможности "убирать за собой", удалять ненужные
        объекты, чистить память и т.п., хотя в данном случае это не так
        и критично.)
     */ 


    ShowWindow(hwnd, ishow);
    UpdateWindow(hwnd);

   
    while(1)
 {
  // Получаем сообщения, если они есть
  if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
  {
   if(msg.message == WM_QUIT)
    break;
    
   TranslateMessage(&msg);
   DispatchMessage(&msg);
  }
  else
  {
   // Здесь проводим все сложные вычисления
  }

 }

 // Перед завершением программы немного "приберемся"

 // Выбираем обратно старые HPEN и HBRUSH
 SelectObject(hdc,old_pen);
 SelectObject(hdc,old_brush);

 // Освобождаем HPEN и HBRUSH, которые мы создавали
 DeleteObject(hpen);
 DeleteObject(hbrush);

 // Освобождаем глобальный HDC
 ReleaseDC(hwnd,hdc);

 return msg.wParam;
}



// Оконная процедура
LRESULT CALLBACK WinProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
     
 PAINTSTRUCT ps;

 int midX = WIN_WIDTH / 2;  // Это координата X центра окна
 int midY = WIN_HEIGHT / 2; // Это координата Y центра окна
 
 // В зависимости от сообщения выполняем различные действия
    switch(message)
    {
        // Это сообщение посылается каждый раз при нажатии или удерживании клавиши
 case WM_KEYDOWN:
  switch(wparam) // wparam хранит виртуальный код нажимаемой клавиши
   {      
    
   case VK_F1: // Если была нажата клавиша 'F1'

    ClearScreen(); // Очищаем экран

    /* Можете ли вы угадать, что будет нарисовано?
     Если вы считаете, что это эллипс, то вы правы.
     hdc это контекст устройства, куда мы будем рисовать.
     Эллипс рисуется с использованием текущего HPEN для
     контура и hbrush для заливки.
     Следующие два параметра указывают координаты левого
     верхнего угла прямоугольника, описывающего эллипс.
     И последние два параметра указывают координаты правого
     нижнего угла прямоугольника, описывающего эллипс.
     */

     Ellipse(hdc, midX - 25, midY - 25, midX + 25, midY + 25);
      break;
     

   case VK_F2: // Если была нажата клавиша 'F2'
   
    ClearScreen(); // Очищаем экран

    /* Да, что будет нарисовано сейчас, тоже несложно угадать.
     hdc - контекст устройства, где будет происходить рисование.
     (midX - 25,midY - 25) - верхний левый угол прямоугольника.
     (midX + 25,midY + 25) - нижний правый угол прямоугольника.
     Также, как и для эллипса, текущее HPEN (то, что было выбрано
     в "hdc") будет использоваться для рисования границы прямоугольника.
     Текущая HBRUSH используется для заливки прямоугольника.
    */

     Rectangle(hdc, midX - 25, midY - 25, midX + 25, midY + 25);
      break;

     

   case VK_F3: // Если была нажата клавиша 'F3'
   {
    
    ClearScreen(); // Очищаем экран

    LPPOINT lp_point=NULL; 

    /* Это указатель long pointer на точку. Единственная
    причина объявлять его - это то, что мы должны
    передать LPPOINT в MoveToEx(). Эта функция 
    заполнит его "предыдущей текущей позицией"   */
     
    // Передвигаемся в положение, определяемое координатами    
    // midX - 25, midY - 25 (x,y) -- Заполняем "lp_point" предыдущим
    // текущим положением
    MoveToEx(hdc,midX - 25,midY - 25,lp_point);

    LineTo(hdc,midX + 25, midY + 25); 
    // Рисуем линию из "текущего положения"
    // до пискеля, определяемого midX+25,midY+25 (x,y)

    break;        

    // LineTo() использует для рисования линии текущее перо
    
   }


   case VK_F4: // Если была нажата клавиша 'F4'
   {
    
    ClearScreen(); // Очищаем экран
     
    // Создаем массив из четырех случайных точек нашего окна
    POINT poly_pts[4] = { {rand()%WIN_WIDTH,rand()%WIN_HEIGHT},
       {rand()%WIN_WIDTH,rand()%WIN_HEIGHT},
       {rand()%WIN_WIDTH,rand()%WIN_HEIGHT},
       {rand()%WIN_WIDTH,rand()%WIN_HEIGHT}   };
     
    // Мы хотим нарисовать многоугольник.
    // hdc -- контекст устройства, где рисовать
    // poly_pts -- массив точек POINT, составляющих многоугольник
    // 4 -- количество точек POINT в массиве
    Polygon(hdc,poly_pts,4);
     break;

    // Polygon() использует текущее перо для рисования края многоугольника
    // и текущую кисть для его заполнения
   }

   case VK_F5: // Если была нажата клавиша 'F5'
   {
    
    ClearScreen(); // Очищаем экран
 
    /* Хотя это не слишком сложно, можете посмотреть в MSDN для
    технического объяснения работы функции.
    hdc -- контекст устройства, где мы делаем все
    (midX - 50,midY - 50) -- верхняя левая координата описывающего дугу прямоугольника
    (midX + 50,midY + 50) -- нижняя правая координата описывающего дугу прямоугольника
    (midX - 25,midY - 25) -- начальная координата дуги
    (midX + 25,midY + 25) -- конечная координата дуги
    */

    // В Win95 и Win98 Arc() всегда рисует против часовой стрелки

    Arc(hdc,midX - 50,midY - 50,midX + 50,midY + 50,
     midX - 25,midY - 25,midX + 25,midY + 25);
    break;
    // Arc() рисует текущим пером
   }

   } // конец switch(wparam)
   
    return 0;


         
 case WM_PAINT:
            
  BeginPaint(hwnd,&ps);
  EndPaint(hwnd,&ps);
  return 0;

        case WM_DESTROY:
 case WM_CLOSE:
           
  PostQuitMessage(0);
             return 0;

    } // конец switch(message)

    return DefWindowProc(hwnd, message, wparam, lparam);
}

void ClearScreen()
{
// Еще один способ нарисовать прямоугольник

 // "rect" заполняется текущими размерами нашего окна
 RECT rect={0,0,WIN_WIDTH,WIN_HEIGHT};

 // Функция заполняет "rect", связанный с "hdc", кистью, которую мы передаем
 FillRect(hdc, &rect, (HBRUSH)GetStockObject(WHITE_BRUSH));

 // Экран будет заполнен белым --
 // Если заменить "WHITE_BRUSH" на "BLACK_BRUSH" -- ClearScreen() заполнит окно черным

}


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

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

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

--------------------------------------------------------------

Архив рассылки вы найдете по адресам http://subscribe.ru/catalog/comp.games.gamecoder и http://www.gamecoder.nm.ru/subscribe.htm.

Евгений Казеко.
kazeko@list.ru
www.gamecoder.nm.ru
-----------------------------
Рассылка "Создание компьютерных игр", выпуск 17.
Выпускается еженедельно по пятницам.



http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу

В избранное