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)
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.)
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.
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.
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);
}
}
}
}
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.
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.
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.
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.
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.
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.
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:
Rendering time
I want to lower the resolution of the display and take the full screen so it zooms in
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.