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

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


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

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

Выпуск 12. (от 15 августа 2003 года)
Через тернии к звездам.

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

Сегодня я рад как никогда приветствовать всех вас!

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

Но сперва, как всегда, вводная часть, обеспечивающая своего рода обратную связь с читателями. В ней я затрону вот какой вопрос. Я получаю очень много интересных писем, на которые с удовольствием отвечаю. Хочу сказать всем, кто их пишет - большое спасибо! Отвечая на одно из писем, я подумал - а ведь эта информация будет интересна не только получателю письма, но и всем читателям рассылки. Поэтому я решил с позволения читателей со следующего выпуска публиковать наиболее интересные из них, конечно же с ответами. Увы, я не смогу спрашивать разрешения у каждого. Так что, если вы НЕ хотите, чтобы ваше письмо попало в рассылку, указывайте пожалуйста это в письме.

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

Что же такое контекст устройства? Как всегда, я не буду вдаваться в теоретические дебри архитектуры Windows (хотя я очень надеюсь, что вы обзаведетесь хорошей книгой или иной документацией по Windows API и будете по необходимости заполнять теоретические пробелы самостоятельно - поверьте, очень пригодится). Контекст устройства это и есть то, что нам нужно получить, чтобы выводить туда графику. Устройством может быть все что угодно, даже принтер, но нас интересует, разумеется, экран. Причем не весь экран, а конкретное окно, так как каждое окно имеет свой собственный контекст устройства.

Есть два способа получить контекст устройства окна. Один из них - вызов функции GetDC, а второй - вызов функции BeginPaint. Мы будем использовать оба способа, на то есть свои причины, которые вы скоро узнаете.

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


------------------------------ Начало программы -----------------------------------



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


// Cперва определим несколько параметров нашего окна


#define WIN_WIDTH 800   // Ширина окна
#define WIN_HEIGHT 600   // Высота окна
#define CAPTIONBAR_TEXT "Stars" // Текст заголовка окна


// Максимальное количество звезд

#define MAXSTARS 250



// А здесь мы определяем структуру, хранящую координаты звезды и ее план
// У нас будет три плана, на каждом из которых звезды будут двигаться с
// разной скоростью (чем "ближе", тем быстрее).

typedef struct _STAR 
{ 
       int x, y;               // позиция звезды 
       unsigned char plane;    // Номер плана (всего 3 [задний, средний, передний] )
} STAR;






// Глобальные переменные


HDC hdc;  // Контекст устройства (тот самый!)
  // (ну конечно не сам контекст, а переменная
  // типа Handle to Device Context (HDC), которая
  // на него ссылается).

int speed = 1;  // скорость обновления экрана (и движения звезд).

STAR stars[MAXSTARS]; // массив, хранящий все звезды





// Объявления функций

LRESULT CALLBACK WinProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
bool TimeToMove(); // Определяет, пора ли обновлять экран
void UpdateScreen(); // Обновление экрана
void UpdateStars(); // Обновление положения звезд






int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hprev, PSTR cmdline, int ishow)
{

    char class_name[] = "Windowclass";
    
    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.hCursor = LoadCursor (NULL, IDC_ARROW) ;
    wndclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); // Задаем черный фон окна
    wndclass.lpszClassName = class_name;

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

  
    // Создаем на его основе окно, на которое будет ссылаться переменная hwnd   
    // (Handle to Window)

    hwnd = CreateWindow(class_name,
   CAPTIONBAR_TEXT, // Заголовок окна
   WS_SYSMENU,  // Окно будет постоянного размера
   CW_USEDEFAULT,  // Окно будет размещаться с координатой x по умолчанию
   CW_USEDEFAULT,  // Окно будет размещаться с координатой y по умолчанию
   WIN_WIDTH,  // Ширина окна
   WIN_HEIGHT,  // Высота окна
   NULL,
   NULL,
   hinstance,
   NULL);


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




    srand( GetTickCount() ); // Сбрасываем генератор случайных чисел, используя для этого
        // системное время

    /* К сожалению, до сих пор я так и не затронул тему случайных чисел.
       В принципе, ничего сложного в ней нет. Сначала необходимо сбрасывать
       генератор случайных чисел, иначе случайные числа будут одними и теми
       же при каждом запуске программы.  А затем мы вызываем функцию rand, 
       после чего берем остаток от деления на число, задающее диапазон
       случайных чисел. То есть если нам нужны числа в диапазоне от нуля до
       двух, мы делим на 3 то, что возвращает rand. Именно это мы и делаем
       в цикле ниже.

    */
 

    // Для каждой звезды массива генерируем случайное положение и план.
    for (int i = 0; i < MAXSTARS; i++) 
    { 
        stars[i].x = rand() %  WIN_WIDTH; 
        stars[i].y = rand() % WIN_HEIGHT; 
        stars[i].plane = rand() % 3;      
    }



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

     
    // С этого момента мы можем рисовать
     


    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
  {

   // А также если уже пора обновлять экран
   if(TimeToMove())
   {
    UpdateStars();
    UpdateScreen();
   
   }

  }
    }


    // Если мы здесь, то программа получила сообщение WM_QUIT "выход из программы"

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




 return msg.wParam;

    // Программа завершена
}





//********************************** Window Procedure ****************************************

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

    /* А это структура рисования. Она необходима нам для получения контекста устройства
       вторым способом. Зачем нам это нужно? Дело в том, что когда системе необходимо
       обновить содержимое окна (скажем, если изменился его размер), она посылает
       сообщение WM_PAINT. Обрабатывая его, мы должны заботиться, чтобы состояние
       экрана всегда было актуальным, т.е. в нашем случае мы просто заново рисуем
       все звезды. А чтобы мы могли это сделать, нам необходимо вызвать BeginPaint
       и EndPaint. Они вызываются обязательно в паре, и для их вызова как раз и
       нужна переменная ps.
       
    */    

    // В зависимости от сообщения выполняем различные действия
    switch(message)
    {

  // Это сообщение определяет, была ли нажата клавиша.
                // А по параметру wparam оконной процедуры мы можем
                // узнать, что это была за клавиша.

  case WM_KEYDOWN:

   switch(wparam) // wparam хранит виртуальный код нажимаемой клавиши
   {      
    case VK_ESCAPE:   // Если нажата Esc, выходим из программы
     
    PostQuitMessage(0);
    break;
  
   }
   
    return 0;
           
  case WM_PAINT:
            
   BeginPaint(hwnd,&ps);

   UpdateScreen();

   EndPaint(hwnd,&ps);
   return 0;


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

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

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

//********************************** Window Procedure End ****************************************







// ****************** Пора ли обновлять экран ***************

// Эта функция опрделяет, пора ли обновлять экран, и если пора, возвращает
// истинное значение.

bool TimeToMove()
{
 // Переменная static для того, чтобы она не обнулялась каждый раз
        // при вызове функции. Вместо этого мы могли бы сделать ее глобальной.
 static int lastTime = 0;

 // Запрашиваем системное время в миллисекундах
 int currentTime = GetTickCount(); 


 // Если прошедший с последнего вызова функции временной
 // интервал превышает заданный, значит пора обновлять экран
 if((currentTime - lastTime) >= (10 / speed))
 {
  // Сохраняем время последнего вызова функции
  lastTime = currentTime; 
  return true;
 }

 return false;
}









// ********** Обновляем экран  **********

void UpdateScreen()
{ 
 int i;

 // Эта переменная специального типа для хранения цвета.
        // Как задается цвет, вы увидите ниже.
 COLORREF color;

 
 // Рисуем звезды в цикле

 for (i = 0; i < MAXSTARS; i++)
 {

  switch(stars[i].plane)
  {
  case 0: // передний план
   color = RGB(128,128,128);
   
   break;

  case 1: // средний план
   color = RGB(150,150,150);
   break;

  case 2: // задний план
   color = RGB(240,240,240);
   break;
  }

 SetPixel (hdc, stars[i].x, stars[i].y, color) ;
  
 }


 // Как видите, функции SetPixel, рисующей пиксель, в качестве параметра
        // требуется ссылка на контекст устройства.





}






// ********** Обновляем звезды  **********



void UpdateStars()
{


 for (int i=0; i < MAXSTARS; i++) 
 { 
        
  
  // Гасим звезды

  SetPixel (hdc, stars[i].x, stars[i].y, RGB(0,0,0)) ;
  
  
  
 // Двигаем звезду с номером [i] вправо, скорость зависит от 
        // показателя 'plane' 

           stars[i].x += (stars[i].plane + 1) ; 

        // Проверяем, не вылетела ли она за границы экрана 

           if (stars[i].x > WIN_WIDTH) 
           { 
           // Если да, то возвращаем ее влево 
              stars[i].x = 0; 
           // и случайным образом меняем координату 'y' 
              stars[i].y = rand() % WIN_HEIGHT; 
           } 

 }

}
        




// **************end************


--------------------------------------- Конец программы -----------------------------

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

И еще одно замечание. Лучше всего наш "звездный экран" будет смотреться при низком разрешении, так как звезды станут больше. Попробуйте установить, к примеру, 640*480, только не забудьте подправить в самом начале WIN_WIDTH и WIN_HEIGHT, иначе окно не поместится на экране, а также можете, если хотите, попробовать заменить WS_SYSMENU при создании окна на WS_POPUP, тогда окно будет без рамки и заголовка, и при правильно подобранном разрешении займет весь экран.

До следующего выпуска!

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

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

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



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

В избранное