Эксперимент 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(). Отличие заключается в том, что при изменении координаты кладовщика на игровом поле, мы не только рисуем его в новом месте, но и стираем в предыдущем. Это делается с помощью рисования черного прямоугольника по старым координатам кладовщика. Если «старого» кладовщика не закрасить, то он никуда не исчезнет, а просто появится еще один в новом месте. Но почему же мы не закрашивали «старые» ящики при смене их координат? Потому что ящик изменяет координаты только когда его толкает кладовщик. И он сам оказывается на старом месте ящика и закрашивает его собой.

Класса стен не будет. Мы будем пользоваться нашим двумерным массивом в котором уже описали план уровня.

Теперь, когда мы имеем лабиринт и классы для ящика, кладовщика и цели, самое время отобразить это все на дисплее.

Схема эксперимента

Схема не изменилась.

Рисунок 1. Монтажная схема эксперимента с 8 выводами

Рисунок 2. Монтажная схема эксперимента с 11 выводами

Программный код эксперимента

Exp53.ino
  1. #include <SPI.h>
  2. #include <Adafruit_ST7735.h>
  3. #include "LittleFS_ImageReader.h"
  4.  
  5. #define PIN_CS 2
  6. #define PIN_DC 4
  7. #define PIN_RST 5
  8.  
  9. Adafruit_ST7735 tft = Adafruit_ST7735(PIN_CS, PIN_DC, PIN_RST);
  10. LittleFS_ImageReader reader;
  11.  
  12. bool Map[10][8] = {
  13. {1,1,0,1,1,1,0,1},
  14. {0,1,1,1,1,1,1,0},
  15. {1,1,0,0,0,1,1,1},
  16. {0,1,0,1,0,1,0,1},
  17. {0,1,0,0,0,1,0,1},
  18. {1,1,1,1,0,0,0,1},
  19. {1,0,0,0,0,0,0,1},
  20. {1,0,0,0,1,0,0,1},
  21. {1,0,0,0,1,1,1,1},
  22. {1,1,1,1,1,0,0,0}
  23. };
  24.  
  25. struct Pos {
  26. int x = 0;
  27. int y = 0;
  28.  
  29. bool operator == (const Pos &pos) const {
  30. return x == pos.x && y == pos.y;
  31. }
  32. };
  33.  
  34. class Box {
  35. private:
  36. Adafruit_ST7735 *tft_ptr;
  37. LittleFS_ImageReader *reader_ptr;
  38. Pos pos;
  39. String picture = "/box.bmp";
  40. String picture_on_gate = "/boxngate.bmp";
  41. bool on_gate = false;
  42.  
  43. public:
  44. Box(Adafruit_ST7735 *_tft_ptr, LittleFS_ImageReader *_reader_ptr, int x, int y) {
  45. tft_ptr = _tft_ptr;
  46. reader_ptr = _reader_ptr;
  47. pos.x = x;
  48. pos.y = y;
  49. }
  50.  
  51. void draw() {
  52. if (on_gate) reader_ptr->drawBMP(picture_on_gate, *tft_ptr, pos.x * 16, pos.y * 16);
  53. else reader_ptr->drawBMP(picture, *tft_ptr, pos.x * 16, pos.y * 16);
  54. }
  55.  
  56. void setOnGate(bool state) {
  57. on_gate = state;
  58. }
  59.  
  60. bool getOnGate() const {
  61. return on_gate;
  62. }
  63.  
  64. Pos getPos() const {
  65. return pos;
  66. }
  67.  
  68. void setPos(Pos _pos) {
  69. pos.x = _pos.x;
  70. pos.y = _pos.y;
  71. draw();
  72. }
  73. };
  74.  
  75. class Gate {
  76. private:
  77. Adafruit_ST7735 *tft_ptr;
  78. LittleFS_ImageReader *reader_ptr;
  79. Pos pos;
  80. String picture = "/gate.bmp";
  81.  
  82. public:
  83. Gate(Adafruit_ST7735 *_tft_ptr, LittleFS_ImageReader *_reader_ptr, int x, int y) {
  84. tft_ptr = _tft_ptr;
  85. reader_ptr = _reader_ptr;
  86. pos.x = x;
  87. pos.y = y;
  88. }
  89.  
  90. void draw() const {
  91. reader_ptr->drawBMP(picture, *tft_ptr, pos.x * 16, pos.y * 16);
  92. }
  93.  
  94. Pos getPos() const {
  95. return pos;
  96. }
  97. };
  98.  
  99. class Man {
  100. private:
  101. Adafruit_ST7735 *tft_ptr;
  102. LittleFS_ImageReader *reader_ptr;
  103. Pos pos;
  104. String picture = "/man.bmp";
  105.  
  106. public:
  107. Man(Adafruit_ST7735 *_tft_ptr, LittleFS_ImageReader *_reader_ptr, int x, int y) {
  108. Serial.println("Man constructor");
  109. tft_ptr = _tft_ptr;
  110. reader_ptr = _reader_ptr;
  111. pos.x = x;
  112. pos.y = y;
  113. }
  114.  
  115. void draw() const {
  116. reader_ptr->drawBMP(picture, *tft_ptr, pos.x * 16, pos.y * 16);
  117. }
  118.  
  119. Pos getPos() const {
  120. return pos;
  121. }
  122.  
  123. void setPos(Pos _pos) {
  124. tft_ptr->fillRect(pos.x * 16, pos.y * 16, 16, 16, ST77XX_BLACK);
  125. pos.x = _pos.x;
  126. pos.y = _pos.y;
  127. draw();
  128. }
  129. };
  130.  
  131. const int boxes_number = 3;
  132. Box boxes[boxes_number] = {
  133. {&tft, &reader, 3, 4},
  134. {&tft, &reader, 4, 6},
  135. {&tft, &reader, 2, 7},
  136. };
  137.  
  138. const int gates_number = 3;
  139. Gate gates[gates_number] = {
  140. {&tft, &reader, 6, 3},
  141. {&tft, &reader, 6, 4},
  142. {&tft, &reader, 6, 5},
  143. };
  144.  
  145. Man man(&tft, &reader, 5, 6);
  146.  
  147. void setup() {
  148. Serial.begin(9600);
  149. Serial.println();
  150. Serial.println("Setup");
  151. LittleFS.begin();
  152. tft.initR(INITR_BLACKTAB);
  153. tft.setRotation(2);
  154. tft.fillScreen(ST77XX_BLACK);
  155.  
  156. for (int y = 0; y < 10; y++) {
  157. for (int x = 0; x < 10; x++) {
  158. if (Map[y][x]) reader.drawBMP("/brick.bmp", tft, x * 16, y * 16);
  159. }
  160. }
  161.  
  162. for (int i = 0; i < boxes_number; i++) {
  163. boxes[i].draw();
  164. }
  165.  
  166. for (int i = 0; i < gates_number; i++) {
  167. gates[i].draw();
  168. }
  169.  
  170. man.draw();
  171. }
  172.  
  173. void loop() {
  174. }

Создаем массив объектов — ящиков c координатами: 3, 4; 4, 6; 2, 7.

  1. const int boxes_number = 3;
  2. Box boxes[boxes_number] = {
  3. {&tft, &reader, 3, 4},
  4. {&tft, &reader, 4, 6},
  5. {&tft, &reader, 2, 7},
  6. };

Создаем массив объектов — целей c координатами: 6, 3; 6, 4; 6, 5.

  1. const int gates_number = 3;
  2. Gate gates[gates_number] = {
  3. {&tft, &reader, 6, 3},
  4. {&tft, &reader, 6, 4},
  5. {&tft, &reader, 6, 5},
  6. };

Создаем кладовщика:

  1. Man man(&tft, &reader, 5, 6);

Вызываем метод draw() для элементов массивов ящиков и целей, и для кладовщика.

  1. for (int i = 0; i < boxes_number; i++) {
  2. boxes[i].draw();
  3. }
  4.  
  5. for (int i = 0; i < gates_number; i++) {
  6. gates[i].draw();
  7. }
  8.  
  9. man.draw();

Теперь мы имеем игровое поле с лабиринтом, кладовщика, ящики и цели. В следующем эксперименте мы научим кладовщика двигаться при нажатиях на кнопки.