===== Эксперимент 53. Классы ящиков, человека и цели ===== В прошлом эксперименте мы подключили TFT дисплей, создали игровую карту в виде двумерного массива и отобразили ее на экране. Теперь давайте поймем какие вообще объекты задействованы в игровом процессе. Как мы уже поняли, это стены, ящики, кладовщик и цели, куда нужно перетащить ящики. Теперь нужно описать каждый из классов. === Структура позиция === В связи с тем, что в некоторых методах следующих классов нам нужно будет возвращать положение объектов состоящее из двух переменных (координаты х и y), а методы могут возвращать только одну переменную, создадим структуру ''Pos''. Структуру можно описать как упрощенную версию класса. Обычно структуры не имеют методов, хотя могут. struct Pos { int x = 0; int y = 0; bool operator == (const Pos &pos) const { return x == pos.x && y == pos.y; } }; Определенная нами структура содержит две переменные координат и определение оператора равенства. Две переменные типа ''Pos'' равны только тогда, когда попарно равны их координаты. === Класс ящика === Начнем с класса ящика. Главные свойства ящика, это его координаты на карте и его состояние — находится он на цели или нет. class Box { private: Adafruit_ST7735 *tft_ptr; LittleFS_ImageReader *reader_ptr; Pos pos; String picture = "/box.bmp"; String picture_on_gate = "/boxngate.bmp"; bool on_gate = false; public: Box(Adafruit_ST7735 *_tft_ptr, LittleFS_ImageReader *_reader_ptr, int x, int y) { tft_ptr = _tft_ptr; reader_ptr = _reader_ptr; pos.x = x; pos.y = y; } void draw() { if (on_gate) reader_ptr->drawBMP(picture_on_gate, *tft_ptr, pos.x * 16, pos.y * 16); else reader_ptr->drawBMP(picture, *tft_ptr, pos.x * 16, pos.y * 16); } void setOnGate(bool state) { on_gate = state; } bool getOnGate() const { return on_gate; } Pos getPos() const { return pos; } void setPos(Pos _pos) { pos.x = _pos.x; pos.y = _pos.y; draw(); } }; В при создании экземпляра класса следующим свойствам будут присвоены значения по умолчанию: имя спрайта для ящика вне цели ''String picture = "/box.bmp";'' и на цели ''String picture_onGate = "/boxngate.bmp"''; состояние ящика на цели или нет ''bool on_gate = false;''. В конструктор мы передаем такие параметры как указатели на объект дисплея и считывателя изображений, координата x, координата y. Координата ящика указывается не в пикселях, а в клетках игрового поля. Метод ''draw()'' предназначена для отрисовки ящика. Если ящик находится на цели, то функция отображает спрайт ''picture_on_gate'', иначе ''picture''. Координата ящика указывается не в пикселях, а в клетках игрового поля, поэтому для отображения картинки ее необходимо пересчитать в пиксели. Для этого координата умножается на размер ящика в пикселях. Метод ''setOnGate()'' служит для установки свойства нахождения на цели. А метод ''getOnGate()'' для чтения этого свойства. Метод ''getPos'' возвращает текущую позицию ящика. А метод ''setPos'' устанавливает новые координаты ящика, после чего перерисовывает его на новом месте. === Класс цели === Класс цели намного проще. Цель никуда не перемещается и свойств не имеет. Цель имеет только координаты и должна отображаться на экране с помощью своего спрайта. class Gate { private: Adafruit_ST7735 *tft_ptr; LittleFS_ImageReader *reader_ptr; Pos pos; String picture = "/gate.bmp"; public: Gate(Adafruit_ST7735 *_tft_ptr, LittleFS_ImageReader *_reader_ptr, int x, int y) { tft_ptr = _tft_ptr; reader_ptr = _reader_ptr; pos.x = x; pos.y = y; } void draw() const { reader_ptr->drawBMP(picture, *tft_ptr, pos.x * 16, pos.y * 16); } Pos getPos() const { return pos; } }; Метод ''draw()'' служит для отрисовки спрайта цели в заданных координатах. Так как координаты цели задаются не в пикселях, а в клетках поля, необходимо пересчитать их в пиксели для отображения. Поэтому координаты в клетках умножаются на размер клетки — 16. === Класс кладовщика === Кладовщик также имеет координаты своего нахождения на поле и имеет свой спрайт. class Man { private: Adafruit_ST7735 *tft_ptr; LittleFS_ImageReader *reader_ptr; Pos pos; String picture = "/man.bmp"; public: Man(Adafruit_ST7735 *_tft_ptr, LittleFS_ImageReader *_reader_ptr, int x, int y) { Serial.println("Man constructor"); tft_ptr = _tft_ptr; reader_ptr = _reader_ptr; pos.x = x; pos.y = y; } void draw() const { reader_ptr->drawBMP(picture, *tft_ptr, pos.x * 16, pos.y * 16); } Pos getPos() const { return pos; } void setPos(Pos _pos) { tft_ptr->fillRect(pos.x * 16, pos.y * 16, 16, 16, ST77XX_BLACK); pos.x = _pos.x; pos.y = _pos.y; draw(); } }; Конструктор и методы ''draw()'' и ''getPos()'' аналогичны одноименным методам предыдущих классов, поэтому не будем повторно заострять на них внимание. Небольшая разница есть в методе ''setPos()''. Отличие заключается в том, что при изменении координаты кладовщика на игровом поле, мы не только рисуем его в новом месте, но и стираем в предыдущем. Это делается с помощью рисования черного прямоугольника по старым координатам кладовщика. Если "старого" кладовщика не закрасить, то он никуда не исчезнет, а просто появится еще один в новом месте. Но почему же мы не закрашивали "старые" ящики при смене их координат? Потому что ящик изменяет координаты только когда его толкает кладовщик. И он сам оказывается на старом месте ящика и закрашивает его собой. Класса стен не будет. Мы будем пользоваться нашим двумерным массивом в котором уже описали план уровня. Теперь, когда мы имеем лабиринт и классы для ящика, кладовщика и цели, самое время отобразить это все на дисплее. ==== Схема эксперимента ==== Схема не изменилась. {{ :products:esp-iot:exp26_mont.png?direct&600 |}} //Рисунок 1. Монтажная схема эксперимента с 8 выводами// {{ :products:esp-iot:exp26_mont_11pin.png?direct&600 |}} //Рисунок 2. Монтажная схема эксперимента с 11 выводами// ==== Программный код эксперимента ==== #include #include #include "LittleFS_ImageReader.h" #define PIN_CS 2 #define PIN_DC 4 #define PIN_RST 5 Adafruit_ST7735 tft = Adafruit_ST7735(PIN_CS, PIN_DC, PIN_RST); LittleFS_ImageReader reader; bool Map[10][8] = { {1,1,0,1,1,1,0,1}, {0,1,1,1,1,1,1,0}, {1,1,0,0,0,1,1,1}, {0,1,0,1,0,1,0,1}, {0,1,0,0,0,1,0,1}, {1,1,1,1,0,0,0,1}, {1,0,0,0,0,0,0,1}, {1,0,0,0,1,0,0,1}, {1,0,0,0,1,1,1,1}, {1,1,1,1,1,0,0,0} }; struct Pos { int x = 0; int y = 0; bool operator == (const Pos &pos) const { return x == pos.x && y == pos.y; } }; class Box { private: Adafruit_ST7735 *tft_ptr; LittleFS_ImageReader *reader_ptr; Pos pos; String picture = "/box.bmp"; String picture_on_gate = "/boxngate.bmp"; bool on_gate = false; public: Box(Adafruit_ST7735 *_tft_ptr, LittleFS_ImageReader *_reader_ptr, int x, int y) { tft_ptr = _tft_ptr; reader_ptr = _reader_ptr; pos.x = x; pos.y = y; } void draw() { if (on_gate) reader_ptr->drawBMP(picture_on_gate, *tft_ptr, pos.x * 16, pos.y * 16); else reader_ptr->drawBMP(picture, *tft_ptr, pos.x * 16, pos.y * 16); } void setOnGate(bool state) { on_gate = state; } bool getOnGate() const { return on_gate; } Pos getPos() const { return pos; } void setPos(Pos _pos) { pos.x = _pos.x; pos.y = _pos.y; draw(); } }; class Gate { private: Adafruit_ST7735 *tft_ptr; LittleFS_ImageReader *reader_ptr; Pos pos; String picture = "/gate.bmp"; public: Gate(Adafruit_ST7735 *_tft_ptr, LittleFS_ImageReader *_reader_ptr, int x, int y) { tft_ptr = _tft_ptr; reader_ptr = _reader_ptr; pos.x = x; pos.y = y; } void draw() const { reader_ptr->drawBMP(picture, *tft_ptr, pos.x * 16, pos.y * 16); } Pos getPos() const { return pos; } }; class Man { private: Adafruit_ST7735 *tft_ptr; LittleFS_ImageReader *reader_ptr; Pos pos; String picture = "/man.bmp"; public: Man(Adafruit_ST7735 *_tft_ptr, LittleFS_ImageReader *_reader_ptr, int x, int y) { Serial.println("Man constructor"); tft_ptr = _tft_ptr; reader_ptr = _reader_ptr; pos.x = x; pos.y = y; } void draw() const { reader_ptr->drawBMP(picture, *tft_ptr, pos.x * 16, pos.y * 16); } Pos getPos() const { return pos; } void setPos(Pos _pos) { tft_ptr->fillRect(pos.x * 16, pos.y * 16, 16, 16, ST77XX_BLACK); pos.x = _pos.x; pos.y = _pos.y; draw(); } }; const int boxes_number = 3; Box boxes[boxes_number] = { {&tft, &reader, 3, 4}, {&tft, &reader, 4, 6}, {&tft, &reader, 2, 7}, }; const int gates_number = 3; Gate gates[gates_number] = { {&tft, &reader, 6, 3}, {&tft, &reader, 6, 4}, {&tft, &reader, 6, 5}, }; Man man(&tft, &reader, 5, 6); void setup() { Serial.begin(9600); Serial.println(); Serial.println("Setup"); LittleFS.begin(); tft.initR(INITR_BLACKTAB); tft.setRotation(2); tft.fillScreen(ST77XX_BLACK); for (int y = 0; y < 10; y++) { for (int x = 0; x < 10; x++) { if (Map[y][x]) reader.drawBMP("/brick.bmp", tft, x * 16, y * 16); } } for (int i = 0; i < boxes_number; i++) { boxes[i].draw(); } for (int i = 0; i < gates_number; i++) { gates[i].draw(); } man.draw(); } void loop() { } Создаем массив объектов — ящиков c координатами: 3, 4; 4, 6; 2, 7. const int boxes_number = 3; Box boxes[boxes_number] = { {&tft, &reader, 3, 4}, {&tft, &reader, 4, 6}, {&tft, &reader, 2, 7}, }; Создаем массив объектов — целей c координатами: 6, 3; 6, 4; 6, 5. const int gates_number = 3; Gate gates[gates_number] = { {&tft, &reader, 6, 3}, {&tft, &reader, 6, 4}, {&tft, &reader, 6, 5}, }; Создаем кладовщика: Man man(&tft, &reader, 5, 6); Вызываем метод ''draw()'' для элементов массивов ящиков и целей, и для кладовщика. for (int i = 0; i < boxes_number; i++) { boxes[i].draw(); } for (int i = 0; i < gates_number; i++) { gates[i].draw(); } man.draw(); Теперь мы имеем игровое поле с лабиринтом, кладовщика, ящики и цели. В следующем эксперименте мы научим кладовщика двигаться при нажатиях на кнопки.