Making my code faster

Greetings,

Im trying to make a Minecraft-like game on my ESP32-2432S028R (The Cheap Yellow Display), The game uses Isometric projection, The world is 30x30x18, and it has 4 diifferent blocks, as you read in the title, drawing the whole world everytime i move, place or break a block is a bit slow, like 0.5s, and I really don't like watching the entire world be redrawn everytime. I work on this code for a bit now and I can't find any way to make it faster, even a normal frame buffer will work!

The only problem for the frame buffer is memory, I've tried making a big sprite covering the screen, writing the frame in there and then pushing it on the screen, but it doesn't have enough memory, making the World array smaller worked, but I don't want a 5x5x5 world to play.

Heres a video:

The cursor is a black block outline moving in 6 directions depending of the side of the screen you touch.

When I move the world is still, but when the cursor reaches the border then the world moves too in the same direction (the offsets are called pushX and pushZ)

The cose uses the TFT_eSPI library (GitHub - Bodmer/TFT_eSPI: Arduino and PlatformIO IDE compatible TFT library optimised for the Raspberry Pi Pico (RP2040), STM32, ESP8266 and ESP32 that supports different driver chips)

Every block in the world is saved in a 3 dimensional array (uint8_t WD[18][30][30])
The code cycles trough the whole array every time I move, place or break a block.

The value in the array is the block type, and the position of the value in the array is the position in the 3 Dimensional Isometric GRID.

The TFT_eSPI llibrary uses sprites, so every block is a sprite drawn on a certain position.

Every block covered from the 3 front sides don't get drawn on the screen (I needed to check if the block is on the side of the array, so to always be shown).

Im gonna attach the part of the code that I need help with: (The drawWorld function is called every time I move, place or break a block)

void drawCube(int8_t t, int8_t x, int8_t y, int8_t z) { //Drawing the cube pixel by pixel
  int xx = (x - z) << 4;  //Calculation to find the X position of the of the block in the isometric grid realatively to the world array position
  int yy = ((x + z) << 3) + (y << 4);  //Calculation to find the Y position of the of the block in the isometric grid realatively to the world array position

  switch (t) { //check the block type
    case 1:
      grassblock.pushSprite(xx, yy, TFT_BLACK); //Draws the block on the screen according to the xx and yy coordinates of the screen (TFT_BLACK just makes black pixels of the sprite transparent)
      break;

    case 2:
      dirtblock.pushSprite(xx, yy, TFT_BLACK);
      break;

    case 3:
      stoneblock.pushSprite(xx, yy, TFT_BLACK);
      break;

    case 4:
      bedrockblock.pushSprite(xx, yy, TFT_BLACK);
      break;
    case 5:
      cursorblock.pushSprite(xx, yy, TFT_BLACK);
      break;
  }
}

void drawWorld() {
  tft.fillScreen(TFT_SKYBLUE); //Clear the screen to eliminate excess pixels
  WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = 5; //Sets the cursor sprite in the world if moved
  for(int8_t y=17; y>=0; --y) { //Start drawing the world from the bottom, so blocks don't overlap when drawing
    for(int8_t z=0; z<29; ++z) { //for reference WD[0, 0, 0] is the top back block, and WD[17, 29, 29] is the front bottom block
      for(int8_t x=0; x<29; ++x) {
        if(WD[y-1][z][x] == 0 or WD[y][z+1][x] == 0 or WD[y][z][x+1] == 0 or WD[y-1][z][x] == 5 or WD[y][z+1][x] == 5 or WD[y][z][x+1] == 5 or x == 29 or z == 29 or y == 0/* and (x < pushX + 10 and x > pushX - 10 and z < pushZ + 10 and z > pushZ - 10)*/) { //checks if the block is hidden from the sides
          if(WD[y][z][x] != 0) {  //If in the world array the block is 0 it's air so do not draw it
            drawCube(WD[y][z][x], x+5-pushX, y-5, z-4-pushZ); //WD[y][z][x] returns the block, and the 3 other inegers are the x, y and z position of the block, pushX and pushZ are offsets to move the world when the cursor moves. (the other numbers are just for offsetting the world on the center)
          }
        }
      }
    }
  }
}

Any help is apprecieated!
(sorry for my bad english writing, I'ts not my main language.)

Some help please?

2 Likes

IMO, the only way to stop the screen flickering is to use a sprite that covers the entire screen.

Small block sprites can be pushed to a sprite that covers the entire screen. This will be dramatically faster because it will be pushed from memory to memory. (However, DMA will not work.)

So, there are two ways to go. Move all static data to flash memory to increase the free space in main memory, or allocate all sprites in PSRAM.

You have probably already considered the former. On the other hand, unfortunately, CYD does not have PSRAM (because it's cheap!), so you will need to change to a microcontroller that has PSRAM, such as ESP32-S3. I have seen an article about modifying CYD and replacing its MCU with ESP32-S3 to utilize PSRAM, but it is not easy.

For reference, in TFT_eSPI, if the conditions are met, sprites will be created in PSRAM by the following code.

One more thing. Don't expect too much effect...

void drawWorld() {
  tft.startWrite();
  ...
  tft.endWrite();
}

Already tried, I don't know if it's memory OR the IDE restricting it (said by this post: ESP32 with TFT_eSPI Lib - Size of Sprites)

exactly, it's not easy, after studying the datasheets the pin layout it's different, but with a PCB adjusting the pins from the ESP32-s3 to the CYD PCB then it's possible.

This is the fastest I've got my code to go:

inline void drawCube(int8_t t, int8_t x, int8_t y, int8_t z) {
  // Precompute screen positions outside the switch
  int16_t xx = (x - z) << 4;
  int16_t yy = ((x + z) << 3) + (y << 4);

  // Directly draw the block based on type
  static TFT_eSprite* sprites[] = { nullptr, &grassblock, &dirtblock, &stoneblock, &bedrockblock, &cobblestoneblock, &planksblock, &redbricksblock, &cursorblock };
  sprites[t]->pushSprite(xx, yy, TFT_BLACK);
}

void drawWorld() {
  tft.startWrite();
  tft.fillScreen(TFT_SKYBLUE); //Clear the screen to eliminate excess pixels
  if (WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] == 0 or WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] == 8) {
    WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = 8; //Sets the cursor sprite in the world if moved
  }
  for(int8_t y=17; y>=0; --y) { //Start drawing the world from the bottom, so blocks don't overlap when drawing
    for(int8_t z=0; z<29; ++z) { //for reference WD[0, 0, 0] is the top back block, and WD[17, 29, 29] is the front bottom block
      for(int8_t x=0; x<29; ++x) {
        if(WD[y-1][z][x] == 0 or WD[y][z+1][x] == 0 or WD[y][z][x+1] == 0 or WD[y-1][z][x] == 8 or WD[y][z+1][x] == 8 or WD[y][z][x+1] == 8 or x == 29 or z == 29 or y == 0/* and (x < pushX + 10 and x > pushX - 10 and z < pushZ + 10 and z > pushZ - 10)*/) { //checks if the block is hidden from the sides
          if(WD[y][z][x] != 0) {  //If in the world array the block is 0 it's air so do not draw it
            drawCube(WD[y][z][x],x+5-pushX,y-5,z-4-pushZ); //WD[y][z][x] returns the block, and the 3 other inegers are the x, y and z position of the block, pushX and pushZ are offsets to move the world when the cursor moves. (the other numbers are just for offsetting the world on the center)
          }
          if(x==cursorPos[0] and y==cursorPos[1] and z==cursorPos[2]) {
            drawCube(8, cursorPos[0]+5-pushX, cursorPos[1]-5, cursorPos[2]-4-pushZ);
          }
        }
      }
    }
  }
  tft.endWrite();
}

Maybve using linked lists instead of arrays should read them faster?

It may be effective to eliminate switch-case statements and make them into arrays.

Also, since how the compiler handles inline declarations is up to the compiler, it is good to declare the always_inline attribute as follows just to be safe.

inline void drawCube(int8_t t, int8_t x, int8_t y, int8_t z) __attribute__((always_inline));
inline void drawCube(int8_t t, int8_t x, int8_t y, int8_t z) {
...
}

It may also be better to change all int8_t to int16_t or int. However, I can't say for sure without seeing the assembler code...

Even with all of the above, the flickering will not fundamentally become less noticeable unless you reduce the 30x30x18=16200 accesses to the SPI bus.

So, even if it's not possible to cover the entire screen, how about allocating a slightly smaller sprite.

Looking at the triple loop of x, y, z, it seems like there would be no problem if you switched the order. So I tried it like this.

void setup() {
  Serial.begin(115200);
  delay(2000);
}

inline void drawCube(int t, int x, int y, int z) __attribute__((always_inline));
inline void drawCube(int t, int x, int y, int z) {
  int xx = (x - z) << 4;
  int yy = ((x + z) << 3) + (y << 4);
  Serial.printf("xx: %d, yy: %d\n", xx, yy);
}

void loop() {
  int t = 0;
  for(int x = 0; x < 29; ++x) {
    for(int z = 0; z < 29; ++z) {
      for(int y = 17; y >= 0; --y) {
        drawCube(t, x, y, z);
        delay(100);
      }
    }
  }
}

The result is as follows.

xx: 0, yy: 272
xx: 0, yy: 256
xx: 0, yy: 240
xx: 0, yy: 224
xx: 0, yy: 208
xx: 0, yy: 192
xx: 0, yy: 176
xx: 0, yy: 160
xx: 0, yy: 144
xx: 0, yy: 128
xx: 0, yy: 112
xx: 0, yy: 96
xx: 0, yy: 80
xx: 0, yy: 64
xx: 0, yy: 48
xx: 0, yy: 32
xx: 0, yy: 16
xx: 0, yy: 0
xx: -16, yy: 280
xx: -16, yy: 264
xx: -16, yy: 248
xx: -16, yy: 232
xx: -16, yy: 216
xx: -16, yy: 200
xx: -16, yy: 184
xx: -16, yy: 168
xx: -16, yy: 152
xx: -16, yy: 136
xx: -16, yy: 120
xx: -16, yy: 104
xx: -16, yy: 88
xx: -16, yy: 72
xx: -16, yy: 56
xx: -16, yy: 40
xx: -16, yy: 24
xx: -16, yy: 8
xx: -32, yy: 288
xx: -32, yy: 272
xx: -32, yy: 256
xx: -32, yy: 240
xx: -32, yy: 224
...

You may be able to allocate a small sprite that is 16 pixels wide and 16x18=288 pixels high, and once you've finished drawing the 18 blocks of sprite there, push the small sprite to the LCD. It's a "sprite on sprite". (And perhaps off-screen sprites can be omitted.)

This will reduce the number of interactions with the display over the SPI bus to 30x30=900.

Good luck!

If there are only 4 block types including no block, you only need 2 bits per block, 4 blocks per byte, you can have 2 buffers. With 16 bits you can store 2x2x2 blocks as a unit.

But why redraw all for one change? You can only change a visible block anyway, right? So just change one, calculating where it is should be quicker than redrawing them all.

After a lot of debugging I've managed to make it work...
but it's slower than before, heres the code if you want to take a look:

#include <SPI.h>
#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>
#include "gb.h"
#include "sb.h"
#include "db.h"
#include "bb.h"
#include "cb.h"
#include "pb.h"
#include "rbb.h"
#include "tnt.h"
#include "ct.h"
#include "fr.h"
#include "ss.h"
#include "ol.h"
#include "cursor.h"
#include "hotslot.h"

TFT_eSPI tft = TFT_eSPI();
TFT_eSprite columnbuffer = TFT_eSprite(&tft);
TFT_eSprite grassblock = TFT_eSprite(&tft);
TFT_eSprite dirtblock = TFT_eSprite(&tft);
TFT_eSprite stoneblock = TFT_eSprite(&tft);
TFT_eSprite bedrockblock = TFT_eSprite(&tft);
TFT_eSprite cursorblock = TFT_eSprite(&tft);
TFT_eSprite cobblestoneblock = TFT_eSprite(&tft);
TFT_eSprite planksblock = TFT_eSprite(&tft);
TFT_eSprite redbricksblock = TFT_eSprite(&tft);
TFT_eSprite tnt = TFT_eSprite(&tft);
TFT_eSprite craftingtable = TFT_eSprite(&tft);
TFT_eSprite furnace = TFT_eSprite(&tft);
TFT_eSprite smoothstone = TFT_eSprite(&tft);
TFT_eSprite oaklog = TFT_eSprite(&tft);

// Touchscreen pins
#define XPT2046_IRQ 36   // T_IRQ
#define XPT2046_MOSI 32  // T_DIN
#define XPT2046_MISO 39  // T_OUT
#define XPT2046_CLK 25   // T_CLK
#define XPT2046_CS 33    // T_CS

SPIClass touchscreenSPI = SPIClass(VSPI);
XPT2046_Touchscreen touchscreen(XPT2046_CS, XPT2046_IRQ);

#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define FONT_SIZE 2

int x, y, z;

int xx = (x - z) << 4;
int yy = ((x + z) << 3) + (y << 4);

int pushX = 0;
int pushZ = 0;

int block = 1;

int oldBlock = 0;

uint8_t WD[18][30][30]={0}; //World Array

int8_t cursorPos[3]={0,11,0};
int8_t cursorScreenPos[3]={0,0,0};

inline void drawCube(int8_t t, int8_t x, int8_t y, int8_t z) {
  // Precompute screen positions outside the switch
  yy = ((x + z) << 3) + (y << 4);

  // Directly draw the block based on type
  static TFT_eSprite* sprites[] = { nullptr, &grassblock, &dirtblock, &stoneblock, &bedrockblock, &cobblestoneblock, &planksblock, &redbricksblock, &tnt, &craftingtable, &furnace, &smoothstone, &oaklog, &cursorblock };
  sprites[t]->pushToSprite(&columnbuffer, 0, yy, TFT_BLACK);
}

void drawWorld() {
  tft.startWrite();
  tft.fillScreen(TFT_SKYBLUE); //Clear the screen to eliminate excess pixels
  if (WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] == 0 or WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] == 13) {
    WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = 13; //Sets the cursor sprite in the world if moved
  }
  for(int8_t z=0; z<29; ++z) { //Start drawing the world from the bottom, so blocks don't overlap when drawing
    for(int8_t x=0; x<29; ++x) { //for reference WD[0, 0, 0] is the top back block, and WD[17, 29, 29] is the front bottom block
      for(int8_t y=17; y>=0; --y) {
        if(WD[y-1][z][x] == 0 or WD[y][z+1][x] == 0 or WD[y][z][x+1] == 0 or WD[y-1][z][x] == 13 or WD[y][z+1][x] == 13 or WD[y][z][x+1] == 13 or x == 29 or z == 29 or y == 0/* and (x < pushX + 10 and x > pushX - 10 and z < pushZ + 10 and z > pushZ - 10)*/) { //checks if the block is hidden from the sides
          if(WD[y][z][x] != 0) {  //If in the world array the block is 0 it's air so do not draw it
            drawCube(WD[y][z][x],x+10-pushX,y-5,z-9-pushZ); //WD[y][z][x] returns the block, and the 3 other inegers are the x, y and z position of the block, pushX and pushZ are offsets to move the world when the cursor moves. (the other numbers are just for offsetting the world on the center)
          }
          if(x==cursorPos[0] and y==cursorPos[1] and z==cursorPos[2]) {
            drawCube(13, cursorPos[0]+10-pushX, cursorPos[1]-5, cursorPos[2]-9-pushZ);
          }
        }
      }
      xx = (x+10-pushX - z-9-pushZ) << 4;
      if(xx <= SCREEN_WIDTH && xx >= 0) {
        columnbuffer.pushSprite(xx, 0, TFT_BLACK);
        columnbuffer.fillSprite(TFT_BLACK);
      }
    }
  }
  tft.endWrite();
}

void drawGUI() {
  //tft.fillRect(SCREEN_WIDTH/2 - 40, SCREEN_HEIGHT - 80, 80, 80, TFT_BLACK);
  tft.pushImage(0, SCREEN_HEIGHT-64, 64, 64, hotslot, TFT_BLACK);
  switch(block) {
    case 1:
      grassblock.pushSprite(16, SCREEN_HEIGHT-48, TFT_BLACK);
      break;
    case 2:
      dirtblock.pushSprite(16, SCREEN_HEIGHT-48, TFT_BLACK);
      break;
    case 3:
      stoneblock.pushSprite(16, SCREEN_HEIGHT-48, TFT_BLACK);
      break;
    case 4:
      bedrockblock.pushSprite(16, SCREEN_HEIGHT-48, TFT_BLACK);
      break;
    case 5:
      cobblestoneblock.pushSprite(16, SCREEN_HEIGHT-48, TFT_BLACK);
      break;
    case 6:
      planksblock.pushSprite(16, SCREEN_HEIGHT-48, TFT_BLACK);
      break;
    case 7:
      redbricksblock.pushSprite(16, SCREEN_HEIGHT-48, TFT_BLACK);
      break;
    case 8:
      tnt.pushSprite(16, SCREEN_HEIGHT-48, TFT_BLACK);
      break;
    case 9:
      craftingtable.pushSprite(16, SCREEN_HEIGHT-48, TFT_BLACK);
      break;
    case 10:
      furnace.pushSprite(16, SCREEN_HEIGHT-48, TFT_BLACK);
      break;
    case 11:
      smoothstone.pushSprite(16, SCREEN_HEIGHT-48, TFT_BLACK);
      break;
    case 12:
      oaklog.pushSprite(16, SCREEN_HEIGHT-48, TFT_BLACK);
      break;
  }
}

void generateWorld() { //Generating the world
  for(int8_t i=17; i>0; --i) {
    for(int8_t j=0; j<29; ++j) {
      for(int8_t k=0; k<29; ++k) {
        if(i==17){
          WD[i][j][k] = 4;
        }
        else if(i==16){
          WD[i][j][k] = 3;
        }
        else if(i==15){
          WD[i][j][k] = 3;
        }
        else if(i==14){
          WD[i][j][k] = 2;
        }
        else if(i==13){
          WD[i][j][k] = 2;
        }
        else if(i==12){
          WD[i][j][k] = 1;
        }
      }
    }
  }
}

void drawInterface() {
  //tft.drawRect();
}

void setup() {
  Serial.begin(115200);
  pinMode(22, INPUT_PULLDOWN);
  pinMode(35, INPUT_PULLDOWN);
  touchscreenSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
  touchscreen.begin(touchscreenSPI);
  touchscreen.setRotation(1);
  tft.init();
  tft.initDMA();
  tft.setRotation(1);
  tft.fillScreen(TFT_SKYBLUE);
  tft.setTextColor(TFT_BLACK, TFT_SKYBLUE);
  int centerX = SCREEN_WIDTH / 2;
  int centerY = SCREEN_HEIGHT / 2;
  //tft.setSPISpeed(40000000); // 40MHz
  tft.setSwapBytes(true);
  grassblock.createSprite(32, 32);
  grassblock.setSwapBytes(true);
  dirtblock.createSprite(32, 32);
  dirtblock.setSwapBytes(true);
  stoneblock.createSprite(32, 32);
  stoneblock.setSwapBytes(true);
  bedrockblock.createSprite(32, 32);
  bedrockblock.setSwapBytes(true);
  cursorblock.createSprite(32, 32);
  cursorblock.setSwapBytes(true);
  tnt.createSprite(32, 32);
  tnt.setSwapBytes(true);
  craftingtable.createSprite(32, 32);
  craftingtable.setSwapBytes(true);
  furnace.createSprite(32, 32);
  furnace.setSwapBytes(true);
  smoothstone.createSprite(32, 32);
  smoothstone.setSwapBytes(true);
  oaklog.createSprite(32, 32);
  oaklog.setSwapBytes(true);
  cobblestoneblock.createSprite(32, 32);
  cobblestoneblock.setSwapBytes(true);
  planksblock.createSprite(32, 32);
  planksblock.setSwapBytes(true);
  redbricksblock.createSprite(32, 32);
  redbricksblock.setSwapBytes(true);
  columnbuffer.createSprite(32, 288);
  cursorblock.setSwapBytes(true);
  grassblock.pushImage(0, 0, 32, 32, gb);
  dirtblock.pushImage(0, 0, 32, 32, db);
  stoneblock.pushImage(0, 0, 32, 32, sb);
  bedrockblock.pushImage(0, 0, 32, 32, bb);
  cobblestoneblock.pushImage(0, 0, 32, 32, cb);
  tnt.pushImage(0, 0, 32, 32, tn);
  craftingtable.pushImage(0, 0, 32, 32, ct);
  furnace.pushImage(0, 0, 32, 32, fr);
  smoothstone.pushImage(0, 0, 32, 32, ss);
  oaklog.pushImage(0, 0, 32, 32, ol);
  planksblock.pushImage(0, 0, 32, 32, pb);
  redbricksblock.pushImage(0, 0, 32, 32, rbb);
  cursorblock.pushImage(0, 0, 32, 32, cursor);
  generateWorld();
  tft.drawCentreString("Welcome!", centerX, 30, FONT_SIZE);
  tft.drawCentreString("Touch screen to play", centerX, centerY, FONT_SIZE);
  tft.drawRect(0, 0, SCREEN_WIDTH/2 - 40, SCREEN_HEIGHT/2, TFT_BLACK);
  tft.drawRect(SCREEN_WIDTH/2 + 40, 0, SCREEN_WIDTH/2 - 40, SCREEN_HEIGHT/2, TFT_BLACK);
  tft.drawRect(0, SCREEN_HEIGHT/2, SCREEN_WIDTH/2 - 40, SCREEN_HEIGHT/2, TFT_BLACK);
  tft.drawRect(SCREEN_WIDTH/2 + 40, SCREEN_HEIGHT/2, SCREEN_WIDTH/2 - 40, SCREEN_HEIGHT/2, TFT_BLACK);
}

void loop() {
  if(digitalRead(22) == HIGH) {
    WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = block;
    oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
    drawWorld();
    drawGUI();
  }
  if(digitalRead(35) == HIGH) {
    WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = 0;
    oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
    drawWorld();
    drawGUI();
  }
  // Checks if Touchscreen was touched, and prints X, Y and Pressure (Z) info on the TFT display and Serial Monitor
  if (touchscreen.tirqTouched() && touchscreen.touched()) {
    // Get Touchscreen points
    TS_Point p = touchscreen.getPoint();
    // Calibrate Touchscreen points with map function to the correct width and height
    x = map(p.x, 200, 3700, 1, SCREEN_WIDTH);
    y = map(p.y, 240, 3800, 1, SCREEN_HEIGHT);
    z = p.z;
    if (x < 64 and y > SCREEN_HEIGHT - 64) {
      if(block < 12) {
        block++;
      }
      else {
        block = 1;
      }
    }
    else if (x < SCREEN_WIDTH/2 - 40 and y < SCREEN_HEIGHT/2 and cursorPos[0] > 0) {
      WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = oldBlock;
      cursorPos[0]--;
      if(cursorScreenPos[0] > -3) {
        cursorScreenPos[0]--;
      }
      else {
        pushX--;
      }
      oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
      drawWorld();
    }
    else if (x > SCREEN_WIDTH/2 + 40 and y < SCREEN_HEIGHT/2 and cursorPos[2] > 0) {
      WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = oldBlock;
      cursorPos[2]--;
      if(cursorScreenPos[2] > -3) {
        cursorScreenPos[2]--;
      }
      else {
        pushZ--;
      }
      oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
      drawWorld();
    }
    else if (x < SCREEN_WIDTH/2 - 40 and y > SCREEN_HEIGHT/2 and cursorPos[2] < 28) {
      WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = oldBlock;
      cursorPos[2]++;
      if(cursorScreenPos[2] < 3) {
        cursorScreenPos[2]++;
      }
      else {
        pushZ++;
      }
      oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
      drawWorld();
    }
    else if (x > SCREEN_WIDTH/2 + 40 and y > SCREEN_HEIGHT/2 and cursorPos[0] < 28) {
      WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = oldBlock;
      cursorPos[0]++;
      if(cursorScreenPos[0] < 3) {
        cursorScreenPos[0]++;
      }
      else {
        pushX++;
      }
      oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
      drawWorld();
    }
    else if (x < SCREEN_WIDTH/2 + 40 and x > SCREEN_WIDTH/2 - 40 and y > SCREEN_HEIGHT/2 and cursorPos[1] < 17) {
      WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = oldBlock;
      cursorPos[1]++;
      oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
      drawWorld();
    }
    else if (x < SCREEN_WIDTH/2 + 40 and x > SCREEN_WIDTH/2 - 40 and y < SCREEN_HEIGHT/2 and cursorPos[1] > 0) {
      WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = oldBlock;
      cursorPos[1]--;
      oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
      drawWorld();
    }
    drawGUI();
    delay(100);
  }
}

Just made more blocks.

It's not as easy as it sounds,I need to check every block in front of the camera and select witch side to render, this video explains it better, it even explains a solution to make it faster but I couldn't make it in C: Isometric Minecraft

Do you really have to check every block before the camera when the block you change must be visible to choose?
When you choose it, you identify the block and position.. you have already done the work for that.

Oh, sorry about that.

I picked up my CYD, which I hadn't used since I bought it a few months ago, and tried to recreate your program (I replaced the images with some PNGs). The flickering is certainly noticeable.

So I used the code below and put ShowInfo() at the end of setup() to check the memory usage.

void ShowInfo(void) {
  Serial.printf("MCU model   : %s R%d\n", ESP.getChipModel(), ESP.getChipRevision());
  Serial.printf("ESP-IDF ver : %d.%d.%d\n", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH);
  Serial.printf("Core 1 stack: %7d\n", uxTaskGetStackHighWaterMark(NULL));
  Serial.printf("Heap total  : %7d\n", ESP.getHeapSize());
  Serial.printf("Heap lowest : %7d\n", ESP.getMinFreeHeap());
  Serial.printf("PSRAM total : %7d\n", ESP.getPsramSize());
  Serial.printf("PSRAM lowest: %7d\n", ESP.getMinFreePsram());
  Serial.printf("Sketch free : %7d\n", ESP.getFreeSketchSpace());
  Serial.printf("Sketch size : %7d\n", ESP.getSketchSize());

  // https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/mem_alloc.html
  Serial.printf("Min heap since boot: %7d\n", esp_get_minimum_free_heap_size());
  Serial.printf("Free internal heap : %7d\n", esp_get_free_internal_heap_size());
  Serial.printf("MALLOC_CAP_INTERNAL: %7d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL));
  Serial.printf("MALLOC_CAP_DMA     : %7d\n", heap_caps_get_minimum_free_size(MALLOC_CAP_DMA));
  Serial.printf("Total image size   : %d\n", sizeof(gb) + sizeof(sb) + sizeof(db) + sizeof(bb) + sizeof(cb) + sizeof(pb) + sizeof(rbb) + sizeof(tn) + sizeof(ct) + sizeof(fr) + sizeof(ss) + sizeof(ol) + sizeof(cursor) + sizeof(hotslot));
}

The notable to pay attention to is Min heap since boot, which indicates the unused minimum size of the heap area. The original is 147816, so it is true that a 320x240 sprite image (which requires more than 153600) cannot be allocated.

I think the 14 sprites are using the most space in the heap area.

So I tried reducing each 32x32 sprite to 24x24, then Min heap since boot increased to 155328, so I reduced the size of the display area to 256x192 and enlarged columnbuffer from 32x288 to 256x192. At this time, Min heap since boot is 75244, that is still some room.

Of course, I deleted the code that pushes columnbuffer to tft in the middle of the code, and pushed it to tft just before tft.endWrite(). By doing this, the flickering is neatly eliminated.

However, the processing time of drawWorld() is still 297msec, and the FPS is only about 3. I think this is because drawCube() is still drawing a lot of outside the range of the screen. I tried narrowing the range with the following code, and the processing time of drawWorld() was reduced to 32msec.

#define SPRITE_SIZE 24

inline void drawCube(int8_t t, int8_t x, int8_t y, int8_t z) {
  // Precompute screen positions outside the switch
  int16_t xx = (x - z) << 4;
  int16_t yy = ((x + z) << 3) + (y << 4);

  if (xx + SPRITE_SIZE > 0 && xx < SCREEN_WIDTH && yy + SCREEN_HEIGHT> 0 && yy < SCREEN_HEIGHT) {
    // Directly draw the block based on type
    static TFT_eSprite* sprites[] = { nullptr, &grassblock, &dirtblock, &stoneblock, &bedrockblock, &cobblestoneblock, &planksblock, &redbricksblock, &tnt, &craftingtable, &furnace, &smoothstone, &oaklog, &cursorblock };
    sprites[t]->pushToSprite(&columnbuffer, xx, yy, TFT_BLACK);    
  }
}

I don't know if this code is correct. And of course, if the player moves, the range that needs to be drawn will increase. If the range that needs to be drawn increases or decreases, the frame rate will not be constant, but I think it is possible to maintain a constant frame rate by applying the "Blink without delay" example.

I'm sure you're reluctant to shrink the screen to 256x192, but I think you can go ahead with it for now.

Also, regarding @GoForSmoke 's advice, I can assume you're narrowing it down with the if statements in drawWorld(), but I would recommend narrowing it down further in drawCube().

I know this sounds irresponsible since I simply copy-pasted your code and have little understanding of your logic, but I think it's best if you can narrow down x, y, and z to the range you need first.

Good luck.

Just two more minor points.

  1. Clearing the entire screen with sky blue before drawing the field will cause flickering in drawWorld().
    The sky can be limited to just a portion of the screen. The flickering is reduced in the field where the texture doesn't change.
    If you want to apply the sprite to the entire screen, just clear the sprite.
  1. In the code below, when y is 0, y-1 becomes negative, which means that the array is being accessed outside its range. It won't crash, but but I feels weird.
  for(int8_t z=0; z<29; ++z) { //Start drawing the world from the bottom, so blocks don't overlap when drawing
    for(int8_t x=0; x<29; ++x) { //for reference WD[0, 0, 0] is the top back block, and WD[17, 29, 29] is the front bottom block
      for(int8_t y=17; y>=0; --y) {
        if(WD[y-1][z][x] == 0 or WD[y][z+1][x] == 0 or WD[y][z][x+1] == 0 or WD[y-1][z][x] == 13 or WD[y][z+1][x] == 13 or WD[y][z][x+1] == 13 or x == 29 or z == 29 or y == 0/* and (x < pushX + 10 and x > pushX - 10 and z < pushZ + 10 and z > pushZ - 10)*/) { //checks if the block is hidden from the sides
          if(WD[y][z][x] != 0) {  //If in the world array the block is 0 it's air so do not draw it
            drawCube(WD[y][z][x],x+10-pushX,y-5,z-9-pushZ); //WD[y][z][x] returns the block, and the 3 other inegers are the x, y and z position of the block, pushX and pushZ are offsets to move the world when the cursor moves. (the other numbers are just for offsetting the world on the center)
          }
          if(x==cursorPos[0] and y==cursorPos[1] and z==cursorPos[2]) {
            drawCube(13, cursorPos[0]+10-pushX, cursorPos[1]-5, cursorPos[2]-9-pushZ);
          }
        }
      }
    ...

I'd like maintaining the block size to 32x32 to have more fidelty to the real game.

Unfortunatly, the resolution is static, I need a way to lower the resolution of the display and STILL take the entire screen so it zooms in the world a bit, do you know if it's possible?

I've just implemented that, it didn't make it a lot faster but it's ok!

I have this idea:
I create the framebuffer sprite at the start and then I create the sprite I need before pushing it to the frame buffer, then after I've pushed the sprite to the frame buffer I delete the sprite and then create the other sprite I need before pushing it to the buffer, and it goes on...
So i only need one sprite for the block and another sprite for the frame buffer.
I'm afraid that it will solve the problem, but it will take more time to render a new frame because I need to load the image into the sprite when I create it (and I need to do it for every block), and then delete it to create the next sprite, and it takes time, while with your method I do not need to delete and create the same sprites everytime.

I've just tried that but it STILL doesn't have enough memory to create the frame buffer!
I've read on this post (ESP32 with TFT_eSPI Lib - Size of Sprites) that it's an IDE limitation, and after trying i've managed to make the frame buffer sprite max 220x240.

The main problems are:

  1. Rendering time
  2. I want to lower the resolution of the display and take the full screen so it zooms in
  3. It doesn't create the frame buffer 320x240!!!
#include <SPI.h>
#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>
#include "gb.h"
#include "sb.h"
#include "db.h"
#include "bb.h"
#include "cb.h"
#include "pb.h"
#include "rbb.h"
#include "tnt.h"
#include "ct.h"
#include "fr.h"
#include "ss.h"
#include "ol.h"
#include "cursor.h"
#include "hotslot.h"

TFT_eSPI tft = TFT_eSPI();
TFT_eSprite blockbuffer = TFT_eSprite(&tft);
TFT_eSprite framebuffer = TFT_eSprite(&tft);

// Touchscreen pins
#define XPT2046_IRQ 36   // T_IRQ
#define XPT2046_MOSI 32  // T_DIN
#define XPT2046_MISO 39  // T_OUT
#define XPT2046_CLK 25   // T_CLK
#define XPT2046_CS 33    // T_CS

SPIClass touchscreenSPI = SPIClass(VSPI);
XPT2046_Touchscreen touchscreen(XPT2046_CS, XPT2046_IRQ);

#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define FONT_SIZE 2

int x, y, z;

int pushX = 0;
int pushZ = 0;

int block = 1;

int oldBlock = 0;

uint8_t WD[18][30][30]={0}; //World Array

int8_t cursorPos[3]={0,11,0};
int8_t cursorScreenPos[3]={0,0,0};

inline void drawCube(int8_t t, int8_t x, int8_t y, int8_t z) {
  // Precompute screen positions outside the switch
  int16_t xx = (x - z) << 4;
  int16_t yy = ((x + z) << 3) + (y << 4);

  // Directly draw the block based on type
  if (xx + 32 > 0 && xx < SCREEN_WIDTH && yy + 32 > 0 && yy < SCREEN_HEIGHT) {
    switch(t) {
      case 1:
        blockbuffer.pushImage(0, 0, 32, 32, gb);
        break;
      case 2:
        blockbuffer.pushImage(0, 0, 32, 32, db);
        break;
      case 3:
        blockbuffer.pushImage(0, 0, 32, 32, sb);
        break;
      case 4:
        blockbuffer.pushImage(0, 0, 32, 32, bb);
        break;
      case 5:
        blockbuffer.pushImage(0, 0, 32, 32, cb);
        break;
      case 6:
        blockbuffer.pushImage(0, 0, 32, 32, pb);
        break;
      case 7:
        blockbuffer.pushImage(0, 0, 32, 32, rbb);
        break;
      case 8:
        blockbuffer.pushImage(0, 0, 32, 32, tn);
        break;
      case 9:
        blockbuffer.pushImage(0, 0, 32, 32, ct);
        break;
      case 10:
        blockbuffer.pushImage(0, 0, 32, 32, fr);
        break;
      case 11:
        blockbuffer.pushImage(0, 0, 32, 32, ss);
        break;
      case 12:
        blockbuffer.pushImage(0, 0, 32, 32, ol);
        break;
      case 13:
        blockbuffer.pushImage(0, 0, 32, 32, cursor);
        break;
    }
    blockbuffer.pushToSprite(&framebuffer, xx, yy, TFT_BLACK);
  }
}

void drawWorld() {
  tft.startWrite();
  if (WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] == 0 or WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] == 13) {
    WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = 13; //Sets the cursor sprite in the world if moved
  }
  framebuffer.fillSprite(TFT_SKYBLUE); //Clear the screen to eliminate excess pixels
  for(int8_t y=17; y>=0; --y) { //Start drawing the world from the bottom, so blocks don't overlap when drawing
    for(int8_t z=0; z<29; ++z) { //for reference WD[0, 0, 0] is the top back block, and WD[17, 29, 29] is the front bottom block
      for(int8_t x=0; x<29; ++x) {
        if(WD[y-1][z][x] == 0 or WD[y][z+1][x] == 0 or WD[y][z][x+1] == 0 or WD[y-1][z][x] == 13 or WD[y][z+1][x] == 13 or WD[y][z][x+1] == 13 or x == 29 or z == 29 or y == 0/* and (x < pushX + 10 and x > pushX - 10 and z < pushZ + 10 and z > pushZ - 10)*/) { //checks if the block is hidden from the sides
          if(WD[y][z][x] != 0) {  //If in the world array the block is 0 it's air so do not draw it
            drawCube(WD[y][z][x],x+5-pushX,y-5,z-4-pushZ); //WD[y][z][x] returns the block, and the 3 other inegers are the x, y and z position of the block, pushX and pushZ are offsets to move the world when the cursor moves. (the other numbers are just for offsetting the world on the center)
          }
          if(x==cursorPos[0] and y==cursorPos[1] and z==cursorPos[2]) {
            drawCube(13, cursorPos[0]+5-pushX, cursorPos[1]-5, cursorPos[2]-4-pushZ);
          }
        }
      }
    }
  }
  framebuffer.pushSprite(0, 0);
  tft.endWrite();
}

void drawGUI() {
  //tft.fillRect(SCREEN_WIDTH/2 - 40, SCREEN_HEIGHT - 80, 80, 80, TFT_BLACK);
  tft.pushImage(0, SCREEN_HEIGHT-64, 64, 64, hotslot, TFT_BLACK);
  switch(block) {
    case 1:
      tft.pushImage(16, SCREEN_HEIGHT-48, 32, 32, gb, TFT_BLACK);
      break;
    case 2:
      tft.pushImage(16, SCREEN_HEIGHT-48, 32, 32, db, TFT_BLACK);
      break;
    case 3:
      tft.pushImage(16, SCREEN_HEIGHT-48, 32, 32, sb, TFT_BLACK);
      break;
    case 4:
      tft.pushImage(16, SCREEN_HEIGHT-48, 32, 32, bb, TFT_BLACK);
      break;
    case 5:
      tft.pushImage(16, SCREEN_HEIGHT-48, 32, 32, cb, TFT_BLACK);
      break;
    case 6:
      tft.pushImage(16, SCREEN_HEIGHT-48, 32, 32, pb, TFT_BLACK);
      break;
    case 7:
      tft.pushImage(16, SCREEN_HEIGHT-48, 32, 32, rbb, TFT_BLACK);
      break;
    case 8:
      tft.pushImage(16, SCREEN_HEIGHT-48, 32, 32, tn, TFT_BLACK);
      break;
    case 9:
      tft.pushImage(16, SCREEN_HEIGHT-48, 32, 32, ct, TFT_BLACK);
      break;
    case 10:
      tft.pushImage(16, SCREEN_HEIGHT-48, 32, 32, fr, TFT_BLACK);
      break;
    case 11:
      tft.pushImage(16, SCREEN_HEIGHT-48, 32, 32, ss, TFT_BLACK);
      break;
    case 12:
      tft.pushImage(16, SCREEN_HEIGHT-48, 32, 32, ol, TFT_BLACK);
      break;
  }
}

void generateWorld() { //Generating the world
  for(int8_t i=17; i>0; --i) {
    for(int8_t j=0; j<29; ++j) {
      for(int8_t k=0; k<29; ++k) {
        if(i==17){
          WD[i][j][k] = 4;
        }
        else if(i==16){
          WD[i][j][k] = 3;
        }
        else if(i==15){
          WD[i][j][k] = 3;
        }
        else if(i==14){
          WD[i][j][k] = 2;
        }
        else if(i==13){
          WD[i][j][k] = 2;
        }
        else if(i==12){
          WD[i][j][k] = 1;
        }
      }
    }
  }
}

void drawInterface() {
  //tft.drawRect();
}

void setup() {
  Serial.begin(115200);
  pinMode(22, INPUT_PULLDOWN);
  pinMode(35, INPUT_PULLDOWN);
  touchscreenSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
  touchscreen.begin(touchscreenSPI);
  touchscreen.setRotation(1);
  tft.init();
  tft.initDMA();
  tft.setRotation(1);
  tft.fillScreen(TFT_SKYBLUE);
  tft.setTextColor(TFT_BLACK, TFT_SKYBLUE);
  int centerX = SCREEN_WIDTH / 2;
  int centerY = SCREEN_HEIGHT / 2;
  tft.setSwapBytes(true);
  blockbuffer.createSprite(32, 32);
  blockbuffer.setSwapBytes(true);
  if(framebuffer.createSprite(220, 240)) {
    Serial.println("Created");
  }
  else {
    Serial.println("Not enough memory");
  }
  generateWorld();
  tft.drawCentreString("Welcome!", centerX, 30, FONT_SIZE);
  tft.drawCentreString("Touch screen to play", centerX, centerY, FONT_SIZE);
  tft.drawRect(0, 0, SCREEN_WIDTH/2 - 40, SCREEN_HEIGHT/2, TFT_BLACK);
  tft.drawRect(SCREEN_WIDTH/2 + 40, 0, SCREEN_WIDTH/2 - 40, SCREEN_HEIGHT/2, TFT_BLACK);
  tft.drawRect(0, SCREEN_HEIGHT/2, SCREEN_WIDTH/2 - 40, SCREEN_HEIGHT/2, TFT_BLACK);
  tft.drawRect(SCREEN_WIDTH/2 + 40, SCREEN_HEIGHT/2, SCREEN_WIDTH/2 - 40, SCREEN_HEIGHT/2, TFT_BLACK);
}

void loop() {
  if(digitalRead(22) == HIGH) {
    WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = block;
    oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
    drawWorld();
    drawGUI();
  }
  if(digitalRead(35) == HIGH) {
    WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = 0;
    oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
    drawWorld();
    drawGUI();
  }
  // Checks if Touchscreen was touched, and prints X, Y and Pressure (Z) info on the TFT display and Serial Monitor
  if (touchscreen.tirqTouched() && touchscreen.touched()) {
    // Get Touchscreen points
    TS_Point p = touchscreen.getPoint();
    // Calibrate Touchscreen points with map function to the correct width and height
    x = map(p.x, 200, 3700, 1, SCREEN_WIDTH);
    y = map(p.y, 240, 3800, 1, SCREEN_HEIGHT);
    z = p.z;
    if (x < 64 and y > SCREEN_HEIGHT - 64) {
      if(block < 12) {
        block++;
      }
      else {
        block = 1;
      }
    }
    else if (x < SCREEN_WIDTH/2 - 40 and y < SCREEN_HEIGHT/2 and cursorPos[0] > 0) {
      WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = oldBlock;
      cursorPos[0]--;
      if(cursorScreenPos[0] > -3) {
        cursorScreenPos[0]--;
      }
      else {
        pushX--;
      }
      oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
      drawWorld();
    }
    else if (x > SCREEN_WIDTH/2 + 40 and y < SCREEN_HEIGHT/2 and cursorPos[2] > 0) {
      WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = oldBlock;
      cursorPos[2]--;
      if(cursorScreenPos[2] > -3) {
        cursorScreenPos[2]--;
      }
      else {
        pushZ--;
      }
      oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
      drawWorld();
    }
    else if (x < SCREEN_WIDTH/2 - 40 and y > SCREEN_HEIGHT/2 and cursorPos[2] < 28) {
      WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = oldBlock;
      cursorPos[2]++;
      if(cursorScreenPos[2] < 3) {
        cursorScreenPos[2]++;
      }
      else {
        pushZ++;
      }
      oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
      drawWorld();
    }
    else if (x > SCREEN_WIDTH/2 + 40 and y > SCREEN_HEIGHT/2 and cursorPos[0] < 28) {
      WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = oldBlock;
      cursorPos[0]++;
      if(cursorScreenPos[0] < 3) {
        cursorScreenPos[0]++;
      }
      else {
        pushX++;
      }
      oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
      drawWorld();
    }
    else if (x < SCREEN_WIDTH/2 + 40 and x > SCREEN_WIDTH/2 - 40 and y > SCREEN_HEIGHT/2 and cursorPos[1] < 17) {
      WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = oldBlock;
      cursorPos[1]++;
      oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
      drawWorld();
    }
    else if (x < SCREEN_WIDTH/2 + 40 and x > SCREEN_WIDTH/2 - 40 and y < SCREEN_HEIGHT/2 and cursorPos[1] > 0) {
      WD[cursorPos[1]][cursorPos[2]][cursorPos[0]] = oldBlock;
      cursorPos[1]--;
      oldBlock = WD[cursorPos[1]][cursorPos[2]][cursorPos[0]];
      drawWorld();
    }
    drawGUI();
    delay(100);
  }
}

I was hesitant to suggest the above, but it is possible if you use LovyanGFX.

LovyanGFX has a function to affine transform sprites and display them on the LCD, so for example, you can fill a 160x120 sprite with a small 16x16 sprite, and then use pushRotateZoom() to enlarge it by 2 times and output it to the LCD.

Although the apparent resolution will be slightly reduced, I think it will serve its purpose.

LovyanGFX instructions are often compatible with TFT_eSPI, and a header called LGFX_TFT_eSPI.h is provided to absorb the compatibility differences, but porting will still take some effort.

I will share the results of benchmarking each GFX library on ESP32-S3. In most cases, LovyanGFX is faster, and I prefer LovyanGFX to TFT_eSPI.

Benchmarks are performed by absorbing the differences between each GFX instructions.

If you want LovyanGFX, I will support you :wink:

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.