[WIP] Ardunaut (previously Parallax rocket launch)

So I’ve started getting more into it and decided to go with everyXframe for the gravity… I know it’s a little cheaper and I would honestly rather use floating or fixed points (just to learn them), but if there are rounding errors as you mentioned then I could already forsee this causing issues.

Aside from setting earths gravity to a specified height window (so the player can now leave earths gravity, fly around, and return), all I’ve tried to do so far is the background scrolling, but had no luck. I know its pretty large, but the background tiles are 64x64 so I could fit it once along the screen height, and i tried to tile it twice so it would be 2x the screen height (in two separate draw commands so I could work with them each individually, if needed) and since I had to offset it back because it draws down I had to subtract 64 from one and 128 from the other (during the draw command so the actual values are unchanged), then I tried to set it so when the background Y point reached 64 it would set it back to 0. This did nothing.

I’m sure it’s harder to visualize what I mean without the code, sorry, just spent the better part of my night playing my Arduboy (naturally :P), but I’m going to move most of my game code to separate header files now so i won’t be spamming the forum, then I’ll post it when it’s cleaned up a bit.

EDIT: can’t upload over the game that’s already on there and don’t feel like having to fight with it for however long (again) so I’m just gonna have to do the cleanup later.

Here is my current code, with my last attempt to offset the background when it reached a certain point (with no luck) in the drawGame() function. Next im just going to print the value to the screen and see what it is, in case its a simple oversight on my part.

EDIT 2: A couple things worth noting… it seems that my background scrolling is offsetting by 1 each time (so as the player takes off and lands repeatedly it becomes 1, then 2, then 3, etc.) so ill have to address this before moving on. Judging by the similar issue with the player sprite i would think its likely something similar to that. Also, when i printed the value to the screen it showed the number decreasing in value rather than increasing, so changing the 64 to negative got it to do what i wanted (although now i will have to figure out how to make it work in the opposite direction, but will worry about implementing this after i fix the other issue). Have since removed the statement to shift the background at a certain value, but have added a counter for testing.

EDIT 3 (last one, promise): Got the bug fixed, just had to set the height increment for gravity to the beginning, rather than the end (not sure why this worked when its opposite the input, but doesnt seem to affect the player sprite any) so I posted over top the code that was there. Now moving on to the background looping…

#include <Arduboy2.h>
Arduboy2 arduboy;

uint16_t heightCounter = 0;

struct Timer {
  unsigned long currentMillis;
  unsigned long previousMillis = 0;
};

struct PointXY {
  int x = 0;
  int y = 0;
};

struct Launchpad {
  int x1;
  int y1;
  int x2;
  int y2;
};

struct FuelBar {
  uint16_t x;
  uint16_t y;
  uint8_t height;
  uint8_t width;
  uint16_t rate;
};

struct Arrow {
  uint16_t x1;
  uint16_t x2;
  uint16_t x3;
  uint16_t y1;
  uint16_t y2;
  uint16_t y3;


};

Timer timer;
PointXY playerXY;
PointXY background1;
PointXY background2;
PointXY background3;
FuelBar fuelBar { 0, 20, 30, 5, 2000 };
constexpr Launchpad launchpad { 61, 58, 73, 58 };
constexpr Arrow arrow { 61, 73, 67, 58, 58, 64 };
constexpr int worldWidth = 128;
constexpr int worldHeight = 128;
constexpr int tileSize = 64;
uint8_t const *playerSprite = nullptr;

PROGMEM const unsigned char playerUp[] = {
  // Bitmap Image. No transparency
  // Width: 9 Height: 9
  9, 9,
  0x00, 0x00, 0xE0, 0x7C, 0x47, 0x7C, 0xE0, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

  0x00, 0x00, 0xE0, 0x7C, 0xC7, 0x7C, 0xE0, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

  0x00, 0x00, 0xE0, 0x7C, 0xC7, 0x7C, 0xE0, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,

  0x00, 0x00, 0xE0, 0x7C, 0xC7, 0x7C, 0xE0, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

PROGMEM const unsigned char playerDown[] = {
  9, 9,
  0x00, 0x00, 0x0E, 0x7C, 0xC4, 0x7C, 0x0E, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,

  0x00, 0x00, 0x0E, 0x7C, 0xC6, 0x7C, 0x0E, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,

  0x00, 0x00, 0x0E, 0x7C, 0xC7, 0x7C, 0x0E, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,

  0x00, 0x00, 0x0E, 0x7C, 0xC6, 0x7C, 0x0E, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
};

PROGMEM const unsigned char playerLeft[] = {
  // Bitmap Image. No transparency
  // Width: 9 Height: 16
  9, 9,
  0x01, 0x06, 0x2A, 0x74, 0xA8, 0x1C, 0x08, 0x10, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

  0x01, 0x06, 0x2A, 0x74, 0xA8, 0x3C, 0x48, 0x10, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

  0x01, 0x06, 0x2A, 0x74, 0xA8, 0x3C, 0x48, 0x90, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

  0x01, 0x06, 0x2A, 0x74, 0xA8, 0x3C, 0x48, 0x10, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

PROGMEM const unsigned char playerRight[] = {
  // Bitmap Image. No transparency
  // Width: 9 Height: 16
  9, 9,
  0x00, 0x10, 0x08, 0x1C, 0xA8, 0x74, 0x2A, 0x06, 0x01,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

  0x00, 0x10, 0x48, 0x3C, 0xA8, 0x74, 0x2A, 0x06, 0x01,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

  0x00, 0x90, 0x48, 0x3C, 0xA8, 0x74, 0x2A, 0x06, 0x01,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

  0x00, 0x10, 0x48, 0x3C, 0xA8, 0x74, 0x2A, 0x06, 0x01,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

PROGMEM const unsigned char backgroundTile1[] = {
  // Bitmap Image. No transparency
  // Width: 64 Height: 64
  64, 64,
  0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

PROGMEM const unsigned char backgroundTile2[] = {
  // Bitmap Image. No transparency
  // Width: 64 Height: 64
  64, 64,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

PROGMEM const unsigned char backgroundTile3[] = {
  // Bitmap Image. No transparency
  // Width: 64 Height: 64
  64, 64,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};

void setup() {
  arduboy.boot();
  arduboy.setFrameRate(10);
  playerSprite = playerUp;
  arduboy.clear();
}

uint8_t playerFrame = 0;

void loop() {

  if (!arduboy.nextFrame()) {
    return;
  }
  timer.currentMillis = millis();
  arduboy.clear();
  testing();
  AtLaunch();
  arduboy.display();
}

void gravity(){
  if (!arduboy.pressed(UP_BUTTON) && heightCounter >= 1 && heightCounter <= 150) {
    //if  height counter is less than 20 move player, if greater scroll background
    --heightCounter;
    if (heightCounter <= 20) {
      movePlayerDown();
    }
    if (heightCounter >= 20) {
      backgroundScrollUp();
    }
  }
}


void handleInput(){
  //movement
  //if up button pressed
  if (arduboy.pressed(UP_BUTTON)) {
    //if height counter is less than 20 move player, if greater scroll background
    //change player sprite and frame
    playerSprite = playerUp;
    playerAnimate();
    if (heightCounter <= 20) {
      movePlayerUp();
    }
    if (heightCounter >= 20) {
      backgroundScrollDown();
    }
    //if time elapsed is greater than fuel rate, lower bar
    if ((timer.currentMillis - timer.previousMillis >= fuelBar.rate && fuelBar.height >= 1)) {
      ++fuelBar.y;
      --fuelBar.height;
      timer.previousMillis = timer.currentMillis;
    }
    //raise height counter
    ++heightCounter;
  }
  if (arduboy.pressed(DOWN_BUTTON)) {
    //change player sprite and frame
    playerSprite = playerDown;
    playerAnimate();
    //if height counter is less than 20 move player, if greater scroll background
    if (heightCounter <= 20) {
      movePlayerDown();
    }
    if (heightCounter >= 20) {
      backgroundScrollUp();
    }
    //if time elapsed is greater than fuel rate, lower bar
    if ((timer.currentMillis - timer.previousMillis >= fuelBar.rate && fuelBar.height >= 1)) {
      ++fuelBar.y;
      --fuelBar.height;
      timer.previousMillis = timer.currentMillis;
    }
    //lower height counter
    --heightCounter;
  }
  //if left button is pressed and background is less than 75, scroll background and animate sprite
  if (arduboy.pressed(LEFT_BUTTON)) {
    backgroundScrollRight();
    playerSprite = playerLeft;
    playerAnimate();
  }
  //and right
  if (arduboy.pressed(RIGHT_BUTTON)) {
    backgroundScrollLeft();
    playerSprite = playerRight;
    playerAnimate();

  }
  //if no buttons are pressed, frame for player animation is 0
  if (!arduboy.pressed(UP_BUTTON) && !arduboy.pressed(DOWN_BUTTON) && !arduboy.pressed(LEFT_BUTTON) && !arduboy.pressed(RIGHT_BUTTON)) {
    playerFrame = 0;
  }
}

void movePlayerUp() {
  --playerXY.y;
}

void movePlayerDown() {
  ++playerXY.y;
}

void playerAnimate() {
  if (playerFrame < 3) {
    ++playerFrame;
  }
  else
    playerFrame = 0;
}

void drawBackground() {
  //For each row in the column, draw background
  for (int x = 0; x < worldWidth; x += tileSize ) {
    for (int y = 0; y < worldHeight; y += tileSize) {
      Sprites::drawSelfMasked(x + background1.x, y - background1.y - 64, backgroundTile1, 0 );
      Sprites::drawSelfMasked(x + background2.x, y - background2.y - 64, backgroundTile2, 0 );
      Sprites::drawSelfMasked(x + background3.x, y - background3.y - 64, backgroundTile3, 0 );
    }
  }
}

void backgroundScrollLeft(){
  if (background1.x > -75) {
    background1.x -= 1;
    background2.x -= 2;
    background3.x -= 3;
  }
}

void backgroundScrollRight(){
  if (background1.x < 75) {
    background1.x += 1;
    background2.x += 2;
    background3.x += 3;
  }
}

void backgroundScrollUp(){
  background1.y += 1;
  background2.y += 2;
  background3.y += 3;
}

void backgroundScrollDown(){
  background1.y -= 1;
  background2.y -= 2;
  background3.y -= 3;
}

void drawLaunchpad() {
  arduboy.drawLine(launchpad.x1 + background1.x, launchpad.y1 - background1.y, launchpad.x2 + background1.x, launchpad.y2 - background1.y);
}

void drawPlayer() {
  Sprites::drawSelfMasked(playerXY.x + 63, playerXY.y + 50, playerSprite, playerFrame);
}

void drawArrow() {
  if (heightCounter >= 30 && heightCounter <=150) {
    arduboy.fillTriangle(arrow.x1 + background1.x, arrow.y1, arrow.x2 + background1.x, arrow.y2, arrow.x3 + background1.x, arrow.y3);
  }
}

void drawHUD() {
  arduboy.setCursor(0,0);
  arduboy.print("alt.");
  arduboy.print('\n');
  arduboy.print(heightCounter);
  arduboy.print("00");
  arduboy.fillRect(fuelBar.x, fuelBar.y, fuelBar.width, fuelBar.height);
}

void drawGame() {
  drawBackground();
  drawPlayer();
  drawHUD();
}

void AtLaunch() {
  drawGame();
  drawLaunchpad();
  gravity();
  handleInput();
  drawArrow();
}

void testing(){
  arduboy.setCursor(40,7);
  arduboy.print(background1.y);
}

Okay! Ive gotten the scrolling to work (along the Y axis), but had to change the launchpad from constant so i could keep the position updated. I also had to add two additional sets of backgrounds to make sure the scrolling was continuous when going up or down, but im hoping i can just use two, just wanted to try to iron that out a bit more then ill post it.

Sorry to be spamming talking to myself again, just excited with the progress I’ve been making and made a few noteworthy additions/improvements.

First, I got the background scrolling to work now with only two backgrounds in each axis (not sure why I used three initially, just tired and brain was a little jelly, I guess), using the initial background plus one more in x, and one more in y, then a fourth background at the intersection of those two (basically a square with four 128x64, the player starting in the one to the bottom left), so now the scrolling for each tile and layer seamlessly repeats along the x and y axes.

Also, since I had to reset the y position of the background to do this, I update the position of the landing pad before doing so in order to keep track of it (so you can fly out as long as you want and still fly back and it will be where it should be). I didn’t update the position in x though, so it repeats every every other 128 px (I liked this in order to keep the game space reasonable).

And, I reintegrated the menu I used in the gamestates version, and as such did redo the gamestates part I did before, but this time only for the menu and gameplay area.

And last (but certainly not least :P) shortly before finishing all this I was able to upload it onto my Arduboy, so I can finally organize its all into header files to cut down on the mess it’s turned into. Just wanted to do that real quick before posting (to spare everyone the headache).

I’m going to advise against it.

I’ve recently been helping out/advising someone else with a game project that has a bit of overlap with this project (in terms of physics and using tile-based rendering) and something I found with the everyXFrame trick is that it can cause problems because of the timing.

What the everyXFrame trick is supposed to do is to simulate decimal numbers/fractions, but instead of accumulating the fractional part of the distance you’re just delaying until you can add a whole part.

I.e. if you had a ship travelling at 4 pixels per second (1 pixel every 1/4 of a second) then your movement is going to look like this:

Method \ Frames 1 2 14 15
Using float 0.0666 0.1333 0.9333 1.0
Using everyXFrames 0 0 0 1

I’m doubtful that it will be a problem, and I’ll explain why…

The error depends on the format.
Whether it’s going to affect you or not depends on what the smallest fraction speed you’ll have to represent is.

The smallest fractional value that a floating or fixed point type can repsresent is called its ‘epsilon’.
(The epsilon is also the smallest value larger than zero that the type can represent.)

float's epsilon on the Arduboy is:
0.000000000000000000000000000000000000011754944324493408203125

Which I think it’s safe to say is pretty tiny.

UQ8x8 and SQ7x8 have an epsilon of 0.00390625 (1 / pow(2, 8)).
It’s significantly larger than float's (more specifically ‘orders of magnitude’ larger),
but it might not be that much of an issue.

UQ16x16 and SQ15x16 have an epsilon of 0.000015258878 (1 / pow(2, 16)).
That’s still comfortably small.

What you have to ask is what sorts of values are you going to be handling.
If you wanted to move 1 pixel per second at 60 fps then you need to move 0.01666… pixels per second.
0.01666… is still over 4 times UQ8x8's epsilon and over 1092 times UQ16x16's epsilon.
With UQ8x8, theoretically you could go as slow as 1 pixel every 4 seconds before you’d encounter problems.


I’m not sure what you’ve done so far.
That was all a bit too much for me to process.
I’ll wait until I know that your GitHub is up to date and then I’ll have a peek.

Before I sink too much time into it, I think I need to know a bit more about what you want.

What’s your intention for the background?

Are you going to have the same 64x64 block repeated infinitely?
Are you just going to have stars in the background?
Stars and planets?
How much variation do you want in the star patterns?

While thinking about it I’ve had an idea that I think you might like, but I need a bit of time to test it.

Whne I have a better idea of what your intention is and how the background is going to be drawn, I’ll start discussing the movement of the background with you.

When it comes to tile games there are two approaches:

  • Move the camera and keep the world static
  • Keep the camera static and move the world

I think you might be better off with a static world and a moving camera, but we’ll see.

1 Like

Sorry about that, thats understandable. At first I found a bug, then fixed it, then got the background scrolling working so you could take off and scroll infinitely up, left, or right (or at least until the value became too large).

Im still kicking around ideas where to go next, though… Id like to make something bigger than may be possible so I might end up just using the different ideas for their own separate games, but im still hoping to make something that captures the adventurous spirit i was kind of shooting for when i started… I know i couldnt, but id like to make something you could take off and land on the moon and walk around (from a top down perspective), even if it was just in the same looping tile space (like I have set up for launch/space), but even making the most minimal player sprite and background i could takes up 6717 bytes of ROM and 1221 bytes of RAM (and i hadnt even tiled it yet), and this game takes up 14098 ROM and 1336 RAM (leaving me at 99% RAM), so ill probably take it a more simplistic route (maybe so you can get out but keep in the environment as is) or just remove the parallax.

While I think of it, here’s an example of a game that uses fixed points for its movement calculations:

(In fact I think it’s the first game that ever made use of my fixed points library.)

In theory the movement should be smoother than games that don’t use fixed points or floating points,
but I don’t really play enough Arduboy games to know if that’s the case.

1 Like

Actually, that’s kind of what my idea is about.

I was going to write a demo to make sure it would work before telling you because I didn’t want to get your hopes up, but I’ll tell you anyway.

Basically I remembered all that discussion we had about procedurally generated worlds and it suddenly occurred to me that if you generated the background tiles using a hash function then you should be able to achieve very large worlds (not infinite, but large).

When you were looking at the idea of a top-down world the hash function was a bit of a problem because it only generates noise rather than cohesive structures, which is why all the extra steps were needed.
But with a space setting, all you really need is patterns of stars,
so noise should be perfectly adequate for that.

So what I’m thinking of doing is basically:

const auto value = hash(tileX, tileY);
const uint8_t index = (value % 8);
Sprites::drawOverwrite(drawX, drawY, stars, index);

For each tile to be drawn.

Cheap. Cheerful. Incredibly large.

Is your GitHub code up to date?
I’m sure it’s possible to reduce that.

1 Like

My github code is up to date, although it isn’t organized into header files yet (doing that now) so it’s still a bit of a mess.

And I meant the program as it is on github plus a new program I wrote (for the top down perspective part as minimally as possible) equals 99% RAM, although I was hoping there might be a way to reduce that (like chunking) because I had to draw so many backgrounds in order for it to loop (which is why I was also thinking of doing away with the parallax, potentially), wasn’t sure if there might be a better way to do it.

And as for the hash function, I can somewhat follow (just generating noise for star placement), but how does it actually work? Also, what would the output look like (does it just populate the screen, does it populate a larger area outside, how large, etc), and what size tiles were you thinking? Sorry, just asking so I follow. Thanks!

1 Like

I modified Flappy Ball (one of the earliest games ported to the Arduboy) to use fixed point math for the gravity calculations. The fixed point code was done by myself, before @Pharap’s fixed point library existed.

1 Like

I’ll have a read of it tomorrow.

I won’t look right now because it’s late and I want a spin on Oblivion before the night is over.

There almost always is.

Ah, when I heard this I had to have a peek.

I’m not exactly sure what you’re doing with all the different backgrounds, but I’m almost certain there’s a better/easier way.

Which part? The hash function or the tiling?

Imagine 8-16 black rectangles.
Imagine those rectangles filled with a pattern of white dots.
Imagine those white-dotted rectangles tiled across the screen in a random order.

That’s what it will look like.

Probably 8x8, 16x16 or 32x32.

The world size limit will be determined by what type you use for coordinates because your ship’s coordinates will always be in pixels.

No worries, like I said it’s a bit of a mess, hopefully I’ll have it more cleaned up by the time you can get back to it.

And yeah, basically what I did was tile the background three more times, once more in x, once more in y, and once more diagonally (like quadrants of a rectangle) and when each respective background traveled the specified distance (so that it was offscreen) I moved it to wrap back around.

But since the parallax scrolling uses 3 layers I ended up with 12 backgrounds overall, hadn’t really planned ahead and didn’t realize til it was getting a little out of hand, just kinda went with the first method that came to mind :stuck_out_tongue:

Occurred to me after the fact that I probably could have even tried chunking, but I don’t mind burning rubber on this kind of stuff, helps me to sharpen my skills better using the information I learned more recently (so it’ll be less of a blunt force trauma :P), if anything.

And that definitely helps to clarify a bit, thanks! I’m just not so sure how it works under the hood yet but I’ll just sit with it a bit and digest and I should get a better idea. Thanks again!

I started the day intending to write an example of the stars demo but I’ve been sidetracked half a dozen times.


I saw your thread about memory usage (and made a few comments).

People are right that in general it does depend on the game and the context,
general tips are only useful if you know when and where to apply them.

I think that a large chunk of your RAM usage has been chewed up by the background drawing,
but I’m struggling to see a way to reduce it because I’m struggling to track what all the backgrounds are doing.

My instinct says that you could possibly just generate all the background coordinates on the fly using a single pair of x and y offsets as a base.

One thing I’d like to point out.
Instead of doing -= 1, -= 2, -= 3,
you could just store one set of coordinates,
and then when it comes time to draw it:

Sprites::drawSelfMasked(x + (background.x * 1), y - (background.y * 1) - 64, backgroundTile1, 0);
Sprites::drawSelfMasked(x + (background.x * 2), y - (background.y * 2) - 64, backgroundTile2, 0);
Sprites::drawSelfMasked(x + (background.x * 3), y - (background.y * 3) - 64, backgroundTile3, 0);

And if you were to pack your backgroundTiles into a single array with multiple frames, you could further reduce that to:

for(uint8_t index = 0; index < 3; ++index)
{
	const uint8_t scale = (index + 1);
	Sprites::drawSelfMasked(x + (background.x * scale), y - (background.y * scale) - 64, backgroundTiles, index);
}

I’m not sure that would help here.
It depends how you were thinking of using it.

Chunking is primarily for generating terrain dynamically (in bite-size chunks, so to speak).

1 Like

I finally found time to do it.
This is what I was talking about:

StarTerrain.hex (22.3 KB)

And here’s the code:


Basically the world is static and the camera moves around the world.
The rendering code decides which tiles are on screen and renders only those tiles.
The rendering loop takes the coordinates of the tiles,
feeds them into a hash function,
and then limits the output of that hash function to an index of one of following 4 images:

Star0 Star1 Star2 Star3

If that doesn’t make sense,
I’ll write a clearer explanation later.


I’m sure there’s a way to add parallax,
but it’s going to involve some maths so I’ll have to mull it over.
I know it will involve scaling coordinates at least, it’s just a question of how.

1 Like

Ah, thank you! Ill be away from the computer a bit but ill take a look as soon as I’m able

1 Like

Sorry, just to try to keep things from jumping around too much, back to this thread…

I can understand things up to the hash fuctions, then im lost, just because there are too many operators I dont know about or understand yet (more specifically the different bitwise operators). I looked into them each individually and have an idea of what each does on its own, just seeing it all together i get a bit lost as to whats going on. But Im just going to keep reading about what i dont get yet (to try and spare you too long an explaination) and hopefully things will make more sense.

Oh, you should have said so sooner.
I didn’t realise it was the operators you were having problems with.


^= is the compound form of ^, which is the xor operator.
<< and >> are the bit shifting operators.

To understand them you first need to understand binary.
https://www.mathsisfun.com/binary-number-system.html

Personally I find that thinking of binary numbers like this helps a lot with understanding them:
powers-of-2

(This way of looking at them is also fundamental to certain types of calculations where binary numbers are treated as polynomials. Polynomials still scare me slightly.)

<< shifts the binary digits left.
So for a single shift 0110 becomes 1100, and 0101 becomes 1010.

>> shifts the binary digits right.
So for a single shift 0110 becomes 0011, and 0101 becomes 0010.

(Note: ‘bit’ is actually short for ‘binary digit’.)

^ is a bit different, it takes two numbers and applies the outcome of a ‘truth table’ over every bit:

left right output
0 0 0
1 0 1
0 1 1
1 1 0

Or to put it another way, the output is 1 if only one of the two bits is 1.

So 0110 ^ 0101 would become 0011 because:
0 ^ 0 becomes 0
1 ^ 1 becomes 0
1 ^ 0 becomes 1
0 ^ 1 becomes 1


To help you understand what’s going on, here’s a version that only introduces new variables rather than modifying old ones:

uint32_t hash(uint32_t value)
{
	const uint32_t v0 = value;
	const uint32_t t0 = (v0 << 13);
	const uint32_t v1 = (v0 ^ t0);
	const uint32_t t1 = (v1 >> 17);
	const uint32_t v2 = (v1 ^ t1);
	const uint32_t t2 = (v2 << 5);
	const uint32_t v3 = (v2 ^ t2);
	return v3;
}

So basically I’m just mixing up all the bits and making a real hash of them.


But frankly, the hash function could have just used arithmetic operators.
Some hash functions do.

Ultimately unless you’re a particularly good mathematician, all you can do with hash functions is either borrow someone else’s function or do trial and error until you get some nice output.

In the cases of generating backgrounds/terrain you don’t need to worry too much about the quality of the function because of the way it’s being used.

This page has some examples of some hash functions (all operating on arrays of characters, but don’t worry too much about that, the point is the mish-mash of operations being used):
http://www.partow.net/programming/hashfunctions/#AvailableHashFunctions


Usually I end up getting sidetracked because I end up clicking on many different links or doing something else part way through an explanation.
(Or just getting lost in thought.)

1 Like

Well, to be fair there’s still a bit more i dont understand yet, its also just a matter of identifying what all that is too… partly being as much a beginner as I am and it also doesnt help I have ADHD so i can just get a bit overwhelmed looking at a lot of new information at once, can just be a lot to take in at first, but i just wanted to try to break it down and identify what each part was before asking too many questions (so i could do as much of my own homework first).

But that is incredibly helpful, thanks a lot! I sort of understood the concept of bit shifting, didnt really understand anything about the xor operator and didnt really know anything about binary at all though… knew what 8 bit meant but didnt even know what bit was short for. But that helps clarify a lot, and between the explanation and reference material i can see theres a sort of pattern to the counting/numerical values, thanks a lot!

And I definitely understand more about the concept of hash values, just as for whats actually going on under the hood its a little beyond me at the moment, but like i said, i still have my own homework to do yet to get a better idea just where im lacking and what i can get some idea of on my own, then ill have some more questions. Thanks again!

Which things?

If you think you’re overwhelmed,
imagine what it must be like in my head. :P

A worked example of XOR

The truth table for XOR is as thus:

left right output
0 0 0
1 0 1
0 1 1
1 1 0

Assume you want to apply xor to two 4-bit values:
0110 and 0101.

The right most bit (the ‘least significant bit’) is the 0th bit.
The left most bit (the ‘most significant bit’) is the 3rd bit.

So you start with the 0th bit.
The 0th bit of 0110 is 0.
The 0th bit of 0101 is 1.

So your ‘left’ bit is 0 and your ‘right’ bit is 1.
Looking at the truth table, a ‘left’ bit of 0 and a ‘right’ bit of 1 gives an output of 1.

Then you move on to the next bit and repeat that process,
so you end up with the following steps:

index left right output
0th 0 1 1
1st 1 0 1
2nd 1 1 0
3rd 0 0 0

Thus 0110 ^ 0101 == 0011.

An alternative approach

An alternative way to look at it is to think of each bit as representing a boolean value.
Think of any 0 bit as false and any 1 bit as true.

Then you can express XOR in terms of logic:

If either a or b is true, then the output is true
If both a and b are true, then the output is false
If neither a nor b is true, then the output is false

(Which is exactly what the ‘truth table’ says.)

This way of viewing it is closer to XOR’s origins in boolean logic.

It’s the same pattern in any number system with any base.

How bases work

Base ‘X’ means that the system has ‘X’ symbols that it can use to represent its digits.

  • Binary is base 2, so there are 2 symbols:
    • 0 1
  • Decimal is base 10, so there are 10 symbols:
    • 0 1 2 3 4 5 6 7 8 9
  • Hexadecimal is base 16, so there are 16 symbols:
    • 0 1 2 3 4 5 6 7 8 9 A B C D E F

It starts to get complicated when you need a second digit though:

  • In base 10, the value ‘10’ means you have cycled through all 10 symbols once, thus is has a value of 10 (in decimal)
  • In base 2, the value ‘10’ means you have cycled through all 2 symbols once, thus it has a value of 2 (in decimal)
  • In base 16, the value ‘10’ means you have cycled through all 16 symbols once, thus is has a value of 16 (in decimal)

Then things get even trickier as more digits come into play.
The best way to understand what happens then (though not the easiest way is to understand how the relationship between the digit’s position and the number’s base.
That relationship is exponentiation.

The value of any digit in any numeral system is the numbers’ base raised to the power of the digit’s position.

So the Nth digit (using 0-indexing) in a base M system is a multiple of MN.
Some examples:

  • The 4th digit in a base 10 system represents multiples of 104, i.e. multiples of 10000
  • The 4th digit in a base 2 system represents multiples of 24, i.e. multiples of 16
  • The 4th digit in a base 16 system represents multiples of 164, i.e. multiples of 65536

If it didn’t make sense before then perhaps this will make sense now:

powers-of-2

If that still doesn’t make sense…
You could try counting on your fingers:
https://www.mathsisfun.com/numbers/binary-count-fingers.html

That’s not really a problem.
It’s very rare to need to write your own hash function.

1 Like

Well like I said, I’m still trying to determine just what it is i don’t understand, can just tend to bled together a bit at first :stuck_out_tongue:

I can definitely follow those parts now though, thanks! I also didn’t quite know what msb and lsb were- knew what they meant but didn’t know how to identify them, so that was helpful for that too, thanks again!

1 Like

Terminology

For the record, I made this image last year in november.
You’re not the first person to have that problem and you won’t be the last.

1 Like