Super Crate Buino

Have been working on a port of ‘Super Crate Buino’ from the Gamebuino to play on my MiSTer-based FPGA implementation of the Arduboy - and it’s a really great little game!

NODATE-screen_0002

Sound is the only thing I don’t have working yet, still need to make time to sit down and decipher the undocumented format of the Gamebuino sound ‘pattern’ values… (and decide what to do about the noise channel…)

But it should be fairly easy to modify this port of the game to work on a real Arduboy too, if someone is looking for a project?

Would need to swap out the normal EEPROM functions in place of my modified readEEPROM / writeEEPROM functions for a start, and implement INVERT support for drawPixel (Arduboy2’s drawPixel function is so optimised that it doesn’t support INVERT?!), also BLACK and WHITE are sometimes opposite in the code because the Gamebuino screen is an LCD not an OLED… and I also did some fairly hacky things to easily work with the way the Gamebuino libraries expect things to be - Gamebuino functions expect a global ‘color’ variable instead of passing color into each function (for this I just recklessly hijacked textColor), and Gamebuino code also expects to directly access x and y coordinates for text, instead of using setCursor (so I just made them public).

Apart from that, there are a few exclusively Gamebuino functions and defines required by the game, so I just added them onto the end of my already modified versions of Arduboy2.cpp / Arduboy2.h - but they could be put in an ‘Arduboy2Ext’ instead? (Also didn’t bother changing those functions to make BLACK into WHITE, and WHITE into BLACK - which is why the game appears inverted, which I honestly prefer anyway!)

  void     drawBitmap(int8_t x, int8_t y, const uint8_t *bitmap);
  void     drawBitmap(int8_t x, int8_t y, const uint8_t *bitmap, uint8_t rotation, uint8_t flip);
  boolean  getBitmapPixel(const uint8_t* bitmap, uint8_t x, uint8_t y);
  boolean  collideRectRect(uint16_t x1, uint16_t y1, uint16_t w1, uint16_t h1 ,uint16_t x2 ,uint16_t y2, uint16_t w2, uint16_t h2);

And there is a fair bit of empty space (where I have stuck the score counter) because of the extra width of the Arduboy’s display resolution… so, room for some pixel-art magic?! (or potentially room for puzzling out how to expand the world view to take advantage of the extra space?)

So… yeah, obviously a lot that could stand to be tidied up, but much of the major hump and head-scratching is already done if someone(s) else feel motivated to make this playable on an actual Arduboy!

4 Likes

Great job.

1 Like

This looks really good! Hope it gets done soon!

Are these function you added or functions you need?

Manipulating individual pixels is surprisingly expensive when there are 8 pixels per byte and your CPU doesn’t have a barrel shifter.

That said, it should actually be easy enough to modify drawPixel to support invert.
It would come at a small performance cost though.

The original is:

// Some assembly for calculating 'bit' and 'row_offset' (which I have omitted
// ...

uint8_t data = sBuffer[row_offset] | bit;
if (!(color & _BV(0))) data ^= bit;
sBuffer[row_offset] = data;

So to support inverting you’d need something like:

constexpr uint8_t colourBit = (1 << 0);
constexpr uint8_t invertBit = (1 << 1);

uint8_t data = sBuffer[row_offset]

if ((color & colourBit) != 0)
	data |= bit;
else
	data &= ~bit;

if ((color & invertBit) != 0)
	data ^= bit;

sBuffer[row_offset] = data;

Or, if you ban ‘invert’ from being used with white then theoretically…

constexpr uint8_t colourBit = (1 << 0);
constexpr uint8_t invertBit = (1 << 1);

uint8_t data = sBuffer[row_offset]

if ((color & invertBit) == 0)
	data |= bit;

if ((color & colourBit) == 0)
	data ^= bit;

sBuffer[row_offset] = data;

That’s what arduboy.invert(true) is for - it inverts the display:

Added. I borrowed them from the Gamebuino library port by @akkera102

OK, got stuck on a plane circling Melbourne tonight, due to:

  1. Stormy Weather
  2. Bushfire Smoke
  3. All Of The Above

…so took the opportunity to get the laptop out and make the changes myself - YOU’RE WELCOME! :rofl:

Super-Crate-Buino.ino.hex (66.4 KB)

…instructions for the game are here:

http://legacy.gamebuino.com/forum/viewtopic.php?f=17&t=3151

PS. Hold UP and press DOWN for a weapons cheat! :wink:

PPS. As I said, still room for improvement… and still needs sound!

3 Likes

Impressive! Does it use the whole screen?

No - Rodot wrote it to fit the Gamebuino’s 84x48 LCD screen, so there is a 44x48 empty space on the right-hand side of the screen… room for a nice looking pixel-art frame showing the score / high-score / next-unlock variables maybe? :wink:

1 Like

I’d rather work on expanding the view to be honest…

I did try just increasing the LCDWIDTH / LCDHEIGHT defines, but that didn’t do it… so the view must be hard-coded at least a little in some way?

I will bite, will have a look at this with Filmote. And if memory permits will maybe have a go at enhancements…

1 Like

Cool! Well here’s a branch with the new .ino and modified Arduboy2.cpp / Arduboy2.h I used:

EDIT: the changes to Arduboy2.* are the Gamebuino functions pasted in at the end, and making cursor_x / cursor_y public, and substituting in this drawPixel:

void Arduboy2Base::drawPixel(int16_t x, int16_t y, uint8_t color)
{
  #ifdef PIXEL_SAFE_MODE
  if (x < 0 || x > (WIDTH-1) || y < 0 || y > (HEIGHT-1))
  {
    return;
  }
  #endif

  switch (color)
  {
    case WHITE:  sBuffer[x + (y/8)*WIDTH] |=  (1 << (y&7)); break;
    case BLACK:  sBuffer[x + (y/8)*WIDTH] &= ~(1 << (y&7)); break;
    case INVERT: sBuffer[x + (y/8)*WIDTH] ^=  (1 << (y&7)); break;
  }
}

Had a small look at it and begin to think that the ‘SCALE’ define combined with the LCD_WIDTH/HEIGHT define might be something to look at…

I wasn’t having much luck with compiling it because of conflicts with the already installed Arduboy2 library, so I decided to write a new header file that declares an Arduboy2Gamebuino class that inherits Arduboy2 and extends it with the new functions.

I got it compiling, but unfortunately the map isn’t drawing properly, so I’ll have to investigate.

If you just overwrite the Arduboy2.cpp and Arduboy2.h files in your libraries/Arduboy2 folder with the ones I posted it will compile. And then work backwards from there, writing a new class until those files can be restored to the unmodified ‘official’ versions?

It took a bit of trial and error, but I finally found the problem.
The x and y coordinates for the drawBitmap functions needed to be int16_t.

The real question is “why did it work when putting the functions directly into Arduboy2?”.

I never like when I can fix a problem but can’t explain how I fixed it,
because it implies that I probably haven’t actually fixed it.


Unfortunately GitHub is still playing up for me, so you’ll have to make do with some in-line code:

#pragma once

#include <Arduboy2.h>

// Gamebuino LCD size
#define LCDWIDTH 84
#define LCDHEIGHT 48

// for Gamebuino extended bitmap function
#define NOROT 0
#define ROTCCW 1
#define ROT180 2
#define ROTCW 3
#define NOFLIP 0
#define FLIPH 1
#define FLIPV 2
#define FLIPVH 3

class Arduboy2Gamebuino : public Arduboy2
{
public:
  // Bring the cursor variables into public view
  using Arduboy2::cursor_x;
  using Arduboy2::cursor_y;

  // Prevent the inherited 'drawBitmap' functions from being hidden
  using Arduboy2::drawBitmap;

  static bool getBitmapPixel(const uint8_t* bitmap, uint8_t x, uint8_t y)
  {
    return pgm_read_byte(bitmap + 2 + y * ((pgm_read_byte(bitmap) + 7) / 8) + (x >> 3)) & (B10000000 >> (x % 8));
  }

  static bool collideRectRect(uint16_t x1, uint16_t y1, uint16_t w1, uint16_t h1 ,uint16_t x2 ,uint16_t y2, uint16_t w2, uint16_t h2){
      return !( x2     >=  x1+w1  ||
                x2+w2  <=  x1     ||
                y2     >=  y1+h1  ||
                y2+h2  <=  y1     );
  }

  void drawBitmap(int16_t x, int16_t y, const uint8_t *bitmap)
  {
    int8_t w = pgm_read_byte(bitmap);
    int8_t h = pgm_read_byte(bitmap + 1);

    bitmap = bitmap + 2;


    uint8_t * buffer  = getBuffer();
    const uint8_t col = textColor;
    const uint8_t bw  = (w + 7) / 8;

    // clip
    if (x >= WIDTH)  return;
    if (y >= HEIGHT) return;
    if (x + w <= 0)     return;
    if (y + h <= 0)     return;

    if (y < 0)
    {
      h += y;
      bitmap -= bw * y;
      y = 0;
    }

    if (y + h > HEIGHT)
    {
      h = HEIGHT - y;
    }

    uint8_t x1 = max(0, x);
    uint8_t x2 = min(WIDTH, x + w);

    // draw
    uint8_t first_bitmap_mask = 0x80 >> ((x1 - x) & 7);
    const uint8_t * bitmap_line = bitmap + (x1 - x) / 8;
    uint8_t screen_mask = 0x01 << (y % 8);
    uint8_t * screen_row = buffer + (y / 8) * WIDTH + x1;

    for (uint8_t dy = 0; dy < h; dy++, bitmap_line += bw)
    {
      const uint8_t * bitmap_ptr = bitmap_line;
      uint8_t bitmap_mask = first_bitmap_mask;
      uint8_t pixels = pgm_read_byte(bitmap_ptr);
      uint8_t * dst = screen_row;

      if (col == BLACK)
      {
        for (uint8_t sx = x1; sx < x2; sx++, dst++)
        {
          if (pixels & bitmap_mask)
          {
            *dst |= screen_mask;
          }
          bitmap_mask >>= 1;

          if (!bitmap_mask)
          {
            bitmap_mask = 0x80;
            pixels = pgm_read_byte(++bitmap_ptr);
          }
        }
      }
      else if (col == WHITE)
      {
        uint8_t inv_screen_mask = ~screen_mask;

        for (uint8_t sx = x1; sx < x2; sx++, dst++)
        {
          if (pixels & bitmap_mask)
          {
            *dst &= inv_screen_mask;
          }
          bitmap_mask >>= 1;

          if (!bitmap_mask)
          {
            bitmap_mask = 0x80;
            pixels = pgm_read_byte(++bitmap_ptr);
          }
        }
      }
      else // invert
      {
        for (uint8_t sx = x1; sx < x2; sx++, dst++)
        {
          if (pixels & bitmap_mask)
          {
            *dst ^= screen_mask;
          }
          bitmap_mask >>= 1;

          if (!bitmap_mask)
          {
            bitmap_mask = 0x80;
            pixels = pgm_read_byte(++bitmap_ptr);
          }
        }
      }

      screen_mask <<= 1;

      if (!screen_mask)
      {
        screen_mask = 1;
        screen_row += WIDTH;
      }
    }
  }

  void drawBitmap(int16_t x, int16_t y, const uint8_t *bitmap, uint8_t rotation, uint8_t flip)
  {
    if ((rotation == NOROT) && (flip == NOFLIP))
    {
      // use the faster algorithm
      drawBitmap(x, y, bitmap);
      return;
    }

    uint8_t w = pgm_read_byte(bitmap);
    uint8_t h = pgm_read_byte(bitmap + 1);

    bitmap = bitmap + 2;


    int8_t i, j; //coordinates in the raw bitmap
    int8_t k, l; //coordinates in the rotated/flipped bitmap
    int8_t byteNum, bitNum, byteWidth = (w + 7) >> 3;

    rotation %= 4;

    for (i = 0; i < w; i++)
    {
      byteNum = i / 8;
      bitNum  = i % 8;

      for (j = 0; j < h; j++)
      {
        if (pgm_read_byte(bitmap + j * byteWidth + byteNum) & (B10000000 >> bitNum))
        {
          switch (rotation)
          {
            // no rotation
            case NOROT:
              k = i;
              l = j;
              break;

            // 90 counter-clockwise
            case ROTCCW:
              k = j;
              l = w - i - 1;
              break;

            // 180
            case ROT180:
              k = w - i - 1;
              l = h - j - 1;
              break;

            // 90 clockwise
            case ROTCW:
              k = h - j - 1;
              l = i;
              break;
          }

          if (flip)
          {
            flip %= 4;

            // horizontal flip
            if (flip & B00000001)
            {
              k = w - k - 1;
            }

            // vertical flip
            if (flip & B00000010)
            {
              l = h - l;
            }
          }

          // place the bitmap on the screen
          k += x;
          l += y;
          if (textColor == BLACK){
            drawPixel(k, l);
          }
          else if (textColor == WHITE){
            drawPixel(k, l, BLACK);
          }
        }
      }
    }
  }
};

Name it whatever, preferably Arduboy2Gamebuino.h, then change:

#include <Gamebuino.h>
Gamebuino gb;

In the original Super-Crate-Buino.ino to:

#include "Arduboy2Gamebuino.h"
Arduboy2Gamebuino gb;

And it should all work automagically.

I intend to spruce it up a bit at some point.
There’s lots of room for improvement in those functions.

Nice. You’ll still need a drawPixel that can do INVERT, and can probably re-write the sketch to use Arduboy2’s collide instead of that collideRectRect?

Yes, but I’ll wait until GitHub’s done disagreeing with me before I go messing around with those.

I figured out why GitHub was misbehaving.

Sure enough, it was my own fault, but it was something I did months back, if not over a year ago, so I’m not surprised it took me the best part of a day to realise what it was.

Before I make a proper repo:

  • Did you write those functions yourself?
  • If not, where are the originals, and what licences did they have?

Also I’d like to point out that you’re probably violating the LGPL because you didn’t ‘make the work’:

carry prominent notices stating that you modified it, and giving a relevant date.

But how the licence behaves in this case is a bit hard to tell because the LGPL was intended to be used with libraries, not standalone programs.
(It literally uses the word ‘library’ about half a dozen times in its licence text.
I’m not sure Mr Rodot did his research properly.)

…as mentioned above, got them from:

Which got them from:

and the drawPixel is from:

EDIT: isn’t a GitHub fork by its nature a clear notice of the original source, the modifications made, and the relevant dates?

1 Like