CraftDuino v2.0
  • - это CraftDuino - наш вариант полностью Arduino-совместимой платы.
  • CraftDuino - настоящий конструктор, для очень быстрого прототипирования и реализации идей.
  • Любая возможность автоматизировать что-то с лёгкостью реализуется с CraftDuino!
Просто добавьте CraftDuino!

По следам черного самурая, или делаем игру "Sokoban" своими руками (Часть 3 из 3)


Описание основной программы.
К данному этапу мы уже умеем рисовать на экране, проигрывать мелодию и отслеживать нажатие кнопок. Да и основа будущей игры тоже уже кстати готова. Теперь нужно все это собрать и скомпоновать в единый программно-аппаратный продукт.
Вся программа будет представлять собой все тот же конечный автомат. Для этого определим нужные нам состояния (режимы):
-игра
-вывод логотипа
-меню

Как говорится — дешево и сердито.
Если в дальнейшем что-то нужно будет добавить, то такой принцип построения программы легко позволит это сделать.
Итак, после начальной инициализации всех модулей, мы попадаем в режим вывода логотипа, где выводим на экран заставку и ждем нажатия кнопки «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 месяцев (от идеи до финального релиза), плюс написание статьи — целый месяц. Приличный срок, но я ни сколько не жалею о потраченном времени на изготовление этой приставки :)
Разумеется, не возможно оценить время потраченное на создание этой игровой консоли, равно как и душу в нее вложенную, поэтому придется ограничиться небольшой калькуляцией деталек и модулей. В моем случае, практически все элементы были извлечены из закромов (кроме корпуса и накладок на кнопки), но на тот случай если описание вдохновит кого-то пройти по моим стопам (а возможно доработать и улучшить то, что получилось), я приведу примерные цены в городе-герое Минске:

Комментарии (4)

RSS свернуть / развернуть
+
0
Супер! Действительно — получилось очень здорово!
avatar

admin

  • 22 мая 2015, 16:30
+
+1
Спасибо. Очень приятно что вы оценили. А это вы добавили видео? А то у меня не получалось :(
avatar

Ghost_D

  • 22 мая 2015, 19:45
+
0
Разумеется :)
avatar

admin

  • 25 мая 2015, 09:29
+
0
Прикольная получилась игрулька. Маленькая, но много чего в себя берет (по разработкам).
avatar

ai_first

  • 2 июня 2015, 12:59

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.