Storing and drawing more than a single map

while i’m waiting for the arduboy mini to ship i wanted to learn how to use the fx chip, so i wanted to make a little dungeon/roguelike game

for now what i managed to do is storing sprites on the chip, reading and drawing them
and made 2 simple maps to also see if i could draw the tiles from there and that’s working
(for now the way i’m using to draw the map it was just for testing, i remember pharap told me there was better ways to do it, i have to look for them now)

since my game is supposed to have more than one map(rooms on this case), how can i do to store them and then draw each one whenever is needed?

for example what i’m thinking to do is each room will be full screen size, you kill 2 enemies, after that a little boss appear, when killed he gives you a key to open the door for next room
when you open the door and enter, there is when the map should change from “map_1” to “map_2” and it repeats until you finish or die

https://github.com/graziel666/first_fx_try
this is what i have until now

There’s lots of different ways to do this, but you haven’t really given enough information about your game to say what the best option for you is.

For one thing, are you asking how to have multiple maps, or how to store your maps on the FX chip, or both?

That aside…

  • Will your maps be stored as arrays of uint8_t or will you be creating an enumeration (enum class) such as TileType to make your tiles easier to deal with?
  • Will your map be immutable or will you want to be able to modify it while the game is running?
    • E.g. will your ‘unlocking doors’ be part of the map or separate objects?
  • Will all your maps be the same size?

In the meantime, you might want to have a look at this:

1 Like

i was thinking if that was possible, but for now i prefer learning how to do it on a normal way before digging into something more complicated

storing them as uint8_t is easier for me, since i’m using the export data from Tiled
(i draw the map on Tiled using the same image, export as a .csv and use that data for the array of uint8_t)
but i’m not sure if doing it that way would be harder to set some tiles as solid and others to damage

my idea was making the doors and chest unlockable, changing the sprite, for doors the door would open and the chest would just dissapear
the doors should be the most important on that since i wanna have the option to make multiple paths and be able to get to a dead end and have to backtrack to test another (depending on how hard/possible that would be)

and yes, all the maps would be the same size, 16x8 tiles, i want them to be one screen size

I see.

That doesn’t mean you can’t also use an enumeration, just that you’d have to be more clever about it.

The main benefits of using an enumeration are:

  • You can use named values like TileType::OpenDoor instead of unexplained numbers.
  • A (scoped) enumeration is type safe, so you can’t accidentally mix up an enumeration with an integer value.

For things like damage and solidity, it won’t make much difference whether you use uint8_t or an enumeration, but which values you assign to which tiles could make a difference in terms of how much code you have to write, and whether you have to handle damage specifically for different kinds of tiles or can use the same rule(s) (and thus the same code) for all tiles.

There’s two ways of doing that:

  • Make the map unchangeable, and represent the doors and chests as objects
    • This can use less RAM if you only have a few objects because only the objects need to be in RAM
    • But it’s a bit more awkward to work with because you need to store those items in lists separate to the actual map
  • Make the map modifiable, and represent the doors and chests as tiles
    • This is generally easier to work with because you can focus all your logic on tiles
    • But it potentially uses more RAM because the map has to be stored in RAM instead of progmem

If you were to go for a modifiable map then you might end up with something like this:

(I had to modify it to work for classic Arduboy because my FX unit doesn’t work with the default uploader and I haven’t got around to fixing it.)


A less structured example (untested):

constexpr uint8_t mapWidth = 16;
constexpr uint8_t mapHeight = 8;

constexpr uint8_t map_0[mapHeight][mapWidth] PROGMEM
{
	{ 0, 1, 1, 1, 1, 1, 1, 14, 1, 1, 1, 1, 1, 1, 1, 2 },
	{ 5, 19, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 19, 7 },
	{ 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7 },
	{ 5, 6, 6, 6, 16, 6, 6, 6, 6, 6, 6, 6, 6, 15, 15, 7 },
	{ 5, 6, 18, 6, 6, 6, 6, 6, 6, 6, 6, 15, 15, 15, 6, 7 },
	{ 5, 6, 6, 6, 6, 6, 6, 6, 6, 15, 15, 15, 6, 6, 6, 7 },
	{ 5, 19, 6, 6, 6, 6, 6, 6, 6, 15, 6, 6, 6, 6, 19, 7 },
	{ 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12 }
};

constexpr uint8_t map_1[mapHeight][mapWidth] PROGMEM
{
	{ 0, 1, 1, 14, 1, 1, 1, 1, 2, 17, 17, 17, 17, 17, 17, 17 },
	{ 5, 19, 6, 6, 6, 6, 6, 15, 7, 17, 17, 17, 17, 17, 17, 17 },
	{ 5, 6, 6, 6, 6, 6, 15, 15, 7, 17, 17, 17, 17, 17, 17, 17 },
	{ 5, 6, 6, 6, 6, 6, 15, 15, 8, 1, 1, 1, 1, 1, 1, 2 },
	{ 5, 6, 6, 6, 6, 6, 15, 19, 15, 6, 6, 6, 6, 6, 18, 7 },
	{ 5, 6, 6, 6, 6, 6, 6, 15, 6, 6, 6, 6, 6, 6, 6, 7 },
	{ 5, 19, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 19, 7 },
	{ 10, 11, 11, 11, 11, 11, 11, 13, 11, 11, 11, 11, 11, 11, 11, 12 }
};

constexpr uint8_t mapCount = 2;
constexpr uint8_t firstMapIndex = 0;
constexpr uint8_t lastMapIndex = (mapCount - 1);

// An array of all stored maps
constexpr uint8_t * maps[mapCount] PROGMEM
{
	&map_0[0][0],
	&map_1[0][0],
};

// Get a pointer to the specified map
const uint8_t * getMap(uint8_t index)
{
	return static_cast<const uint8_t *>(pgm_read_ptr(&maps[index]));
}

enum class TileType : uint8_t
{
	Invalid,
	Ground,
	Wall,
	ClosedDoor,
	OpenDoor,
	ClosedChest,
	OpenChest,
	// ... et cetera
};

TileType room[mapHeight][mapWidth];

TileType getTile(uint8_t x, uint8_t y)
{
	if((x > mapWidth) || (y > mapHeight))
		return TileType::Invalid;

	return room[y][x];
}

// Overwrites a tile in the map
void setTile(uint8_t x, uint8_t y, TileType tile)
{
	if((x > mapWidth) || (y > mapHeight))
		return;

	room[y][x] = tile;
}

// Loads a map
void load(const uint8_t * data)
{
	// Copies a map from progmem into RAM
	memcpy_P(room, data, sizeof(room));
}

void loadIndexedMap(uint8_t index)
{
	roomMap.load(getMap(index));
}

The most important parts being that:

  • The maps array stores pointers to the maps
  • The memcpy_P function copies the maps from progmem into the room array

(I couldn’t use the word map on its own because Arduino already defines a map macro, which is really annoying! If it had been a function I could have at least used a namespace to avoid the problem, but nope, it had to be a macro!)

2 Likes

i think i’m getting it, now time to test some things and try to get something close to my idea
thanks for the help!

i noticed this when i tried to use map, and took me more than i’m willing to admit to notice it was that reason hahah

1 Like

Let me know if you have any questions about how it works or if it doesn’t suit what you need. Like I say, there’s lots of different ways to do this sort of thing, and I couldn’t remember how much you know about things like progmem, pointers, and classes.

If/when you want to adapt the code to use maps stored on the FX, you’ll need to find a way to copy the maps from the FX into the room array/Map. (I.e. the load/Map::load function.)

It would also be ideal to store the maps array on the FX chip as well, which would require changing how getMap and possibly loadIndexedMap work.

In either case you’d have to use uint24_t instead of pointers and the FX library instead of pgm_read_ptr and memcpy_P. (As for how to replace them, I couldn’t say without checking and thinking it through.)


By the way if you are going to use an enumeration (enum class) (which I recommend doing) then you need to make sure the values match up to what your map contains.

E.g.

enum class TileType : uint8_t
{
	// ...
	OpenDoor = 13,
	ClosedDoor = 14,
	Spikes = 15,
	// ...
	Invalid = 255,
};

I didn’t do that originally because I didn’t know what your tiles were, and then I forgot to update it after I pulled your tilesheet apart. I’m still not sure what some of the tiles are, or at least what you want to call them, so I only named the ones I could figure out.

Thinking about it, you may not need an Invalid tile or the boundary guards if your maps are only ever going to fit the screen. I did that out of habit because usually I’m dealing with games that use a camera that can look outside the map, in which case having an ‘outside the map’ tile is useful.


Actually, I made a small mistake there.
I double-checked and it isn’t a macro, it’s a function.

I was getting mixed up with min and max, which Arduino does define as functions.
(That would also explain why #undef didn’t work when I tried it. One of these days I’ll learn to stop trying to write code when I’m half asleep.)

There are ways around it, e.g. putting some of the map code in a namespace, but you might find that cumbersome to work with if you aren’t used to it. Just using a different name (room, dungeon, et cetera) is the easiest option.

You can use my library for that FateHack (heap,object-prototyping roguelike library) - Games - Arduboy. I can help you with its code and improve it by your needs.

Library stores maps as object unique chars:

const char scene_home[] PROGMEM = "S"
"#####"
"#q==#"
"#+=e#"
"r@  #"
"#   #"
"#   #"
"#####";

Its very easy to modify map based on some scripts (for example to create boss on killing some enemies).

Its also has a pathfinding cave generation algorithm.

this worked just fine :smiley: but there’s a way to load maps depending on which direction i go?
for example now i can go from map 1 to 2 or from 2 to 1 going up and down
but for example if i wanna go from map 1 to 3 going to the right, or form 1 to 4 going to left
i coudlnt think on a way to do this with how the index work now

also one question, i tried using your platformer example for the colissions, now just testing it with the left button
but i noticed that this function

// Returns 'true' if a tile is solid
constexpr bool isSolid(TileType tileType)
{
	// Currently all tiles apart from 'None' are solid
	return (tileType != TileType::None);
}

is working on the oposite way for me, it returns only the tile i put there as solid (also how can i add more tiles?)

The code I gave you was just an example showing how to cycle through the maps in order.

Realistically you’ll probably want to move to other maps in response to a particular event. E.g. the player walking into an open door.

It’s doable, but it’s going to complicate things because you’ll need some kind of data structure to keep track of which maps/rooms are connected to other rooms.

Again, I can’t give a proper example without knowing more about your game. E.g.

  • How is the player supposed to move to other maps/rooms?
    • Just doors?
    • Doors and stairs?
    • Walking outside the map in a particular direction?
  • Is there a limit on how many exits a map/room might have?
  • Would only being allowed to have e.g. 4 exits be a problem?

That’s because you’ve got your logic backwards…

You’re supposed to move the player if the tile isn’t solid.

TileType tile = roomMap.getTile(tileX, tileY);
if(!isSolid(tile))
  hero.x = newX;

(! means ‘not’, so read the above as ‘if not is solid (tile) then hero x equals new x’.)

In a virtual world, a ‘solid’ object is just an object that the player can’t move through.

// Note: Can't be 'constexpr' in C++11 because 'switch' is used
bool isSolid(TileType tileType)
{
	switch(tileType)
	{
		// List all the solid types
		case TileType::TopCornerLeft:
		case TileType::TopMiddleWall:
		case TileType::TopCornerRight:
			// Return 'true', because it's solid
			return true;
		
		// Any tile not listed above is non-solid
		default:
			// Return 'false', because it's not solid
			return false;
	}
}

A few other things to note…


Firstly…

Doing == true is redundant (x == true is equivalent to just x), and you should use ! instead of != false (x == false is equivalent to !x).

Hence the above would be better as:

if(!hero.weapon)
{
	if(hero.facingLeft)
		hero.frame = 0;

	if(!hero.facingLeft)
		hero.frame = 1;
}

if(hero.weapon)
{
	if(hero.facingLeft)
		hero.frame = 2;

	if(!hero.facingLeft)
		hero.frame = 3;
}

You can also make use of else, so the above can be simplified further…

if(hero.weapon)
{
	if(hero.facingLeft)
		hero.frame = 2;
	else
		hero.frame = 3;
}
else
{
	if(hero.facingLeft)
		hero.frame = 0;
	else
		hero.frame = 1;
}

In this case, you can even go one better - you can use the conditional operator (? :). In particular, z = c ? x : y is roughly equivalent to if(c) z = x; else z = y;, meaning you can do:

if(hero.weapon)
{
	hero.frame = (hero.facingLeft) ? 2 : 3;
}
else
{
	hero.frame = (hero.facingLeft) ? 0 : 1;
}

Or even just:

hero.frame = (hero.weapon) ?
	((hero.facingLeft) ? 2 : 3) :
	((hero.facingLeft) ? 0 : 1);

(Two conditionals inside a conditional.)

It’s also worth mentioning that if the value of hero.frame is dependant entirely on hero’s other variables then you possibly don’t need to add frame to hero in the first place - you can just calculate it when you need it, thus you can use a local variable or a function, which would save a bit of RAM.

E.g.

uint8_t calculateHeroFrame()
{
	return (hero.weapon) ?
		((hero.facingLeft) ? 2 : 3) :
		((hero.facingLeft) ? 0 : 1);
}

void drawHero()
{
	uint8_t heroFrame = calculateHeroFrame();
	FX::drawBitmap(hero.x, hero.y, FXhero, heroFrame, dbmNormal);
}

Secondly…

I notice your Characters struct is inheriting from Rect, but you’re then adding new x and y variables that hide the originals, as well as adding w and h variables when Characters will already be inheriting width and height from Rect.

Either you don’t actually want to inherit from Rect, or you don’t want to be adding those x, y, w, and h variables.

I’m presuming you probably don’t want Characters to inherit from Rect because your code looks like it would still work if Characters wasn’t inheriting from Rect.

There will be a problem if one map has free tile in border position and on next map there will be a block (especially if maps autogenerated). So you somehow need to place your character on nearest free transition tile on next map. You can do it with pathfinding algorithm iterating paths until first free tile and this way you can get code space economy.

I’m presuming from the fact @graziel is using tiled that all the maps will be edited manually rather than being autogenerated, but I could be wrong.

Not necessarily.

If I remember correctly, the collision algorithm I used for my platformer game prevents a player from walking into a solid block, but allows a player to walk out of a solid block. So if that were used and the map transition just teleports the player into the next block, they wouldn’t become stuck in the wall. (They’d have trouble getting back though.)

But either way, making sure the map is ‘sane’ is something that could be handled by a map editing tool (or some other external tool), or just by the programmer doing manual checking.

After all, it’s possible to do something stupid like fill an entire map with solid tiles or not providing an exit, or spawning the player into a tile that’s entirely surrounded by solid tiles. Trying to get the game to handle everything that could possibly go wrong with a map will just burn through progmem.

(Trying to validate those scenarios with an external tool however is much more reasonable given how powerful modern computers are.)

it’s supposed to move using only doors, after the room is clear i’m thinking on adding a key to the inventory, once you have it you can go to a door and open it
most of the rooms will have 2 doors (enter and exit) and some i want them to have 4 doors that will be open by default, so you can choose a path to go

the basic idea of the game will be something more closer to a dungeon crawler, where you select a path, fight enemies and try to find the boss room, if you choose the path bad you’ll have to backtrack and choose another
and for now i’m just thinking on adding a sword and the key as an item

god… i can’t believe i didn’t noticed that, trying to write code at 2am is not good

i tend to write like that so is easier for me to read what’s happening, i can get really lost
but i didn’t knew that way of writting with operators

here i was lazy and copied my character struct from cooties attack, and forgot to remove that part :see_no_evil:

for now i’ll make the maps manually, at least until i can get it working properly

Ok…

There’s several problems here that you’re going to need to resolve:

  • You’ll need some way of knowing which door connects to which map.
    • That information will have to be stored along with each map.
  • If you can backtrack then you’ll either have to:
    • Somehow remember which doors were open.
    • Allow the doors to close again after exiting the room/map.

For the first issue, you could have an array of Doors, with each Door including the door’s coordinates and the map it connects to, e.g.

struct Door
{
  uint8_t x;
  uint8_t y;
  uint8_t mapIndex;
};

Then when the player steps on an open door, you check all the doors on the map to find the one you’re stood on and load the map index.

You may also want to be able to specify where on the map you’ll be teleported to (e.g. so a left-hand door will take you to the right-hand side of the map you’re entering), e.g.

struct Door
{
  uint8_t x;
  uint8_t y;
  uint8_t mapIndex;
  uint8_t destinationX;
  uint8_t destinationY;
};

Actually storing and loading these will be tricky.

I know quite a few techniques that would allow you to use less memory, but I get the feeling it’s best to keep this simple so you won’t have as much to learn.

It’s a bit late for me to be writing something now, but I’ll write an example tommorrow if you haven’t got it sorted by the time I’m next around.

In the meantime, you may as well start figuring out how to do items since you’ll need them to unlock the doors, or enemies perhaps.

The best thing to do is to just add lots of comments explaining what’s happening.

(You can comment in your native language if you want, there’s nothing that says you have to use English for everything.)

Also just practice using ! to mean ‘not’. You’ll see it all the time in other people’s code.

For one thing, if you accidentally write = when you mean to write == then you’ll have a bug because if(x = true) is not the same as if(x == true).

Sometimes people don’t find out about it because you can manage without it (because if and else can do (mostly) the same thing), but it comes in very handy. E.g. for function calls you can do someFunction(condition ? optionA : optionB); instead of having to create a local variable and then do an if-else to set it.

Actually, it’s going to have to be either tomorrow or the day after now.

I ended up being busier than expected on Friday, and today was the coronation, so I didn’t get chance to do it today either.

I’ve got two possibilities in mind, but they’re both probably a bit more advanced than what your used to.

Both will involve the Door struct and adding an array of Door to the Map, but there’s two possibilities as to how to store them in progmem depending on what you feel more comfortable with. I’ll probably opt for the one that I think will be a bit more recognisable for you.

7 posts were split to a new topic: Coronation Quiche

@graziel So basically there’s two main ways you can go about this…

One option is to store the map as an array of bytes (uint8_t/unsigned char) and then have the function manually read the bytes. This is a very low-level approach and is a bit closer to what you’d have to do with the FX, or reading a file on desktop. But to use it well you’d need to understand how structs are laid out in memory, which I’m presuming you don’t know much about, so I’m going to demonstrate the other technique instead, which is closer to what you’re already doing…

The other way is to have arrays of doors alongside your maps. E.g.

constexpr Door map_0_doors[] PROGMEM
{
  // X, Y, map
  // Door at tile (7, 0) leads to map 1
  { 7, 0, 1 }
};

And then load those into the map.

It’s not quite as memory efficient, but it’ll hopefully be a bit easier for you to understand because it’s closer to what you’re already doing.

I’ve crammed a lot into just two commits, and I can’t test because my FX unit isn’t working, but hopefully this will work:

The idea is that maps now come with an accompanying array of doors:

Which are referenced by a new structure called MapData:

And Map can now load maps from that MapData

On top of that, there’s now a function for locating a door in the map’s list of doors that matches the specified coordinates…

And that in turn is used when an open door is interacted with:

While I was at it I split the tile interaction code off into a separate function called interactWith, which is called from checkTile:

(And I made a few minor changes to checkTile to make it easier to follow, like using a switch and getting rid of an unnecessary condition.)

Lastly I’ll point out that because both load and getMap were changed to accomodate MapData, loadIndexedMap didn’t need to change at all:

Again, I’m hoping this will work but it’s completely untested.
I did at least check that it compiles though.

If you have any questions, ask away.

1 Like

really thanks for the help!! i compiled it to test and it works just fine
now i really need to give this a slow read to understand whats happening hahaha

1 Like

Like I say, if you need anything explained, feel free to ask.
I tried to keep it simple and close to what it was before, even if that’s not the most efficient way.

There’s definitely things I’m expecting that you won’t understand (e.g. constructors and the template), but I added that bit of complexity to make certain other parts easier. (Without that you’d have to do something like { map_0, map_0_doors, sizeof(map_0_doors) / sizeof(map_0_doors[0]) }, which is quite awkward.)