После создания игры Теннис и Теннис для двоих я долго раздумывал, можно ли сделать на плате Марсоход еще что нибудь эдакое. Проблем здесь на самом деле хватает и самая главная - это ограниченные ресурсы нашей микросхемы ПЛИС. Для создания более или менее сложной игры хорошо бы иметь память. Тот же Тетрис не сделать без памяти. Если стакан для Тетриса будет шириной 10 кубиков, а высота 20 кубиков, то вот уже нужно 200 бит только для хранения текущего состояния игры.
Поразмыслив, я решил оставить Тетрис "на потом", а пока попробовать сделать что нибудь попроще, например, игру "Питон". Дело оказалось очень непростое. И вот, потратив несколько вечеров, я сделал вот что:
Далее расскажу, как я это делал.
Ну собственно аппаратная часть у меня осталась без изменений, такая, как и раньше. Сигналы видео R, G и B выходят на разъем платы Марсоход с контактами F3, F4, F5 соответственно. Сигналы синхронизации выходят на контакты F1 (HSYNC) и F0 (VSYNC). Все эти сигналы (и еще "Земля") идут на разъем VGA. Ну еще хорошо бы подать сигналы через резисторы. Некоторым мониторам не нравится слишком высокий уровень сигнала.
Теперь о самой игре. Конечно нужно было придумать как хранить состояние змейки в ПЛИС. Поскольку змейка после нескольких поворотов выглядит как ломаная линия я решил, что самый правильный способ - хранить информацию о каждом сегменте в отдельности. Для представления каждого сегмента нужно знать X и Y координаты одного конца сегмента, длину сегмента, его ориентацию и направление движения. Если считать, что поле движения - это квадрат 16х16, то для представления координат нужно два 4-х разрядных регистра, для представления длины - еще один 4-х разрядный регистр, ну и для запоминания ориентации сегмента и направления движения еще по одному регистру. Итого: 4+4+4+1+1 = 14. Четырнадцать триггеров для хранения одного сегмента. В моей программе на языке Verilog это выглядит так:
reg [3:0]seg0_x;
reg [3:0]seg0_y;
reg [3:0]seg0_len;
reg seg0_vert; //0-horz, 1-vert
reg seg0_mov; //0-left/up, 1-right/down
Я решил, что для начала мне хватит и трех сегментов. Ну вот такое ограничение. Змейка не может делать больше 2-х поворотов.
Представление змейки в ввиде сегментов кажется довольно удобным, поскольку при очередном повороте второй сегмент становится третьим, первый становится вторым, а сам первый становится новым и очень коротким сегментом.
Дальше во время движения длина первого сегмента увеличивается, а длина последнего (у которого длина не нулевая) уменьшается.
Все вот так и кажется просто, но при реализации задуманного на самом деле я столкнулся с массой проблем.
В проекте нужно описать поведение каждого регистра каждого сегмента в отдельности и для этого нужно четко представлять себе что же происходит с сегментом при том или ином событии.
Вот, например, рассмотрим движение короткой змейки гризонтально и потом ее поворот:
Змейка двигалась вправо и должна повернуть на право. У первого сегмента были координаты [1,2] и была длина 5 (фиолетовым цветом на рисунке слева), а стали [5,2] и длина 1 (желтым цветом на рисунке справа). Кроме того, появился второй сегмент, которого раньше не было. Его координаты [1,2] и длина 4.
При движении в другую сторону. Поведение первого сегмента будет иным:
Теперь змейка движется влево и при повороте, координаты первого сегмента остаются предыдущими, но только меняется длина.
Поведение только координаты X для первого сегмента змейки я определил на языке Verilog вот так:
//define behave of snake segment0 x-coord
always @(posedge clk_slow or posedge game_reset)
begin
if(game_reset)
seg0_x <= 1; //on game reset snake starts from fixed place
else
begin
//X coord can change only if horizontal segment
if(!seg0_vert)
begin
if(turn)
seg0_x <= seg0_x + ((seg0_len - 1) & {4{seg0_mov}});
else
//seg0_x may be increased or decreased by 1 or stay unchanged
seg0_x <= seg0_x + {!seg0_mov,!seg0_mov,!seg0_mov,( (seg0_mov ^ tail0) & (~eat) ) | (!seg0_mov)};
end
end
end
Довольно мудрено, правда? Ведь поведение регистра как оказалось зависит от ориентации сегмента, направления движения и факта съедания кролика. Теперь представьте, что мне нужно описать одновременное поведение всех остальных регистров всех сегментов на все случаи жизни... В общем оказалось не очень просто - ведь они меняют состояние одновременно по событию шаг или событию поворот.
Следующая проблема - останов игры, когда змейка врезается в стенку. Первое, что приходит на ум - сравнение координат головы змейки и координат бордюра. На самом деле этот метод для меня оказался не очень хорош, так как в этом случае пришлось бы устанавливать в проект несколько сравнивателей, а это довольно много занимает места. Не подходит.
Решение пришло следующее. Я рисую бордюр зеленым цветом, а змейка у меня фиолетовая (красный на синем фоне). Если змейка наползет на бордюр, то в этом месте экрана появится желтый цвет - это может служить признаком останова игры. Вот так и было сделано:
//when snake head comes over border then color is mixed on screen and we should stop
reg stop;
always @(posedge char_clock or posedge game_reset)
if(game_reset)
stop = 0;
else
if(video_r & video_g)
stop = 1;
Примерно такой же механизм был использован и для определения факта съедания кролика. Кролик у меня один, он на экране черным цветом. Когда змейка наползает на него, то в этом месте экрана получается красный цвет (видимо кровища). Когда одновременно луч видеосигнала на экране рисует и первый сегмент (seg0_visible==1) и рисует кролика (rabbit_visible), то запоминаем факт съедания:
//calculate when rabbit was eaten
//if during screen refresh we meet simultaneous snake segment visible and rabbit over
reg eat;
always @(posedge char_clock or posedge game_reset)
if(game_reset)
eat <= 0;
else
if(seg0_visible & rabbit_visible)
eat <= 1;
else
if(clk_slow)
eat <= 0;
Конечно в программе у меня много чего там. Я только обозначаю основные ключевые моменты.
Вот то же проблема. Сначала я хотел сделать появление кролика в случайных местах. Ну там можно было бы сделать какой-то псевдо-случайный выбор места нового кролика. К сожалению места в микросхеме не хватило на все это. В результате кролик имеет у меня всего два положения и описывается одним битом. Ну не страшно. Мне главное идею продемонстрировать. В результате и так у меня проект в чипе ПЛИС занял 239 логических элемента из 240!
Как обычно, проект целиком можно взять на нашем сайте:
Пробуйте!
PS: Возможно вы обнаружите несколько глюков. Так, например, после съедания кролика не нужно тут же поворачивать, а то змейку стошнит и ее длина не увеличится. Это из за того, что сигнал turn имеет больший приоритет.
Еще проблема - если держать кнопку поворота чуть дольше, чем нужно змейка начинает ползти поверх себя и игра ломается. Может вы придумаете что-нибудь..
Подробнее...