Привет! Решил и я запостить историю о том как получил автомат по программированию на delphi, создав игрушку и не сдав половину лабораторных работ. Под катом история, фрагменты кода, скриншоты, исходники и ссылки для скачивания.
Дисклеймер: Весь код, написаный ниже, является говнокодом и не подлежит оценке с точки зрения оптимизации или красоты архитектуры.
Итак, наш преподаватель дал понять, что готов поставить автомат на экзамене за оригинальный и проработанный проект. Людей, серьёзно увлекающихся программированием в группе было порядка 5-6 штук, но делать скучные лабы, а потом сдавать экзамен со скучными вопросами не захотелось только двоим. Мы оба решили делать игры — я долго выбирал между «танчиками» и «марио» с Nintendo — но выбрал всё же «арканоид», сам не знаю почему. Мой одногрупник же решил замахнуться на святое 3D и сделать копию Minecraft, с блекджеком и сами знаете чем. Забегая вперед скажу, что дальше реализации вращения камерой и удаления кубов, на которые тыкается мышка у него дело не пошло, и он сдавал лабы и экзамен как простые смертные.
Я же начал писать свой арканоид. Т.к. дома я пользовался Ubuntu, в универе — WinXP, а сейчас на работе — Win7 — то я предпочел Lazarus а не Delphi. Ни о каких гитах и эсвээнах я еще не знал, а если и знал то не использовал. Поэтому ветки разработки арканоида находились на флешке, в папках вроде arcanoid, arcanoid_backup, arcanoid_backup2, arcanoid_new, arcanoid_new2, arcanoid_old и некоим аналогом master-ветки в Dropbox. Понятно, что хаос был страшный и неоднократно, важные апдейты затирались и пропадали. Самым печальным из подобных случаев была потеря строк кода, которые отвечали за выпадение бонусов из блоков. Заново реализовывать эту возможность я уже не стал.
Главной трудностью, связанной собственно с программированием, была обработка столкновений шарика со стенками и блоками. Для хранения координат и векторов я создал тип vektor_mas = array [1..2] of real;
Вектор движения шарика я хранил в переменной vektor : vektor_mas
В просторах сети была найдена формула, вычисляющая отскок объекта от стенки (правда код был на Action Script, но перевести на паскаль особого труда не составило.
function TForm1.otskok (ball: vektor_mas;wall: vektor_mas):vektor_mas;
var k:real;
begin
k:= 2* (ball[1] * wall[1] + ball[2] * wall[2]) / (wall[1]*wall[1] + wall[2]*wall[2] + 0.1);
otskok[1]:=wall[1]*k - ball[1];
otskok[2]:=wall[2]*k - ball[2];
end;
Но чтобы мячик отскочил, нужно еще определить что-он с чем то столкнулся, и определить направляющую прямую, того с чем он столкнулся. В коде события onTimer можно найти следующее:
{Обработка столкновений}
if (Image1.Top<=10) then begin // верхняя граница
ar[1]:=ClientWidth;
ar[2]:=0;
vektor:=otskok(vektor,ar);
booms:=false;
end;
if (Image1.Left<=5) or (Image1.Left+Image1.Width>=(Panel4.Left-5)) then begin // левая и правая
ar[1]:=0;
ar[2]:=ClientHeight;
vektor:=otskok(vektor,ar);
booms:=false;
end;
// столкновение с игроком
if stolk(Image1,ProgressBar1) and (booms = false) then
begin
ar[1]:=ProgressBar1.Width;
ar[2]:=round((Image1.Left-ProgressBar1.Left)-(ProgressBar1.Width/2)+(Image1.Width/2));
vektor:=otskok(vektor,ar);
booms:=true;
end;
// столкновение с нижней границей
if (Image1.Top>=ClientHeight-10) then begin
vektor[1]:=0;
vektor[2]:=0;
prilip:=true;
hp:=hp-1;
end;
Волшебная переменная booms
показывает с чем было последнее столкновение — true
в случае если это была ракетка игрока, и false
если это была стенка. Не спрашивайте меня, зачем это мне понадобилось. С помощью round((Image1.Left-ProgressBar1.Left)-(ProgressBar1.Width/2)+(Image1.Width/2));
реализован неравномерный отскок от края и центра ракетки. Image1
— это шарик, а ProgressBar1
— ракетка игрока (на самом деле он тоже картинка, но когда-то давно, наверное был progressbar-ом). Переменная prilip
означает, как явствует из названия, «прилип» ли шарик к ракетке.
Блоков всего десять, не больше и не меньше. А так как они, почему то не захотели складываться в массив, все операции над ними производятся примерно таким образом:
// столкновение с блоком
if stolk(Image1,Block1) and Block1.Enabled then del_block(Block1)
else if stolk(Image1,Block2) and Block2.Enabled then del_block(Block2)
else if stolk(Image1,Block3) and Block3.Enabled then del_block(Block3)
else if stolk(Image1,Block4) and Block4.Enabled then del_block(Block4)
else if stolk(Image1,Block5) and Block5.Enabled then del_block(Block5)
else if stolk(Image1,Block6) and Block6.Enabled then del_block(Block6)
else if stolk(Image1,Block7) and Block7.Enabled then del_block(Block7)
else if stolk(Image1,Block8) and Block8.Enabled then del_block(Block8)
else if stolk(Image1,Block9) and Block9.Enabled then del_block(Block9)
else if stolk(Image1,Block10) and Block10.Enabled then del_block(Block10);
Больше в таймере практически нет ничего интересного, если не считать пару магических строк в конце:
//небольшая хитрость чтобы шарик не застревал горизонтально
if (vektor[1]<>0) and (round(vektor[2])=0) then vektor[2]:=random(2)-1;
// антилаг
if (antilag>0) then antilag:=antilag-1;
Из-за округления, довольно часто случалось так, что шарик начинал летать строго горизонтально, на некоторой высоте над ракеткой, так, что больше никак на него повлиять было невозможно — приходилось начинать игру заново. Поэтому, было использовано такое, не очень «честное» решение — если шарик после отскока должен полететь горизонтально — то он летит или чуть вверх, или чуть вниз. Ну а antilag
решал другую, не менее серьезную проблему — когда шарик попадал между двумя блоками, то столкновение происходило с ними обоими, и оба удалялись, придавая вектору движения шарика неожиданные направления. Для этого был написан вот такой говнокостыль — при столкновении с блоком проверялась переменная antilag
— если она была больше нуля, то столкновение не происходило. При удачном же столкновении с блоком в antilag присваивалось некое число, например 5. И 5 кадров шарик не мог ни с чем столкнуться. Проблему это конечно же не устранило, а наоборот, добавило несколько новых, вроде того что шарик иногда оказывался внутри блока и результаты столкновений стали совсем непредсказуемы
Процедура обработки столкновения шарика с блоком представляет собой шедевр говнокода, на который могут молиться все индусские программисты:
{ Процедура обработки столкновения с блоком }
procedure TForm1.del_block(block: TImage);
var ar: vektor_mas;
begin
if (antilag=0) then begin
score:=score+1+dif;
if (Image1.Top-(block.Top+block.Height)>-2) then // шарик снизу
begin
if(Image1.Left-block.Left)<3 then // шарик снизу слева
begin
ar[1]:=block.Width; ar[2]:=block.Height;
end;
if (block.Left+block.Width)-(Image1.Left+Image1.Width)<3 then // шарик снизу справа
begin
ar[1]:=block.Width; ar[2]:=-block.Height;
end
else begin // шарик снизху в центре
ar[1]:=block.Width; ar[2]:=0;
end;
end;
if block.Top-(Image1.Top+Image1.Height)>-2 then // шарик сверху
begin
if(Image1.Left-block.Left)<3 then // шарик сверху слева
begin
ar[1]:=block.Width; ar[2]:=-block.Height;
end;
if (block.Left+block.Width)-(Image1.Left+Image1.Width)<3 then // шарик сверху справа
begin
ar[1]:=block.Width; ar[2]:=block.Height;
end
else begin // шарик сверху в центре
ar[1]:=block.Width; ar[2]:=0;
end;
end;
if (Image1.Left-block.Left)<2 then // шарик слева
begin
if block.Top-(Image1.Top+Image1.Height)>-3 then // шарик слева сверху
begin
ar[1]:=block.Width; ar[2]:=-block.Height;
end;
if (Image1.Top-(block.Top+block.Height)>-3) then // шарик слева снизу
begin
ar[1]:=block.Width; ar[2]:=block.Height;
end
else begin // шарик слева в центре
ar[1]:=0; ar[2]:=block.Height;
end;
end;
if (Image1.Left+Image1.Width)-(block.Left+block.Width)>-2 then // шарик справа
begin
if block.Top-(Image1.Top+Image1.Height)>-3 then // шарик справа сверху
begin
ar[1]:=block.Width; ar[2]:=block.Height;
end;
if (Image1.Top-(block.Top+block.Height)>-3) then // шарик справа снизу
begin
ar[1]:=block.Width; ar[2]:=-block.Height;
end
else begin // шарик справа в центре
ar[1]:=0; ar[2]:=block.Height;
end;
end;
Еще из интересных особенностей можно выделить сохранение результатов игры в текстовый файл scores.txt и вывод нескольких лучших в отсортированном виде. Уровни (а их всего десять), могут загружаться из файла levels.txt, а если его нет, то грузятся стандартные, прописанные в код. После десятого уровня в игре, карты начинают повторяться (1 = 11 = 21 = 31, 2 = 12 ...), но дойти даже до 10-го уровня не так уж просто. На каждом втором уровне стартовая скорость шарика увеличивается на 1 пиксель / кадр.
Структура файла levels.txt не сложная:
координата x 1-го блока на 1-ом уровне
координата y 1-го блока на 1-ом уровне
координата x 2-го блока на 1-ом уровне
координата y 1-го блока на 1-ом уровне
...
Создавать такие файлы вручную довольно утомительно, поэтому я написал специальный «редактор карт», который, к сожалению потерялся.
Итак, немного обещаных скриншотов из игры:
Исходники: лежат в Dropbox (не стал порочить гитхаб таким б-гомерзким кодом)
Скомпилированная под Win7: лежит в Dropbox
Linux под рукой к сожалению нет, но могу заверить, что под Ubuntu игра работала гораздо плавнее чем под Windows.
Автор: Tairesh