- PVSM.RU - https://www.pvsm.ru -

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

Здравствуйте.

Хочу поделиться своей историей знакомства с игровой платформой 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 [1].

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

Характеристики 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;
  • онлайн-инструменты для подготовки графики, аудио, шрифта и видео;

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

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

Как-то, блуждая по просторам Большого театра интернета, я наткнулся на интересный проект для Ардуино — логическая игра „Columns“ [6], написанная под обычный недорогой цветной китайский дисплей 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();                  // Команда на копирование данных из видеобуфера на экран (аппаратная функция графического процессора)
}

Результат:

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

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

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

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

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

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

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

Демонтрационный скетч 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();
}

Вид:

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

Чтобы не углубляться в дебри, я не стал комментировать весь код, а прокомментировал только нужные строчки кода, которые скопировал в свой рабочий скетч.

Для добавления картинки звездного неба в качестве заднего фона пришлось сделать следующее: во-первых, изменить черный цвет линий и текста на белый (чтобы они были видны на черном фоне), записать на микро SD карту файл slotgag.gd2, в котором хранится изображение, добавить в папку проекта slotgag_assets.h и добавить в скетч необходимые 8 строчек кода.

В итоге игра приобрела вот такой вид:

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

И конечно же, какая игра без звукового оформления? Осталось добавить звуковые эффекты, тем более что они представлены в хорошем качестве и разнообразии.

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};

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

Для демонстрации работы платы выкладываю видео [7] со звуком (Осторожно! Громкий звук!).
Спасибо за внимание.

Автор: FedAll

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/292887

Ссылки в тексте:

[1] FTDI: http://www.ftdichip.com/Products/ICs/FT800.html

[2] тут: http://mymcu.ru/storage/content/articles/FTDI/EVE.pdf

[3] тут: https://github.com/jamesbowman/gd2-lib

[4] Руководство по программированию: http://excamera.com/files/gd2book_v0.pdf

[5] Много полезной информации: http://excamera.com/sphinx/gameduino2/code.html

[6] логическая игра „Columns“: https://mysku.ru/blog/diy/65413.html

[7] видео: https://youtu.be/az3YBakPDSo

[8] Источник: https://habr.com/post/423577/?utm_campaign=423577