Make Your Own Arduboy Game: Part 9 - Mapping DinoSmasher

Thank you so much for this. Shrinking the size of the screen did make it clearer, since I was confused about how the actual drawing was happening. And I think the modulo makes more sense now - it’s causing the smoother effect of the map moving because it’s keeping the offset smaller than mapx and mapy? Or am I still thinking of that wrong?

And when you mention “drawing part of the map offscreen”, do you mean actually drawing tiles that you can’t see on the screen or removing tiles that are no longer within the screen view?

No need to bust out your scanner, I’m sure I’ll get this after reading through your post and dissecting it some more.

Sort of.

The non-smooth map moving is what happens when the tiles are drawn to the same location on screen every time.

The smooth map moving is what happens when the tiles are offset by a value from 0 to -15.
(The offset is the remainder of the map offset when divided by the tile size.)

If mapx and mapy were positive then they’d represent the player’s position in world coordinates and mapx % tileWidth and mapy % tiley would actually be the player’s offset into the particular tile that they’re standing on, but crait has chosen the approach of moving the map rather than moving the player.

I mean that technically the code tries to draw tiles (or part of the tiles) outside of the screen’s boundaries,
but obviously it can’t actually draw outside the screen because there’s nothing to draw to,
(it’s like a painter trying to paint outside his canvas - he just ends up waving his brush in the air)
but the Sprites code accounts for that and ignores pixels that would be drawn ‘outside the screen’.

I had a quick go anyway:

So you can see the + mapx % TILE_SIZE part actually causes the map to start being rendered a few pixels outside the top left corner of the screen.

If it were just + mapx then after the first tile the map representation would end up being drawn further and further outside the screen until it wasn’t actually being rendered on-screen at all.

The diagram on the left is supposed to illustrate how the code isn’t actually drawing the whole map,
it’s looking at a rectangular section and only drawing the tiles touched by that section.

(The grids are really imprecise because I’m too impatient to draw accurate grids, and I couldn’t be bothered to grab my graph paper.)

I also found something online that might help to illustrate how the screen is only rendering a portion of the map:

viewport-culling

There’s another thread on here where I discussed tile rendering in detail,
I can dig that out too if your interested in tile maps,
though I’m now sure how much it would answer your question.

Edit:

It was this thread:

I don’t directly explain everything, but the diagrams might be useful for understanding tile rendering.

Thank you! It’s making more sense. One of the diagrams you drew in that other thread you linked to is really helpful. I’m going to digest it some more and I’ll be back with more questions, if I still can’t get it straight.

At the end of the day I understand what it’s doing, I’m just struggling to understand exactly (mathematically) what every number in this function is doing. Maybe that isn’t necessary.

2 Likes

The important thing to remember is that you’re working with multiple coordinate systems.

x * TILE_SIZE and y * TILE_SIZE are screen coordinates.
mapx and mapy are map offsets in world coordinates.
Then % TILE_SIZE is used to get the player’s offset within a tile (sort of),
which is then used to offset the rendered representation of the world (the map is not the territory).

You could try working out the numbers on paper (maybe with a calculator) to see how they behave.
Often just stepping through the code manually makes it easier to understand, and that’s probably the closest you can get tp being able to do that.

You could also try this:

constexpr uint8_t TILE_SIZE = 16;

void drawworld()
{
	const int tileswide = WIDTH / TILE_SIZE + 1;
	const int tilestall = HEIGHT / TILE_SIZE + 1;

	for (int y = 0; y < tilestall; y++)
	{
		for (int x = 0; x < tileswide; x++)
		{
			const int tilex = x - mapx / TILE_SIZE;
			const int tiley = y - mapy / TILE_SIZE;
			if (tilex >= 0 && tiley >= 0 && tilex < WORLD_WIDTH && tiley < WORLD_HEIGHT)
			{
				Sprites::drawOverwrite(x * TILE_SIZE + mapx % TILE_SIZE, y * TILE_SIZE + mapy % TILE_SIZE, tiles, world[tiley][tilex]);
			}
		}
	}

	arduboy.fillRect(0, 0, 48, 8, BLACK);
	arduboy.setCursor(0, 0);
	
	// Map coordinates
	arduboy.print(mapx);
	arduboy.print(',');
	arduboy.print(mapy);
	arduboy.print('\n');
		
	// Map coordinate remainder
	arduboy.print(mapx % TILE_SIZE);
	arduboy.print(',');
	arduboy.print(mapy % TILE_SIZE);
	arduboy.print('\n');
	
	// Render coordinate without modulo
	arduboy.print(1 * TILE_SIZE + mapx);
	arduboy.print(',');
	arduboy.print(1 * TILE_SIZE + mapy % TILE_SIZE);
	arduboy.print('\n');
		
	// Render coordinate with modulo
	arduboy.print(1 * TILE_SIZE + mapx % TILE_SIZE);
	arduboy.print(',');
	arduboy.print(1 * TILE_SIZE + mapy % TILE_SIZE);
	arduboy.print('\n');
}

Smasher.hex (26.4 KB)

Hopefully you’ll be able to see that not using the modulo increases the size indefinitely, while using the modulo just provides the 0 to -15 offset (the player’s offset into the tile they’re currently on).

2 Likes

I’m getting this now :). I did working through this stuff manually but was doing something wrong with the modulo, so it wasn’t matching up with the numbers being printed. With all these numbers being printed it started making sense, I was doing the process wrong with the additions and modulo stuff. I appreciate your patience.

1 Like

If you’re on Windows, the calculator actually has a Mod button (in both scientific and programmer mode).

It’s a shame regular calculators don’t tend to have it.

Ah, ok, that makes sense.

No problem.
Answering people’s questions and explaining things is something I do a lot.
If you have any more questions, don’t hesitate to ask.

I still think you missed your calling in life (a teacher!). Actually, its not too late.

I would, but it would mean spending N years getting teaching qualifications,
and then I’d have to mark students’ work and put up with their bad behaviour,
and try to make the students not hate me.

Maybe I’ll become a tutor instead. (£20-£40 an hour is a nice incentive.)

4 Likes

5 posts were split to a new topic: Tile Collisions?

Hello! Thank you for the tutorial! First time coder here.
I’ve have done your tutorial but decided it would be fun to have the map be created randomly. I have it working but I have a puzzle I don’t understand. Here is my code based mostly from yours:

  for (int x = 0; x < WORLD_HEIGHT; x++) {
    for (int y = 0; y < WORLD_WIDTH; y++) {
      world[x][y] = random(0, 1);
    }
  }

So I’m trying to change every value of the array “world” to either 0 or 1 (which would be 0=grass and 1=water).

When I do random(0, 1) it gives me grass for ALL tiles
But if I do random(0, 2) it works – it gives me grass and water. I don’t even have a “2” ready to go yet.

What is going on? I don’t understand. Thank you for your help!

1 Like

random() stupidly accepts a min value inclusive and a maximum value exclusive.

So if you want it to return 0 and 1s, you need to do random(0, 2) as you have worked out by trial and error.

3 Likes

Ah ok got it! Thank you :slight_smile:

2 Likes

hello, quick question, what happens if i create the “const ins variables” at the beggining of the code instead of inside the functions? it’s easier for me to keep track of everything on that way, but don’t have idea if that affects something on the arduboy memory

It’s contrary to the expectations of a layman, but having an inclusive lower bound and an exclusive upper bound occurs a lot in computer science for various reasons.

I read a thing once (I think it was by Edsger Dijkstra) explaining a number of useful properties that arrangement has.

That gives you a ‘global variable’ instead of a ‘local variable’.

Global variables exist for the whole of the program so they have to be allocated in RAM and thus can be more costly to access (because RAM has to be accessed with an address and usually uses more machine code instructions to access).

Local variables only exist for the duration of the scope that they’re in (e.g. the duration of the function, the duration of an if block…) and thus can be kept in things called ‘registers’, which are a kind of faster, easier to access memory.

(Some CPUs use what’s referred to as a ‘load-store architecture’, which means they have to load the data from RAM into registers before they can operate on the data, and then they have to store the data back into RAM.
The loading and storing requires extra machine code instructions. I’m fairly sure the Arduboy’s CPU is one of these.)

In what way?

If you only need a variable for the duration of one function then it makes sense to only care about it when you’re reading that function’s code.
The rest of the time you can simply forget it exists.
You don’t need to remember every single variable.

The variables you put at the top of the code, the global variables,
should be the things that have to exist for the entire duration of the program,
or at least exist for more than one function.
E.g. the player, enemies, the game state.

Hi there,

I am new to the arduboy and these tutorials helped me a lot. Thanks for that.
I customised my DInosmasher a bit but it lacks a lot of features.

Do you know when/if the rest of the tutorial will be out ?
Take care

1 Like

Tell me please, when i try to replace the black square with a sprite i have nothing showing on the map. But that nothing can move around the map. So, how to make the image visible?
my try:

void drawplayer() {
  const unsigned char PROGMEM noname[] =
  {
    // width, height,
    16, 16,
    // TILE 00
    0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xb9, 0xb1, 0xb1, 0xb9, 0x83, 0x1f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0x07, 0x73, 0x7b, 0x7b, 0x78, 0x3f, 0x9f, 0xdf, 0x9f, 0xbf, 0x38, 0x79, 0x39, 0xb3, 0x87, 
  };
}

@NoobGeek,
Welcome to the Arduboy Forum!

First:
When you post code in this forum, please enclose it in markdown code tags:

Start with a line containing three backticks followed by cpp .
Insert your code starting on the following line.
On the line following you code, put another three backticks.
The backtick character is commonly on the key below the Esc key at the top left of a U.S. keyboard. If you can’t find it on your keyboard, you can copy and paste from the tags here:

```cpp
The first line of your code
More code
The last line of your code
```

I’ve added the tags to your post above but please do it yourself in the future.


You’ll have to be more specific and/or provide more code showing how you’re trying to display and move your sprite. I wrote a simple sketch that draws and allows you to move your sprite using the D-pad. It may provide information that helps you.

#include <Arduboy2.h>

Arduboy2 arduboy;

const unsigned char PROGMEM noname[] =
{
  // width, height,
  16, 16,
  // TILE 00
  0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xb9, 0xb1, 0xb1, 0xb9, 0x83, 0x1f, 0xff, 0xff, 0xff, 0xff,
  0xff, 0x07, 0x73, 0x7b, 0x7b, 0x78, 0x3f, 0x9f, 0xdf, 0x9f, 0xbf, 0x38, 0x79, 0x39, 0xb3, 0x87,
};

int16_t xPos = 0;
int16_t yPos = 0;

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

void loop() {
  if(!arduboy.nextFrame()) {
    return;
  }

  arduboy.pollButtons();
  arduboy.clear();

  if (arduboy.justPressed(UP_BUTTON)) {
    --yPos;
  }
  if (arduboy.justPressed(DOWN_BUTTON)) {
    ++yPos;
  }
  if (arduboy.justPressed(LEFT_BUTTON)) {
    --xPos;
  }
  if (arduboy.justPressed(RIGHT_BUTTON)) {
    ++xPos;
  }

  Sprites::drawOverwrite(xPos, yPos, noname, 0);

  arduboy.display();
}

NoobGeek1.hex (19.5 KB)

1 Like

I had a closer look at @crait’s “completed code” and now know what you’re trying to do.

The spite that you’ve created for the player is just data. You need to draw it with one of the Sprites functions. Since the bitmap array you’ve provided doesn’t include a mask, the drawOverwrite() function is likely the best to use.

You need to put your noname array in the sketch as “global” data. For my testing, I put it before the tiles array

[...]
int mapx = 0;
int mapy = 0;

const unsigned char PROGMEM noname[] = {
  // width, height,
  16, 16,
  // TILE 00
  0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xb9, 0xb1, 0xb1, 0xb9, 0x83, 0x1f, 0xff, 0xff, 0xff, 0xff, 
  0xff, 0x07, 0x73, 0x7b, 0x7b, 0x78, 0x3f, 0x9f, 0xdf, 0x9f, 0xbf, 0x38, 0x79, 0x39, 0xb3, 0x87, 
};

const unsigned char tiles[] PROGMEM  = {
// width, height,
16, 16,
[...]

Then, you need to replace the fillRect() function in drawplayer() with the function to draw the sprite

#define PLAYER_SIZE      16
#define PLAYER_X_OFFSET    WIDTH / 2 - PLAYER_SIZE / 2
#define PLAYER_Y_OFFSET    HEIGHT / 2 - PLAYER_SIZE / 2
void drawplayer() {
//  arduboy.fillRect(PLAYER_X_OFFSET, PLAYER_Y_OFFSET, PLAYER_SIZE, PLAYER_SIZE, BLACK);
  Sprites::drawOverwrite(PLAYER_X_OFFSET, PLAYER_Y_OFFSET, noname, 0);
}

You should now have your noname sprite drawn instead of a black rectangle.


Without masking, all of the pixels in your sprite are drawn over the background. So, the next thing you’ll probably want to do is create a sprite with a mask and use drawPlusMask() to draw it.

Edit:
Here’s the array (I renamed it to player) and drawplayer() functions with the outside of the image masked

// player in drawPlusMask() format (mask included in image)
const unsigned char PROGMEM player[] = {
  // width, height,
  16, 16,
  // FRAME 00
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xb8, 0xfe, 0xb4, 0xfe,
  0xb4, 0xfe, 0xb8, 0xfe, 0x80, 0xfc, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0xf8, 0x70, 0xfc, 0x78, 0xfc, 0x78, 0xfc, 0x78, 0xff, 0x3f, 0xff, 0x1f, 0x7f,
  0x1f, 0x3f, 0x1f, 0x7f, 0x3f, 0x7f, 0x38, 0xff, 0x78, 0xfe, 0x38, 0xfe, 0x30, 0x7c, 0x00, 0x78
};

void drawplayer() {
  Sprites::drawPlusMask(PLAYER_X_OFFSET, PLAYER_Y_OFFSET, player, 0);
}

NoobGeek2.hex (26.5 KB)

2 Likes

Thanks, it worked! I am surprised that you answered my question so quickly, and with a detailed answer. I am very grateful to you for that! From now on I will ask questions in the correct form. And I will try even more to delve into the software part before asking the community! Thanks again!

3 Likes

2 posts were split to a new topic: How do I create and control multiple objects (bullets)