Форум программистов
 

Восстановите пароль или Зарегистрируйтесь на форуме, о проблемах и с заказом рекламы пишите сюда - alarforum@yandex.ru, проверяйте папку спам!

Вернуться   Форум программистов > разработка игр, графический дизайн и моделирование > Gamedev - cоздание игр: Unity, OpenGL, DirectX
Регистрация

Восстановить пароль
Повторная активизация e-mail

Купить рекламу на форуме - 42 тыс руб за месяц

Ответ
 
Опции темы Поиск в этой теме
Старый 12.09.2012, 22:48   #1
Slin
Пользователь
 
Регистрация: 06.09.2012
Сообщений: 14
По умолчанию Игра "Змейка" на Delphi

Здравствуйте, уважаемые форумчане, поздравляю Вас с прошедшим праздником!
Решил я тоже заняться программированием, написал несколько программ: прогноз погоды, парсер логинов, конвертер, редактор для работы с БД и т.д.
Но подумал, что интереснее будет постигать азы программирования, если заняться геймдевом, решил написать игру "Змейка".
Но тут у меня возникли небольшие проблемы. Движение головы змейки, еду - я написал, рост - наполовину, с ним у меня и проблемы. Дело в том, что когда змейка "съедает" еду, то создается shape с координатами головы, первый созданный сегмент двигается за головой, а остальные - остаются на месте первого сегмента и все, первый сегмент продолжает двигаться за головой, а другие - остаются на месте создания.
Вот код движения (привожу обработчик только для кнопки вверх, остальные-аналогично):
Код:
procedure TForm1.Move_up(Sender: TObject);
begin
if (Shape1.top<=0) then
begin
Timer1.Enabled:=false;
ShowMessage('Game Over!:(');
Game_over(self);
end
else
begin
  xx:=Shape1.Left;   //присваиваются координаты головы
  yy:=shape1.Top;    //присваиваются координаты головы
  Shape1.Top:=Shape1.top-20;     //происходит движение вверх
  if (Shape3.Left=Shape1.Left) and (Shape3.Top=Shape1.Top) then  //если координаты головы=координатам еды, то
    begin
      dlina:=dlina+1; //длина +1 (Изначально равна 0)
      eda(self);  //Размещение еды в другом месте
      Form1.Caption:=IntToStr(Dlina);    //длина змеи, не считая головы
      Test(self);  //создание сегмента змеи
    end;
   Ros(self);  //процедура движения
end;
end;
Процедура роста:
Код:
procedure TForm1.Test(Sender: TObject);
var
i,j:Integer;
begin
for i := 1 to dlina do
begin
snake[i]:=TShape.Create(self);
snake[i].Parent:=Self;
snake[i].Visible:=True;
snake[i].Left:=0;
snake[i].Top:=0;
snake[i].Width:=20;
snake[i].Height:=20;
snake[i].Shape:=stCircle;
snake[i].Pen.Color:=clLime;
snake[i].Pen.Width:=5;
end;
end;
Процедура движения сегментов:
Код:
procedure TForm1.Ros(Sender: TObject);  //процедура движения
var i,j:integer;
begin
for i := 1 to dlina do     //цикл, который выполняет движение тела
begin
  snake[i].Left:=xx;   //сегменту 'I' присваивается прошлая позиция головы змейки
  snake[i].top:=yy;    //аналогично предыдущему
  xx:=0;
  yy:=0;
  xx:=snake[i].Left;   //'XX'присваивается позиция сегмента 'I' по оси Х
  yy:=snake[i].Top;    //'YY' присваивается позиция сегмента 'I' по оси У
end;
end;
Начальная позиция:

После съедания 1 "еды", все хорошо, сегмент двигается за головой:

После того, как съедено еще 2 "еды":


Кто подскажет, почему так получается?
Весь проект вот: Простая змейка.rar

Последний раз редактировалось Slin; 12.09.2012 в 22:50.
Slin вне форума Ответить с цитированием
Старый 13.09.2012, 07:36   #2
phomm
personality
Старожил
 
Аватар для phomm
 
Регистрация: 28.04.2009
Сообщений: 2,882
По умолчанию

Такс.

Желательно такую тему постить сразу в раздел игр, если согласны, попросите модераторов о переносе.

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

Итак, код, во вложении пофиксеный файл модуля, мои исправления там по комментариям видно. Советую пробежаться в уме по коду и раскомментировывая код, смотреть последствия, чтобы понять, что и как.

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

Ну, и критика. много критики. но во благое дело - сразу привить хороший кодстайл.
1. Кодстайл: именование - плохое, не надо писать транслитом, использовать сразу ёмкие, говорящие англ идентификаторы. Следить за форматированием кода. Писать говорящий код, либо , где просятся - комментарии.
2. Литералы - убирать в константы/поля всё, что можно (первые кандидаты - размер клетки 20, имена файлов, размеры поля в клетках и пикселях и т.п.)
3. Код - лапша, много копипасты, выделять больше кода по процедурам, выносить похожие вещи, разносить разные вещи.
4. Делить вообще на модель игры и на интерфейс. Пробовать уйти от контроло-зависимого кода, пробовать рисовать всё ручками, а не шейпами. Реогранизовать ход программы - начало игры написать так, чтобы оно работало также и для переигровки (сейчас длина сохраняется и т.п.), загрузку данных(рисунки) сделать динамическую.
5. Особняком (ибо, скорее к грамотности и расширяемости кода относится, а не кодстайлу) стоит слежение за временем жизни и областью видимости элементов кода - удаление созданных объектов, уменьшение числа глобальных переменных, повышение параметризации методов.

Матчасть от GunSmoker (и его переводы)
КодСтайл
Комменты, Рефакторинг
Многое из Разного
На всякий случай Программирование

Ну, и чисто от себя хочу посоветовать один из лучших курсов, из которого особое внимание советую уделить на ООП Курс Дельфи
Вложения
Тип файла: zip Unit1.zip (1.6 Кб, 90 просмотров)

Последний раз редактировалось phomm; 13.09.2012 в 07:39.
phomm вне форума Ответить с цитированием
Старый 13.09.2012, 20:48   #3
Slin
Пользователь
 
Регистрация: 06.09.2012
Сообщений: 14
По умолчанию

Благодарю за ответ!
Вы правы, писал вначале по урокам, потом решил писать сам, а если не понятно или что-то не получалось - гугл помогал.
Реализацией "врезания" я пока не занимался, т.к. змейки, как таковой, и не было- врезаться не во что.
Про еду я думал, но как-то вылетело из головы- забыл. Надо исправить.
Спасибо за критику, буду работать над собой и своим кодом.
Отдельное спасибо за статьи, очень интересные и познавательные!

Последний раз редактировалось Slin; 13.09.2012 в 22:08.
Slin вне форума Ответить с цитированием
Старый 13.09.2012, 22:12   #4
Slin
Пользователь
 
Регистрация: 06.09.2012
Сообщений: 14
По умолчанию

Реализовал "Врезания", теперь играть интересней стало.=)
Также улучшил читаемость кода, как Вы и советовали.
phomm, не могли бы Вы помочь с генерацией еды?
Написал код:
Код:
begin
  Randomize;
  x_sh := Random(19) * 20;
  y_sh := Random(23) * 20;
  if (x_sh = Shape1.Left) and (y_sh = Shape1.Top) then
  begin
    while (x_sh = Shape1.Left) and (y_sh = Shape1.Top) do
    begin
      x_sh := Random(19) * 20;
      y_sh := Random(23) * 20;
    end;
  end;
end;
Он исключает возможность генерации еды на месте головы змейки. А вот написать код, исключающий генерацию еды на месте сегментов - не получается. Пробовал и через while и repeat - не выходит. Завтра еще попробую, сегодня уже поздно, может поэтому не выходит, мысли не лезут в голову.
И как можно "очистить" массив? Хочу очистить массив сегментов при проигрыше. Как Вы ранее заметили, при проигрыше и затем начале новой игры- длина змейки сохраняется, чтобы обнулить длину, думаю, необходимо "очистить" массив, так ли это?

Последний раз редактировалось Slin; 13.09.2012 в 22:15.
Slin вне форума Ответить с цитированием
Старый 13.09.2012, 22:36   #5
phomm
personality
Старожил
 
Аватар для phomm
 
Регистрация: 28.04.2009
Сообщений: 2,882
По умолчанию

Вызов Randomize надо делать один раз при запуске программы - обычно в Oncreate.
Перебор сегментов змейки сделать в цикле - и проверять, куда попало случайное число, при попадании в змейку заново генерировать и проверять в цикле, но это если в лоб, по уму надо просто "запомнить" все клетки где нет змейки , и сгененрировать по их количеству случайное число, и сопоставляя число клетке - поставить еду (алгоритм будет не ресурсоёмкий, в отличие от первого, но его посложнее написать).

Очищать массив, конечно же, надо ( snake[i].free ), ну и саму переменную длины тоже.
phomm вне форума Ответить с цитированием
Старый 20.09.2012, 18:10   #6
Slin
Пользователь
 
Регистрация: 06.09.2012
Сообщений: 14
По умолчанию

Здравствуйте! Мне опять нужна Ваша помощь. Появилось свободное время и я опять занялся своим "грандиозным" проектом. Начал редактировать рандомную генерацию еды. Попробовал сначала так:
Цитата:
Перебор сегментов змейки сделать в цикле - и проверять, куда попало случайное число, при попадании в змейку заново генерировать и проверять в цикле
Но у меня получилось так, что небольшой шанс попадания на змейку остался. Поэтому перешел на это:
Цитата:
"запомнить" все клетки где нет змейки
Вроде бы все сделал, а оно не работает!
Добавил глобальную переменную:
Код:
pole: array [1 .. 22, 1 .. 22] of Boolean;
Это- массив, его я и заполняю, если в клетке змейка, то True, если пустая клетка, то False.
Процедура, в которой я провожу проверку:
Код:
procedure TForm1.pole_r(Sender: TObject);
var
  i, j, a: Integer;
begin
  for i := 0 to 22 do  //все клетки свободны для записи
    for j := 0 to 22 do
      pole[i, j] := False;

   //для головы, она у меня отдельный shape
  for i := 0 to 22 do // столбик
    for j := 0 to 22 do // строка
    begin
      if (Shape1.Left = i * 20) and (Shape1.Top = j * 20) then
      begin
        pole[j, i] := true;
        Memo1.Lines.add('x=' + IntToStr(i * 20) + ' y=' + IntToStr(j * 20));
      end;
    end;

//для тела змейки
  for a := 1 to dlina do
  begin
    for i := 0 to 22 do // столбик
      for j := 0 to 22 do // строка
      begin
        if (snake[a].Left = i * 20) and (snake[a].Top = j * 20) then
        begin
          pole[j, i] := true;
          Memo1.Lines.add('x=' + IntToStr(i * 20) + ' y=' + IntToStr(j * 20));
        end;
      end;
  end;

end;
А вот сама процедура генерации еды:
Код:
procedure TForm1.eda(Sender: TObject);
var
  x_sh, y_sh, i, j, x1, y1, a, b: Integer;
  k_x: array [1 .. 100] of Integer;
  k_y: array [1 .. 100] of Integer;
begin
  pole_r(self);
  x_sh := Random(19) * 20;
  y_sh := Random(23) * 20;

  while pole[x_sh, y_sh] = true do
  begin
    x_sh := Random(19) * 20;
    y_sh := Random(23) * 20;
  end;
  Shape3.Left := x_sh;
  Shape3.Top := y_sh;
end;
Не подскажете, что неправильно сделал? Еда все равно, бывает, попадает на тело змейки.
И вот еще одна менее значимая проблема:

После того, как змейка съедает еду, происходит пересчет пустых (занятых) клеток на поле. И все время координаты последнего элемента ранвны 0;0 почему такое возможно?
Вот обрезок кода:
Код:
      Test(self);//процедура добавления еще одного элемента к змейке
      eda(self);  //в этой процедуре происходит проверка поля но последний элемент не видится, почему-то.
Вот такие у меня проблемы возникли, помогите разобраться, пожалуйста.

UPD:
Смотрю я на свой скриншот и понимаю, что проверка поля на занятые клетки- криво как-то работает...

UPD2:
Хотя вроде бы хорошо работает.. только между головой и первым сегментом разница не в 20, а в 40, по Y... странно, код тот же самый..

UPD3:
Вообще непонятки какие-то: если змейка горизонтально, то различается по X на 40, а если змейка вертикально, то различается по Y на 40.


UPD4:
Оказывается, это почему-то первый сегмент после головы пропускается => отсюда и разница в 40 pxl...
P.s: думается мне, что таких UPD будет много...=)

Последний раз редактировалось Slin; 20.09.2012 в 21:29.
Slin вне форума Ответить с цитированием
Старый 20.09.2012, 22:05   #7
phomm
personality
Старожил
 
Аватар для phomm
 
Регистрация: 28.04.2009
Сообщений: 2,882
По умолчанию

Да я заметил , что количество записей не соответствует числу сегментов, отсюда и непонятки - думаю просчеты в циклах, и последний элемент читается из незанятой памяти, потому и 0. для случаев на скрине, для 4 вертикальных сегментов - только 2 записи (по коордам видно) - отсюда и предполагаю что по циклам не срастается.

Ну и киньте опять же архивчиком проект, завтра с утра по-бырику разберу, что там с едой.

Ну и опять же замечания насчет лапши - сами же запутываетесь. Надо бы улучшать технику и стиль кодирования, статьи же читаете, думаю, давайте уже применяйте ))
phomm вне форума Ответить с цитированием
Старый 20.09.2012, 22:41   #8
Slin
Пользователь
 
Регистрация: 06.09.2012
Сообщений: 14
По умолчанию

Вот проект:Змейка.rar.
Посмотрите, пожалуйста!)
Slin вне форума Ответить с цитированием
Старый 21.09.2012, 07:09   #9
phomm
personality
Старожил
 
Аватар для phomm
 
Регистрация: 28.04.2009
Сообщений: 2,882
По умолчанию

Я недоволен, что за неделю не поступили исправления, код почти не отличается по грамотности от первого варианта. Много ошибок чисто по невнимательности, которые как раз и исключаются грамотным стилем программирования !
1. массивы гуляют по индексам - объявлено одно, в некоторых циклах другое, в иных даже третье (1..22, 0..22, 0..21), такая свистопляска только чудом не приводит к порче памяти. Во избежание лучше включить опцию компилятора Range checking (Project-Options-Compiler-Range checking), либо сразу, руководствуясь хорошим стилем, объявить 4 (или 2 - в случае только квадратного поля) константы - начала диапазона и конца диапазона (конец диапазона также можно в определённом смысле считать размером поля) и везде использовать только их - так не будет мешанины чисел.
2. Гуляют переменные-индексаторы массива - то [j,i] то [i,j] - применяя хороший стиль, поименовав их Col, Row - не запутаетесь, правда, попутно надо принять единую нотацию в проекте, что первое измерение массива - только для колонок, или только для строк. В код особо не вчитывался, но смесь всяких х, хх, х_х опять же чревата, лучше повнятнее каждую описать, чтобы код читался почти как текст.
Да и единую нумерацию массивов принять - с 1 или с 0 (советую с 0), особенно учитывая, что везде происходит работа с индексами, и не отслеживается валидность - например тот же random(19)*20 выдаёт число от 0*20 до 18*20 и может быть 0, а массив по объявлению - от 1.
3. Прописаны, но не задействованы константы - конкретно числа типа 20 и 440 - советую поменять везде, хотя на данном этапе они не приводят к ошибкам, но чуть что и код поплывёт, будете рыскать при дебаге что и где сыпется.
4. Хотя и не ожидаю от Вас чудес, но напомню, что 4 метода и кой-какой ещё код дублирующийся можно свернуть в единые методы, параметризуя специфику (например, через параметр и case в коде) - таким образом, будете избегать проблем - менять 4 метода, с нехилой возможностью где-то ошибиться в каком-нибудь минусе при каждом мелком изменении, а иметь всего 1 метод, который отдебажил и забыл.
5. Опять же, не жду сразу, но советую уже обращать внимание на разделение работы логики и визуализации, и делать шаги в эту сторону.

Алгоритмизация грамотного рандома не понята до конца - советую расписать на бумаге, чтобы внести ясность, а не сидеть над кодом пытаясь разными костылями подпереть в надежде, что заработает.
Конкретно можно указать на то, что сделаны вложенные циклы, хотя сами top left можно сразу использовать для индексов в массиве, без этих циклов.
Однако , оговорюсь - использовать внешние по отношению к алгоритму данные (а именно положение шейпов) - неправильно, лучше иметь собственные данные, в какой клетке есть сегмент змейки с каким номером, но такие изменения позже лучше делать, когда уже будет понимание.

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

Основываясь на текущем коде я бы написал что-то такое, вместо реализации текущей pole_r (тут я, правда, не слежу за индексами, Вам надо сперва принять во всем коде едино и потом уже выверять индексы) :
Код:
pole[Shape1.Left div kletka, Shape1.top div kletka] := true;
// поменять i,j Memo1.Lines.add('x=' + IntToStr(i * 20) + ' y=' + IntToStr(j * 20));
for a := 1 to dlina do
begin
  pole[snake[a].Left div kletka, snake[a].top div kletka] := true;
// поменять i,j   Memo1.Lines.add('x=' + IntToStr(i * 20) + ' y=' + IntToStr(j * 20));
end;
И вообще, если привязываться к шейпам, то можно сделать массив структур типа "сегмент змейки", где хранить координаты, номер сегмента, соответствующий шейп (а то понаделаете сейчас везде snake[a].top div kletka которые я только для примера привел). И уже везде работать с ним, а не с разрозненными глобальными переменными.

Последний раз редактировалось phomm; 21.09.2012 в 07:23.
phomm вне форума Ответить с цитированием
Старый 21.09.2012, 18:16   #10
Slin
Пользователь
 
Регистрация: 06.09.2012
Сообщений: 14
По умолчанию

Здравствуйте! Спасибо еще раз за критику, проект я доделал, теперь все работает.
Еще спасибо за Ваш "пинок под зад": я пробежался по коду, объединил копипаст, который можно в отдельную процедуру засунуть, изменил названия процедур, почистил неиспользуемые переменные, прокомментировал код, правда только кое-где, начал использовать константы, таким образом код стал меньше более чем на 100 строк. Вы мне указывали на все это, верно?))) Или я до сих пор чего-то не понимаю?) Если я не понимаю, то прошу еще один пинок.
Вот проект: Snake.rar
Так же, я планирую поработать над интерфейсом и добавить разных бонусов в игру.

Последний раз редактировалось Slin; 22.09.2012 в 11:06.
Slin вне форума Ответить с цитированием
Ответ


Купить рекламу на форуме - 42 тыс руб за месяц



Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
Игра "Змейка" на 2 персоны [Dethklok] Assembler - Ассемблер (FASM, MASM, WASM, NASM, GoASM, Gas, RosAsm, HLA) и не рекомендуем TASM 1 07.06.2011 14:24
Assembler.Игра "змейка". Пупкин Помощь студентам 0 27.05.2010 00:08
Игра "Змейка" program123 Общие вопросы Delphi 2 08.03.2009 23:49
Игра "Змейка" spamer Общие вопросы Delphi 1 09.01.2009 04:22
Ещё одна игра "Змейка" Simply-Art Софт 17 05.07.2007 04:10