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