Описание основной программы.
К данному этапу мы уже умеем рисовать на экране, проигрывать мелодию и отслеживать нажатие кнопок. Да и основа будущей игры тоже уже кстати готова. Теперь нужно все это собрать и скомпоновать в единый программно-аппаратный продукт.
Вся программа будет представлять собой все тот же конечный автомат. Для этого определим нужные нам состояния (режимы):
-игра
-вывод логотипа
-меню
Как говорится — дешево и сердито.
Если в дальнейшем что-то нужно будет добавить, то такой принцип построения программы легко позволит это сделать.
Итак, после начальной инициализации всех модулей, мы попадаем в режим вывода логотипа, где выводим на экран заставку и ждем нажатия кнопки «SELECT» для перехода к режиму “ИГРА”. В котором, собственно, и начинается погружение в игровой процесс. А из режима “ИГРА” мы можем перейти в режим “МЕНЮ” все той же кнопкой «SELECT». В режиме «МЕНЮ» пользователю предоставлена возможность контроля процесса игры (сохранение/восстановление/сброс).
Вполне достаточно. Все необходимые моменты в организации работы консоли красиво вписываются в эти три режима.
Состояние “ИГРА”.
В этом режиме нам нужно отобразить на экране текущую игровую ситуацию, считать с кнопок команду от игрока, проанализировать возможность такого хода и проверить выполнение условий завершения уровня.
Отображение уровня игры практически не изменилось, только поменялось назначение вывода — не в сом-порт, а на экран.
Кроме упомянутых в прошлой части статьи процедур работы с экраном, добавлены только вывод строки и вывод логотипа.
Дополнительно, для придания солидности, будем отслеживать количество ходов и отображать номер текущего уровня.
Кстати, про отображение ходов… Пришлось хорошо поднапрячься выводя трехзначные значения. Долго рисовал на бумажке алгоритм выделения десяток и сотых частей. Хотя для двухзначных цифр все прошло как то быстро.
Получилось вот так:
для вывода двухзначного числа (номер уровня):
LCD_out_XY(12,6,'L'); LCD_out_XY(13,6,':'); if (curLevel<10) LCD_out_XY(14,6,' '); else LCD_out_XY(14,6,(curLevel/10)+48); LCD_out_XY(15,6,(curLevel%10 +48));
А вот для трехзначного (счетчик ходов):
LCD_out_XY(12,1,31); // символ человечка if (stepCount<100) LCD_out_XY(13,1,'0'); else LCD_out_XY(13,1,(stepCount/100)+48); //а далее работает с остатком от деления на 100 if ((stepCount%100)<10) LCD_out_XY(14,1,'0'); else LCD_out_XY(14,1,((stepCount%100)/10)+48); LCD_out_XY(15,1,((stepCount%100)%10 +48));
Для красоты добавлена озвучка (простенький “биип”)перехода “кладовщика” на новую позицию.
Изображения элементов игровой площадки (спрайты) я нарисовал так:
Ну, не знаю. По-моему, вышло неплохо. Особенно симпатичным, на мой взгляд, получилось изображение кирпичной стены 🙂 Смею заметить, что у бета-тестеров проблем с пониманием “что к чему” не было. Эти спрайтики я разместил в самом начале массива-знакогенератора.
Хранение игровых уровней.
Хранить уровни в виде массива (96 байт - лабиринт и 4 байта для положения "кладовщика". Итого 100 байт на уровень) никакой памяти не хватит.
На элементы которые нужно хранить в массиве (пусто, коробка, стена, место под коробку) достаточно двух бит. Но вполне вероятна ситуация, когда уровень содержит в себе уже установленную коробку на место. И тогда двух бит недостаточно. Три бита хватило бы с лихвой, но получается как то некрасиво, т.к. нет бинарной кратности. Поэтому было решено: “Не будем экономить на спичках”.
Я рассматривал разные способы компрессии, но решил остановиться на самом простом варианте: 1 байт использовать для хранения информации о двух игровых клетках. Итого для хранения лабиринта уровня нам достаточно 48 байт.
Для компрессии уровней применен все тот же могучий Exel. И очень простая формула: значение первой клетки умножаем на 16 и к полученному результату добавляем содержимое второй клетки. Начальные координаты “кладовщика” тоже сжимаем таким же образом. Здесь уже получилась 4-х кратная компрессия :). Вместо четырех байт нужно хранить один.
Да-да... Сгенерированную таким образом строку (48 значений) я копировал из EXEL-я и вручную добавлял запятые. Таким образом я набил (на текущий момент) 30 уровней. Причем старался отбрасывать примитивные и малоинтересные.
Процедура декомпрессии получилась такой:
labirint[count*2-2]=pgm_read_byte (&levels[num_level*49+count])>>4; //старшая часть байта labirint[count*2-1]=pgm_read_byte (&levels[num_level*49+count])&0xF; //младшая часть байта
Хранение массивов данных.
Во всех платах Arduino существуют обычно три типа памяти:
1. Флэш-память, которая используется для хранения программ или скетчей;
2. Оперативная память или ОЗУ. Она необходима для выполнения различных операций и для временного хранения данных. Как правило, ее размер ОЧЕНЬ маленький: для моего Arduino nano - всего 512 байт (!!БАЙТ!!);
3. Энергонезависимая память (EEPROM). Она используется для постоянного хранения данных, даже при выключенном питании.
При написании программ для микроконтроллеров очень ВАЖНО рационально использовать и правильно выбирать место хранения объявленных данных. Не правильное написание программы может привести к тому, что вся оперативная память (ОЗУ) может быть израсходована, и это приведет к "зависанию".
По умолчанию все объявленные переменные используют оперативную память. При объявлении больших массивов данных оперативной памяти может не хватить. К примеру следующий код объявляет массив знакогенератора на 11 символов:
// массив символов byte my_char[]={ 62,81,73,69,62,0,0,0, //0 0,66,127,64,0,0,0,0, //1 66,97,81,73,70,0,0,0, //2 33,65,69,75,49,0,0,0, //3 24,20,18,127,16,0,0,0, //4 39,69,69,69,57,0,0,0, //5 60,74,73,73,48,0,0,0, //6 1,113,9,5,3,0,0,0, //7 54,73,73,73,54,0,0,0, //8 6,73,73,41,30,0,0,0, //9 0,0,0,0,0,0,0,0 //null };
Каждый символ занимает один байт в оперативной памяти. А это 88 байт драгоценного ОЗУ. Для того чтобы избежать переполнения, будем использовать для хранения больших массивов память программ или флэш. Для этого при объявлении данных используется ключ PROGMEM.
Во Flash-память я разместил все крупные массивы (данные из которых мы используем только для чтения), а это: знакогенератор, логотипы, набор игровых уровней, частоты звучания нот.
Как это реально сделать? Ниже маленький пример:
byte PROGMEM BitMap [9] = { 9, 8, 7, 6, 5, 4, 3, 2, 1 }; // ^^^^^^^ //указываем на хранение массива во флэш void setup() { Serial.begin(9600); } void loop() { for (int i = 0; i < 9; i++) { Serial.print(BitMap[i]); Serial.print(" "); Serial.print(pgm_read_byte (&BitMap[i])); // ^^^^^^^^^^^^^^^^^^^^^^^^^ читаем из флэш Serial.println (); } delay(1000); }
Следующие состояние - Логотип.
Логотип имеет размер 40х32 пикселя или 5х4 знакоместа.
Принцип точно такой же как и в случае вывода одного символа, меняется только размер и смещение в массиве знакогенератора.
С оцифровкой логотипа пришлось здорово повозиться. Я распечатал рисунки с клеточками и вручную (правда под пивко на даче) заполнил массив цифрами.
Я нарисовал только три логотипа: стартовая заставка, Надпись Ok (ничего умнее не пришло в голову) и разряженная батарейка. Выход из режима отображения логотипа - нажатие кнопки Select.
В ходе бета-тестирования в офисе, оказалось, что фоновая музыка во время игры сильно раздражает. Поэтому эта фича работает только в режиме вывода логотипа. Этот кусок кода остался практически без изменений, единственное, что для генерации звука я отказался от штатной функции tone(), а использовал альтернативную версию NewTone(). Эта библиотека полностью совместима со штатной и по синтаксису и по работе, но занимает меньше места после компиляции. После замены я разницы в воспроизведении не заметил. В архиве эта библиотека присутствует. Мелодия осталась только одна, на все случаи жизни. Кусочек из саундтрека к фильму «Миссия невыполнима» показался мне наиболее подходящим по смыслу.
Ах, да. В режим вывода логотипа мы будем попадать и в случае если напряжение на АКБ станет меньше чем 3 Вольта. Именно при таком напряжении перестает работать Step-Up преобразователь (честно говоря, он продолжал работать и при напряжении на АКБ 2.7 вольта, но для перестраховки оставим 3 вольта).
Каюсь, в прошлой части статьи я совсем забыл рассказать о назначении на схеме этого узла:
А это ничто иное, как контроль уровня заряда АКБ. Точнее напряжения на АКБ.
Для начала я нашел коэффициент пересчета со значений получаемых с АЦП в реальные Вольты.
Я просто делал замеры мультиметром на входе АЦП и сопоставлял их с "оцифрованными" значениями:
4V - 850 (4/850=0.0047)
3V - 640 (3/640=0.0046)
2V - 425 (2/425=0.0047)
Отлично, вполне все ровно и хорошо. Однако этого недостаточно... Еще нужно учесть влияние делителя R4-R5. Напряжение на точке их соединения будет в (100+47)/47=3.2 раза меньше реального на АКБ. Таким образом, для получения реального значения напряжения на АКБ в вольтах, нужно значение с АЦП (в попугаях) умножить на (0.0047*3.2=)0.015.
Если напряжение на АКБ станет меньше порогового значения можно будет наблюдать такое предупреждение на экране:
И все, только предупреждающая надпись. На совести игрока принятие решения: или подключить зарядное или сохранить текущее состояние. Это сообщение-напоминание будет появляться на экране каждую минуту.
Организация меню.
Меню - простое до безобразия. Никаких тебе вложений, древовидных структур, настроек и т.п. Я выделил следующие ВАЖНЫЕ пункты меню, которые есть смысл реализовать для данной игры:
PLAY - возврат в игру;
SAVE - сохранение текущего состояния игры: номер уровня, количество ходов, расстановка элементов на игровом поле;
LOAD - восстановление сохраненного состояния игры;
RESET - простой сброс на начало. Уровень = 0.
Заводим переменную, указывающую на номер пункта меню. Далее выводим список названий пунктов меню, и рисуем в соответствующей строке стрелочку указатель текущего пункта. Нажатие кнопок “ВВЕРХ” и “ВНИЗ” перемещают указатель. По нажатию кнопки "SELECT" происходит активация соответствующего пункта.
Процедуры восстановления/сохранения.
Просто-напросто сохраняем или читаем в EEPROM текущую игровую ситуацию: рабочий массив, номер уровня, координаты человечка и количество ходов.
Маленькое пасхальное яйцо.
От тестирования и отладки остался принудительный переход на следующий уровень (долгое нажатие кнопки "ВВЕРХ"). При этом, выводится стартовый логотип и соответствующая надпись. Это сделано было для того, чтобы не жульничали типа: “Смотри, я прошел этот уровень!”. И я решил, что это неплохо и оставил эту возможность в релизе.
Управление игрой.
Даже не знаю, что тут может быть не понятно 🙂 Кнопка “ВВЕРХ” - смещение (если возможно) человечка вверх, соответственно, “ВНИЗ” - вниз и т.д.
Красная кнопка (меню или "SELECT") что-то типа компьютерного ENETR-а. По ее короткому нажатию в режиме игра происходит рестарт текущего уровня. Если нажать и подержать кнопку “SELECT”, то спустя секунду попадаем в режим “МЕНЮ”. В режиме МЕНЮ нажатием этой кнопки выполняется выбор нужного пункта.
Дети (Владислав и его младшая сестра) практически мгновенно разобрались в управлении. Так что я решил, что система управления и навигации интуитивно понятна.
Кстати, детвора наотрез отказывалась верить, что это самоделка, а решили, что я где-то купил этот “Раритетный PSP”. Только наличие в игре нарисованных Владиславом уровней и наглядная компиляция из исходного кода (в котором присутствуют целые блоки от СОМ-портовой версии) смогли переубедить их в этом.
Интересное наблюдение.
Первые компьютерные игры были восьмибитные. Далее компьютеры и игровые приставки совершенствовались и все совершеннее становились игры. Восьмибитная графика стала у геймеров считаться «отстоем». Прозрачность воды, детализация каждой травинки, которую качает ветер, все это стало неотъемлемой частью игровых продуктов выходящих в свет. Никто уже не хотел играть в 8 битные игры. Но все в этом мире развивается по спирали. Потихоньку стали входит в нашу жизнь телефоны, PDA, а далее и смартфоны.
И “О, Чудо!”, эти устройства стали потихоньку наполняться теми же восьмибитными играми ( с тем же уровнем графики, музыки и геймплея). И почему-то снова они (игры) стали интересными и все с удовольствием в них играют.
Эта консоль около месяца пролежала у меня на работе. Есть даже пресловутый шестой уровень (позже к нему добавился еще и 11-ый), который действительно сложен. С ним целая история. Одно время ни у кого не получалось его пройти. Даже засомневались в его проходимости. Витала в воздухе идея сделать в офисе некий фонд, и каждому участнику, сделавшему взнос, отводилось бы определенное время для прохождения этого “непроходимого”уровня. Победителю банк, а если никто не пройдет - купим для всех торт и сделаем праздник. Но после того как один мой коллега сумел его пройти, эта идея сошла на нет. И что самое интересное, если бы я предложил пройти этот (или любой другой уровень), но в виде приложения на мобильном телефоне, я на 100% уверен, что это было бы никому не интересно.
Дело сделано, и полученный результат превзошел все мои ожидания. И судя по реакции и отзывам моих бета-тестеров, они солидарны со мной. В ходе написания этой статьи, я часто ловил себя на мысли, что тот или иной узел можно было бы сделать не так, что-то можно было бы улучшить, что-то заменить. Ясное дело, полученное устройство нельзя ставить в один ряд с брендовыми консолями типа PSP, NDS и даже GameBoy. Но у этого “Sokoban”-а, тем не менее, есть некий свой чарующий шарм. Поэтому пусть все остается так как есть. Как успокоил меня один мой приятель: “Не парься, главное — все работает. Тебя же абсолютно не интересует схема или код, которые управляют твоим телевизором или мультиваркой"
Все материалы: схемы, печатные платы, фото, скетчи и т.п. забираем одним архивом тут.
Можно конечно петь всякие лестные дифирамбы, но народная мудрость гласит: "Лучше один раз увидеть - чем сто раз услышать". Последуем этой поговорке. Здесь можно посмотреть небольшое видео.
Маленькая "ПИЧАЛЬКА".
Несмотря на всю "клевость" получившейся игровой консоли, не могу сказать, что основная цель этого проекта- заинтересовать подростка написанием игр, на 100% достигнута. Да, результат есть: он заинтересовался, узнал много нового и полезного, вполне сносно разобрался с принципом внутреннего устройства игровых программ, стал представлять себе что такое алгоритмы... Но, этой небольшой экскурсии оказалось недостаточно чтобы полностью переманить его под знамена разработчиков игр.
P.S. На непосредственное изготовление консоли у меня ушло чуть более 2-x месяцев (от идеи до финального релиза), плюс написание статьи - целый месяц. Приличный срок, но я ни сколько не жалею о потраченном времени на изготовление этой приставки 🙂
Разумеется, не возможно оценить время потраченное на создание этой игровой консоли, равно как и душу в нее вложенную, поэтому придется ограничиться небольшой калькуляцией деталек и модулей. В моем случае, практически все элементы были извлечены из закромов (кроме корпуса и накладок на кнопки), но на тот случай если описание вдохновит кого-то пройти по моим стопам (а возможно доработать и улучшить то, что получилось), я приведу примерные цены в городе-герое Минске:
0 комментариев на «“По следам черного самурая, или делаем игру «Sokoban» своими руками (Часть 3 из 3)”»
Супер! Действительно — получилось очень здорово!
Спасибо. Очень приятно что вы оценили. А это вы добавили видео? А то у меня не получалось 🙁
Разумеется 🙂
Прикольная получилась игрулька. Маленькая, но много чего в себя берет (по разработкам).