MicroTD - Tower Defense for Arduboy


(Miloslav Číž) #1

microtd_logo

I am releasing my tower defense game. It’s my first Arduboy game. Hope you’ll like it.

preview

As always, I could spend more time fine-tuning it, but you have to release it at some point, so here it is. Any uncaught bugs shall be fixed in post-release versions.

Gameplay is as usual with the TD genre: you build towers in order to keep creeps that spawn at start location(s) and run to the finish location(s). By killing creeps and finishing rounds you earn $$$ to buy more towers and upgrades. There are 5 maps, 8 tower types, most with 2 upgrades, and 11 creep types. Each map has an infinite number of rounds that get progressively more difficult. The goal is to last as long as you can.

There is a very simple sound (has to be turned on in the menu) and high-score saving to EEPROM.

The whole game and it’s source code, including the assets, are free and open source software, released under CC0 (public domain). You can do whatever you want with it without any requirements. You can even use it commercialy. I encourage you to share and modify the code (find a list of mod suggestions in the README). What I appreciate (but don’t require):

  • credit
  • Sharing the changes you make and your own original games as free and open source as well. If you want to thank me, this is how you do it :slight_smile:

Here are all the files:

ProjectABE emulator link

And here is a simple manual:

tower     $  range  speed  damage  upgrades             notes
-------------------------------------------------------------------------------
guard     8   *      **     *      +range,  +speed      Shoots arrows.
cannon    8   *      *      *      +range,  +damage     Does splash damage.
ice      17   *      *             +speed,  +range      Slows down enemies.
electro  30   **     *      **     +damage, shock       Shoots lightning.
sniper   45   *****  **     **     +speed,  +range      Covers huge range.
magic    60   *      *      *      +damage, speed aura  Support tower.
water   100   ***    ***    ****   +range               Can knock enemies back.
fire    100   ***    **     ****   +range               Does splash damage.

                                                           can be attacked by
creep       hp    speed  $ notes                         G  C  I  E  S  M  W  F 
-------------------------------------------------------------------------------
spider      *      **    1                               .  .  .  .  .  .  .  .
lizard      *      ***   1                               .  .  .  .  .  .  .  .
snake       **     **    1                               .  .  .  .  .  .  .  .
wolf        ***    **    1 Good against cold.            .  . 50% .  .  .  .  .
bat         **     ***   1                               .  NO .  .  .  .  .  .    
ent         ****   *     2                               .  .  .  .  .  .  .  .
big spider  ***    **    2 Spawns 2 small ones on death. .  .  .  .  .  .  .  .
ghost       ***    **    2                               NO NO .  .  NO .  .  .
ogre        *****  **    3                               .  .  .  .  .  .  .  .
dino        *****  ***   3                               .  .  .  .  .  .  .  .
demon       *****  ***   3 Supposed to make you lose.    NO NO NO NO NO NO .  .

Also, there are two road tilesets available:

tilesets

You can switch between them via a macro in the source code.

Thanks to this community for being awesome and I hope you enjoy the game :slight_smile:

Also:

  • Being my first game, there are probably many bugs, inefficiencies etc. I’ll appreciate any fixes and improvements. You can either just report them or send fixes, preferably as a diff or a merge request at GitLab.
  • The game uses 80% RAM for globals, but seem to run well. Is this ok? (E.g. Microcity uses 91%)
  • Can someone please comfort me and double check there is no way on Earth I can destroy somebody’s EEPROM? I checked several times, but I have anxiety attacks from working with EEPROM.

EDIT:

  • v1.1 released (incorporates minor code improvements and prevents possible bugs)

potential TODOs:

  • Add cheats. done (but can add more)
  • Make the game deterministic. done
  • Add game “replay” recording (in a form of an ASCII string).
  • Add sound control to the game menu.
  • Add possibility to speed up time (with a button combo).
  • Replace byte with uint8_t.
  • Detect also directional button “repeats” in addition to presses.
  • Hide the cursor during wave in progress and use an arrow key for game speedup.

What’s the best Arduboy game?
(Pharap) #2

I’ve had a long skim over the code and made note of various issues/improvements.

I don’t have a GitLab account and I have no idea how to go suggesting improvements without one, so I’m posting a list here.


Sound

My biggest complaint is that you’re using arduboy.audio.on() in setup.
You should use arduboy.audio.begin() instead because it respects the player’s sound settings instead of forcing sound on.


Graphics

If you changed your sprites to the format used for Sprites instead of Arduboy2::drawBitmap then you could do away with drawBlack and use Sprites::drawOverwrite instead, which would be significantly faster because it would do the same job in a single pass instead of two passes.


EEPROM

For the sake of EEPROM, as long as you’re not overwriting the library reserved bytes, you’re using update and/or put instead of write and you’re not continually writing to EEPROM then you should be fine.

I’d like to point out that you’re only saving the lower byte there though, you’re not saving the whole uint16_t.
EEPROM.update is declared as void update( int idx, uint8_t val ), so what’s going on here is that uint16_t is implicitly cast to uint8_t (a lovely example of why I hate implicit narrowing casts).
If you want to write the whole uint16_t, use EEPROM.put, which is declared as template< typename T > const T &put( int idx, const T &t ) (yay templates!).

Using EEPROM’s index operator is uncommon, but it does the same job.

(To be honest I don’t think the EEPROM library is particularly well written anyway. It’s the source of lots of compiler warnings and I have suspicions that it generates larger code than it could do if it was rewritten to not depend on EEPtr and EERef.)

I think the EEPROM initialisation stuff should be in its own function rather than dumped directly into setup.


Redundancies

recordWritten = false is redundant in setup because recordWritten will already be zero initialised to false by that point.

mSound is redundant, you can rely completely on arduboy.audio.
Have a read of the source code for it and you’ll see it has toggle and enabled (among other things), which you can use to replace mSound in most cases.
If you’re worried about how it works, look at the .cpp file and it will soon put your mind at rest.

Strictly speaking using arduboy.audio is redundant because all the functions are static so you can just do Arduboy2Audio::begin() etc, but I find most people opt for arduboy.audio (presumably because they aren’t aware of the functions being static).


Memory & Efficiency issues

This is false. It doesn’t load the characters into RAM, but it does still eat memory because the chars are turned into a ‘load immediate’ compiler instruction and then the functions are called, so you end up with a long chain of function calls, which is bad for progmem.

I’d like to introduce you to strcpy_P.

You could easily have const char mapText[] PROGMEM = " map 0 "; somewhere (e.g. a static local variable would suffice) and then just strcpy_P(result.mText, mapText);, after which you can result.mText[6] += index + 1 and it should have the same result, but hopefully slightly cheaper (especially if you use strcpy_P in all cases).
You can do the same for mSubText and the other case of mText.

I’d try to ward you off sprintf too, but I think I know what the answer will be.
(I’ll make a note to make an implementation of Print that can write to a buffer someday (assuming there isn’t already one available somehwere).)

A nice no-brainer, this:

  #define C(c) arduboy.print(c);   C('r')C('e')C('a')C('l')C('l')C('y')C('?')C(' ')   C('A')C('=')C('y')C(' ')C('B')C('=')C('n')   #undef C

Can become:

arduboy.print(F("really? A=y B=n"));

Which stores the string entirely in progmem.

In the case of this I’d suggest not using mText for that and instead maintaining a const __FlashStringHelper * so you can do:

case TOWER_GUARD: flashString = F("guard"); break;
case TOWER_CANNON: flashString = F("cannon"); break;
case TOWER_ICE: flashString = F("ice"); break;
case TOWER_ELECTRO: flashString = F("electro"); break;
case TOWER_SNIPER: flashString = F("sniper"); break;
case TOWER_MAGIC: flashString = F("magic"); break;
case TOWER_WATER: flashString = F("water"); break;
case TOWER_FIRE: flashString = F("fire"); break;

You could even go one better and use a lookup array in progmem.

I’m sure the map creation where you’ve got a long chain of addSegment could be turned into something using a lookup array in progmem.

Theoretically arduboy.print(F("< ")) should be cheaper than arduboy.write('<'); arduboy.write(' '); in terms of memory (though not in terms of speed).


I also wrote a section about stylistic issues, but I’ve left those out for now in case you’re not interested.
If you are interested, I’ll add another comment with those in.


(Miloslav Číž) #3

Thank you @Pharap :slight_smile: I’ll incorporate these tomorrow. Maybe other people will leave more suggestions until then.

If I’m narrowing to byte from uint16_t in EEPROM, that’s a mistake. It should all be bytes, but I had uint16_ts before and it probably got mixed up.


(Pharap) #4

For most of them there’s no rush because they’re mostly improvements rather than bugfixes

You are.

There’s no EEPROM.update overload for uint16_t, only for uint8_t.
Changing it to EEPROM.put is all you need to do.
Whether that bugfix is backwards compatible or not depends on whether AVR is big endian or little endian.


Let me know if you want my comments about the stylistic choices.
I’ve left those out for those because whether they’re improvements or not is mostly subjective.


(Miloslav Číž) #5

Okay, I changed mRound to byte - it has to be compatible with mRecords, plus you’re not supposed to get to a round that’s in the hundreds anyway.

Let me know if you want my comments about the stylistic choices.

Leave me some basic points, I’m definitely curious, but let’s not start another rant, as we like to do :slight_smile:


(Stephane C) #6

I really like the game… But is it normal that tower even in range just don’t shoot at some creeps?


(Miloslav Číž) #7

But is it normal that tower even in range just don’t shoot at some creeps?

Some creeps are immune to some towers. There’s a table of what can shoot what somewhere in that wall of text :slight_smile:


(Stephane C) #8

My bad I didn’t went thru it all, sorry.

Anyway I enjoy the game a lot. Thank you.


(Pharap) #9

Okay, here’s the shortened version…


Stylistic issues

uint8_t should be preferred over byte because byte is an arduinoism.

stdint.h should be conditionally compiled so that the Arduino version uses stdint.h but the PC version uses cstdint because stdint.h is deprecated in a ‘proper’ C++ environment.

Your typedef struct is redundant because your code won’t work with C and the typedef struct idiom has no use in C++.

enum class or even a plain enum would be better for a state system than using #defines.

And there appears to be a rogue new line here.

You ought to be using references instead of pointers in a lot of cases.

I would advise preferring &result.mSubText[6] over result.mSubText + 6, because the former is obviously a pointer, even if taken out of context, whereas the latter could be mistaken for an integer operation at first glance.

Most people I know don’t prefix their members with m, though I’m aware that both m and m_ are common in certain programming circles.


(Miloslav Číž) #10

This is all very relevant and I agree, I’m going to take these into account as well.

This is exactly why code review is important - you simply type some things automatically without much thought. So thank you.

A lot of these are just me talking C++ with C accent :slight_smile:

If you find more things worth mentioning, you can put them under EDIT in your posts. I’ll get back to them tomorrow evening to make v 1.1.


(Scott) #11

No need to call anything. A call to arduoby.audio.begin() is already done in arduboy.begin()

Also, using your own flag, mSound, isn’t required to keep track of the sound mute state. You can just use the arduboy.audio functions directly.

  • arduboy.audio.enabled() will return the current mute state, which can be used in place of your mSound flag.
  • arduboy.audio.on() will turn sound on.
  • arduboy.audio.off() will mute the sound.
  • arduboy.audio.toggle() will toggle the state.
  • arduoby.audio.saveOnOff() will save the current state to system EEPROM so it will be persistent across reboots and other sketch uploads.

(Pharap) #12

Fair enough, I hadn’t noticed that before.

I covered that in the ‘redundancies’ section:


(Scott) #13
  1. The audio subclass has already been instantiated in Arduboy2.h so using it is easy (and shorter).
  2. Even if a class/function is static, instantiating the class and calling it that way doesn’t produce more code and can sometimes be of benefit. For instance, there is a BeepPin1 class and a BeepPin2 class. They contain identical functions, the only (main) difference being which speaker pin is used. By creating a class instance, I can switch which speaker pin is used simply by which class I instantiate. Otherwise, I would have to change every line of code containing BeepPin1:: to BeepPin2::

(Pharap) #14

I’m aware of that.
The issue with it is ensuring that the code can ‘see’ the instance of the class.
That requires either having a header where a global variable is externed and a source file where it’s defined,
or passing the variable around (which does produce more code).

It’s easier to let a scope see the type by including the header that declares the class than it is to create a header with an externed global and a source file implementing the global (that’s assuming the instance actually even is global, it might not be).

Or you could use a type alias.

using BeepPin = BeepPin1;

BeepPin::noTone(); // or whatever

Then changing is as simple as changing the alias:

using BeepPin = BeepPin2;

Which is why type aliases are so ubiquitous in the C++ stdlib.


(Miloslav Číž) #15

What do you mean? There’s a difference, right? Not using typedef I’d have to write struct before everything, no?

I don’t want to write struct before everything, as I don’t have to write class before things.


(Miloslav Číž) #16

@Pharap, here’s how I address the issues. The code can be seen in the dev branch. Once all is resolved and tested, I’ll merge it into master as 1.1.

My biggest complaint is that you’re using arduboy.audio.on() in setup.

Deleted arduboy.audio.on() and initializing game.mSound from arduboy.audio.enabled().

I’d like to point out that you’re only saving the lower byte there though, you’re not saving the whole uint16_t.

Fixed yesterday by changing mRound to byte.

recordWritten = false is redundant in setup because recordWritten will already be zero initialised to false by that point.

Minor thing, left untouched, I like to manually init global variables even if not neccesary, to help clarity.

mSound is redundant, you can rely completely on arduboy.audio.

The part of code where I’m using it is outside the Arduboy only part of code, that’s why I made it this way. So again leaving as is.

If you changed your sprites to the format used for Sprites instead of Arduboy2::drawBitmap then you could do away with drawBlack and use Sprites::drawOverwrite instead

My ignorance again, big thank you. This actually improves the graphics as with the mask now the road doesn’t shine through the creeps’ eyes. Awesome :slight_smile:

This 5 is false. It doesn’t load the characters into RAM, but it does still eat memory because the chars are turned into a ‘load immediate’ compiler instruction

I meant it saves RAM by not permanently storing the string there. It stores it in the instruction immediate data in progmem. But yeah, it’s kind of a hack.

arduboy.print(F("really? A=y B=n"));

OMG, thank you so much. I knew there had to be something like this, sorry for my ignorance. Really awkward.

You know what I think we need? A tutorial, or maybe a cheatsheet, for programmers who already know C++, but are new to Arduino/Arduboy.

I just don’t want to read through walls of beginner tutorials where these things are probably shown, but 99% is just waste of my time. Could anyone make this, pretty please? :slight_smile:

uint8_t should be preferred over byte because byte is an arduinoism.

I have a typedef for byte for the PC version, but I’ll stick to uint8_t or unsigned char next time.

stdint.h should be conditionally compiled so that the Arduino version uses stdint.h but the PC version uses cstdint because stdint.h is deprecated in a ‘proper’ C++ environment.

Done.

Your typedef struct is redundant because your code won’t work with C and the typedef struct idiom has no use in C++.

TODO (see my post above please).

EDIT: fixed

enum class or even a plain enum would be better for a state system than using #defines.

Yeah, I sometimes have problems identifying what should be a define and what should be an enum (like the struct vs class problem). Sometimes you e.g. have multiple unrelated constants (TARGET_NONE, MENU_ITEM_NULL, …) that you feel like grouping into an enum, but aren’t sure about. So I sometimes resort to using only defines, which is the case here. I don’t feel like changing this, it’s a minor thing in my view.

And there appears to be a rogue new line here

It’s because I fit my code into 80 columns, but this one is really an unfortunate formatting, tried to change it so that it doesn’t look like “if nothing”.

You ought to be using references instead of pointers in a lot of cases.

I know by the book I should, but again it comes down to me preferring the C-style. References are safer etc., but … leving this for the rant thread :smiley:

I would advise preferring &result.mSubText[6] over result.mSubText + 6

Good point, agree 100%. (Or maybe 90% - I am never sure about the & operator priority, so I tend to write &(result.mSubText[6]) which gets long.) changed!

Most people I know don’t prefix their members with m, though I’m aware that both m and m_ are common in certain programming circles.

I got used to this at OpenMW. They have a nice clarity-focused style (m/g/s prefixes, 4-space indent, fully qualified identifiers, …) because it’s a big codebase with many devs, but I like to use parts of it in all my projects.


(Pharap) #17

Not in C++, no you wouldn’t.

If you look at the ‘Breaking The Misconceptions’ section in my resource collection you’ll find a few links proving that C isn’t a subset of C++.

In this case, look at the ““All C is legal C++” - No it isn’t” link, which leads here, to Wikipedia.

Scroll down past the int N; int N = 10 example and you’ll find:

C allows declaring a new type with the same name as an existing struct, union or enum which is not allowed in C++, as in C struct, union, and enum types must be indicated as such whenever the type is referenced whereas in C++ all declarations of such types carry the typedef implicitly.

So basically:

struct A {};
union A {};
enum A {};

In C these three things are classified as different types, the first is struct A, the second is union A and the third is enum A.

Whereas in C++, you get a redefinition error because C++ sees all of these as trying to create a type called A.

I’d advise initialising at the point of declaration then.
If you initialise in setup there may be an additional code cost (only a few bytes, but I’ve learnt to not take bytes forgranted on AVR chips).

No problem.
It seems few (if any) of the tutorials cover Sprites, which is a shame because I tend to find it’s the more commonly used format.

Note that image masks on the Arduboy are the inverse of traditional bitmasks for some reason.
(I’m presuming because having them inverted makes ‘drawSelfMasked’ viable.)

Even an unadorned string doesn’t live in RAM permenantly.
It lives in progmem and it’s then copied to RAM when it’s needed, and disposed of when the scope ends.

I can also explain how it works if you want to know how print knows to look for the string in progmem instead of RAM.

I started writing one the other week, but you know me - so much to do, so much to see.

I started writing something like that for Pokitto once (C++11 for C++03 programmers) but forgot about it.

I think it would be easier to write than a tutorial for beginners, so if I can clear a hole in my schedule I’ll consider it.
Then again, so much to do, so much to see…

Guess what byte is actually defined as? Answer.

Personally I advise sticking to uint8_t for semantic reasons.
uint8_t is a way of telling the world “I want this type to be an unsigned 8 bit type”.

unsigned char on the other hand is “large enough to store any member of the implementations basic character set” (actual wording of the standard).
Legally that could be 16 bits or 32 bits on some really obscure platform that only supports reading whole words from RAM.
It’s better to refuse to compile because a platform can’t handle an 8 bit type than to silently compile and end up with runtime bugs.

(Another reason not to use byte is because it could be confused for std::byte (as of C++17).)

Enumerations are for groups of semantically related values like state identifiers.

For example, all the buttons ought to be an enum class so there’s a clear Button type that represents the identifier for a particular button.
Likewise Direction is a good candidate for an enum class (and is actually incredibly common in Arduboy games). We had one in Dark & Under.
And of course TileTypes.

It saves you doing typedef byte Direction; and by using enum class instead of plain enums you get a guarantee of type safety so you can’t accidentally assign an enum value to the wrong type.

TARGET_NONE shoud be a constexpr global because it’s a kind of sentry value.
In a properly robust program IndexPointer would be a class designed so that integers can be assigned to it but it can’t be assigned to integers (or something along those lines), but that’s probably overkill for a game of this scale.

I can’t find a MENU_ITEM_NULL, but if menu items were indices then it would be a constexpr global and if menu items were a finite set of identifiers then it would be an enum class.

Basically it’s all about semantics and grouping.

Type theory sometimes thinks of a type as a finite set of symbols, and that’s what an enum class gives you - a finite set of typesafe symbols represented in a cheap and efficient way (as integers).

That explains why there’s a large strip of white down the right hand side of the code.

You know they invented wordwrap, right? :P

Seriously though, the ‘refactor into smaller functions’ suggestion would probably help with the column limit.

It comes after indexing, that much I know.
The best way to remember is that it has the same precedence as unary *, !, -, + and ~.

. and [] have the same precedence aparently, which is level 2, which is the second most important.
(There’s only one level 1 operator, the scope resolution operator - :: - which I’m pretty sure is evaluated at compile time.)

I use this-> instead because it’s got compiler support - if the following identifier isn’t a member of the enclosing class then you get a compiler error.
Which also means no ambiguity because it’s constrained by the compiler rather than by convention.
(If I redesigned C++ I’d probably make the this-> mandatory.)

I’d get confused with g and s because globals have static linkage.
Global variables are just static variables at global scope instead of class or function scope.

I’m firmly in the tabs camp.

That one I generally agree with.

Though I’ll admit to using using namespace std; in function scope (but never global scope), or (for example) using std::swap; (again, function scope).


(Miloslav Číž) #18

Ooooh, okay, will change it then :slight_smile:

I know, it has to be clear just from the fact you can use C++ keywords as identifiers in C :slight_smile:

+1, will do.

Every time I used plain arduboy.print("abc") the RAM usage report increased by a significant amount. Will have to investigate this.

Agree.

Not the same, it simply stupidly overflows. It’s good to fit into 80 columns when you have multiple windows side-by-side. Even if there’s only one file, I typically have it open at multiple places.

Fair enough - m is shorter and helps fit into 80 columns :slight_smile:

I think tabs are evil (better not ask why here) :smiley: Again, it’s a religion war. As long as you’re the only one working on the code it doesn’t matter because others can simply search-replace with spaces, but when it comes to collaboration, there can be real wars about tabs vs spaces.

Fun fact (don’t take too seriously please): Stack Overflow found out that on average you earn more when using spaces. Then again, you almost certainly earn more money by programming than me. With certainty I can say you don’t earn less than me. (Okay, let me say it: I make $0 from programming.)

Will take your advice into account and probably push 1.1 before midnight.


(Pharap) #19

That was meant to be C isn’t a subset of C++.

There’s a lot more impactful things than just that though.
Cases where programs are valid in both but behave differently.
(For example, in C++ 'a' is a char, but in C 'a' is an int.)

Hrm, good point. (It’s been a while since I’ve used bare strings on the Arduboy.)
Most likely the RAM is statically allocated then.
So I suppose technically they’re more like function-local static variables, in which case they’d be instantiated on the first time the function is called.

I often have things side by side, but my screen is wide enough to manage more than 80 columns per half.
The ‘80 columns’ rule is archaic, and by archaic I mean punch-card level archaic.

I’ve heard every argument anyway, none of them have convinced me.

Most people here use spaces because they dont know how to make the ArduinoIDE use tabs. :P

@filmote and I have managed not to have a war over it… somehow.

I’ve heard that one before.
Incidentally the highest paid programming jobs are usually maintaining decade old systems (or they’re at Google).
COBOL programmers are surprisingly well paid.

I don’t make $, I earn £. :P

That said, I don’t get paid to program.
I’m still technically an amateur.


(Miloslav Číž) #20

Units only matter with non-zero numbers, we don’t have $ here either - I don’t know how to type euro of pound :smiley: Might as well have used Japanese Yen.

TBH that’s why I like it probably, I have a thing for these good old times :smiley:

I don’t use computers at all. It’s hard to find a job that aligns with my views :frowning:

Usually lower salary == more passion :slight_smile: