
В этом уроке мы создадим игру змейка (питон) на матрице из 48 светодиодов и 4 кнопок. Обычно, светодиодные матрицы состоят из светодиодов, аноды и катоды которых соединены по столбцам и строкам. В таких матрицах, светодиоды зажигаются поочерёдно. В нашем уроке, все светодиоды будут гореть постоянно, так как они, как и кнопки, подключены к Arduino через расширители выводов.
Нам понадобится:
- Arduino Uno х 1шт.
- Trema модуль - расширитель на 8 Входов/Выходов. x 8шт.
- Trema модуль - светодиод (синий) x 48шт.
- Trema модуль - кнопка (синяя) x 4шт.
- Trema Shield x 1шт.
- Конструктор для создания каркаса.
Для реализации проекта нам необходимо установить библиотеку:
- iarduino_I2C_IO для работы с расширителями выводов.
О том как устанавливать библиотеки, Вы можете ознакомиться на странице Wiki - Установка библиотек в Arduino IDE.
Видео:
Схема подключения:
- Расширители выводов подключаются к Arduino по аппаратной шине I2C. На одной шине можно разместить до 8 расширителей, адреса которым задаются при помощи переключателей на модуле, от 0x20 до 0x27.
- Светодиоды подключаются к первым 6 выводам каждого расширителя (если подключить к другим выводам, то нужно изменить их номера в скетче).
- Кнопки подключаются к 7му выводу первых 4х расширителей (если подключить к другим выводам, или модулям расширителей, то их номера и адреса, нужно изменить в скетче).

Алгоритм работы:
При старте:
- Подключение библиотеки и создание объектов для каждого модуля расширителя выводов. Объекты можно создавать по отдельности, но в нашем уроке, мы создали объекты в виде элементов одного массива, так проще выводить информацию на матрицу.
- Конфигурация выводов, для светодиодов OUTPUT, для кнопок INPUT. Выводы конфигурируются вызовом функции pinMode, для каждого используемого вывода, каждого объекта.
- Создание переменных, для хранения координат «съедобной» точки, которую должен съесть питон. Создание двух массивов для хранения координат всех точек питона (в одном массиве координаты по оси X, а в другом, по оси Y). В этих массивах должны быть установлены координаты первых двух точек питона. Создание переменной для хранения длины питона. Создание переменной для хранения направления питона. Прорисовка питона (включение двух светодиодов в соответствии с координатами в массивах).
В коде loop:
- Продвигаем питона на один шаг вперёд с интервалом в 500 мс, для этого:
- Сдвигаем элементы массивов, хранящие координаты точек питона (ось X и ось Y), на один элемент к концу массива. После сдвига, значение нулевого элемента, будет совпадать со значением первого элемента, в обоих массивах. А длина питона, хранящаяся в отдельной переменной, останется неизменной, значит питон не вырос.
- Меняем значение 0 элемента (голова питона) на значение соседней координаты (сверху, снизу, справа, слева), в зависимости от направления питона.
- Проверяем условия игры:
- Если координата головы питона выходит за пределы поля, значит игра проиграна
- Если координата головы питона совпадает с координатой любой из точек тела питона, значит игра проиграна
- Если координата головы питона совпадает с координатой «съедобной» точки, значит увеличиваем значение длины питона и создаём координату новой точки
- Если длина питона >= 15, значит игра выиграна
- Выключаем светодиод, координата которого соответствует концу питона (элементы массивов с индексом = длина питона + 1)
- Включаем светодиод, координата которого соответствует голове питона (элементы массивов с индексом 0)
- Мигаем «съедобной точкой» с интервалом в 100 мс.
- Читаем состояние выводов к которым подключены кнопки, меняем направление питона при их нажатии
Если игра выиграна, то выводим улыбающийся смайлик, а если проиграна, то грустный смайлик..
Код программы:
#include <Wire.h> // Подключаем библиотеку для работы с аппаратной шиной I2C, до подключения библиотеки iarduino_I2C_IO.
#include <iarduino_I2C_IO.h> // Подключаем библиотеку iarduino_I2C_IO для работы с модулями
iarduino_I2C_IO Md[8 ]={0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27}; // Создаём массив из 8 объектов, для работы с модулями, указывая адрес каждого модуля на шине I2C
int8_t Ms[3][8]; // Создаём массив для прорисовки смайликов в конце игры
int8_t Px[16]={2,2}; // Создаём массив с кооржинатами точек питона по оси X
int8_t Py[16]={6,7}; // Создаём массив с кооржинатами точек питона по оси Y
int8_t Sx = 0; // Создаём переменную для хранения координаты «съедобной» точки по оси X
int8_t Sy = 0; // Создаём переменную для хранения координаты «съедобной» точки по оси Y
uint8_t Pl = 2; // Создаём переменную для хранения длины питона (количество точек)
uint8_t Pn = 0; // Создаём переменную для хранения направления питона (0-вверх, 1-вниз, 2-влево, 3-вправо)
uint32_t Tm = 0; // Создаём переменную для хранения времени, прошедшего с начала старта скетча (в миллисекундах)
uint32_t Pm = 500; // Создаём переменную для хранения интервала времени, с которым ползет питон (в миллисекундах)
uint32_t Sm = 100; // Создаём переменную для хранения интервала времени, с которым мигает «съедобная» точка (в миллисекундах)
bool Fp(uint8_t x, uint8_t y, uint8_t z = 0); // Объявляем функцию которая будет искать совпадения координат xy с координатами точек в теле питона, начиная с точки z
void Fs(uint8_t); // Объявляем функцию которая будет выводить смайлик на экран
void (*Fr)() = 0; // Создаём указатель на функцию, но вместо адреса функции, указываем адрес 0. Если обратиться к указателю Fr(), то вместо вызова функции, программа начнёт выполняться с 0 адреса (перезагрузка)
void setup(){
// Заполняем массив со смайликами
Ms[0][0]=B00000000; Ms[1][0]=B00000000; Ms[2][0]=B00000000;
Ms[0][1]=B00000000; Ms[1][1]=B00110011; Ms[2][1]=B00110011;
Ms[0][2]=B00000000; Ms[1][2]=B00110011; Ms[2][2]=B00110011;
Ms[0][3]=B00000000; Ms[1][3]=B00000000; Ms[2][3]=B00000000;
Ms[0][4]=B00000000; Ms[1][4]=B00000000; Ms[2][4]=B00000000;
Ms[0][5]=B00000000; Ms[1][5]=B00100001; Ms[2][5]=B00011110;
Ms[0][6]=B00000000; Ms[1][6]=B00011110; Ms[2][6]=B00100001;
Ms[0][7]=B00000000; Ms[1][7]=B00000000; Ms[2][7]=B00000000;
// Конфигурируем выводы
for(int i=0; i<8; i++){Md[i].begin(); // Проходим по всем модулям
for(int j=0; j<6; j++){Md[i].pinMode(j, OUTPUT); // Конфигурируем очередной вывод, со светодиодом, в режим выхода (к ним подключены светодиоды)
}} Md[0].pinMode(7, INPUT); // Конфигурируем 7 вывод 0 модуля в режим входа (к нему подключена кнопка «вверх»)
Md[1].pinMode(7, INPUT); // Конфигурируем 7 вывод 1 модуля в режим входа (к нему подключена кнопка «вниз»)
Md[2].pinMode(7, INPUT); // Конфигурируем 7 вывод 1 модуля в режим входа (к нему подключена кнопка «влево»)
Md[3].pinMode(7, INPUT); // Конфигурируем 7 вывод 1 модуля в режим входа (к нему подключена кнопка «вправо»)
// Чистим дисплей
Fs(0);
// Создаём координаты «съедобной точки»
randomSeed(analogRead(0)); // Выбираем случайную начальную позицию в последовательности чисел выводимых функцией random
do{Sx=random(6); Sy=random(8);} while(Fp(Sx,Sy)); // Создаём координату новой «съедобной» точки, но так, чтобы она не попала на тело питона
// Прорисовываем питона
for(int i=0; i<Pl; i++){Md[Py[i]].digitalWrite(Px[i], HIGH);} // Прорисовываем начальные точки питона
}
void loop(){
Tm = millis();
// Сдвигаем питона на одну точку, через каждые 500 миллисекунд
if(Tm%Pm<5){ // Если прошло Pm мс, то ...
// Сдвигаем массивы точек питона
memmove(&Px[1], Px, Pl); // Cдвигаем элементы массива (координаты точек питона по оси X), на 1 элемент назад
memmove(&Py[1], Py, Pl); // Cдвигаем элементы массива (координаты точек питона по оси Y), на 1 элемент назад
// Создаём координаты первой точки в массивах точек питона
switch(Pn){ // Cоздаём новую первую точку питона (его голову), в зависимости от его направления (Pn)
case 0: Py[0]--; break; // - cверху
case 1: Py[0]++; break; // - снизу
case 2: Px[0]++; break; // - слева
case 3: Px[0]--; break; // - справа
}
// Проверяем условия игры
if (Fp(Px[0], Py[0], 1)) {Fs(0); Fs(2); delay(1000); Fr();} // Если голова питона (Px[0], Py[0]) столкнулась с его телом, то начинаем игру заново
if (Px[0]<0||Px[0]>5||Py[0]<0||Py[0]>7){Fs(0); Fs(2); delay(1000); Fr();} // Если голова питона вышла за пределы поля, то начинаем игру заново
if (Px[0]==Sx && Py[0]==Sy) {do{Sx=random(6); Sy=random(8);}while(Fp(Sx,Sy)); Pl++;} // Если точка съедена, то создаём координату новой «съедобной» точки, но так, чтобы она не попала на тело питона и увеличиваем длину питона
if (Pl>=15) {Fs(0); Fs(1); delay(1000); Fr();} // Если длина питона >= 15 точек, то выводим смайлик
// Гасим старую и зажигаем новую точку питона
Md[Py[Pl]].digitalWrite(Px[Pl], LOW ); // Гасим старую точку (хвост питона)
Md[Py[0] ].digitalWrite(Px[0] , HIGH); // Зажигаем новую точку (голова питона)
}
// Мигаем «съедобной» точкой
if (Tm%Sm<5){Md[Sy].digitalWrite(Sx, !Md[Sy].digitalRead(Sx)); delay(5);} // Если прошло Sm мс, то читаем состояние на выходе «съедобной» точки и меняем его на противоположное
// Меняем направление (значение переменной Pn)
if (Md[0].digitalRead(7) && Pn!=1){Pn=0;} // Если нажата кнопка «вверх» и питон не двигался вниз, то меняем его направление
if (Md[1].digitalRead(7) && Pn!=0){Pn=1;} // Если нажата кнопка «вниз» и питон не двигался вверх, то меняем его направление
if (Md[2].digitalRead(7) && Pn!=3){Pn=2;} // Если нажата кнопка «влево» и питон не двигался вправо, то меняем его направление
if (Md[3].digitalRead(7) && Pn!=2){Pn=3;} // Если нажата кнопка «вправо» и питон не двигался влево, то меняем его направление
}
bool Fp(uint8_t x, uint8_t y, uint8_t z){for(int i=z; i<Pl; i++){if(Px[i]==x && Py[i]==y){return true;}}return false;}
void Fs(uint8_t a){for(int i=0; i<8; i++){for(int j=0; j<6; j++){Md[i].digitalWrite(j,Ms[a][i]&bit(j));}}}

Обсуждение