Pull to refresh

Создание логической игры для игровой платформы

Reading time 20 min
Views 9.6K
Здравствуйте.

Хочу поделиться своей историей знакомства с игровой платформой Gameduino 3, а также немного расскажу о программировании простейшей логической игры под данную платформу, используемой совместно с Arduino Uno.

Что же такое Gameduino 3? Gameduino 3 — это плата расширения, которая позволяет превратить Arduino в современную карманную (имеется в виду размер) игровую консоль. К моему удивлению, мне не удалось найти на хабре какой-либо подробной информации о данной плате. Хотел бы восполнить этот пробел, тем более что плата, на мой взгляд, заслуживает внимания.

Немного истории


Автором проекта под названием Gameduino является Джеймс Боуман (James Bowman), который в 2011 году создал первую версию платы. Тогда она позиционировалась как модуль VGA для Arduino. Плата называлась Gameduino и была выполнена на основе программируемой логической матрицы FPGA компании Xilinx семейства Spartan-3A. На плате были установлены разъемы для подключения VGA монитора и стерео акустики.image

Характеристики Gameduino (1):
  • 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, аудио выход на наушники.

image

«Сердцем» платы стал графический контроллер EVE (Embedded Video Engine — по-русски можно перевести как «встраиваемый видеомодуль») FT800, который обладает мощными вычислительными возможностями, совмещая в себе одновременно несколько функций: формирование изображения и вывод его на экран TFT-дисплея, обработка сенсорного экрана, генерирование звука.

Функциональная схема графического контроллера FT800
image

В структуру микросхемы включены следующие функциональные блоки: графический контроллер, контроллер аудио, контроллер резистивной сенсорной панели. Микросхема FT800 предназначена для управления дисплеями с разрешением до 512 х 512 пикселей. FT800 также поддерживает LCD WQVGA (480 x 272) и QVGA (320 x 240). EVE (Embedded Video Engine) FT800 – это готовое решения для создания графического пользовательского интерфейса. Микросхема формирует сигналы управления дисплеем, имеет встроенные графические функции для отображения точек, линий, растровых картинок, объемных кнопок, текстов и т. д.


Структура системы на основе графического контроллера FT800
Формирование изображения происходит на основе набора команд (дисплей-листа), который передается управляющим микроконтроллером в FT800 через интерфейс I2C или SPI (в Gameduino 2 связь между Arduino и FT800 осуществляется по интерфейсу SPI). Возможности FT800 позволяют существенно разгрузить хост-контроллер системы.

image

Например, для вывода ряда кнопок достаточно передать в графический контроллер одну команду (четыре 32-х разрядных слова), и FT800 самостоятельно сформирует изображение этих кнопок на экране TFT-дисплея. Набор команд графических контроллеров FTDI включает в себя более 50 функций, с помощью которых можно выводить различные изображения на экран дисплея с теми или иными эффектами.

Подробное руководство по программированию контроллера и примеры работы с различными средами проектирования можно найти в Application Notes на сайте FTDI.

На русском языке хорошее описание функциональных возможностей, общих принципов и примеров работы есть тут.

Характеристики Gameduino 2:
  • разрешение экрана 480x272 пикселей в 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 Мбайт оперативная память и др.

Характеристики Gameduino 3:
  • видео декодер для полноэкранного 30 fps видео;
  • 1 мегабайт внутреннего ОЗУ;
  • разъемы для подключения microSD-карты и аудиовыход;
  • высококонтрастная ЖК-панель с диагональю 4,3 «480x272 с резистивным сенсорным экраном;
  • поддержка карт, созданных с помощью редактора Tiled Map;
  • загрузка изображения PNG с microSD;
  • ускоренное декодирование JPEG;
  • аппаратное переключение портрета / альбомной ориентации;
  • поддержка плат Arduino, ESP8266 и Teensy 3.2;
  • онлайн-инструменты для подготовки графики, аудио, шрифта и видео;


Джеймс Боуман опубликовал для своего проекта библиотеку с множеством примеров, которые работают „прямо из коробки“. Актуальная библиотека, которую мне удалось найти, находится тут. Руководство по программированию (на английском языке), где подробно все описывается. Много полезной информации по установке IDE, и т.п., и т.д.

Программирование


Как-то, блуждая по просторам Большого театра интернета, я наткнулся на интересный проект для Ардуино — логическая игра „Columns“, написанная под обычный недорогой цветной китайский дисплей 128х160 пикселов. Мне захотелось повторить эту игру, но уже на своей плате, назову её FT810 (по названию графического процессора), которая к тому времени уже была у меня на руках. Руководство по программированию и примеры из библиотеки я тоже уже успел изучить, поэтому руки просто „чесались“ от желания написать что-нибудь свое. К чему я незамедлительно и приступил.

Первое, что мне предстояло сделать — это вывод текста на экран.

Благодаря наличию встроенных шрифтов вывод текста на экран осуществляется достаточно легко. Приведу демонстрационный скетч из библиотеки (с моими комментариями):

Скетч helloworld.ino
#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();                  // команда на копирование данных из видеобуфера на экран (аппаратная функция графического процессора).
}
В итоге на экране получим вот такой красивый текст:
image

Далее необходимо было нарисовать геометрические фигуры, например: линии.
Чтобы нарисовать линии, необходимо использовать Begin(LINES) или Begin(LINE_STRIP).
LINES соединяет каждую пару вершин, тогда как LINE_STRIP объединяет все вершины вместе.

Приведу следующий демоскетч из библиотеки (с моими комментариями):

Скетч lines.ino
#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();             // Команда на копирование данных из видеобуфера на экран (аппаратная функция графического процессора).
}

Линии на экране:

image

От рисования линий перейдем к рисованию прямоугольников.

Чтобы нарисовать прямоугольники, используем Begin (RECTS) и устанавливаем противоположные углы прямоугольника. Порядок двух углов не имеет значения. Прямоугольники рисуются с закругленными углами, используя ширину текущей линии в качестве радиуса угла. Закругленные углы выходят за границы прямоугольника, поэтому увеличение радиуса угла приводит к увеличению количества пикселей. Этот пример рисует прямоугольник 420 × 20 три раза с увеличением радиуса скругления угла.

Скетч rectangles.ino
#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();              // Команда на копирование данных из видеобуфера на экран (аппаратная функция графического процессора)
}

Результат:
image

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



Я привел примеры основных операций, используя которые уже можно было смело переходить к созданию игры. Конечно игра создавалась не с нуля, а за основу был взят готовый рабочий код, который был адаптирован (с сохранением графики), поэтому первоначальный вариант игры был очень похож на оригинал.

Первый вариант оформления игры:



Поиграв несколько дней, захотелось что-нибудь изменить в оформлении, например добавить вместо белого фона какой-нибудь другой, необычный. И тут я вспомнил один пример из библиотеки, в котором на заднем фоне красовалось звездное небо:

Демонтрационный скетч slotgag.ino
#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

Итог


В итоге получился такой скетч:

Cкетч Columns.ino
#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);
    }
  }
}
slotgag_assets.h
#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};

Считаю, что задачу по созданию своего первого рабочего скетча для данной платы я выполнил. Надеюсь, что хотя бы одному человеку было интересно читать мой рассказ. Критика и замечания приветствуются. В планах не останавливаться, двигаться дальше и, конечно же, делиться опытом, знаниями.

Для демонстрации работы платы выкладываю видео со звуком (Осторожно! Громкий звук!).
Спасибо за внимание.
Tags:
Hubs:
+28
Comments 1
Comments Comments 1

Articles