Арканоид на Lazarus

в 7:05, , рубрики: arcanoid, Delphi, game development, lazarus, метки: , ,

Привет! Решил и я запостить историю о том как получил автомат по программированию на delphi, создав игрушку и не сдав половину лабораторных работ. Под катом история, фрагменты кода, скриншоты, исходники и ссылки для скачивания.

Арканоид на Lazarus

Дисклеймер: Весь код, написаный ниже, является говнокодом и не подлежит оценке с точки зрения оптимизации или красоты архитектуры.

Итак, наш преподаватель дал понять, что готов поставить автомат на экзамене за оригинальный и проработанный проект. Людей, серьёзно увлекающихся программированием в группе было порядка 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-ом уровне
...

Создавать такие файлы вручную довольно утомительно, поэтому я написал специальный «редактор карт», который, к сожалению потерялся.

Итак, немного обещаных скриншотов из игры:
Арканоид на Lazarus
Арканоид на Lazarus
Арканоид на Lazarus
Арканоид на Lazarus

Исходники: лежат в Dropbox (не стал порочить гитхаб таким б-гомерзким кодом)
Скомпилированная под Win7: лежит в Dropbox

Linux под рукой к сожалению нет, но могу заверить, что под Ubuntu игра работала гораздо плавнее чем под Windows.

Автор: Tairesh


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js