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

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


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

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

Выпуск 18. (от 19 декабря 2003 года)
Лабиринт.

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

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

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

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

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

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

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


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




/*
 Специально для новичков в программировании еще раз объясню про
 директиву препроцессора define. Она нужна для того, чтобы было легче
 писать и читать программу. Возьмем для примера значение STEP, задающее
 относительный размер элементов лабиринта 20 пикселей. Директивой define
 мы говорим компилятору, что текст STEP следует воспринимать как число
 20. И при компиляции компьютер заменит в программе текст STEP на число 20,
 везде, где найтет этот текст. Это очень удобно. Почему? Да потому что
 теперь, если нам нужно, чтобы размер элемента лабиринта был не 20, а
 15 пикселей, мы, вместо того, чтобы рыскать по всей программе и отыскивать
 число 20 (можно конечно воспользоваться поиском и заменой, но что если
 у нас есть и другие числа 20, которые менять не нужно?) просто 
 меняем его в самом начале программы. Кстати, попробуйте сделать это, только
 не выбирайте значение STEP слишком маленьким, а то некоторые элементы
 (золотые монетки) просто не отобразятся.

*/

// Итак, наши определения *******************************************

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

// Здесь мы определим размерность нашего лабиринта по X и Y
#define MATRIX_X 22  
#define MATRIX_Y 10  

// Шаг отображения элементов лабиринта (их размер)
#define STEP 20   

// Смещение в пикселях от левого верхнего угла окна - точка,
// откуда мы будем рисовать лабиринт

#define START_X 20 
#define START_Y 20

// Направления движения игрока
#define NORTH 1
#define SOUTH 2
#define EAST 3
#define WEST 4


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

// Контекст устройства, кисти и перья
HDC hdc; 

HPEN player_pen; // Для игрока
HPEN wall_pen;  // Для стен
HPEN gold_pen;  // Для монеток
HPEN background_pen; // Для фона
HPEN old_pen;  // Для сохранения "старых" текущих объектов

HBRUSH player_brush;
HBRUSH wall_brush;
HBRUSH gold_brush;
HBRUSH background_brush;
HBRUSH old_brush;


// Игровые переменные

// Инициализируем массив игрового поля, делая его пустым
int matrix[MATRIX_X][MATRIX_Y] = {0};

// Координаты игрока
int x,y;

// Счетчик золота (в игре не задействован)
int gold_counter;



// Определения (прототипы) функций **********************************

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

// Функции рисования
void ClearScreen();    // Функция очистки экрана
void DrawField();    // Функция рисования игрового поля

// Функции игровой логики
void InitField();    // Функция определения игрового поля
void MovePlayer(int direction);   // Функция передвижения игрока



// Начало программы ***************************************************

// Думаю, что с WinMain все уже хорошо знакомы

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(WHITE_BRUSH);
    wndclass.lpszClassName = class_name;

    // Регистрируем окно
    RegisterClass(&wndclass); 
  
    // Создаем окно, пока что еще только в памяти
    hwnd =  CreateWindow(class_name,
   CAPTIONBAR_TEXT,
   WS_SYSMENU,
   CW_USEDEFAULT,   
   CW_USEDEFAULT,   
   WIN_WIDTH,
   WIN_HEIGHT,
   NULL,
   NULL,
   hinstance,
   NULL);

    // Проверяем, создалось ли окно
    if(!hwnd) return EXIT_FAILURE; 

    // Получаем контекст устройства окна
    hdc = GetDC(hwnd); 
  
    // Проверяем, удалось ли нам это
    if(!hdc) return EXIT_FAILURE; 



    // Создаем нужные нам перья и кисти
 player_pen = CreatePen(PS_SOLID, 1, RGB(0, 200, 0));
 gold_pen = CreatePen(PS_SOLID, 1, RGB(255, 255, 0));
        wall_pen = CreatePen(PS_SOLID, 1, RGB(128, 128, 128));
 background_pen = CreatePen(PS_SOLID, 1, RGB(0, 0, 0));


 player_brush = CreateSolidBrush(RGB(0,200,0)); 
 gold_brush = CreateSolidBrush(RGB(255,255,0)); 
        wall_brush = CreateSolidBrush(RGB(128,0,0)); 
        background_brush = CreateSolidBrush(RGB(0, 0, 0));

    // Выбираем кисть и перо (делаем их текущими).
    // Теперь функции рисования будут использовать именно их.
    // При этом не забываем сохранить "старые" текущие кисть и перо.

    old_pen = (HPEN)SelectObject(hdc,wall_pen);
    old_brush = (HBRUSH)SelectObject(hdc,background_brush);


 // Создаем наш лабиринт, здесь же определяются
 // координаты игрока
 InitField();
 
 // Выводим окно на экран.
 // Игровое поле рисуется благодаря автоматическому вызову
 // сообщения WM_PAINT оконной процедуры (где мы и указываем,
 // что нам рисовать в окне).
    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);

 DeleteObject(player_pen);
 DeleteObject(gold_pen);
 DeleteObject(wall_pen);
 DeleteObject(background_pen);

 DeleteObject(player_brush);
 DeleteObject(gold_brush);
 DeleteObject(wall_brush);
 DeleteObject(background_brush);



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

 UnregisterClass(class_name, hinstance);


 return msg.wParam;
}



/* Оконная процедура сегодня особенно интересна, так как она
 (наконец-то!) обрабатывает некоторые сообщения, придавая
 программе функциональность. 
 Мы обрабатываем сообщение WM_KEYDOWN, которое посылается 
 при нажатии клавиши.
 Тем самым мы перемещаем игрока по лабиринту, после чего
 перерисовываем лабиринт. Ни к чему делать в нашей программе
 перерисовку экрана с частотой 60 кадров в секунду или выше,
 как это обычно делается в играх. Мы перерисовываем его 
 только тогда, когда "картинка изменилась", то есть когда
 игрок поменял свое местоположение.
 Кроме того, важно помнить о необходимости перерисовки при
 передвижении окна, перекрытии его другим окном и т.п.
 Для этого мы обрабатываем сообщение WM_PAINT. Всегда делайте
 это! Само по себе окно перерисуется совсем не так, как вам нужно.

  */



// Оконная процедура

LRESULT CALLBACK WinProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
    PAINTSTRUCT ps;

    // В зависимости от сообщения выполняем различные действия
    switch(message)
    {
 
 // Если нажата какая-либо клавиша
 case WM_KEYDOWN:

  switch(wparam) // wparam хранит виртуальный код нажимаемой клавиши
  {      
   case VK_ESCAPE:
    PostQuitMessage(0);
    break;

   case VK_UP: // стрелка вверх
    // перемещаем игрока
    MovePlayer(NORTH);
    // после чего перерисовываем экран
    DrawField();
    break;

   case VK_DOWN: // стрелка вниз
    MovePlayer(SOUTH);
    DrawField();
    break;

   case VK_LEFT: // стрелка влево
    MovePlayer(WEST);
    DrawField();
    break;

   case VK_RIGHT: // стрелка вправо
    MovePlayer(EAST);
    DrawField();
    break;
  }
   
  return 0;

    
    // Сообщение посылается, когда необходимо перерисовать экран
 case WM_PAINT:
  // сперва перерисуем окно так, как этого хочет Windows      
  BeginPaint(hwnd,&ps);
  EndPaint(hwnd,&ps);
  // после этого очистим экран и перерисуем его нашей функцией
  ClearScreen();
  DrawField();

  return 0;


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

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

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




// ********** Очистка экрана **********

// Эта функция создает прямоугольник размером с окно и закрашивает его
// белым цветом

void ClearScreen()
{

 RECT rect={0, 0, WIN_WIDTH, WIN_HEIGHT};

 FillRect(hdc, &rect, (HBRUSH)GetStockObject(BLACK_BRUSH));
}




/* Функция рисования лабиринта.
   Именно здесь мы будем использовать наш двумерный массив для определения
   наличия или отсутствия игрового элемента в заданном месте.
*/
void DrawField()
{ 
 int i,j;
 int SquareX, SquareY;


 // В массиве нумерация элементов начинается с нуля, то есть
 // первый элемент будет matrix[0][0], а последний - matrix[21][21].

 // Эти вложенные циклы "пробегаются" по всему массиву построчно.
 // При этом мы ищем ненулевые элементы, и если находим, рисуем
 // в нужных ячейках сетки квадратики или кружки.

 for (j = 0; j < MATRIX_Y; ++j)
  for (i = 0; i < MATRIX_X; ++i)
  {
   // определяем начальную точку рисования следующего элемента
   SquareX = START_X + i * STEP;
   SquareY = START_Y + j * STEP;

   // в зависимости от элемента массива определяем,
   // что нужно рисовать
   switch (matrix[i][j])
   {
   case 1: // стена
     
    SelectObject(hdc,wall_pen);
    SelectObject(hdc,wall_brush);

    // Рисуем прямоугольник текущей кистью и пером, указав при этом его
    // начальные и конечные координаты
    Rectangle(hdc, SquareX, SquareY , SquareX + STEP, SquareY + STEP);
    break;
   
   case 2: // золотая монетка
       
    SelectObject(hdc,gold_pen);
    SelectObject(hdc,gold_brush);

    Ellipse(hdc, SquareX + 5, SquareY + 5 , SquareX + STEP - 5, SquareY + STEP - 5);
    break;

   case 3: // игрок
       
    SelectObject(hdc,player_pen);
    SelectObject(hdc,player_brush);

    Ellipse(hdc, SquareX, SquareY , SquareX + STEP, SquareY + STEP);
    break;
   
   default: // фон
   
    SelectObject(hdc,background_pen);
    SelectObject(hdc,background_brush);
    Rectangle(hdc, SquareX, SquareY , SquareX + STEP, SquareY + STEP);
    break;
   }

  }
 
}

/*  Функция, которая задает вид лабиринта. 
 Как мы это делаем? Сперва мы создаем одномерный массив, причем
 такого же размера, как и наш двумерный, после чего записываем в него
 данные о нашем лабиринте линейно. Для начала поговорим о том, зачем
 нам это нужно. Мы могли бы сразу записать эти данные в наш двумерный массив,
 но, дело в том, что при записи элементов перечислением их через запятую, 
 двумерный массив будет заполняться по столбцам, а не по строкам, что
 очень неудобно и ненаглядно. А одномерному массиву абсолютно все равно,
 он просто берет числа из списка и записывает их в свои ячейки одно за
 другим линейно. Кстати, таким же образом компьютер хранит данные в памяти.
 (Ну хорошо, не всегда, но в большинстве случаев).
 И после этого мы просто переписываем эти данные, заполняя наш главный
 двумерный массив строка за строкой.
*/



void InitField()
{
 int i,j;
 int num = 0; // счетчик элементов одномерного массива

 /*  Создаем одномерный массив, такого же размера, как и двумерный.
  (22 элемента в строке * 10 строк = 220 элементов. Если
  хотите, можете пересчитать циферки в фигурных скобках :))
  Затем заполняем эти 220 элементов значениями.
  Можно это сделать и одной длинной строкой, но гораздо
  нагляднее придать этой строке "двумерный" вид.
  Компилятору абсолютно все равно, он все равно
  воспримет этот ряд чисел как одну строку. А вот зато нам
  очень удобно редактировать "карту лабиринта". Как вы уже
  поняли, 1 это стена, 2 это золотая монетка, 3 это игрок,
  а 0 это пустое место.
  Конечно же, обычно никто не рисует карты и уровни прямо 
  в исходном коде программы. Создается редактор карт, 
  записывается файл, который потом и читается игрой.


 */
 int field[MATRIX_X * MATRIX_Y] =             {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
      1,0,2,0,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
      1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,2,1,
      1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,
      1,0,0,2,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
      1,0,0,1,1,0,1,0,0,0,0,1,1,1,0,0,0,0,0,0,0,1,
      1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,
      1,0,0,0,0,0,0,0,3,0,0,1,2,0,0,0,0,0,0,0,0,1,
      1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,
      1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1};

 // Теперь пробегаем построчно наш главный массив
 for (j = 0; j < MATRIX_Y; ++j)
  for (i = 0; i < MATRIX_X; ++i)
  {
   matrix[i][j] = field[num]; // записываем очередной элемент
                              // в нужную ячейку

   // А так мы задаем начальное положение игрока, которое,
   // как вы помните, определяется в игре переменными x и y.
   if (matrix[i][j] == 3) 
   {
    x = i;
    y = j;
   }

   // увеличиваем счетчик элементов на единицу.
   num++;
   
  }
}


// Функция, обеспечивающая перемещение игрока по лабиринту
// и взаимодействие с игровыми элементами. В качестве параметра
// мы передаем направление движения игрока.

void MovePlayer(int direction)
{
 int prev_x, prev_y;
 
 // сохраним предыдущее положение игрока
 prev_x = x;
 prev_y = y;
 
 switch(direction)
 {
 case NORTH:
  // проверяем, не находится ли в точке, куда мы хотим
  // передвинуть игрока, стена.
  // если да, то выходим из функции
  if (matrix[x][y-1] == 1)
   return;

  // а если нет, то перемещаем игрока
  else
   y--;
  break;

 case SOUTH:
  if (matrix[x][y+1] == 1)
   return;
  else
   y++;
  break;

 case WEST:
  if (matrix[x-1][y] == 1)
   return;
  else
   x--;
  break;

 case EAST:
  if (matrix[x+1][y] == 1)
   return;
  else
   x++;
  break;
 }

 // Если мы добрались досюда, значит игрок переместился

 // Хотя в данной версии игры это и не используется, но
 // тем не менее - если игрок обнаружил золотую монетку,
 // увеличиваем счетчик золотых монет.
 if (matrix[x][y] == 2)
  gold_counter++;

 // Рисуем игрока в новом месте
 matrix[x][y] = 3;

 // Рисуем фон в старом
 matrix[prev_x][prev_y] = 0;
  
}



Очень надеюсь на то, что вы обратите внимание на раздельность игровой логики и графики. В программе две функции используются для реализации игрового мира и перемещения по нему (InitField и MovePlayer) и две - для отображения игрового мира и его событий (ClearScreen и DrawField). У нас реализованы игра и "графический движок". Мы можем заменить графические функции, изменив тем самым отображение игры на экране, но игровая логика останется неизменной.

Это очень важный момент. Сейчас мы выводим графику средствами Windows GDI, позже (очень на это надеюсь) мы перейдем на более сложную графическую библиотеку, такую как DirectX или OpenGL, но при этом программирование игровой логики от этого нисколько не изменится - мы можем выводить "игровую картинку" и в текстовом режиме консольного окна. Так что следует различать игровое программирование и программирование графики.

Именно по этой причине мы сразу не стали изучать DirectX. Сперва мы освоим игровое программирование, а потом, если потребуется, займемся и графикой. И к тому же не стоит считать GDI таким уж плохим графическим интерфейсом и отказываться от его изучения. Вспомните такую отличную программу, как WinAmp. Для рисования ее элементов используется исключительно GDI (если вы считаете, что графические элементы окна WinAmp это еще не графика, вспомните про плагины визуализации.)

В заключение пара слов о планах на будущее. Очень надеюсь, что мне удастся подготовить еще один выпуск до Нового Года, хотя особо рассчитывать на это не стоит, также как и на обновления сайта "Школа создателей компьютерных игр" www.gamecoder.nm.ru. (Дойдя до самых интересных графических обучалок, я остановился, так как в них помимо основной темы наворочено много лишних, второстепенных вещей, затрудняющих понимание программы, и я еще не решил, то ли переводить их как есть, то ли упрощать). Но уже не за горами такие темы, как вывод файлов bmp и работа с растровыми изображениями. Так что оставайтесь с нами, и до встречи в новом году!

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

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

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



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

В избранное