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


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

    • Спасибо. Очень приятно что вы оценили. А это вы добавили видео? А то у меня не получалось 🙁

Добавить комментарий

Arduino

Что такое Arduino?
Зачем мне Arduino?
Начало работы с Arduino
Для начинающих ардуинщиков
Радиодетали (точка входа для начинающих ардуинщиков)
Первые шаги с Arduino

Разделы

  1. Преимуществ нет, за исключением читабельности: тип bool обычно имеет размер 1 байт, как и uint8_t. Думаю, компилятор в обоих случаях…

  2. Добрый день! Я недавно начал изучать программирование под STM32 и ваши уроки просто бесценны! Хотел узнать зачем использовать переменную типа…

3D-печать AI Arduino Bluetooth CraftDuino DIY Google IDE iRobot Kinect LEGO OpenCV Open Source Python Raspberry Pi RoboCraft ROS swarm ИК автоматизация андроид балансировать бионика версия видео военный датчик дрон интерфейс камера кибервесна манипулятор машинное обучение наше нейронная сеть подводный пылесос работа распознавание робот робототехника светодиод сервомашинка собака управление ходить шаг за шагом шаговый двигатель шилд юмор

OpenCV
Робототехника
Будущее за бионическими роботами?
Нейронная сеть - введение