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 ((y11 y22)) 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->xТеперь подлый компьютер будет держать центр своей ракетки точно под мячиком! И попробуй ты его обыграй... Хотите несколько понизить сложность игры? Уменьшите скорость с которой передвигается его ракетка с 3 до 2 и посмотрите, что получится.x+p2->p->w/2) p->x-=3; if (b1->x>p2->x+p2->p->w/2) p->x+=3;
Ну, вот и всё. Если есть вопросы, или если вы нашли ошибки в сем труде (который писался без особой, надо сказать, проверки, да ещё и с перерывом в несколько месяцев), пишите на max-savenkov AT tochka DOT ru или на www.dhost.info/msiu/forum/ (Форум Второго Этажа) в тему Программирование. Да, сразу предупреждаю, что глюков в игре много - в первую очередь, за счёт некорректной проверки отскока от ракетки.
P.S. К журналу прилагается исходный код программы для DOS (DJGPP).
Последнее изменение Sat, 23 Jun 2018 автором Dimouse
Назад в раздел Old-games Diskmag 3