Can I actually use 4x4 tile sets?

Sorry if this is a dumb question. I’m attempting to make a small platformer game. I made 16 4x4 tiles and want to use them in my game. Are 4x4 tiles actually easily usable? Also, how would I draw them?

You might want to have a look at the Arduboy Magazine as it has a number of worked examples of sprites, maps and so forth that may help.

1 Like

Attempted to draw a 4x4 bitmap and the drawBitmap function outputted some garbage that looked like this:
░░█░
█░█░
█░██
░░░░
░░██
░███
░███
░░██
Even though I just inputted this:
░░░░
░██░
░██░
░░░░

Test Code
#include <Arduboy2.h>
Arduboy2 arduboy;
const unsigned char PROGMEM tiles[1][2] = {
  {0xF9, 0x9F} // Block
};
void setup() {
  arduboy.begin();
  arduboy.setFrameRate(15);
}
void loop() {
  if (!(arduboy.nextFrame()))
    return;
  arduboy.clear();
  arduboy.drawBitmap(4,4,tiles[0],4,4);
  arduboy.display();
}

You have a last parameter on the drawBitmap that can either be BLACK or WHITE. It color the bits that are set to “1”. I probably explain this like a potato but…
try
arduboy.drawBitmap(4,4,tiles[0],4,4,WHITE);
and
arduboy.drawBitmap(4,4,tiles[0],4,4,BLACK);
to see how it goes.

1 Like

The default value for color is white

I’m still probably going to use 4x4 tiles just so I can be lazy and not put in scrolling make everything fit on one screen neatly.

Oh yes that’s right the default is set to white. Well since i’m a beginner i have no idea then. I throught you could make 4x4 sprites like you can do 8x8 or 16x16.
Well wait a minute… i can’t find the picture even if i saw it countless times but i read this line in the arduboy2.pdf :
"Each byte in the array specifies a vertical column of 8 pixels, with the least significant bit at the top."
So now i wonder if doing a sprite smaller than 8x8 is actually tricky.

IIRC there is a picture on the forum that explains how the bits are used when you call the draw function (i am sure someone knows it and will post it. I say “thanks” in advance) i think this could help.

1 Like

I’m not sure how the arduboy2 library handles the sprite drawing method, but you could write your own drawing method to handle the 4x4 tiles. You would just need to know how you are storing the data in the code. If you use TeamARG’s tile converter, I think the smallest is 8x8, and their output is an array of HEX characters. So for example if you store the image set as an array of bits, 1 and 0s, then you could write your own method that could handle drawing them. Mostly it will come down to how you store the image.

1 Like

Firstly, Arduboy2 can’t render a bitmap in that condensed format, a 4x4 bitmap in Arduboy2 format would be:

const unsigned char PROGMEM tile0[] =
{
	0xF0, 0x90, 0x90, 0xF0
};

If you want to be able to condense the bitmap further you’d have to write the code for drawing it yourself.

You could take this approach:

#include <Arduboy2.h>
Arduboy2 arduboy;

const unsigned char PROGMEM tiles[][4] =
{
    { 0xF0, 0x90, 0x90, 0xF0 },
    { 0x90, 0xF0, 0xF0, 0x90 }
};


void setup()
{
  arduboy.begin();
}

void loop()
{
  if (!(arduboy.nextFrame()))
    return;
  arduboy.clear();
  arduboy.drawBitmap(4,4,tiles[0],4,4);
  arduboy.drawBitmap(16,16,tiles[1],4,4);
  arduboy.display();
}

Or you could take this approach:

#include <Arduboy2.h>
Arduboy2 arduboy;

const unsigned char PROGMEM tiles[] =
{
	4, 4,
	// Tile 0
	0xF0, 0x90, 0x90, 0xF0,
	// Tile 1
	0x90, 0xF0, 0xF0, 0x90
};


void setup()
{
  arduboy.begin();
}

void loop()
{
  if (!(arduboy.nextFrame()))
    return;
    
  arduboy.clear();
  Sprites::drawSelfMasked(4, 4, tiles, 0);
  Sprites::drawSelfMasked(16, 16, tiles, 1);      
  arduboy.display();
}
2 Likes

Yeah, that’s the approach I’m currently taking. Sure it’ll cost a bit more space, but it won’t waste processing power. Thanks for the help, though!

1 Like

Specifically it will cost twice the space the images would require if you could use the condensed format (which in fairness would be a lot if you have a lot of tiles, if it’s only a small number then there’s nothing to worry about). Also processing power is probably the most abundant resource the Arduboy has. I’ve rarely seen people having speed issues, but I’ve seen plenty of people hitting the size limit.

Ultimately though you’re best off following the YAGNI principle and working with the system that’s already in place. Only worry about looking for memory savings when you’re getting close to hitting the memory cap (80-90%).

1 Like

You mean this?:

From that thread that @emutyworks made.

To do it in a way that doesn’t require padding is not impossible, but it would be less efficient and I certainly wouldn’t want to be the one who had to figure out how to write it :P.

I’m going to arrogantly assume this was the picture you were talking about and say “you’re welcome” :P.

The smallest sprite possible is 1x8, and smaller sizes can be emulated with masks.

3 Likes

I actually meant that I think the smallest tile the TeamARG tilesheet converter will take is a tile that is 8x8. But it is good to know that the smallest sprite is 1x8, and you can use masks to emulate smaller sizes. :smiley:

1 Like

Oh right, sorry.
In that case I have no clue about TeamARG’s thing ¯\_(ツ)_/¯, I use my own hand-written converter.

1 Like

Nice. I need to write my own level editor that will take in tile sheets. So this discussion is quite useful for me to start thinking about how small of a tile I will allow.

2 Likes

Okay, I’m running into a weird problem: my level doesn’t load.

It just shows a few weird glitchy tiles across the middle. Also sorry for the messy code, I’m relatively new to Arduino C

Code
// Platformer
// Another overly ambitious project
// By V360 (@TheV360gameDev)

#include <Arduboy2.h>

Arduboy2 arduboy;

#define LVLWIDTH 32
#define LVLHEIGHT 16
#define LVLNUMBER 1

byte state = 0;

// 64 bytes
const unsigned char PROGMEM tiles[16][4] = {
  {B0000, B0000, B0000, B0000}, // Nothing
  {B1111, B1001, B1001, B1111}, // Block
  {B1110, B1111, B1011, B1110}, // Exit door
  {B1001, B0110, B0110, B1001}, // Mine
  {B0100, B0010, B0100, B0010}, // Water surface
  {B1010, B0000, B0101, B0000}, // Water
  {B0000, B0110, B0110, B0000}, // Key
  {B1111, B1011, B1101, B1111}, // Locked block
  {B1111, B1011, B0101, B1111}, // Left conveyor
  {B1111, B0101, B1011, B1111}, // Right conveyor
  {B0110, B1111, B1101, B0110}, // Gravity orb?
  {B0010, B1110, B1110, B0010}, // Pedestal
  {B1100, B1000, B1000, B1100}, // Toggle switch
  {B0101, B1000, B0001, B1010}, // Off switch block
  {B0111, B1001, B1001, B1110}, // On switch block
  {B1111, B1101, B1111, B1101} // Player
};

// 256 bytes per level
const unsigned char PROGMEM levels[LVLNUMBER][16][16] = {
  {
    {0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11},
    {0x10,0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x10,0xF0,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01},
    {0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11}
  }
};

// 512 bytes for an unsquished level
unsigned char tileMap[32][16];
double x = 0;
double y = 0;
double ax = 0;
double ay = 0;

void setup() {
  // This stuff will run once!

  //Start your Arduboys...
  arduboy.begin();
  arduboy.setFrameRate(30); //30 fps
}

void loop() {
  // I don't want to go on without waiting a frame first!
  if (!(arduboy.nextFrame()))
    return;

  arduboy.clear();
  
  switch (state) {
    case 0:
      arduboy.setCursor(0,0);
      arduboy.print("Platform");
      if (arduboy.pressed(A_BUTTON)) {
        arduboy.initRandomSeed();
        state = 2;
        loadMap(0);
      }
      break;
    case 2:
      drawMap();
      break;
  }

  if (arduboy.pressed(B_BUTTON)) {
    arduboy.setCursor(0,56);
    arduboy.print(arduboy.cpuLoad());
  }
  
  arduboy.display(); // Actually make the stuff I drew visible
}

void clearMap() {
  for (byte j = 0; j < LVLHEIGHT; j++) {
    for (byte i = 0; i < LVLWIDTH; i++) {
      tileMap[i][j] = 0;
    }
  }
}

void loadMap(byte mapIndex) {
  for (byte j = 0; j < LVLHEIGHT; j++) {
    for (byte i = 0; i < LVLWIDTH; i++) {
      if (i % 2 == 0) {
        tileMap[i][j] = levels[mapIndex][j][i/2] && 15;
      } else {
        tileMap[i][j] = levels[mapIndex][j][i/2] >> 4;
      }
      if (tileMap[i][j] == 15) {
        x = i*4;
        y = j*4;
        tileMap[i][j] = 0;
      }
    }
  }
}

void drawMap() {
  for (byte j = 0; j < LVLHEIGHT; j++) {
    for (byte i = 0; i < LVLWIDTH; i++) {
      if (tileMap[i][j] != 0) {
        arduboy.drawBitmap(i*4,j*4,tiles[tileMap[i][j]],4,4);
      }
    }
  }
}

First problem:
tileMap[i][j] = levels[mapIndex][j][i/2] && 15;
tileMap[i][j] = levels[mapIndex][j][i/2] >> 4;

You can’t just copy straight from progmem, you have to use the special macros for it pgm_read_byte and pgm_read_word (among others).

Also you appear to be trying to use the short-circuiting and operator (&&) instead of the bitwise and operator (&). Instead of masking off bits, the arguments are going to be converted into bools (true or false) which in turn will be short-circuit and-ed together into another bool, which is then cast back to a number, which will be 1 or 0.

And the compiler will be using C++11 by default, not C.

1 Like

(dang, i thought i put only one & there)

But I didn’t know that you had to use a special command to copy from PROGMEM. Thanks!
How would I change my code to make PROGMEM actually work?

EDIT: I got PROGMEM working!

Code
void loadMap(byte mapIndex) {
  for (byte j = 0; j < LVLHEIGHT; j++) {
    for (byte i = 0; i < LVLWIDTH; i++) {
      if (i % 2 == 0) {
        tileMap[i][j] = pgm_read_byte(&(levels[mapIndex][(i/2)+(j*16)])) >> 4;
      } else {
        tileMap[i][j] = pgm_read_byte(&(levels[mapIndex][(i/2)+(j*16)])) & 15;
      }
      if (tileMap[i][j] == 15) {
        x = i*4;
        y = j*4;
        tileMap[i][j] = 0;
      }
    }
  }
}

void drawMap() {
  for (byte j = 0; j < LVLHEIGHT; j++) {
    for (byte i = 0; i < LVLWIDTH; i++) {
      if (tileMap[i][j] != 0) {
        arduboy.drawBitmap(i*4,j*4,tiles[tileMap[i][j]],4,4);
      }
    }
  }
}

The only weird thing is that tiles[] is stored in PROGMEM, but when I use it without the macro, it works correctly?

That’s because arduboy.drawBitmap expects the passed pointer to be in progmem. If you passed a bitmap that wasn’t in progmem, it wouldn’t work properly.

1 Like