[WIP]The Depth of Nitemare


#1

I been working on this 13 days ago. I decided to share it today since I’m very far in this game. It’s not done since I have about 3000 bytes to fill.

Nitemare

When I started thinking about making this game. I had a thought to use 16x12 tiles since I can fit 5 tiles vertically instead of 4. Then I realized that it can only accept tiles by 8. So the tiles are 16x16 in ROM, but get overlapped giving me 8x5 visible tiles while 15 of them are off screen vertically. But I got the visual I wanted.

Recently, I tried to load the tileset to RAM and have the routine write those tiles from RAM to screen, so I can change part of the tile set. Such as if I want different walls, as you decend down to the depth of the dungeon. Didn’t work, but I’ll try it again when I get the hang of programming this hardware. The maps data are packed 2 tiles per bytes, with 160 tiles per floor. There’s 13 different floors. I’ve have a lot to learn about this hardware.

There are some bugs like you can pick an — item in battle, which does nothing. And menu quirks.

There’s no save function yet. I wanted to give people a chance to play this game. The instructions for this game is in the Readme file up on github. I’m not a professional programmer, just enjoy making games as a hobby.

Enjoy.


(Pharap) #2

There’s probably an easier way depending on how your tiles are stored.


Would you be happy to accept some suggestions for code improvement when you’re closer to finishing?

I can see various things that might help to reduce code size and make the code cleaner.
E.g. using more local variables rather than global variables, splitting the code up into functions.


(Scott) #3

If you’re going to use boot() instead of begin() you should add arduboy.flashlight() or at least arduboy.safeMode() after it, in case you run into the bootloader “magic key” problem.


#4

Sure. It’ll help me learn more C++.


(Pharap) #5

If I tried to make some PRs now then I might accidentally try to modify the same code that you’re trying to modify and the modifications might conflict,
so I’ll wait until you’re ready to ask for some suggestions.

(In the meantime, I’m happy to answer any C++-related questions if you’ve got any.)


#6

I updated the game. I added 2 new tileset. Going to depth 5 and 10 will switch to the alternate tileset. Added the chance that the monster could go first in battles. Added 2 big monsters. Added that some later monster can cure themselves of being poisoned.

I think the how the ROM and RAM organize the graphic data are different, so that may be why loading or setting graphic data to RAM made the graphic looked messed up. So I decided to just make 2 full tileset instead of swapping out part of it just to fill up the remaining flash memory.

I’m unsure if the game either too easy or too hard. I’m not really sure how to use the eeprom save feature so I’m unsure if that would be important or not. I left 1KB of memory free just in case. I’m guess load data into an int array and transfer that to the EEPROM.

I added flashlight utility.


(Pharap) #7

They are.

On AVR chips the RAM and the flash memory (progmem/ROM) are separate and require different CPU instructions to be accessed.
As a result, to access progmem from C++ code you need some special functions/macros.
(Provided by avr-libc in the header <avr/pgmspace.h>, documented here.)

Both the Arduboy2 class and the Sprites only draw sprites from progmem.

There is a way to do it without using two full tilesets, but the details would depend on whether you’re using Arduboy2 or Sprites for drawing.

If you’re using Arduboy2 then you’d need an array of pointers (const unsigned char *).
If you’re using Sprites then you’d need an array of indices (uint8_t).

It depends on what kind of data you want to save.


#8

Meaning that in flash ROM(left image), the image data may look like this. But being loaded and display in RAM (right image), due to the screen being on it side. I may be wrong.

Image65

In this one, I’m using arduboy2. The next project I’m doing is going to use the sprites.

I was thinking people can save in between floor. It would keep track of what MapID, stage, your MaxHP, Experience, Gold, inventory[6], Quite a bit of data to save. I’m not sure if the game needs the save feature, since it may take less than the hour to get to the goal. I need to track down how to do EEPROM stuff. My next game may require EEPROM.


(Pharap) #9

The screen layout is completely unrelated to the RAM vs progmem issue.

The RAM vs progmem issue means that if you tried to feed a RAM pointer to one of the drawing functions then you’d probably end up with nonsense because RAM and progmem use two completely different address spaces.

Say for example you fed it RAM address 0x00FF,
the drawing function wouldn’t even look in RAM,
it would look at progmem/ROM address 0x00FF,
which would almost certainly be completely unrelated to the contents of RAM.

In that case you’d need the slightly more memory-hungry approach.

If you can point me to which part of your code is currently drawing your tiles then I can demonstrate.
But the basic idea is something like this:


const unsigned char tile0[] PROGMEM =
{
	// Image data
};

const unsigned char tile1[] PROGMEM =
{
	// Image data
};

const unsigned char tile2[] PROGMEM =
{
	// Image data
};

const unsigned char * tileset[] =
{
	tile0, tile1, tile2,
};

If you can store all that in a single struct then it would be quite easy to save using eeprom_update_block and load using eeprom_read_block.

E.g.

struct GameData
{
	uint8_t mapId;
	uint8_t stageNumber;
	uint8_t maxHP;
	uint8_t experience;
	uint8_t gold;
	ItemId inventory[6];
};

GameData gameData;

constexpr uint16_t eepromBaseAddress = (EEPROM_STORAGE_SPACE_START + 600);

void saveGame()
{
	void * eepromPointer = reinterpret_cast<void *>(eepromBaseAddress);
	eeprom_update_block(&gameData, eepromPointer, sizeof(GameData));
}

void loadGame()
{
	const void * eepromPointer = reinterpret_cast<const void *>(eepromBaseAddress);
	eeprom_read_block(&gameData, eepromPointer, sizeof(GameData));
}

There’s two different EEPROM APIs, the one provided by avr-libc and the one provided by Arduino.

Personally I use the one provided by avr-libc because I’ve found the one provided by Arduino to be somewhat inferior.


#10

Ah the drawing function is automatically use pgm_read_byte. That makes sense. Does the memory map for RAM and Flash overlap? Now I’m thinking it may be like the Colecovision like the CPU isn’t mapped to the VDP, but have to ask the VDP to retrieve and put data to it.

Lines 1590-1596 draws the tiles on to the screen.

tile=0;

for(int bgy=0;bgy&lt;240;bgy+=12){

for(int bgx=0;bgx&lt;128;bgx+=16){

arduboy.drawBitmap(bgx,bgy-192+scrolly,background+(tilesetx*512)+sampleMAP[tile]*32,16,16,WHITE);

tile++;//arduboy.display();

}}

Lines 687-701 unpacks 1 byte to 2 tiles to RAM since I’m using 16 tiles for the game. Hopefully the x/16 does 4 ror to divide by 16. I learned this programming Atari 2600 to combine 2 digits to 1 sprite to display points.


(Pharap) #11

I think RAM and progmem use two different address buses because the chip uses two different instructions.
If they were on the same bus then I think the chip would only need one instruction because it could use the high bit of the address to chose between RAM and progmem.

I could be wrong though, I’m not an expert on the hardware.

I have absolutely no clue what bgy - 192 + scrolly and background + (tilesetx * 512) + sampleMAP[tile] * 32 are calculating, but ignoring those for a minute, you could end up with something like this:

constexpr uint8_t backgroundWidth = 240;
constexpr uint8_t backgroundTileWidth = (backgroundWidth / tileWidth);

constexpr uint8_t backgroundHeight = 240;
constexpr uint8_t backgroundTileHeight = (backgroundHeight / tileHeight);

for(uint8_t y = 0; y < backgroundTileHeight; ++y)
{
	uint8_t drawY = (y * tileHeight);
	for(uint8_t x = 0; x < backgroundTileWidth; ++x)
	{
		uint8_t drawX = (y * tileWidth);
		
		TileType tileType = map.getTile(x, y);
		uint8_t tileIndex = getIndex(tileType);
		const unsigned char * tileImage = tileset[tileIndex];
		
		arduboy.drawBitmap(drawX, drawY, tileImage, tileWidth, tileHeight);
	}
}

(constexpr means “this can be calculated at compile time”.)

Don’t worry about the * in calculating drawX and drawY, the compiler should be able to optimise them by adding some hidden variables and doing an addition at each loop step like you were before.
The ATmega32u4 has a multiply instruction anyway, so multiplies aren’t too bad - it’s divides and modulo (%) that you have to worry about.

If I had a better idea about how your data was structured it would be easier to show how to restructure it like that.

I think you mean shr 4, ror 4 would rotate some of those bits back around, so 7 ror 4 would end up as 0x70 (assuming it wasn’t shifted through the carry bit, in which case it would be 0xE0).

At any rate, the chip doesn’t have a barrel shifter, it can only shift 1 bit at a time.
So what you’d probably end up with is:

SHR r0
SHR r0
SHR r0
SHR r0

Or, very specific to the AVR:

SWAP r0
ANDI r0, $0F

(Swap nibbles and then mask off the upper bits.)


#12

Seems block quote wasn’t correct suppose to show a less than symbol.

tile=0;
    for(int bgy=0;bgy<240;bgy+=12){
    for(int bgx=0;bgx<128;bgx+=16){
        
      arduboy.drawBitmap(bgx,bgy-192+scrolly,background+(tilesetx*512)+sampleMAP[tile]*32,16,16,WHITE);
      tile++;//arduboy.display();
    }}

This whole routine print 160 tiles by 8columnx20row from the content of the RAM address. I did complained about that not working when trying to get the map to load from ROM, not knowing about pgm_read_byte(); yet. I decided to unpack 80 bytes to that RAM address since I had plenty of RAM to use up. Scrolly was difficult to get working. I settled with just scrolling vertically since I want to start producing the game.

This prints the tiles to screen. Background[] is the first pointer to the 16 16x16(each image have 4 blank rows) image in the tileset. (tilesetx * 512) move the pointer up to display the next 16 16x16 tiles. As you decend to a specific floor, tileset will change to a 1 or 2, moving the needle to the next set of images. The whole argument is basically pointers to images.

Oh I want to point out that random(1,2) always a 1. I was wondering if that a bug with the random function.


(Pharap) #13

To make that clearer you should replace the magic numbers with named constants.
E.g.

constexpr uint16_t tileWidth = 16;
constexpr uint16_t tileHeight = 16;
constexpr size_t tileSize = ((tileWidth * tileHeight) / 8);

constexpr uint16_t tilesPerTileset = 16;
constexpr size_t tilesetSize = (tilesPerTileset * tileSize);

(constexpr means ‘can be evaluated at compile time’.)

I’d also recommend using & and [] since it makes it clearer that you’re dealing with pointers, and use some more intermediary variables to make it clearer what your calculating.

E.g.

auto tilesetStart = &background[tilesetIndex * tilesetSize];
auto tileImage = &tilesetStart[sampleMap[tile] * tileSize];

Although, you could switch to using a multidimensional array for your images to make things clearer.

constexpr uint16_t tileWidth = 16;
constexpr uint16_t tileHeight = 16;

constexpr size_t tilesetCount = 2;
constexpr size_t tileCount = 16;
constexpr size_t tileSize = ((tileWidth * tileHeight) / 8);

const unsigned char tiles[tilesetCount][tileCount][tileSize] PROGMEM
{
	// Tileset 0
	{
		// Tile 0
		{
			// data...
		},
		// Tile 1
		{
			// data...
		},
		// etc...
	},
	// Tileset 1
	{
		// Tile 0
		{
			// data...
		},
		// Tile 1
		{
			// data...
		},
		// etc...
	}
};

Then you could use it like so:

auto tileImage = &tiles[tileset][tile][0];

Which is much clearer and easier to deal with - you don’t have to worry about calculating the sizes yourself, the compiler figures it out for you.

Combine that with my earlier code example and you get something like:

constexpr uint8_t backgroundWidth = 240;
constexpr uint8_t backgroundTileWidth = (backgroundWidth / tileWidth);

constexpr uint8_t backgroundHeight = 240;
constexpr uint8_t backgroundTileHeight = (backgroundHeight / tileHeight);

for(uint8_t y = 0; y < backgroundTileHeight; ++y)
{
	uint8_t drawY = (y * tileHeight);
	for(uint8_t x = 0; x < backgroundTileWidth; ++x)
	{
		uint8_t drawX = (y * tileWidth);
		
		TileType tileType = map.getTile(x, y);
		uint8_t tileIndex = getIndex(tileType);
		const unsigned char * tileImage = &tiles[tilesetIndex][tileIndex][0];
		
		arduboy.drawBitmap(drawX, drawY, tileImage, tileWidth, tileHeight);
	}
}

It’s not a bug, the upper bound is exclusive, not inclusive.
I.e. if you want a 1 or a 2 then you need to write random(1, 3).
Although I find it odd that you’re using a 1 and a 2 rather than a 0 and a 1,
is 0 supposed to be a special ‘no tileset’ value?


#14

The random(1,2) was a level up stat for your player’s defense, you have a chance to increase your defense by 1 or 2. I was confused why it isn’t working. So now I understand.

0 is the tileset you see in the gif. 1 and 2 will switch to the 2nd and 3rd tilesets as you descend into the depth.


(Pharap) #15

Ok, that makes more sense.
I was assuming it was related to the tiles since you’d just mentioned ‘tileset 1 and 2’.

The reason for this is because of how it’s implemented…
(Sorry in advance if you already know any of this or if this is too much info.)

random() returns a random number from 0 to RAND_MAX (inclusive).
RAND_MAX is 0x7FFF, which is 32767 in decimal, and it’s the highest value representable by a signed 32-bit value.
That core function is provided by avr-libc, which is the library that underpins Arduino for AVR targets.

Arduino’s implementation of the random for translating that number into a number within a specific range is:

long random(long howbig)
{
  if (howbig == 0) {
    return 0;
  }
  return random() % howbig;
}

long random(long howsmall, long howbig)
{
  if (howsmall >= howbig) {
    return howsmall;
  }
  long diff = howbig - howsmall;
  return random(diff) + howsmall;
}

And it’s the % (the modulo operation) that causes the upper bound to be exclusive.
Using a modulo operation to restrict a random number to a particular range is a very common technique,
although it’s also quite a naive technique because it causes an issue called “modulo bias” if the exclusive upper bound isn’t a power of two.


Small note:
I find the decision to return 0 if howbig is 0 and the decision to return howsmall if howsmall >= howbig to be rather puzzling.


#16

I actually ran into a magic key problem last night loading a sketch, Sensitive, that wouldn’t let me load new sketch. So flashlight mode and pressing down loading the value to the RAM address let me load new sketch. So yeah, going to put boot and flashlight routines into all of my games.

I was so close having the game to use 512 bytes just for tiles, which was possible to overwrite the magic key memory address at $800.


(Kevin) #17

Curses Arduino for the magic number problem and curses to me for not knowing about it when I made these.


#18

You have a lot of smart people on this forums getting around the problem. Plus having a reset button is another good design. I made this notes in my program how much space begin vs boot takes up. So begins is 700 bytes more than boot. Flashlight takes about 62 bytes, so it a fair compromise since I included Arduboy logo with my logo.

I used flashlight mode on my phone to find my Arduboy I dropped on the floor last night. I didn’t try to upload an Arduboy game in that state. Plus, ProjectABE emulator do run the Arduboy games on that phone.

I worked with game system that have less than 1000 bytes of system RAM, so I usually pick a method that take more ROM space than RAM since you can read ROM as fast as reading RAM.


(Pharap) #19

I know of at least one way to save a lot of progmem with this, and possibly a bit of RAM too.
But it means restructuring most of your string handling code.

Something you could do in the short term is to make sure all your for loops are using local variables instead of reusing a global variable, then the compiler will actually be able to use a register instead of RAM.

Also, I just spotted a goto where you could use a return.
I.e. instead of goto leaveblacksmith; you could just return 0;.
The compiled code would probably be the same, but it would make the code slightly easier to read.
Similarly the goto mapskip in NewFloor could be avoided by just using else ifs instead of ifs.

And instead of filling loop with while(game == 0), while(game == 1) etc you could change to use the more common pattern of:

void loop()
{
	if(!arduboy.nextFrame())
		return;
	
	arduboy.pollButtons();
	
	arduboy.clear();
	
	switch(game)
	{
		case 0:
			updateGame0();
			break;
		case 1:
			updateGame1();
			break;
		case 2:
			updateGame2();
			break;
		// etc
	}
	
	arduboy.display();
}