В прошлом эксперименте мы подключили 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()
. Отличие заключается в том, что при изменении координаты кладовщика на игровом поле, мы не только рисуем его в новом месте, но и стираем в предыдущем. Это делается с помощью рисования черного прямоугольника по старым координатам кладовщика. Если «старого» кладовщика не закрасить, то он никуда не исчезнет, а просто появится еще один в новом месте. Но почему же мы не закрашивали «старые» ящики при смене их координат? Потому что ящик изменяет координаты только когда его толкает кладовщик. И он сам оказывается на старом месте ящика и закрашивает его собой.
Класса стен не будет. Мы будем пользоваться нашим двумерным массивом в котором уже описали план уровня.
Теперь, когда мы имеем лабиринт и классы для ящика, кладовщика и цели, самое время отобразить это все на дисплее.
Схема не изменилась.
Рисунок 1. Монтажная схема эксперимента с 8 выводами
Рисунок 2. Монтажная схема эксперимента с 11 выводами
#include <SPI.h> #include <Adafruit_ST7735.h> #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) { 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() { 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();
Теперь мы имеем игровое поле с лабиринтом, кладовщика, ящики и цели. В следующем эксперименте мы научим кладовщика двигаться при нажатиях на кнопки.