Introduction: Adaptation of "Space Wars" Game With DumbDisplay

Again, this is just an effort to adapt a Volos Projects' project. This time is TTGO T Display (ESP32) - Space Shooter game- free code.

Hopefully, the end result of the adaption can be close. Anyway, here I will highlight some differences.

In terms of hardware:

  • Virtual graphical LCD is used, with the help of DumbDisplay.
  • Virtual beeper / speaker is used, also with the help of DumbDisplay.
  • Although the target for the adaption is Rabaperry Pi Pico, it runs with Ardunino UNO also, but with some limitations. For Respbarry Pi Pico, a Joystick module and 2 buttons are used. For Arduino UNO, a Joystick Shield is used.

In terms of software:

  • Most importantly: Even DumbDisplay realizes a virtual graphical LCD with your relatively must more powerful Android phone, sending commands to it is text-based serial communication, with relatively high latency. Hence, it is vital that the screen is not redrawn unless necessary. As a result, some effort is put to avoid unnecessary redrawing of the screen. Two areas are most noticable. 1) It will not redraw the screen every loop iteration. 2) Graphical LCD layers (stack of LCD screens) are using for different related aspects of the game rendering.
  • The logic of the game flow should be very close to the original, with some corner cuttings.
  • Software-based button click and joystick "directional press" detection. For more info on the topic, you may want to refer to my previous post Joystick Module Calibration and Press Detection.

Step 1: Setup Arduino IDE

In order to be able to compile and run the sketch shown here, you will first need to install the DumbDisplay Arduino library.

Open your Arduino IDE; go to the menu item Tools | Manage Libraries, and type "dumbdisplay" in the search box there.

Step 2: Connections for Raspbarry Pi Pico

  • Connect VBUS of Pico to +5V pin of Joystick.
  • Connect GND of Pico to GND pin of Joystick.
  • Connect GP26 of Pico to VRX pin of Joystick. Note that GP26 is an analogy input pin.
  • Connect GP27 of Pico to VRY pin of Joystick. Note that GP27 is an analogy input pin.
  • Connect GP21 of Pico to one leg of a button, button 'A'. It is the button to press to fire bullet; it is also the "enter" button.
  • Connect GND of Pico to the other leg of button 'A'
  • Connect GP18 of Pico to one leg of a button, button 'B'. It is the button to press to fire rocket.
  • Connect GND of Pico to the other leg of button 'B'.

Step 3: Connections for Arduino UNO

Since a JoyStick Shield is used, hence there is no need to make any wireling. Simple plug the JoyStick Shield to Arduino UNO.

Notes:

  • The button on the right side is the 'A' button of the game. It is the button to press to fire bullet; it is also the "enter" button.
  • The button on the top side is the 'B' button of the game. It is the button to press to fire rocket.

Step 4: The Sketch

The primary target of this adaption is Raspberry Pi Pico. As shown above, a joystick board is used (like Arduino UNO's Joystick Shield), for both easier wireling, as well as the placements of the joystick and buttons. Nevertheless, the sketch can also be run with Arduino UNO, with some limitations.

// *** 
// * adapted from: TTGO T Display (ESP32) - Space Shooter game- free code
// * -- https://www.youtube.com/watch?v=KZMkGDyGjxQ&t=310s
// ***
#if defined(ARDUINO_AVR_UNO)
// *** config for Arduino UNO, with Joystick Shield
//#define DOWNLOAD_IMAGES
#define DEBUG_LED_PIN 13
#define BTN_A 3
#define BTN_B 2
#define HORIZONTAL A0
#define VERTICAL A1
const bool joystickReverseHoriDir = false;
const bool joystickReverseVertDir = false;
const bool joystickAutoTune = true;
#elif defined(PICO_SDK_VERSION_MAJOR)
// *** config for Raspberry Pi Pico, with Joystick and buttons
//#define SAVE_IMAGES
#define DOWNLOAD_IMAGES
#define SHOW_SPACE
#define DEBUG_LED_PIN 1
#define BTN_A 21
#define BTN_B 18
#define HORIZONTAL 26
#define VERTICAL 27
const bool joystickReverseHoriDir = true;
const bool joystickReverseVertDir = false;
const bool joystickAutoTune = true;
#else
#error not configured for board yet
#endif
#define SHOW_LIVES
#define IF_BACK2 "BA"
#define IF_SENS "SE"
#define IF_GAMEOVER "GO"
#define IF_BROD1 "BR"
#define IF_BULET "BU"
#define IF_ROCKET "RO"
#define IF_EX2 "EX"
#define IF_EXPLOSION "EXP"
#define IF_BUUM "BUM"
#define IF_EBULLET "EB"
#define IF_EARTH(level) ("E-" + String(level))
#define IF_SPACEWARS_IMGS "spacewarsimgs"
const int NOTE_A4 = 466;
const int NOTE_B4 = 523;
const int NOTE_C4 = 277;
const int NOTE_D4 = 311;
const int NOTE_E4 = 349;
const int NOTE_F4 = 370;
const int NOTE_G4 = 415;
const int NOTE_C5 = 554;
const int NOTE_G5 = 831;
#define TFT_BLACK "black"
#define TFT_GREEN "green"
#define TFT_GREY DD_HEX_COLOR(0x5AEB)
#define lightblue DD_HEX_COLOR(0x2D18)
#define orange DD_HEX_COLOR(0xFB60)
#define purple DD_HEX_COLOR(0xFB9B)
#include "dumbdisplay.h"
DumbDisplay dumbdisplay(new DDInputOutput(115200));
#include "Core.h"
void setup(void)
{
pinMode(BTN_B, INPUT_PULLUP);
pinMode(BTN_A, INPUT_PULLUP);
#if defined(HORIZONTAL)
pinMode(HORIZONTAL, INPUT);
pinMode(VERTICAL, INPUT);
#endif
#if defined(DEBUG_LED_PIN)
pinMode(DEBUG_LED_PIN, OUTPUT);
digitalWrite(DEBUG_LED_PIN, 0);
#endif
}
int readyStage = 0;
void loop()
{
if (readyStage == 0) {
main_layer = dumbdisplay.createGraphicalLayer(240, 135);
main_layer->noBackgroundColor();
#if defined(SAVE_IMAGES)
dumbdisplay.writeComment("start caching ...");
dumbdisplay.writeComment("... caching back2 ...");
main_layer->cachePixelImage16(IF_BACK2, back2, 240, 135, "", DD_COMPRESS_BA_0);
if (true)
{
dumbdisplay.recordLayerCommands();
main_layer->drawImageFile(IF_BACK2);
main_layer->fillRect(0, 78, 120, 25, TFT_BLACK);
dumbdisplay.playbackLayerCommands();
}
dumbdisplay.writeComment("... cachine sens ...");
main_layer->cachePixelImage16(IF_SENS, sens, 72, 72, "0>a0", DD_COMPRESS_BA_0);
dumbdisplay.writeComment("... cachine gameOver ...");
main_layer->cachePixelImage16(IF_GAMEOVER, gameOver, 240, 135, "", DD_COMPRESS_BA_0);
dumbdisplay.writeComment("... cachine brod1 ...");
main_layer->cachePixelImage16(IF_BROD1, brod1, 49, 40, "0>a0", DD_COMPRESS_BA_0);
dumbdisplay.writeComment("... cachine bulet ...");
main_layer->cachePixelImage16(IF_BULET, bulet, 8, 8, "0>a0", DD_COMPRESS_BA_0);
dumbdisplay.writeComment("... cachine rocket ...");
main_layer->cachePixelImage16(IF_ROCKET, rocket, 24, 12, "0>a0", DD_COMPRESS_BA_0);
dumbdisplay.writeComment("... cachine ex2 ...");
main_layer->cachePixelImage16(IF_EX2, ex2, 12, 12, "0>a0", DD_COMPRESS_BA_0);
dumbdisplay.writeComment("... cachine explosion ...");
main_layer->cachePixelImage16(IF_EXPLOSION, explosion, 24, 24, "0>a0", DD_COMPRESS_BA_0);
dumbdisplay.writeComment("... cachine buum ...");
main_layer->cachePixelImage16(IF_BUUM, buum, 55, 55, "0>a0", DD_COMPRESS_BA_0);
dumbdisplay.writeComment("... cachine ebullet ...");
main_layer->cachePixelImage16(IF_EBULLET, ebullet, 7, 7, "0>a0", DD_COMPRESS_BA_0);
for (int i = 0; i < LevelCount; i++)
{
int level = i + 1;
dumbdisplay.writeComment("... caching earth-" + String(level - 1) + " ...");
main_layer->cachePixelImage16(IF_EARTH(level), earth[level - 1], 55, 54, "0>a0", DD_COMPRESS_BA_0);
}
dumbdisplay.writeComment("... done caching");
main_layer->saveCachedImageFiles(IF_SPACEWARS_IMGS);
#endif
#if defined(DOWNLOAD_IMAGES)
dumbdisplay.writeComment("download images ...");
// image URL: https://raw.githubusercontent.com/trevorwslee/Arduino-DumbDisplay/master/screenshots/spacewarsimgs.png
download_tunnel = dumbdisplay.createImageDownloadTunnel("https://${DDSS}/spacewarsimgs.png", IF_SPACEWARS_IMGS, false);
dumbdisplay.writeComment("... ...");
#endif
readyStage = 1;
}
if (readyStage == 1)
{
#if defined(DOWNLOAD_IMAGES)
int download_res = download_tunnel->checkResult();
if (download_res == 0)
{
return;
}
if (download_res == -1)
{
dumbdisplay.writeComment("... failed to download images");
delay(2000);
return;
}
dumbdisplay.writeComment("... done download images");
dumbdisplay.deleteTunnel(download_tunnel);
#endif
top_layer = dumbdisplay.createGraphicalLayer(240, 135);
top_layer->noBackgroundColor();
Ebulet_layer = dumbdisplay.createGraphicalLayer(240, 135);
Ebulet_layer->noBackgroundColor();
rocket_layer = dumbdisplay.createGraphicalLayer(240, 135);
rocket_layer->noBackgroundColor();
bulet_layer = dumbdisplay.createGraphicalLayer(240, 135);
bulet_layer->noBackgroundColor();
#if defined(SHOW_SPACE)
for (int i = 0; i < SpaceLayerCount; i++)
{
space_layers[i] = dumbdisplay.createGraphicalLayer(240, 135);
space_layers[i]->noBackgroundColor();
}
#endif
bg_layer = dumbdisplay.createGraphicalLayer(240, 135);
bg_layer->backgroundColor(TFT_BLACK);
int x = 0;
main_layer->loadImageFileCropped(IF_SPACEWARS_IMGS, x, 0, 240, 135, IF_BACK2);
x += 240;
main_layer->loadImageFileCropped(IF_SPACEWARS_IMGS, x, 0, 72, 72, IF_SENS);
x += 72;
main_layer->loadImageFileCropped(IF_SPACEWARS_IMGS, x, 0, 240, 135, IF_GAMEOVER);
x += 240;
main_layer->loadImageFileCropped(IF_SPACEWARS_IMGS, x, 0, 49, 40, IF_BROD1);
#if defined(SHOW_LIVES)
top_layer->loadImageFileCropped(IF_SPACEWARS_IMGS, x, 0, 49, 40, IF_BROD1);
#endif
x += 49;
bulet_layer->loadImageFileCropped(IF_SPACEWARS_IMGS, x, 0, 8, 8, IF_BULET);
x += 8;
rocket_layer->loadImageFileCropped(IF_SPACEWARS_IMGS, x, 0, 24, 12, IF_ROCKET);
x += 24;
main_layer->loadImageFileCropped(IF_SPACEWARS_IMGS, x, 0, 12, 12, IF_EX2);
x += 12;
main_layer->loadImageFileCropped(IF_SPACEWARS_IMGS, x, 0, 24, 24, IF_EXPLOSION);
x += 24;
main_layer->loadImageFileCropped(IF_SPACEWARS_IMGS, x, 0, 55, 55, IF_BUUM);
x += 55;
Ebulet_layer->loadImageFileCropped(IF_SPACEWARS_IMGS, x, 0, 7, 7, IF_EBULLET);
x += 7;
for (int i = 0; i < LevelCount; i++)
{
int level = i + 1;
main_layer->loadImageFileCropped(IF_SPACEWARS_IMGS, x + i * 55, 0, 55, 54, IF_EARTH(level));
}
readyStage = 2;
}
if (fase == 0)
{
handleRestart();
}
else if (fase == 1)
{
handlePlay();
}
else if (fase == 2)
{
handleGameOver();
}
}

Since the whole program for this game is quite lenthy, I have split them into several files. The sketch above is simply a bootstrap to the core of this game.

All the involved files can be found here. To download the folder, consider using DownGit.

  • ddspacewars.ino -- the above sketch
  • Core.h -- the core
  • Misc.h -- additional to the core
  • PressTracker.h -- the button / joystick press detection class definitions
  • all others are the various image definitations

Notes about the images:

  • The definitions of all the images take up much space, and hence it is impossible to include it as part of a sketch for Arduino UNO.
  • Instead, the images are stitched as a single image, that you can download from the Internet.
  • The sketch can download the stitcked image to DumbDisplay app (and save them to your phone). This is controlled by the #define DOWNLOAD_IMAGES. After the successful download of the stitched image, it is croped into the desired images for the game.
  • The sketch can actually send the images to DumbDisplay app (and save them in your phone). This is controlled by the #define SAVE_IMAGES. However, this is normally not done, since it is very slow to do so.
  • For Arduino UNO, sorry, it is not powerful enough to even download the stitched images (regardless of the fact that the actual download is done by DumbDisplay app, not Arduino UNO). Hence, Arduino UNO relies on that the stitched image already downloaded to your phone somehow (see below).

Step 5: Upload the Sketch and Run It

Other than making the needed connections to your Arduino UNO / Raspberry Pi Pico, you will need to prepare two more addition things:

  • You will need to be able to attach your UNO / Pico to your Android phone. To do this, you will need an OTG adapter, which allows you to plug your UNO / Pico to your phone via the usual USB cable.
  • You will certainly need to install the DumbDisplay Android app.

After uploading the sketch, re-plug the USB cable to your phone via an OTG adapter. Then, open the DumbDisplay Android app on your phone, and make connection.

Step 6: Enjoy!

Two more things.

  1. You will need to grant permission for DumbDisplay Android app to access storage for the images.
  2.  It is suggested to turn off "Show Commands" of DumbDisplay Android app.

Enjoy!

Peace be with you. Jesus loves you. May God bless you!

Step 7: P.S. Save Images to DumbDisplay App

Here is a way to save image (the stitched image) to DumbDisplay app.

  • Use your phone's Chrome browser to open the image page
  • Long press the image to bring up the available options
  • Select to share the image with DumbDisplay app
  • Enter the correct name of the image, spacewarsimgs; note that you don't need to give it an extension, since image will be saved as PNG.

Step 8: P.S. Demo Video