Allegro Pong Tutorial: Написание игры Pong

Allegro Pong Tutorial: Написание игры Pong

автор: MaxEd

Народ в последнее время что-то совсем перестал писать игры. Ну, правда ведь, кто у нас на специальности этим занимается?! А ведь вроде программисты... Стыд и позор. Однако, тому есть объективные причины. Во-первых, можно понять чувства человека, смотрящего на, скажем, Doom3, и думающего: "Блин, мне так в жисть не написать, а значит, и пытаться нечего". Тут я помочь ничем не могу. Ну, разве что, укажу на простейший факт: игра не обязательно должна иметь супер-графику, чтобы быть интересной. Пример тому - великолепные PixelShips, за которыми я провёл больше времени, чем за некотороми "профессиональными" игрушками. Так что - не пугайтесь корявости своих творений, дерзайте!!

Но есть и второе обстоятельство, останавливающее многих. Человек говорит сам себе: "да, я хочу написать игру. Но с чего же начать?? Ничего не понимаю...". Вот с этой проблемой я тут и буду бороться, а именно, расказывать, как пишутся игры. Конечно, подобных статей уже до черта и больше, но, надеюсь, эта окажется не лишей... Или уже оказалась, раз вы её читаете?! ;)

Немного о "системных требованиях" к читателю. Здесь я подразумеваю, что вы собираетесь использовать язык C/C++ и кое-что уже о нём знаете. Кроме того, для реализации графики я буду использовать библиотеку Allegro. Если вы ещё не знакомы с этим замечательным продуктом, рекоммендую заглянуть на сайт http://www.allegro.cc (если вы в ладах с английским), или почитать перевод документации на русский, лежащий на http://www.dhost.info/msiu/Learn/Progr/index.html .

Для начала, попробуем написать аналог одной из первых компьютерных игр Pong. Она представляет собой что-то вроде очень упрощённого тенниса: с двух сторон экрана ездят две ракетки, между ними летает мячик, и каждый из игроков пытается этот мяч не упустить за пределы экрана. Всё просто.

Как-то на одном форуме меня спросили, с чего я начинаю писать игру. Я ответил со всей прямотой и честностью, на какую способен: пишу



int main()
{
    return 0;
}

И это чистая правда. С этого и начнём. Создайте где-нибудь в уютной директории файл pong.cpp и занесите в него вышеприведённый фрагмент кода.

Учитывая, что мы собирается использовать библиотеку Allegro, неплохо бы добавить в начало файла строку #include <allegro.h> а после закрывающей фигурной скобки END_OF_MAIN(); Если вы программируете под Windows или Linux, макрос END_OF_MAIN совершенно необходим, без него программа с Allegro работать не будет, но если же вы пишете программу под DOS то его можно опустить.

Теперь надо сказать компилятору, чтобы он использовал библиотеку Allegro. Как это сделать -- зависит от компилятора и среды, в которой вы работаете. Вообще-то, про это хорошо написано в документации к Allegro, но я напомню, что в Linux'е можно создать makefile со строкой g++ pong.cpp -opong `allegro-config --libs` -g, в DJGPP -- создать новый проект, добавить в него файл pong.cpp и в Options->Libraries вписать alleg в 0-евую строчку и поставить около неё галочку, в Visual Studio 6.0 в Projecy->Settings->Linker добавить alld.lib к списку библиотек.

Наконец, можно приступить к программированию. Пусть наша игра будет идти в окне размером 800x600 (под ДОСом, конечно, это будет просто разрешение экрана, и тогда вместо GFX_AUTODETECT_WINDOWED в нижеприведённой строчке надо писать просто GFX_AUTODETECT). Добавим в код строки



allegro_init(); // Инициализация библиотеки Allegro
set_gfx_mode(GFX_AUTODETECT_WINDOWED,800,600,0,0); // Переход в графический режим/создание окна
set_gfx_mode(GFX_TEXT,800,600,0,0); // Переход в текстовый режим/уничтожение окна

Задумаемся: какие объекты будут в нашей игре? Две ракетки и мяч, для начала. Значит, нам понадобятся две картинки. Можете нарисовать их сами, а можете скачать те, что нарисовал я:


(размер 80x30),
(размер 20x20).

Обратите внимание, если будете рисовать сами, что фон изображения должен иметь цвет Magenta (255,0,255 или #FF00FF), так как этот цвет воспринимается функциями рисования Allegro как прозрачный.

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

У нас будет класс, который будет представлять собой одного игрока, и отдельный класс для мяча. Конечно, в нашем случае, можно было бы вместо классов использовать структуры, но это далеко не всегда так.



//Класс "ракетка" ("игрок"):

class pad
{
  int x,y; // Координаты ракетки, управляемой игроком.
  int score; // Очки игрока - "забитые мячи"
  BITMAP *pad_pic; // Структура, содержащая картинку ракетки
  char *name; // Имя игрока
  
  pad(int _x, int _y, char *file, char *_name) // Конструктор класса
  {
   /*
     В следующих двух строках мы сначала выделяем под переменную name столько байт,
     сколько символов с переданном конструктору имени игрока, считанному с клааиатуры,
     на забывая добавить единичку для нулевого сммвола-терминатора, а потом копируем 
     переданную нам строка _name в переменную класса. НИ В КОЕМ СЛУЧАЕ, ДАЖЕ ПОД СТРАХОМ
     СМЕРТИ НЕ ДЕЛАТЬ ВОТ ТАК: "name = _name;"!! Это верный пусть в Ад.
   */
   
      name = new char[strlen(_name)+1];
    strcpy(name,_name);
    x = _x; //Прирванивем переменные класса к переданным значениям
    y = _y;
    
   /*
     В следующей строку мы загружаем картинку из указанного файла. Можно было бы указать
     фиксированной имя файла (load_bmp("pad.bmp",NULL);), но вдруг нам захочется второму 
     игроку дать другую по виду ракетку?! Второй параметр функции load_bmp - структуры типа
     PALETTE в которую была бы помешена палитра загруженного файла, если бы мы работали в
     8-битном режим 256-цветов. Мы работаем в 16-битном, и этот параметр можно смело класть
     NULL.
   */
    
    pad_pic = load_bmp(file,NULL);
    
   /*
     Если загрузка файла по каким-либо причинам не удалась, значение pad станет равно NULL. 
     Проверка на этот факт спасёт вас от многих часов отлаживания, в которые вы, возможно,
     вляпаетесь, если забудете её сделать. Это вообще хорошая привычка -- проверять возвращаемые
     значения там, где это возможно, на ошибки, хотя я иногда этим пренебрегаю -- зачастую, 
     потом жестоко расплачиваясь за это :)
   */
   
    if (pad_pic == NULL)
    {
        // Итак, если файл загрузить не удалось, выходим из программы с кодом ошибки -1.
        exit(-1);
    }
  }
  
  Draw(BITMAP *where); //Прототип единственного, кроме конструктора, метод нашего класса,
                       //который будет рисовать мячик на указанном битмапе.
};

//А теперь, класс, описывающий мяч.
class ball
{
    int x,y;
    int sx,sy;
    BITMAP *pic;
    
    ball(int _x, int _y)
    {
        x = _x;
        y = _y;
    pic = load_bmp("ball.bmp",NULL);

    if (pic == NULL)
    {
        exit(-1);
    }
}
  void draw(BITMAP *where)
  {
   draw_sprite(where,pic,x,y);
  }
  
  void move()
  {
   x+=sx;
   y+=sy;
  }
};

В приведённом выше коде есть несколько моментов, на которых я хотел бы заострить внимание. Во-первых, для того, чтобы можно было пользовать функцией strcpy, необходимо подключить заголовочный файл string.h, добавив в начало программы строку #include <string.h>. Во-вторых, надо пару слов сказать про структуру BITMAP. В таких струтурах Allegro хранит любые картинки. В том числе и видимый экран - он определён как BITMAP *screen. Про внутренее устройство этой структуры я расказывать не буду, так как это вам не особо надо. Из полезных членов у неё, пожалуй, следует упомянуть только w и h, в которых содержатся, соответственно, ширина и высота картинки.
Функции draw и move соответсвенно, рисуют мяч на указанном битмапе и передвигают его, добавляя к координатам значения скоростей по x и y.


С классами всё. Можно было бы, конечно, завести ещё один класс (называемый, скажем, "игровое поле" или "игра"), который создавал и содержал бы все необходимые экземпляры игроков и ракеток и это было бы правильно с точки зрения ООП, но мне никогда не казалось это удобным, поэтому оставим всё как есть.

Теперь напишем несколько вспомогательных функций:

void draw_all()
{                                     
 //Очищаем буффер
 clear_to_color(back,makecol(0,0,0)); 
 //Вызываем рисование мяча и двух ракеток
 b1->draw(back);
 p1->draw(back);
 p2->draw(back);
 //Печатаем на буффере счёт
 textprintf(back,font,0,290,makecol(255,255,255),"%s: %i",p1->name,p1->score);
 textprintf(back,font,0,300,makecol(255,255,255),"%s: %i",p2->name,p2->score);
 //Копируем буффер на экран
 blit(back,screen,0,0,0,0,800,600);
}

Эта функция рисует все игровые объекты. В ней использована одна очень полезная техника, а именно - back buffer. Если бы мы рисовали все объекты сразу на экран (screen), то при перерисовке у нас получалось бы нежелательное мигание картинки. Вместо этого, мы заводим back-buffer и сначала рисуем все объекты на нём, а потом разом копируем его на экран. Эта техника поможет вам практически всегда, к тому же, она проста в реализации. В данной функции есть некая неоптимальность, которая не слищком скажется на работе этой программы, но может проявиться, если у вас будет очень много объектов на экране, или они будут большими: мы каждый раз перерисовываем ВСЕ объекты. Но ведь перерисовка происходит несколько раз в секунду! За это время, скажем, одна из ракеток могла никуда не сдвинуться, зачем же её перисовывать?! Однако реализация улучшенного варианта функции не так проста, а потому ну её нафиг.

Теперь рассмотрми функцию, которая будет говорить нам, столкнулся ли мячик с указанной ракеткой. Метод проверки простейший - считаем и мячик и ракетку прямоугольниками и смотрим, пересекаются ли они.



bool square_collision(ball *bl, pad *pd)
{
    //Описываем прямоугольник вокруг мяча
    double x11 = bl->x;
    double y11 = bl->y;
    double x12 = bl->x+bl->p->w;
    double y12 = bl->y+bl->p->h;

    //Описываем прямоугольник вокруг ракетки
    double x21 = pd->x;
    double y21 = pd->y;
    double x22 = pd->x+pd->p->w;
    double y22 = pd->y+pd->p->h;

    bool fits_by_x = true;
    bool fits_by_y = true;

    /*
    Если левый угол мяча находися правее правого угла ракетки
    или
    правый угол мяча находится левее левого угла ракетки    
    то явно нет никакого столкновения
    */
    if (x12x22) fits_by_x=false;
                                     
    // Аналогично по Y
    if ((y11y22)) fits_by_y=false;
    
    //Если мы "попали" мячом в ракетку и по X и по Y, то вернётся true, иначе false.
    return fits_by_x&&fits_by_y;
}


Теперь - собственно, функция, проверяющая, столкнулись ли мячик или ракетки с чем-нибудь, и чем это закончилось:

void collide(ball *b, pad *pl1, pad *pl2)
{
 //Если одна из ракеток доехала до конца экрана, её нужно дальше не пускать
 if (pl1->x>800-pl1->p->w) pl1->x=800-pl1->p->w;
 if (pl1->x<0) pl1->x=0;
 if (pl2->x>800-pl2->p->w) pl2->x=800-pl2->p->w;
 if (pl2->x<0) pl2->x=0;

 //Если мячик ударился в стенку, то должен отскочить
 if (b->x>800-b->p->w) b->sx=-b->sx;
 if (b->x<0) b->sx=-b->sx;

 //Если мячик ударился об ракетку, то обе компоненты скорости меняют знак
 if (square_collision(b, pl1)) b->sy=-b->sy;
 if (square_collision(b, pl2)) b->sy=-b->sy;
}

Функция, конечно, туповатая. Во-первых, не учитывается, что ракетка у нас толстая, и мячик может ударится об неё сбоку, а не сверху. Во-вторых, хотелось бы, чтобы мячик не всегда отскакивал под 45 градусов... Но это всё лирика, которой вы сможете занятся в свободное время :)

Далее, функция, проверяющая забитие "гола":
 
void victory()
{
 int winner = 0;
 //Если мячик "ударился" о нижнюю стенку, забил первый игрок
 if (b1->y>600) winner = 1;
 //Если мячик "ударился" о верхную стенку, забил второй игрок
 if (b1->y<0) winner = 2;
 //Если никто ничего не забил, выходим из функции
 if (winner==0) return;
 //Соответственно, изменяем счёт
 if (winner==1) p1->score++;
 if (winner==2) p2->score++;
 //И ставим мячик на исходную позицию, с исходными скоростями.
 b1->x=400;
 b1->y=300;
 b1->sx=3;
 b1->sy=3;
}
А теперь, собственно, функция main - инициализация и основной игровой цикл:

 //Инициализируем библиотеку Allegro
 allegro_init();
 //Устанавливаем Allegro'вский обработчик клавиатуры. Если этого не сделать -
 не будут работать функции Allegro для работы с клавой.
 install_keyboard();
 //Устанавливаем глубину цвета 16-бит
 set_color_depth(16);
 //Размер экрана
 set_gfx_mode(GFX_AUTODETECT,800,600,0,0);
 //Создаём back-buffer
 back = create_bitmap(800,600);
 //Создаём игроков и мячик
 b1 = new ball(400,300,3,-3);
 p1 = new pad(400,0,"Player One",true);
 p2 = new pad(400,550,"Player Two");
 //Делаем, чтобы клавиатура не блокировалась
 set_keyboard_rate(1,1);
 //Основной цикл игры:
 while(true)
 {
  //Нарисовать всё
  draw_all();
  //Если нажата клавиша...
  if (keypressed())
  {
   //Прочитать код нажатой клавиши
   int k = readkey();
   //Если это Escape - выйти (константы KEY_??? описаны в файле keyboard.h)
   if (k>>8 == KEY_ESC) break;
   //Если нажаты клавиши движения ракеток (для первого игрока - стрелки, для 
     второго -- Q и W), то изменит координаты ракеток.
   if (key[KEY_LEFT]) p1->x-=10;
   if (key[KEY_RIGHT]) p1->x+=10;
   if (key[KEY_Q]) p2->x-=10;
   if (key[KEY_W]) p2->x+=10;
  }
  //Передвинуть мяч
  b1->move();
  //Проверить, столкнулся ли мяч с одной из ракеток
  collide(b1,p1,p2);
  //Проверить, забил ли кто-нибудь гол
  victory();
 }
 //Восстановить режим экрана, который был до начала игры (в случае для Windows 
   - корректно закрыть окно, если режим был полноэкранный).
 set_gfx_mode(GFX_TEXT,800,600,0,0);
}
END_OF_MAIN();

Последний штрих - перед draw_all объявим глобальные переменные (вообще-то так лучше не делать, но в этот раз можно!):

//Мяч
     ball *b1;
//Две ракетки
     pad *p1;
     pad *p2;
//back-buffer
     BITMAP *back;

Ну вот, собственно, и всё. Осталось только скомпилировать написанный файл и наслаждаться игрой :) Но если вы хотите почувствовать себя настоящим мастером программирования - добавьте в игру Искуственный Интеллект!! Сделать это можно очень и очень просто: подумайте, как вы сами играете в эту игру? В общем и целом - просто следите за мячиком ракеткой. То есть, если он левее вас -- едете влево, а если правее - вправо. Вот пусть и наш "Искуственный Идиот" работает таким же образом. Уберите из программы обработку кнопок Q и W и вставьте в основной цикл после вызова victory() такие строки, достойные солнца русской поэзии:
 
 if (b1->xx+p2->p->w/2) p->x-=3;
 if (b1->x>p2->x+p2->p->w/2) p->x+=3;

Теперь подлый компьютер будет держать центр своей ракетки точно под мячиком! И попробуй ты его обыграй... Хотите несколько понизить сложность игры? Уменьшите скорость с которой передвигается его ракетка с 3 до 2 и посмотрите, что получится.
Ну, вот и всё. Если есть вопросы, или если вы нашли ошибки в сем труде (который писался без особой, надо сказать, проверки, да ещё и с перерывом в несколько месяцев), пишите на max-savenkov AT tochka DOT ru или на www.dhost.info/msiu/forum/ (Форум Второго Этажа) в тему Программирование. Да, сразу предупреждаю, что глюков в игре много - в первую очередь, за счёт некорректной проверки отскока от ракетки.


P.S. К журналу прилагается исходный код программы для DOS (DJGPP).

Последнее изменение Sat, 23 Jun 2018 автором Dimouse


Назад в раздел Old-games Diskmag 3