Хочу поделиться своей историей знакомства с игровой платформой Gameduino 3, а также немного расскажу о программировании простейшей логической игры под данную платформу, используемой совместно с Arduino Uno.
Что же такое Gameduino 3? Gameduino 3 — это плата расширения, которая позволяет превратить Arduino в современную карманную (имеется в виду размер) игровую консоль. К моему удивлению, мне не удалось найти на хабре какой-либо подробной информации о данной плате. Хотел бы восполнить этот пробел, тем более что плата, на мой взгляд, заслуживает внимания.
Немного истории
Автором проекта под названием Gameduino является Джеймс Боуман (James Bowman), который в 2011 году создал первую версию платы. Тогда она позиционировалась как модуль VGA для Arduino. Плата называлась Gameduino и была выполнена на основе программируемой логической матрицы FPGA компании Xilinx семейства Spartan-3A. На плате были установлены разъемы для подключения VGA монитора и стерео акустики.
- VGA видео выход с разрешением 400х300 точек, 512 цветов;
- вся цветовая гамма обрабатывается в FPGA с 15-разрядной точностью;
фоновая графика:
- область символьной фоновой графики 512х512 точек;
- 256 символов, каждый с независимой 4-цветной палитрой;
- реализован эффект заворачивания текстовых строк с пиксельным сглаживанием;
графика переднего плана:
- каждый спрайт имеет разрешение 16х16 точек;
- каждый спрайт может иметь 256-, 16- или 4-цветную палитру;
- поддержка алгоритмов 4-стороннего вращения и горизонтального вращения;
- 96 спрайтов на растровую строку, 1536 элементов текстуры на растровую строку;
- механизм обнаружения возможных пересечений спрайтов;
аудио выход:
- 12-разрядный двухканальный синтезатор частоты;
- 64-голосая полифония в частотном диапазоне 10 — 8000 Гц.
Изображение выводится на экран стандартного VGA монитора с разрешением 400х300 точек,
сохраняется совместимость с любыми стандартными VGA мониторами с разрешением 800х600 точек.
В 2013 году была выпущена вторая версия платы — Gameduino 2, в которой, в отличие от предыдущей версии, уже имелся 4,3 дюймовый резистивный сенсорный дисплей с разрешением 480х272, 3-осевой акселерометр, слот карты памяти microSD, аудио выход на наушники.
«Сердцем» платы стал графический контроллер EVE (Embedded Video Engine — по-русски можно перевести как «встраиваемый видеомодуль») FT800, который обладает мощными вычислительными возможностями, совмещая в себе одновременно несколько функций: формирование изображения и вывод его на экран TFT-дисплея, обработка сенсорного экрана, генерирование звука.
В структуру микросхемы включены следующие функциональные блоки: графический контроллер, контроллер аудио, контроллер резистивной сенсорной панели. Микросхема FT800 предназначена для управления дисплеями с разрешением до 512 х 512 пикселей. FT800 также поддерживает LCD WQVGA (480 x 272) и QVGA (320 x 240). EVE (Embedded Video Engine) FT800 – это готовое решения для создания графического пользовательского интерфейса. Микросхема формирует сигналы управления дисплеем, имеет встроенные графические функции для отображения точек, линий, растровых картинок, объемных кнопок, текстов и т. д.
Например, для вывода ряда кнопок достаточно передать в графический контроллер одну команду (четыре 32-х разрядных слова), и FT800 самостоятельно сформирует изображение этих кнопок на экране TFT-дисплея. Набор команд графических контроллеров FTDI включает в себя более 50 функций, с помощью которых можно выводить различные изображения на экран дисплея с теми или иными эффектами.
Подробное руководство по программированию контроллера и примеры работы с различными средами проектирования можно найти в Application Notes на сайте FTDI.
На русском языке хорошее описание функциональных возможностей, общих принципов и примеров работы есть тут.
- разрешение экрана 480×272 пикселей в 24-битном цвете;
- набор команд в стиле OpenGL;
- до 2000 спрайтов любого размера;
- 256 Кбайт видеопамяти;
- плавный поворот sprite и масштабирование с билинейной фильтрацией;
- гладкий круг и линейный рисунок в аппаратном обеспечении — 16x сглаживание;
- аппаратное декодирование JPEG;
- встроенный рендеринг градиентов, текста, циферблатов и кнопок.
Выход звука осуществляется через усиленный разъем для наушников.
Система поддерживает выбор встроенных образцов и инструментов.
В ПЗУ контроллера уже зашиты:
- высококачественные шрифты (6 размеров);
- образцы из 8 музыкальных инструментов, воспроизводимые нотой MIDI;
- образцы из 10 ударных звуков.
И, конечно же, вы можете загрузить свои собственные шрифты и звуковые фрагменты в ОЗУ 256 Кбайт.
Использование платформы Arduino не является обязательным условием: плату Gameduino 2 можно подключать к любому микроконтроллеру или микроконтроллерной плате с интерфейсом SPI.
В 2017 году была выпущена третья версия платы – Gameduino 3, которая внешне практически не отличается от Gameduino 2. Вместо FT800 используется новый графический контроллер FT810, который имеет обратную программную совместимость с FT800 (т.е. весь код для Gameduino2 работает на Gameduino3), но при этом имеет в 4 раза большие вычислительные возможности, такие, как более быстрое аппаратное декодирование JPEG, декодирование видео, увеличенная до 1 Мбайт оперативная память и др.
- видео декодер для полноэкранного 30 fps видео;
- 1 мегабайт внутреннего ОЗУ;
- разъемы для подключения microSD-карты и аудиовыход;
- высококонтрастная ЖК-панель с диагональю 4,3 «480×272 с резистивным сенсорным экраном;
- поддержка карт, созданных с помощью редактора Tiled Map;
- загрузка изображения PNG с microSD;
- ускоренное декодирование JPEG;
- аппаратное переключение портрета / альбомной ориентации;
- поддержка плат Arduino, ESP8266 и Teensy 3.2;
- онлайн-инструменты для подготовки графики, аудио, шрифта и видео;
Джеймс Боуман опубликовал для своего проекта библиотеку с множеством примеров, которые работают „прямо из коробки“. Актуальная библиотека, которую мне удалось найти, находится тут. Руководство по программированию (на английском языке), где подробно все описывается. Много полезной информации по установке IDE, и т.п., и т.д.
Программирование
Как-то, блуждая по просторам Большого театра интернета, я наткнулся на интересный проект для Ардуино — логическая игра „Columns“, написанная под обычный недорогой цветной китайский дисплей 128х160 пикселов. Мне захотелось повторить эту игру, но уже на своей плате, назову её FT810 (по названию графического процессора), которая к тому времени уже была у меня на руках. Руководство по программированию и примеры из библиотеки я тоже уже успел изучить, поэтому руки просто „чесались“ от желания написать что-нибудь свое. К чему я незамедлительно и приступил.
Первое, что мне предстояло сделать — это вывод текста на экран.
Благодаря наличию встроенных шрифтов вывод текста на экран осуществляется достаточно легко. Приведу демонстрационный скетч из библиотеки (с моими комментариями):
#include <EEPROM.h> #include <SPI.h> #include <GD2.h> void setup() { Serial.begin(1000000); // Инициализация отладочного порта, установка скорости передачи 1000000 бод GD.begin(0); // Инициализация процессора, периферии и пр. } void loop() { GD.ClearColorRGB(0x103000); // Установка цвета фона экрана. GD.Clear(); // Очистка экрана (заполнение заданным цветом фона) GD.cmd_text( // печать текста, где GD.w / 2, // ширина экрана деленная на 2 GD.h / 2, // высота экрана деленная на 2 31, // номер шрифта OPT_CENTER, // опция, обозначающая отрисовку надписи с центром в указанной выше координате. "Hello world"); // текст надписи GD.swap(); // команда на копирование данных из видеобуфера на экран (аппаратная функция графического процессора). }
В итоге на экране получим вот такой красивый текст:
Далее необходимо было нарисовать геометрические фигуры, например: линии.
Чтобы нарисовать линии, необходимо использовать Begin(LINES) или Begin(LINE_STRIP).
LINES соединяет каждую пару вершин, тогда как LINE_STRIP объединяет все вершины вместе.
Приведу следующий демоскетч из библиотеки (с моими комментариями):
#include <EEPROM.h> #include <SPI.h> #include <GD2.h> void setup() { Serial.begin(1000000); // Инициализация отладочного порта, установка скорости передачи 1000000 бод. GD.begin(); // Инициализация процессора, периферии и пр. } static void zigzag(int x) { GD.Vertex2ii(x - 10, 10); // Установка координаты начальной точки GD.Vertex2ii(x + 10, 60); // Установка координаты следующей точки (вершины) GD.Vertex2ii(x - 10, 110); GD.Vertex2ii(x + 10, 160); GD.Vertex2ii(x - 10, 210); GD.Vertex2ii(x + 10, 260); // Установка координаты конечной точки (вершины) } void loop() { GD.Clear(); // Очистка фона (по умолчанию цвет черный - 0х000000) GD.Begin(LINES); // Установка режима прерывистой линии zigzag(140); // Вызов подпрограммы zigzag с аргументом - координата х GD.Begin(LINE_STRIP); // Установка режима непрерывной линии zigzag(240); GD.LineWidth(16 * 10); // Установка ширины отрисовки линии в единицах измерения 1/16 пиксела, т.е. 1/16 * 16 * 10 = 10 пикселов GD.Begin(LINE_STRIP); zigzag(340); GD.swap(); // Команда на копирование данных из видеобуфера на экран (аппаратная функция графического процессора). }
Линии на экране:
От рисования линий перейдем к рисованию прямоугольников.
Чтобы нарисовать прямоугольники, используем Begin (RECTS) и устанавливаем противоположные углы прямоугольника. Порядок двух углов не имеет значения. Прямоугольники рисуются с закругленными углами, используя ширину текущей линии в качестве радиуса угла. Закругленные углы выходят за границы прямоугольника, поэтому увеличение радиуса угла приводит к увеличению количества пикселей. Этот пример рисует прямоугольник 420 × 20 три раза с увеличением радиуса скругления угла.
#include <EEPROM.h> #include <SPI.h> #include <GD2.h> void setup() { Serial.begin(1000000); // Инициализация отладочного порта, установка скорости передачи 1000000 бод. GD.begin(); // Инициализация процессора, периферии и пр. } void loop() { GD.Clear(); // Очистка фона (по умолчанию цвет черный - 0х000000) GD.Begin(RECTS); // Установка режима рисования прямоугольника GD.Vertex2ii(30, 30); // Установка координаты левого верхнего угла прямоугольника GD.Vertex2ii(450, 50); // Установка координаты правого нижнего угла прямоугольника GD.LineWidth(16 * 10); // Установка радиуса скругления угла прямоугольника в единицах измерения 1/16 пиксела, т.е. 1/16 * 16 * 10 = 10 пикселов GD.Vertex2ii(30, 120); // Установка координаты левого верхнего угла прямоугольника GD.Vertex2ii(450, 140); // Установка координаты правого нижнего угла прямоугольника GD.LineWidth(16 * 20); // Установка радиуса скругления угла прямоугольника в единицах измерения 1/16 пиксела, т.е. 1/16 * 16 * 20 = 20 пикселов GD.Vertex2ii(30, 220); // Установка координаты левого верхнего угла прямоугольника GD.Vertex2ii(450, 230); // Установка координаты правого нижнего угла прямоугольника GD.swap(); // Команда на копирование данных из видеобуфера на экран (аппаратная функция графического процессора) }
Результат:
Перейдем к рисованию круга – основе будущих сенсорных кнопок. Вернемся к первому примеру с текстом и добавим в loop() несколько строк.
#include <EEPROM.h> #include <SPI.h> #include <GD2.h> void setup() { Serial.begin(1000000); // Инициализация отладочного порта, установка скорости передачи 1000000 бод GD.begin(0); // Инициализация процессора, периферии и пр. } void loop() { GD.ClearColorRGB(0x103000); // Установка цвета фона экрана. GD.Clear(); // Очистка экрана (заполнение заданным цветом фона) GD.cmd_text( // печать текста, где GD.w / 2, // ширина экрана деленная на 2 GD.h / 2, // высота экрана деленная на 2 31, // номер шрифта OPT_CENTER, // опция, обозначающая отрисовку надписи с центром в указанной выше координате "Hello world"); // текст надписи GD.PointSize(16 * 30); // установка радиуса точки (круга) в единицах измерения 1/16 пиксела, т.е. 1/16 * 16 * 30 = 30 пикселов GD.Begin(POINTS); // устанавливаем режим отрисовки точки (круга) GD.ColorRGB(0xff8000); // устанавливаем цвет orange GD.Vertex2ii(220, 100); // отрисовываем точку (круг) в координате 220,100 GD.ColorRGB(0x0080ff); // устанавливаем цвет teal GD.Vertex2ii(260, 170); // отрисовываем точку (круг) в координате 260,170 GD.swap(); // Команда на копирование данных из видеобуфера на экран (аппаратная функция графического процессора) }
Результат:
Для работы с сенсорными кнопками необходимо организовать обработку нажатий сенсорного экрана. Делается это следующим образом. В кратце объясню принцип. Каждый пиксель (точка) на экране имеет цвет. Он также имеет невидимое значение тега, которое может присваиваться точке (или целому объекту типа линия, круг, прямоугольник и т.д) и в дальнейшем использоваться для обнаружения касаний данного объекта. В следующем скетче приведен пример установки значения тега для цветных кругов на значение 100 и 101.
#include <EEPROM.h> #include <SPI.h> #include <GD2.h> void setup() { Serial.begin(1000000); // Инициализация отладочного порта, установка скорости передачи 1000000 бод GD.begin(0); // Инициализация процессора, периферии и пр. } void loop() { GD.ClearColorRGB(0x103000); // Установка цвета фона экрана. GD.Clear(); // Очистка экрана (заполнение заданным цветом фона) GD.cmd_text( // печать текста, где GD.w / 2, // ширина экрана деленная на 2 GD.h / 2, // высота экрана деленная на 2 31, // номер шрифта OPT_CENTER, // опция, обозначающая отрисовку надписи с центром в указанной выше координате "Hello world"); // текст надписи GD.PointSize(16 * 30); // установка радиуса точки (круга) в единицах измерения 1/16 пиксела, т.е. 1/16 * 16 * 30 = 30 пикселов GD.Begin(POINTS); // устанавливаем режим отрисовки точки (круга) GD.ColorRGB(0xff8000); // устанавливаем цвет orange GD.Tag(100); // устанавливаем значение тега для следующего объекта (круга) GD.Vertex2ii(220, 100); // отрисовываем точку (круг) в координате 220,100 GD.ColorRGB(0x0080ff); // устанавливаем цвет teal GD.Tag(101); // устанавливаем значение тега для следующего объекта (круга) GD.Vertex2ii(260, 170); // отрисовываем точку (круг) в координате 260,170 GD.swap(); // Команда на копирование данных из видеобуфера на экран (аппаратная функция графического процессора) GD.get_inputs(); // производим опрос сенсорного экрана if(GD.inputs.tag > 0) // проверяем условие нажатия сенсорного экрана Serial.println(GD.inputs.tag); // отправляем в последовательный порт значение тега “нажатого” объекта }
Теперь, когда система обнаруживает прикосновение к любому кругу, он сообщает о своем сенсорном коде, в этом случае 100 или 101. При нажатии на объекты (цветные круги) на экране в окне последовательного порта отобразятся значения тегов, соответствующих нажатым объектам:
Я привел примеры основных операций, используя которые уже можно было смело переходить к созданию игры. Конечно игра создавалась не с нуля, а за основу был взят готовый рабочий код, который был адаптирован (с сохранением графики), поэтому первоначальный вариант игры был очень похож на оригинал.
Первый вариант оформления игры:
Поиграв несколько дней, захотелось что-нибудь изменить в оформлении, например добавить вместо белого фона какой-нибудь другой, необычный. И тут я вспомнил один пример из библиотеки, в котором на заднем фоне красовалось звездное небо:
#include <EEPROM.h> #include <SPI.h> #include <GD2.h> #include "slotgag_assets.h" // файл с константами и адресами void setup() { Serial.begin(1000000); GD.begin(); LOAD_ASSETS(); // загрузка изображений в ОЗУ } void loop() { GD.Clear(); // очистка экрана (заполнение заданным, по умолчанию черным цветом фона) GD.ColorMask(1, 1, 1, 0); // установка маски разрешения записи в канал цветности R, G, B, запрет прозрачности GD.Begin(BITMAPS); // установка режима работы с битмап GD.BitmapHandle(BACKGROUND_HANDLE); // установка текущего битмап-дескриптора GD.BitmapSize(NEAREST, REPEAT, REPEAT, 480, 272); // управление отображением текущего растрового изображения на экране GD.Vertex2ii(0, 0, BACKGROUND_HANDLE); // выводим изображение фона с координаты 0,0 GD.ColorMask(1, 1, 1, 1); GD.ColorRGB(0xa0a0a0); GD.Vertex2ii(240 - GAMEDUINO_WIDTH / 2, 136 - GAMEDUINO_HEIGHT / 2, GAMEDUINO_HANDLE); static int x = 0; GD.LineWidth(20 * 16); GD.BlendFunc(DST_ALPHA, ONE); GD.Begin(LINES); GD.Vertex2ii(x, 0); GD.Vertex2ii(x + 100, 272); x = (x + 20) % 480; //' }a GD.swap(); }
Вид:
Чтобы не углубляться в дебри, я не стал комментировать весь код, а прокомментировал только нужные строчки кода, которые скопировал в свой рабочий скетч.
Для добавления картинки звездного неба в качестве заднего фона пришлось сделать следующее: во-первых, изменить черный цвет линий и текста на белый (чтобы они были видны на черном фоне), записать на микро SD карту файл slotgag.gd2, в котором хранится изображение, добавить в папку проекта slotgag_assets.h и добавить в скетч необходимые 8 строчек кода.
В итоге игра приобрела вот такой вид:
И конечно же, какая игра без звукового оформления? Осталось добавить звуковые эффекты, тем более что они представлены в хорошем качестве и разнообразии.
Gameduino 2/3 имеет две звуковые системы. Первый — синтезатор — может генерировать набор фиксированных звуков и музыкальных нот. Синтезатор полезен для быстрого добавления звука в проект, но поскольку набор звуков фиксирован, он не очень гибкий. Второй — воспроизведение образца. Он воспроизводит дискретизированный звук из основной памяти в различных форматах. Эта система намного более гибкая, но вам нужно будет подготовить и загрузить образцы в оперативную память.
Я использовал синтезатор фиксированых звуков. Синтезатор предоставляет несколько коротких «ударных» звуков, в основном для использования в пользовательских интерфейсах. Чтобы воспроизвести звук, необходимо вызвать GD.play () со звуковым идентификатором. Полный список доступных звуков:
CLICK
SWITCH
COWBELL
NOTCH
HIHAT
KICKDRUM
POP
CLACK
CHACK
Итог
В итоге получился такой скетч:
#include <SPI.h> #include <GD2.h> #include <avr/eeprom.h> #include "slotgag_assets.h" #define TAG_BUTTON_LEFT 201 #define TAG_BUTTON_RIGHT 202 #define TAG_BUTTON_ROT 203 #define TAG_BUTTON_DROP 204 #define X_BUTTON_LEFT 50 #define Y_BUTTON_LEFT 222 #define X_BUTTON_RIGHT 430 #define Y_BUTTON_RIGHT 222 #define X_BUTTON_ROT 430 #define Y_BUTTON_ROT 50 #define X_BUTTON_DROP 50 #define Y_BUTTON_DROP 50 // Color definitions #define BLACK 0x000000 #define RED 0xFF0000 #define GREEN 0x00FF00 #define BLUE 0x0000FF #define YELLOW 0xFFFF00 #define MAGENTA 0xFF00FF #define CYAN 0x00FFFF #define WHITE 0xFFFFFF #define DISPLAY_MAX_X 480 #define DISPLAY_MAX_Y 272 #define MaxX 8 #define MaxY 17 #define SmeX 3 #define SmeY 3 #define razmer 18 #define NumCol 6 #define MaxLevel 8 #define NextLevel 80 #define DISP_LEFT ((DISPLAY_MAX_X - MaxX*razmer)/2 - 2) #define DISP_RIGHT ((DISPLAY_MAX_X + MaxX*razmer)/2 + 2) #define DISP_TOP ((DISPLAY_MAX_Y - (MaxY-4)*razmer)/2 - 2) #define DISP_BOT ((DISPLAY_MAX_Y + (MaxY-4)*razmer)/2 + 2) uint8_t MasSt[MaxX][MaxY], MasTmp[MaxX][MaxY], fignext[3]; uint8_t Level=1, dx, dy, tr, flfirst=1; uint32_t MasCol[]={WHITE, BLACK, RED, BLUE, GREEN, YELLOW, MAGENTA, CYAN}; unsigned long Counter, Score=0, TScore=0, Record=0, myrecord; uint16_t tempspeed = 1000; bool fl, Demo=true, Arbeiten=false, FlZ=false; int8_t x,y; int8_t mmm [4][2]={{-1,0},{0,-1},{1,0},{0,1}}; uint16_t MasSpeed[MaxLevel]={500,450,400,350,300,250,200,100}; uint8_t state_game = 0; unsigned long time_count; byte prevkey; uint32_t btn_color = 0xff0000; /****************************************************************************************************************/ void setup(void) { Serial.begin(1000000); Serial.println("Columns"); GD.begin(); LOAD_ASSETS(); GD.BitmapHandle(BACKGROUND_HANDLE); GD.BitmapSize(NEAREST, REPEAT, REPEAT, 480, 272); randomSeed(analogRead(5)); myrecord = eeprom_read_byte((unsigned char *)1); time_count = millis() + 1000; } static struct { byte t, note; } pacman[] = { { 0, 71 }, { 2, 83 }, { 4, 78 }, { 6, 75 }, { 8, 83 }, { 9, 78 }, { 12, 75 }, { 16, 72 }, { 18, 84 }, { 20, 79 }, { 22, 76 }, { 24, 84 }, { 25, 79 }, { 28, 76 }, { 32, 71 }, { 34, 83 }, { 36, 78 }, { 38, 75 }, { 40, 83 }, { 41, 78 }, { 44, 75 }, { 48, 75 }, { 49, 76 }, { 50, 77 }, { 52, 77 }, { 53, 78 }, { 54, 79 }, { 56, 79 }, { 57, 80 }, { 58, 81 }, { 60, 83 }, { 255, 255 } }; //================================================== void loop(void) { GD.get_inputs(); byte key = GD.inputs.tag; int8_t VAL = 0; if (prevkey == 0x00) { switch (key) { case TAG_BUTTON_LEFT: VAL = -1; break; case TAG_BUTTON_RIGHT: VAL = 1; break; case TAG_BUTTON_ROT: if (!FlZ) { GD.play(HIHAT); byte aa=MasSt[x][y]; MasSt[x][y]=MasSt[x][y+2]; MasSt[x][y+2]=MasSt[x][y+1]; MasSt[x][y+1]=aa; } break; case TAG_BUTTON_DROP: if (Arbeiten) { if (!FlZ) { tempspeed=50; GD.play(NOTCH); } } else { GD.play(CLICK); Demo=false; NewGame(); } break; } } prevkey = key; if (VAL!=0 && fig_shift(VAL) && !FlZ) { for (byte i=0;i<3;i++) { MasSt[x+VAL][y+i]=MasSt[x][y+i]; MasSt[x][y+i]=0; } x=x+VAL; } ProcGame(); ViewStacan(); GD.swap(); } //================================================== // redraw one square void ViewQuad(byte i,byte j,byte mycolor) { if (j<3) return; uint16_t wy=DISP_TOP + SmeY+(j-3)*razmer-j; uint16_t wx=DISP_LEFT + SmeX+i*razmer-i; if (mycolor!=0) { GD.LineWidth(16*1); GD.ColorRGB(WHITE); GD.Begin(LINE_STRIP); GD.Vertex2ii(wx,wy); GD.Vertex2ii(wx + razmer-1,wy); GD.Vertex2ii(wx + razmer-1,wy + razmer-1); GD.Vertex2ii(wx,wy + razmer-1); GD.Vertex2ii(wx,wy); GD.Begin(RECTS); GD.ColorRGB(MasCol[mycolor]); GD.Vertex2ii(wx+1, wy+1); GD.Vertex2ii(wx+1 + razmer-2 - 1, wy+1 + razmer-2 - 1); } else { } } //================================================== void ViewStacan(void) { char myStr2[5]; // Draw background fone GD.Clear(); GD.ColorMask(1, 1, 1, 0); GD.Begin(BITMAPS); GD.BitmapHandle(BACKGROUND_HANDLE); GD.BitmapSize(NEAREST, REPEAT, REPEAT, 480, 272); GD.Vertex2ii(0, 0, BACKGROUND_HANDLE); // Print text GD.ColorRGB(WHITE); GD.cmd_text(DISP_LEFT - 30, DISP_TOP + 3, 27, OPT_CENTER, "LEVEL"); GD.cmd_text(DISP_RIGHT + 30, DISP_TOP + 3, 27, OPT_CENTER, "NEXT"); GD.cmd_text(DISP_RIGHT + 30, DISP_TOP + 100, 27, OPT_CENTER, "SCORE"); GD.cmd_text(DISP_LEFT - 30, DISP_TOP + 100, 27, OPT_CENTER, "TOP"); // Print digit Score GD.ColorRGB(RED); sprintf(myStr2,"%05d",Score ); GD.cmd_text(DISP_RIGHT + 30, DISP_TOP + 130, 27, OPT_CENTER, myStr2); // Print digit Top sprintf(myStr2,"%05d",myrecord ); GD.cmd_text(DISP_LEFT - 30, DISP_TOP + 130, 27, OPT_CENTER, myStr2); // Print digit Level sprintf(myStr2,"%02d",Level ); GD.cmd_text(DISP_LEFT - 30, DISP_TOP + 40, 31, OPT_CENTER, myStr2); // Draw color squares for (byte j=3;j<MaxY;j++) for (byte i=0;i<MaxX;i++) ViewQuad(i,j,MasSt[i][j]); // Draw Next Figure for (byte i=0;i<3;i++) { GD.ColorRGB(WHITE); GD.Begin(LINE_STRIP); GD.LineWidth(16*1); GD.Vertex2ii(DISP_RIGHT + 15, DISP_TOP + 20 + razmer*i-i); GD.Vertex2ii(DISP_RIGHT + 15 + razmer-1, DISP_TOP + 20 + razmer*i-i); GD.Vertex2ii(DISP_RIGHT + 15 + razmer-1, DISP_TOP + 20 + razmer*i-i + razmer-1); GD.Vertex2ii(DISP_RIGHT + 15, DISP_TOP + 20 + razmer*i-i + razmer-1); GD.Vertex2ii(DISP_RIGHT + 15, DISP_TOP + 20 + razmer*i-i); GD.Begin(RECTS); GD.ColorRGB(MasCol[fignext[i]]); GD.Vertex2ii(DISP_RIGHT+15+1, DISP_TOP+20+razmer*i-i+1); GD.Vertex2ii(DISP_RIGHT+15+1+razmer-2-1, DISP_TOP+20+razmer*i-i+1+razmer-2-1); } // Draw "stacan" GD.ColorRGB(WHITE); GD.Begin(LINE_STRIP); GD.LineWidth(16*1); GD.Vertex2ii(DISP_LEFT + 1, DISP_TOP); GD.Vertex2ii(DISP_LEFT + 1, DISP_BOT); GD.Vertex2ii(DISP_LEFT + 1 + razmer*MaxX+5-MaxX - 1, DISP_BOT); GD.Vertex2ii(DISP_LEFT + 1 + razmer*MaxX+5-MaxX - 1, DISP_TOP); // Draw 9 vertical lines for (byte i=0; i<9; i++) { GD.ColorRGB(WHITE); GD.Begin(LINE_STRIP); GD.LineWidth(16*1); GD.Vertex2ii(DISP_LEFT + 3 + razmer*i-i, DISP_TOP); GD.Vertex2ii(DISP_LEFT + 3 + razmer*i-i, DISP_BOT - 2); } // Draw 1 horizontal line GD.ColorRGB(WHITE); GD.Begin(LINE_STRIP); GD.Vertex2ii(DISP_LEFT + 3, DISP_BOT - 2); GD.Vertex2ii(DISP_LEFT + 3 + razmer*MaxX-MaxX - 1, DISP_BOT - 2); // Draw "Game Over" if (!Demo && !Arbeiten) { GD.Begin(RECTS); GD.ColorRGB(WHITE); GD.Vertex2ii((DISP_LEFT + DISP_RIGHT)/2 - 60, (DISP_TOP + DISP_BOT)/2 - 40); GD.Vertex2ii((DISP_LEFT + DISP_RIGHT)/2 + 60, (DISP_TOP + DISP_BOT)/2 + 40); GD.ColorRGB(BLACK); GD.Vertex2ii((DISP_LEFT + DISP_RIGHT)/2 - 58, (DISP_TOP + DISP_BOT)/2 - 38); GD.Vertex2ii((DISP_LEFT + DISP_RIGHT)/2 + 58, (DISP_TOP + DISP_BOT)/2 + 38); GD.ColorRGB(RED); GD.cmd_text((DISP_LEFT + DISP_RIGHT)/2, (DISP_TOP + DISP_BOT)/2 - 20, 30, OPT_CENTER, "GAME"); GD.cmd_text((DISP_LEFT + DISP_RIGHT)/2, (DISP_TOP + DISP_BOT)/2 + 20, 30, OPT_CENTER, "OVER"); } // Draw Buttons GD.Begin(POINTS); GD.PointSize(16*50); // Set size of buttons (50 pix) GD.ColorRGB(btn_color); // Set fone color of buttons GD.Tag(TAG_BUTTON_LEFT); // Set TAG for BUTTON_LEFT GD.Vertex2ii( X_BUTTON_LEFT, Y_BUTTON_LEFT); // Place BUTTON1 GD.Tag(TAG_BUTTON_RIGHT); // Set TAG for BUTTON_RIGHT GD.Vertex2ii(X_BUTTON_RIGHT, Y_BUTTON_RIGHT); // Place BUTTON2 GD.Tag(TAG_BUTTON_ROT); // Set TAG for BUTTON_ROT GD.Vertex2ii( X_BUTTON_ROT, Y_BUTTON_ROT); // Place BUTTON3 GD.Tag(TAG_BUTTON_DROP); // Set TAG for BUTTON_DROP GD.Vertex2ii(X_BUTTON_DROP, Y_BUTTON_DROP); // Place BUTTON4 // Draw figures in buttons circles GD.Tag(255); GD.ColorRGB(0xffff00); GD.LineWidth(16*2); GD.Begin(LINE_STRIP); GD.Vertex2ii(X_BUTTON_LEFT + 30, Y_BUTTON_LEFT - 20); GD.Vertex2ii(X_BUTTON_LEFT, Y_BUTTON_LEFT - 20); GD.Vertex2ii(X_BUTTON_LEFT, Y_BUTTON_LEFT - 40); GD.Vertex2ii(X_BUTTON_LEFT - 40, Y_BUTTON_LEFT); GD.Vertex2ii(X_BUTTON_LEFT, Y_BUTTON_LEFT + 40); GD.Vertex2ii(X_BUTTON_LEFT, Y_BUTTON_LEFT + 20); GD.Vertex2ii(X_BUTTON_LEFT + 30, Y_BUTTON_LEFT + 20); GD.Vertex2ii(X_BUTTON_LEFT + 30, Y_BUTTON_LEFT - 20); GD.Begin(LINE_STRIP); GD.Vertex2ii(X_BUTTON_RIGHT - 30, Y_BUTTON_RIGHT - 20); GD.Vertex2ii(X_BUTTON_RIGHT, Y_BUTTON_RIGHT - 20); GD.Vertex2ii(X_BUTTON_RIGHT, Y_BUTTON_RIGHT - 40); GD.Vertex2ii(X_BUTTON_RIGHT + 40, Y_BUTTON_RIGHT); GD.Vertex2ii(X_BUTTON_RIGHT, Y_BUTTON_RIGHT + 40); GD.Vertex2ii(X_BUTTON_RIGHT, Y_BUTTON_RIGHT + 20); GD.Vertex2ii(X_BUTTON_RIGHT - 30, Y_BUTTON_RIGHT + 20); GD.Vertex2ii(X_BUTTON_RIGHT - 30, Y_BUTTON_RIGHT - 20); GD.Begin(LINE_STRIP); GD.Vertex2ii(X_BUTTON_ROT - 40, Y_BUTTON_ROT); GD.Vertex2ii(X_BUTTON_ROT, Y_BUTTON_ROT - 40); GD.Vertex2ii(X_BUTTON_ROT + 40, Y_BUTTON_ROT); GD.Vertex2ii(X_BUTTON_ROT, Y_BUTTON_ROT + 40); GD.Vertex2ii(X_BUTTON_ROT - 40, Y_BUTTON_ROT); GD.Begin(LINE_STRIP); if (Arbeiten) { GD.Vertex2ii(X_BUTTON_DROP - 40, Y_BUTTON_DROP - 10); GD.Vertex2ii(X_BUTTON_DROP + 40, Y_BUTTON_DROP - 10); GD.Vertex2ii(X_BUTTON_DROP, Y_BUTTON_DROP + 30); GD.Vertex2ii(X_BUTTON_DROP - 40, Y_BUTTON_DROP - 10); } else { GD.Vertex2ii(X_BUTTON_DROP - 10, Y_BUTTON_DROP - 40); GD.Vertex2ii(X_BUTTON_DROP + 30, Y_BUTTON_DROP); GD.Vertex2ii(X_BUTTON_DROP - 10, Y_BUTTON_DROP + 40); GD.Vertex2ii(X_BUTTON_DROP - 10, Y_BUTTON_DROP - 40); } } //================================================== void ClearMas(byte MasStx[MaxX][MaxY]) { for (byte j=0;j<MaxY;j++) for (byte i=0;i<MaxX;i++) MasStx[i][j]=0; } //================================================== void Sosed(int i,int j,int dx,int dy, byte mode) { int nx=i+dx; int ny=j+dy; if (nx>=0 && ny>=0 && nx<MaxX && ny<MaxY && MasSt[nx][ny]==MasSt[i][j]) { if (mode==1) MasTmp[i][j]++; else if (mode==2 && (MasTmp[nx][ny]>1 || MasTmp[i][j]>2 )) { MasTmp[nx][ny]=3; MasTmp[i][j]=3; } else { if (mode==3 && MasTmp[nx][ny]==3) { if (MasTmp[i][j]!=3) { MasTmp[i][j]=3; fl=true; } } } } } //================================================== void Sos(int i,int j, byte mode) { for (byte k=0;k<4;k++) Sosed(i,j,mmm[k][0],mmm[k][1],mode); } //================================================== // create next figure void GetNext(void) { x=3; y=0; for (byte i=0;i<3;i++) { if (!Demo) MasSt[x][i]=fignext[i]; fignext[i]=random(NumCol)+2; } if (!Demo) { Counter++; if (Counter==NextLevel) { Counter=0; Level++; if (Level>MaxLevel) Level=MaxLevel; } tempspeed=MasSpeed[Level-1]; } } //================================================== // find onecolor elements bool FindFull(void) { byte i,j,k; bool res; res=false; for (byte k=2;k<8;k++) { // by every color ClearMas(MasTmp); for (j=3;j<MaxY;j++) for (i=0;i<MaxX;i++) if (MasSt[i][j]==k) Sos(i,j,1); for (j=3;j<MaxY;j++) for (i=0;i<MaxX;i++) if (MasTmp[i][j]>1) Sos(i,j,2); do { fl=false; for (j=3;j<MaxY;j++) for (i=0;i<MaxX;i++) if (MasTmp[i][j]>0) Sos(i,j,3); } while (fl); for (j=3;j<MaxY;j++) for (i=0;i<MaxX;i++) if (MasTmp[i][j]==3) { MasSt[i][j]=1; TScore++; } } return(res); } //================================================ // move figure down bool fig_drop(int dy) { if (dy>0 && !FlZ) { if (y+dy+2>MaxY-1 || MasSt[x+dx][y+dy+2]>0) { if (y<3) { gameover(); } else { return true; } } else { if (y+dy+dy+2>MaxY-1 || MasSt[x+dx][y+dy+dy+2]>0) { GD.play(COWBELL); } for (byte i=0;i<3;i++) MasSt[x][y+2-i+dy]=MasSt[x][y+2-i]; MasSt[x][y]=0; y=y+dy; } } return(false); } //================================================ // move figure left/right (shift) bool fig_shift(int dx) { if (x+dx<0 || x+dx>MaxX-1) { GD.play(COWBELL); return(false); } if (dx!=0) { if (MasSt[x+dx][y+dy+2]==0) { if (x+dx+dx<0 || x+dx+dx>MaxX-1) GD.play(COWBELL); else GD.play(CHACK); return(true); } else { GD.play(COWBELL); return(false); } } return(false); } //================================================== // State-machine void ProcGame(void) { byte i,j,k; bool res = false; if (time_count < millis()) { if (Arbeiten) time_count = millis() + tempspeed; else time_count = millis() + 1000; switch (state_game) { // Demo case 0: Score=0; GetNext(); for (byte j=3;j<MaxY;j++) for (byte i=0;i<MaxX;i++) MasSt[i][j]=random(6)+2; state_game = 1; TScore=0; break; case 1: FindFull(); if (TScore>0) { FlZ=true; time_count = millis() + 500; } state_game = 2; break; case 2: for (j=0;j<MaxY;j++) { for (i=0;i<MaxX;i++) { while (MasSt[i][MaxY-1-j]==1) { for (k=0;k<MaxY-2-j;k++) MasSt[i][MaxY-1-k-j] = MasSt[i][MaxY-2-k-j]; res=true; } } } if(res) { if (TScore>7) Score=Score+TScore+(TScore-8)*2; else Score=Score+TScore; state_game = 1; } else { state_game = 0; } break; // Arbeiten case 3: if (fig_drop(1)) { tempspeed=MasSpeed[Level-1]; TScore=0; FindFull(); if (TScore>0) { GD.play(KICKDRUM); FlZ=true; state_game = 4; } else { FlZ=false; GetNext(); } } break; case 4: for (j=0;j<MaxY;j++) { for (i=0;i<MaxX;i++) { while (MasSt[i][MaxY-1-j]==1) { for (k=0;k<MaxY-2-j;k++) MasSt[i][MaxY-1-k-j] = MasSt[i][MaxY-2-k-j]; res=true; } } } if(res) { if (TScore>7) Score=Score+TScore+(TScore-8)*2; else Score=Score+TScore; state_game = 5; FlZ=true; GD.play(CLACK); } else { state_game = 3; FlZ=false; time_count = millis() + 100; } break; case 5: state_game = 3; FlZ=false; break; default: break; } } } //================================================ // start new game void NewGame() { Score = 0; FlZ = false; ClearMas(MasSt); Arbeiten = true; GetNext(); Counter = 0; Level = 1; tempspeed = MasSpeed[0]; Record = myrecord; state_game = 3; } //================================================ // draw "GAME OVER" void gameover() { if (Arbeiten==true) { GD.play(SWITCH); Arbeiten=false; if (Score>myrecord) { myrecord=Score; eeprom_write_byte((unsigned char *) 1, myrecord); } } }
#define LOAD_ASSETS() GD.safeload("slotgag.gd2"); #define BACKGROUND_HANDLE 0 #define BACKGROUND_WIDTH 256 #define BACKGROUND_HEIGHT 256 #define BACKGROUND_CELLS 1 #define GAMEDUINO_HANDLE 1 #define GAMEDUINO_WIDTH 395 #define GAMEDUINO_HEIGHT 113 #define GAMEDUINO_CELLS 1 #define ASSETS_END 220342UL static const shape_t BACKGROUND_SHAPE = {0, 256, 256, 0}; static const shape_t GAMEDUINO_SHAPE = {1, 395, 113, 0};
Считаю, что задачу по созданию своего первого рабочего скетча для данной платы я выполнил. Надеюсь, что хотя бы одному человеку было интересно читать мой рассказ. Критика и замечания приветствуются. В планах не останавливаться, двигаться дальше и, конечно же, делиться опытом, знаниями.
Для демонстрации работы платы выкладываю видео со звуком (Осторожно! Громкий звук!).
Спасибо за внимание.
ссылка на оригинал статьи https://habr.com/post/423577/
Добавить комментарий