Lagunita - A 1D wild west town builder

Hi,
I made a 1D wild west town builder game for arduboy. The source code is in github: lagunita
I still have some space left and some idea to implement, but if you have any suggestion, I’m open to implement them :slight_smile:

lagunita.hex

Changelog:

  • 2019-12-16: two more pages in the help section and a game saved confirmation dialog.
  • 2019-20-31: high score added in the title screen, bug preventing random events to trigger fixed, new random event that triggers when environment is low added.
  • 2019-10-21: decrease tumbleweed as population grows, added waves in the water effect.
  • 2019-10-18: added option to re-flash other games and buildings activates after building animation.
  • 2019-10-16: logo and USB stack removed to bring back the binary in a flashable size.
  • 2019-10-03: removing money from event triggers.
  • 2019-10-02: day/night cycle, days counter, buildings’ construction animation, difficulty tuning, optimizations and bug fixes.
  • 2019-09-26: upgrading system for buildings, secret totems, double clicking up&down change category, load/save to eeprom, added more combos.
  • 2019-09-12: rgb led for stats, double click to move to next available spot, new title screen, help screen has pages, hints on first building and tumbleweed!
  • 2019-09-06: Added stable with horses, mountains, walking on both directions, doubled play area, music compressed, cpu optimizations, proper random level generation, text animation, cleaner code, long press of left and right to move faster across the level and better handling or random events.
  • 2019-08-30: Bug fixes and memory footprint reduction.
  • 2019-08-27: Bug fixes, audio on/off switch.
  • 2019-08-26: Water reflection implemented.
  • 2019-08-25: #LOWREZJAM game jam version
16 Likes

Hi! Very nice game! I really like it! Here’s some suggestions on what to add.

  1. A mute music button, not saying the music is bad, just, 1 bit music isn’t the best

  2. People, Maybe a little sprite of little people walking to their jobs would be cool! Not just walking right aimlessly (also maybe a person with a horse sometime, Read 5)

3.Quick scrool, After your town gets so big, it takes a while to get from end to end, So if you press B+(direction) You will speed on that way

4.Destroy, You can destroy buildings and tress for a cost, self explanatory

5.Stable, If you make a stable (after making the church) The town’s happiness will increase, and also, sometimes a person riding a horse will spawn walking down the town

I think these are good ideas, but you game is great already! Keep up the good work

2 Likes

Thanks for the feedback @TuckerBoo! :slight_smile:
Music mute, quick scroll (on long pressure of R/L buttons) and scroll to next free spot to build (double click of R/L buttons) are already on my TODO list and I’ll start on those right after I manage to implement the lake reflection eye candy effect currently in the working :slight_smile:
I was planning to implement people walk aimlessly right and left :blush:. I though before about actually show them walking to their work, but I’m already at 93% RAM usage and I have no space left to keep the coordinates of hundreds of people… Any suggestion on how to do it?
The destroy feature gets unlocked at 200 people and it’s called “clear”. On the last version (check the link to the hex), it not possible to build on top of buildings or to build on top of trees until the clear is unlocked: that is to make the town a bit more sparse at the beginning and force the player to use more planning and later refactoring. I still haven’t finely tuned the order of some unlockable items, but considering your feedback I should give the clear feature early on :+1:
The stable idea is great! It’s going to be challenging to animate a man mounting a horse, but fun. Maybe I should add a new “traffic” statistic instead of reusing the happiness for the stable… Let’s see how it turns out :slight_smile:
Again thanks for the feedback! Very much appreciated.

2 Likes

You can mute your Arduboy at startup by holding B and pressing down.
You can unmute it by holding B and pressing up instead.


That said…

@renato-grottesi you’re currently forcing the audio on by using arduboy.audio.on().
audio.begin() will already boot the audio using the user’s saved audio settings (i.e. if they’ve set their audio to off, the music won’t play, and if they’ve set their audio on then it will).
But you don’t even need to call that because arduboy.begin() already calls audio.begin().
(See the Arduboy2Audio Class Reference for more info.)

I can think of half a dozen ways to save progmem,
but saving RAM is a bit more difficult.

For one thing you could store tutorialsData in progmem instead of in RAM since you aren’t actually modifying it.
Then you can read it by using memcpy_P, which you’d use like:

Event event;
memcpy_P(&event, &tutorialsData[index], sizeof(Event));

Or, if you like type safety, introduce the function:

template<typename T> T * progmemCopy(T & destination, const T & source)
{
	return static_cast<T*>(memcpy_P(&destination, &source, sizeof(T));
}

Which you could use like:

Event event;
progmemCopy(event, tutorialsData[index]);

You could also get rid of tutor and just use the Tinyfont object’s print functions instead of snprintf, it should be a lot cheaper because it doesn’t have to do the parsing that snprintf has to.
(Speaking of which, if you’re using Tinyfont then you don’t need to use Arduboy2, you can use Arduboy2Core instead, which will save a bit of RAM.)

This won’t matter if you do get rid of tutor,
but since this advice is quite general I’m going to say it anyway…

Writing if(strlen(tutor)) is really wasteful.

strlen has to walk the entire string until it finds a '\0'.
What you’re really asking is if strlen(tutor) > 0,
which could be found in a much cheaper way just by checking if tutor[0] != '\0'.

If you never even need the string’s length then you could get rid of strlen completely, which would free up a decent chunk of progmem.
If you do need the string’s length, then you could just store it to avoid continually recomputing it.

Likewise when you do strlen(tutor) == 0 it would be so much cheaper to just do tutor[0] == '\0'.
(For those who like big-O notation, the former is O(n), the latter is O(1).)


Also, some bugs…

You’re not using generateRandomSeed() as intended by the way.
It’s supposed to generate a seed to be used to seed a pseudo-random number generator.
You should swap it for random() or rand().

And you have a buffer overrun bug in the same bit of code.
You increment i and then set the next tile to empty without checking whether i is still < size.
If i was size - 1 before you i++; then i would become size and you’d end up writing to whatever is after tiles in RAM.

Not really a bug, but I’m confused about why you’re storing Building::IDs as a uint8_t in Event instead of just storing it as a Building::IDs.
Also it seems to me that EventWrapper::triggered should be some kind of enum class,
because it doesn’t seem to be representing an integer.

There may be more bugs and memory savings available,
I’ve only had a quick look at the code so far.

Also sorry if any of my suggestions seem overly critical.

2 Likes

Thanks for detailed code review, @Pharap!
Your suggestion are not overly critical, they are straight to the point and perfectly fine. Also I’m used to code reviews from work :smiley:
I admit that I got confused by the documentation of audio.on() and generateRandomSeed() :blush:
I’ll fix my mistakes and hopefully get more reliable random events :slight_smile:
The bug with me setting the array after the i++ is quite embarrassing, especially considering that I don’t even need to set those arrays to the default value that is already there :laughing:
I’m still using the font in Arduboy2 for the title screen, but I was already planning to replace the entire title screen with a nicer static bitmap. I’ll try to use only Arduboy2Core and see how much I can save.
For the strlen(), I was already planning to remove it (it was more of a readability game jam hack that got spread like a virus) to save CPU power, but I didn’t consider that it could also save progmem :slight_smile:
Finally, thanks a lot for pointing me to memcpy_P: somehow I managed to find strncpy_P in some forum, but I didn’t find any good method to offload non string data in a non-ugly way :slight_smile:
However I think that I can’t still get rid of tutor (although I can make it smaller) since I use it for the random events (like getting robbed). Does print() in Print.h support formatting and I missed that?
I also don’t seem to be able to use progmem as the third parameter of snprintf for the events: am I doing something wrong, or is there a better way to save memory there?

1 Like

I’m not sure what’s confusing about on.
If you turn the sound on then the sound will be played regardless of the user’s EEPROM settings, which generally isn’t desirable.

generateRandomSeed() is fairly explicit about the idea that it should be used to seed a PRNG.
Admittedly it doesn’t say outright that “this function isn’t suitable to be used like a PRNG”,
but it’s supposed to be strongly implied at least.

If you’re wondering why that’s the case, basically the noise it uses as a source is random enough to act as a seed but probably not random enough to be a suitable PRNG.
And even if it were suitable, it’s somewhat slower than a PRNG would be anyway because it has to:

  1. enable the ADC
  2. wait for some pin states to change
  3. disable the ADC

All of which is pretty time consuming compared to the mathematical operations a PRNG would use.

If it’s only for the title screen then yes, an image would probably be a fair bit cheaper.

A common mantra in C++ is that “you don’t pay for what you don’t use”.
In this case if you don’t use a function, it doesn’t end up in the compiled executable (.hex file).

There’s some more useful bits and pieces here.
A lot of people don’t realise this, but the Arduino library is actually backed by an implementation of the C standard library called ‘avr-libc’, which has a number of AVR-specific features, including all the progmem manipulation functions and macros.

If by ‘formatting’ you mean printf-style format strings then no.

But if you mean “can I give it an integer and have the integer printed” then yes, It has overloads for various different types.
So print(10) will call print(int), print(0.5f) will call print(float) et cetera.
(You can see a list of these in Print.h. In case you’re wondering, const __FlashStringHelper * is part of what makes the F macro work.)

Using those functions possibly requires more progmem (it might, it might not - I’ve never measured it),
but because it’s printed directly to where it’s needed you don’t need a char buffer as a middle man so it uses less RAM.

If you absolutely have to have a char buffer then it’s possible that there might be a class somewhere that acts as a char buffer and inherits Print,
in which case that might be cheaper than using snprintf because it wouldn’t need to do the parsing that snprintf has to do.
(If there isn’t one, I’m pretty sure that I know how to write one.)

You’re not doing anything wrong, you actually can’t do that.

Reading from AVR’s progmem actually requires a different machine code instruction to reading from RAM.
The macros pgm_read_byte, pgm_read_ptr et cetera all end up as inline assembly that use that special machine code instruction,
and memcpy_P is essentially a rewrite of memcpy that uses that special progmem-reading instruction instead of the RAM-reading instruction.

Also it’s impossible (as far as I know) to automatically detect whether something is in progmem because objects in progmem and objects in RAM have the same datatype (PROGMEM is actually implemented as a compiler-specific attribute).
There are a few ways to differentiate,
but all the ones I’m aware of require extra work from the programmer.

As mentioned earlier in my comment, using the various overloads of print provided by Print might save memory because they don’t involve parsing a format string.

Any form of printf-like function has to go through the format string, look for %s and then use the following characters to determine which type to interpret its variadic arguments as.

Print::print simply relies on function overloading,
so the type detection is done at compile time instead of runtime.
(There is also a mechanism for introducing new types that can be printed,
but the developers chose a bad way to implement it,
so it costs more memory than it should and it will only work for class types.)


If you have any more questions or want any more suggestions, don’t hesitate to ask.

2 Likes

Thanks for the follow up @Pharap! :+1:
That clarifies many of my doubts.
I’ll ping you back after I clean up the code and hopefully report a RAM usage decrease from 93% to at least 65% :smiley:
While biking to work I also thought that the best way to get rid of the tutor member completely, would be to actually optimize my dialog to not format and print at every frame, so that I can just have the snprintf’s dst in the stack of the function. Instead I can just redraw the sides and keep the video memory where the dialog is rendered untouched. (Hopefully Arduboy doesn’t do double buffering of the video memory, but I would notice and correct that :slight_smile:)

3 Likes

Allocating an array on the stack still consumes RAM, so you’d be in roughly the same position as before.

That would work, but you’d have to have a way to make the call to clear conditional,
and you’d need to be able to make sure that you don’t draw over the text.

Fortunately there’s no need to double buffer, a single buffer is perfectly adequate.

If it did double buffer, that would be 4/5ths of the memory gone,
instead of the 2/5ths that the buffer currently eats.

It’s actually possible to write code that doesn’t use a screen buffer at all,
but doing so generally results in artifacts or tearing,
so it’s very uncommon to see people doing ‘direct’ drawing.

2 Likes

@Pharap, thanks to all the good tips and links to documentation, I managed to reduce the RAM consumption to 70% while keeping the code more or less readable and maintainable.
Sadly I didn’t manage to store class instances in PROGMEM, so I ended up splitting the insides in arrays of fields. If you can provide me with a working example of an array stored in PROGMEM of instances stored in PROGMEM of classes, that would be great, because I didn’t even managed to have the examples on the web of arrays of struct instances working in PROGMEM…

I managed to recruit a friend to help with the programming and we’ll soon move on to implement some more features :slight_smile:

If this were a desktop program you’d probably have just killed your cache right there.
Good thing the Arduboy doesn’t have a cache. :P

I can show you an array of objects stored in progmem, but I’m not sure what you mean by “an array stored in PROGMEM of instances stored in PROGMEM of classes”.

I’ve already got two examples on my GitHub:

But they’re probably not very easy examples to follow.
Here’s something a bit closer to your use case:

struct Event
{
	uint16_t requiredMoney;
	uint16_t requiredPopulation;
	BuildingId unlockedBuilding;
	const char * eventDescription;
	
	constexpr Event(uint16_t requiredMoney, uint16_t requiredPopulation, BuildingId unlockedBuilding, const char * eventDescription)
		: requiredMoney(requiredMoney), requiredPopulation(requiredPopulation), unlockedBuilding(unlockedBuilding), eventDescription(eventDescription)
	{
	}
};

constexpr uint8_t eventCount = 2;
const Event eventList[2] PROGMEM
{
	Event(0, 0, BuildingId::House, textHouse),
	Event(50, 1, BuildingId::Farm, textFarm),
	// Et cetera ...
};

// Normally I'd advise against using an 'out parameter' instead of returning
// but I know from experience that using an 'out parameter' is slightly cheaper than returning a value in this case
void readEvent(uint8_t index, Event & event)
{
	memcpy_P(&result, &eventList[index], sizeof(Event));
}

Normally I would advise against having public members and recommend using getters instead, but in this case having public member variables allows you to do this:

uint16_t readEventRequiredMoney(uint8_t index)
{
	return static_cast<uint16_t>(pgm_read_word(&eventList[index].requiredMoney));
}

uint16_t readEventRequiredPopulation(uint8_t index)
{
	return static_cast<uint16_t>(pgm_read_word(&eventList[index].requiredPopulation));
}

BuildingId readEventUnlockedBuilding(uint8_t index)
{
	return static_cast<BuildingId>(pgm_read_byte(&eventList[index].unlockedBuilding));
}

const char * readEventDescription(uint8_t index)
{
	return static_cast<const char *>(pgm_read_ptr(&eventList[index].eventDescription));
}

Which is cheaper than copying the whole event if you only need one member.

If you need more than one member, you’re best off copying the whole event.

Then you can use it with your current system like:

EventState Events::update(EventState currentState, uint8_t eventId, uint16_t population, uint16_t money)
{
	if(currentState == EventState::untriggered)
	{
		auto requiredMoney = readEventRequiredMoney(eventId);
		auto requiredPopulation = readEventRequiredPopulation(eventId);
		
		if ((money >= requiredMoney) && (population >= requiredPopulation))
			return EventState::justTriggered;
		else
			return EventState::untriggered;
	}
	
	return EventState::triggered;
}

You can also write the list like this:

const Event eventList[2] PROGMEM
{
	{ 0, 0, BuildingId::House, textHouse },
	{ 50, 1, BuildingId::Farm, textFarm },
	// Et cetera ...
};

And if you do it that way then you don’t even need to write your own constructor (though having the custom constructor forces all variables to be initialised or you get a compiler error, so that might be a good thing).

(By the way, your Events class should probably just be a namespace - it’s just static functions and a single constant and you’re hding the constructor anyway. Also to save on having to maintain that constant, you might want to consider using an array size function.)

One final caveat: you can’t store an object in progmem if its constructor does anything more complicated than just initialising its member variables.
(I think the technical definition would either be that it has to be a ‘trivial type’ or a ‘standard layout type’, but I’m not completely sure.)

1 Like

Maybe I hit that or some other caveat, but every way I tried to put those arrays of class instances in PROGMEM failed: memcpy_P was returning garbage while a normal memcpy was working and the compiler wasn’t helping me with any suggestion on why the attribute failed to apply.

But the whole hex file would fit in the L1 cache of a CortexA55 :laughing:
I know what you mean, but there are actually many cases where a struct of array could perform better than an array of structs. For example in the events update loop I check the value of ‘onMoney’ and ‘OnPopulation’ at every iteration, while I read the ‘unlocks’ ant ‘text’ fields at most once: having those 4 fields interleaved in an array of structs would pollute the cache with unnecessary data.

Thank for the examples and the explanation of the caveat :+1: The code is now in encapsulated enough that I can easily give a try to different approaches modifying only one file at a time :slight_smile:

1 Like

Strange.
Are you using the Arduino IDE or something else?
(And which version?)

Perhaps the code actually didn’t compile properly and you loaded a previous build by mistake?

Or perhaps you forgot the & somewhere?
(My readEvent example can be used as readEvent(index, event) - no & needed.)

True.

I’ve gotten so used to the Arduboy’s memory limitations that having more than 1.5KB of RAM to use feels weird. :P

True, but either way it harms readability and makes maintenance more difficult.

It’s not a very thorough explanation,
but you’re unlikely to use the features that would make using PROGMEM unviable,
so I’m not sure a full explanation would be useful.

(Anything you want to store in progmem is likely to just be ‘plain old data’ anyway.)

1 Like

Hahaha :joy: I love this game it’s pretty unique. It’s almost like maybe you are building the world of @crait’s midnight wild!

1 Like

Eheh, thanks :slight_smile:
I think that midnight wild’s graphics is way more charming than lagunita’s.
Also I wouldn’t presume to know more about the wild west than a native texan :laughing:
Although I visited the Alamo once… :thinking:

Wow… programming for the arduboy sure is hard (and rewarding)! :sweat_smile:
With a huge help from my friend Guus, we managed to lower both progmem and ram consumption and we currently are at:

Program: 22034 bytes (67.2% Full)
(.text + .data + .bootloader)

Data: 2029 bytes (79.3% Full)
(.data + .bss + .noinit)

The size of playable field has been doubled to the current 512 available tiles and more content has been added.
I filled some ideas and feature requests in the issues section of the github page, so that you can see what we are going to implement in the short future.

I would really appreciate if someone could play test the game a bit and report here any feelings and bugs :slight_smile:

1 Like

Just to let you know, doing void strncpyName(char dest[8]) doesn’t limit the parameter to only accepting char dest[8].
char dest[8] ‘decays’ to char * dest, so any array size will be accepted.
(See here or here for more info.)

If you want to restrict it to arrays of the correct size, you’d have to use void strncpyName(char & dest[8]).
(See here for more info.)


Also this isn’t a bit array…

I’m not sure what this is supposed to be doing,
but it has almost the same effect as just doing:

Building::IDs tiles[size] = {};

Except there’s an extra level of indirection.

Were you expecting something different?


If you’re calling arduboy.initRandomSeed() then you don’t need to use srand() as well.

You should probably do one or the other, not both.
If you’re using rand then the second form (srand) is probably better - I’m not sure whether or not rand and random use the same engine.


The % 256 here is redundant because % 256 is equivalent to & 0xFF, and & mask will always trim off more bits than & 0xFF would.


Instead of copying the progmem string to a buffer (e.g. here) you can use the FlashStringHelper trick.

E.g. by:

FlashStringHelper Building::getName() const
{
	return asFlashStringHelper(&this->name[0]);
}

FlashStringHelper Building::getName(uint8_t id)
{
	return buildings[id].getName();
}

// ...

tinyfont.print(Building::getName(sel));

Which effectively calls Print::print(const __FlashStringHelper *).
(Note: the while(1) in Print::print is bad style, it should be while(true).)


And you don’t need to resort to itoa here, you can just do tinyfont.print(5 * Building::cost(sel)); and it will call Print::print(int n, int base = 10).

1 Like

Thanks for the code review @Pharap!
I addressed your comments in this commit.
The bit array thing was a bit array and perhaps will become a bit array again, as I have no plans to implement 256 distinct buildings :stuck_out_tongue_closed_eyes:. Probably we’ll use 5 bits for the building type and the remaining 3 bits for building statuses like “on fire” or “under construction”.

1 Like

That’s not quite right. name is casting away the const, it should be:

const __FlashStringHelper * Building::name() const
{
	return reinterpret_cast<const __FlashStringHelper *>(_name);
}

const __FlashStringHelper * Building::name(const uint8_t id)
{
	return buildings[id].name();
}

If you were using C++-style casting instead of C-style casting (which lets you cast away const without so much as a warning) then the compiler would have given you an error message.

Everything else looks right though. Hopefully that saved you some progmem at least.

Technically that’s a struct with bit fields, not a bit array.

A bit array is an array of bits with each bit being a separate value, typically interpreted as a 0 bit representing false and a 1 bit representing true.

Bit fields on the other hand can consist of one or more bits.

It’s generally best to avoid bit fields in C++ anyway.
A lot of details about how they work are implementation defined, which makes them non-portable.
(Technically adjacent bit fields don’t even have to be packed together. Also, see “Notes”.)

So if you’re going to pack multiple values into a single byte then you’re best off writing your own shifting and masking code, even though it takes more effort and results in a lot more code.

Either way, if you’re not going to just make tiles a Building::IDs tiles[size] then you should at least give that struct a name.
If you end up with an error or warning and that struct is involved then you’ll get 'struct<anonymous>' instead of something useful.
E.g. error: 'struct<anonymous>' has no member named 'test'.

That should work out.

Be aware that AVR chips have no barrel shifter though,
so something like a >> 7 actually results in either 7 ‘shift right by 1’ instructions or the equivalent of a shift right by 1’ instruction in a for loop.

In case it helps...
class Tile
{
private:
	static constexpr uint8_t buildingIdSize = 5;
	static constexpr uint8_t buildingIdMask = ((1 << buildingIdSize) - 1);
	static constexpr uint8_t buildingIdShift = 0;
	
	static constexpr uint8_t fireSize = 1;
	static constexpr uint8_t fireMask = ((1 << fireSize) - 1);
	static constexpr uint8_t fireShift = buildingIdSize;

private:
	uint8_t value;
	
public:
	Tile() = default;
	
	constexpr Tile(uint8_t value)
		: value(value)
	{
	}

	constexpr Building::IDs getBuildingId() const
	{
		return static_cast<Building::IDs>((this->value >> buildingIdShift) & buildingIdMask);
	}
	
	void setBuildingId(Building::IDs buildingId)
	{
		constexpr uint8_t clearMask = ~(buildingIdMask << buildingIdShift);
		
		const uint8_t buildingIdValue = static_cast<uint8_t>(buildingId);
		
		this->value = (this->value & clearMask | ((buildingIdValue & buildingIdMask) << buildingIdShift));
	}

	constexpr bool isOnFire() const
	{
		return (((this->value >> fireShift) & fireMask) != 0);
	}
	
	void setFire(bool fire)
	{
		if(fire)
			setFire();
		else
			clearFire();
	}
	
	void setFire()
	{	
		this->value = (this->value | (1 << fireShift));
	}
	
	void clearFire()
	{
		constexpr uint8_t clearMask = ~(fireMask << fireShift);		
		this->value = (this->value & clearMask);
	}
};

(I haven’t actually tested this so I don’t know if it will compile but the theory is sound at least.

1 Like

Thanks for double checking: I fixed the typo, added the const and created an issue to switch the cast style from c to c++.
I didn’t know that the cpu is missing a barrel shifter :open_mouth:
I’ll limit myself with only booleans in the bitfield when the time comes to put them back and I’ll check if the compiler is not smart enough to only mask the booleans instead of shifting them.

1 Like

Now it says ‘filed’ instead of ‘field’.
But don’t worry too much, it’s not a big issue in the grand scheme of things.

It’s almost correct.

If you’re doing a C-style cast then the cast needs to be return (const __FlashStringHelper*)(_name);.
(This is what I mean by C-style casts being trickier that people realise - it’s so easy to accidentally get rid of a const.)

It shouldn’t cause a problem, but technically casting away the const is ‘undefined behaviour’,
which should always be avoided if you can help it.

(I think technically it might be the case that removing the const isn’t actually undefined whilst modifying the non-const result is, but I can’t be bothered to read the standard to check. It’s a bad idea either way.)

If you don’t think you know enough about each kind of cast, there’s a good explanation here.
In 95% of cases you’ll only need static_cast or reinterpret_cast.
You should probably read about const_cast just so you know why you should avoid it,
but you may want to skip reading about dynamic_cast because you almost certainly won’t need it.

Technically it’s not ‘missing’ a barrel shifter, the weirdos who designed the AVR architecture didn’t include one in the designs.

I’ve heard a rumour that they somehow ‘forgot’ to include it until it was too late to add it,
but personally I suspect they purposely omitted it to either save board space, simplify control logic or make the chips cheaper to make.

Either way, it’s arguably the biggest downside of the AVR architecture.

You don’t necessarily have to do that, that’s a bit drastic…

GCC (the compiler used by Arduino) almost certainly will be,
but you can’t guarantee that for all compilers,
so you need to decide how important portability is for you.

If you’re sure you’ll only ever want to have this game on the Arduboy then don’t worry and stick with using bit fields,
but if you think you might one day want to port this game to another system then consider doing manual masking and shfting.

Either way you don’t have to limit yourself to just bool.

1 Like