Micro Towers - a Tower Defense game

#1

Hi everyone, this is my second Arduboy game.
I thought there aren’t enough tower defense games for the Arduboy, so I made one.

GitHub:

Play Online:

https://felipemanga.github.io/ProjectABE/?url=https://raw.githubusercontent.com/hartmann1301/MicroTowers/master/MicroTowers.ino.hex

Features:

  • 8 all different towers, most can rotate
  • Every tower can be upgraded 3 times
  • 5 enemy types that come in 6 different groups
  • 3 different enemy races that behave all the same
  • A fast playing mode
  • A pathfinding algorithm for free tower placement
  • An ingame tower and enemies lexicon to get some infos
  • 20 campain maps to unlock
  • Fancy menus with moving items and map previews
  • An editor to create your own maps on 5 slots
  • EEPROM support to save scores and maps
  • A custom 6x5 pixels font

But gifs say more than many words and pictures:
gifCampain gifEditor

Main Controls:

About the Game:

The towers and the graphics:
I think one reason why there are not many tower defense games on the Arduboy is, because it is really hard to fit many towers plus some enemies and game information on 128x64 pixels. I am really happy with the a bit unconventional 6x6 pixel graphics I chose. With some space right and some space at the bottom there is still space for 9x20 = 180 tiles. Every tower uses 4 tiles, this way they are distinguishable. At first I thought it would not be possible to make 24 towers rotatable without losing the different look of them, but I think it went really well.

The pathfinding:
I use a simple or not very intelligent Dijkstra-like pathfinding algorithm. It spreads from the target in every direction and writes the costs to a global array with all the map tiles. The path is only recalculated if the map changed. The enemies read from this array and every enemy decides then what its best path is. The enemies do not walk in tiles, they walk in pixels, this way they can walk vertical lines. I also added some randomness to the path, so if two paths are equally good the enemy group will maybe split.

The editor and the maps:
I needed a editor to create those maps. The maps where created on my “Arduboy” and than sent via serial and copied to the progmem. This way I could create a lot of maps in a short time. Than I liked the editor and addit it to the game. The maps are really compressed. A map consists of 180 tiles which can be free, rock, tree or headquarter, this means 2 bit pro tile. With a specific difficulty factor and a start coins value a map in total is only 180/4 + 2 = 47 bytes. It is my first time using the Arduboy EEPROM, maybe if someone could take a look over it, that would be nice.

The projectiles:
Those towers can rotate at 16 different directions, so one direction sector is 360°/16 = 22,5°. Because I use those sectors for the projectile movement it means the towers are shooting with a max direction error of ±10,5°. Sometimes especially if a enemy is moving many degrees it can happen that the projectiles misses. I just say to myself that this is more a feature than a bug, makes things more realistic…

The enemies:
The enemies of a race sometimes look really similar. Maybe some will remember that I already used a few of them in my first game (Micro Tank). But if you wanna know what tower is best for the next wave you can look at the icon in front of the waves count. There are three different enemy races to make the look of the game a bit more varied.

In the main folder of my repository there a two hex files. The one with the _DEBUG suffix has many already unlocked maps and it prints the frames pro second to the top left corner. I could not test my game on a arduboy, but it would be nice if someone could say how many frames the arduboy can do while playing or in main menu.

This is version 1.0 it is playable but maybe some fine tuning is necessary. I could spend another week changing some tower damages slightly up and down but I think it is time to release. Maybe there are still some bugs.

If you wanna help and feel a tower is too strong or to week or a map is to difficult or something else, please let me know. Also programming tips or advice, I appreciate every kind of feedback. The code is a mess but I used lots of comments, so anybody should have at least an idea what I did in most of the lines.

That’s it, I will answer any questions and I hope you have some fun playing

12 Likes

Strategy, like old good games from 1990?
(Simon) #2

Wow … nice job. I really like @drummyfish’s version of this and cannot wait to play this version.

I see in your code that this supports the ESP8266. Do you develop using your PS2 ESP8266 beast or do you have a normal Arduboy for development?

Just a comment on the code …

  void draw() {
    // strange switch thing, variable needs to be initialised here
    int8_t flameSize;

    select (....

      case TOWER_FLAME:
        flameSize = 5 - getHighNibble(state);
        if (flameSize < 2) flameSize = 2;
        fillChessRect(x - 1, y - 1, flameSize, flameSize, BLACK);
        break;

If you want to include the declaration of flameSize within the case statement, you can simply wrap the contents in { }. For example:

      case TOWER_FLAME:
        {
          // get frame size
          int8_t flameSize;
          flameSize = 5 - getHighNibble(state);
          if (flameSize < 2) flameSize = 2;
          fillChessRect(x - 1, y - 1, flameSize, flameSize, BLACK);
        }
        break;
2 Likes

#3

I also really like @drummyfish´s tower defense game, anyway my approach was very different.

thanks for the switch {} advice, I didn´t know that.

And yes, i still only have this ESP8266 controller thing. Works fine for me.

0 Likes

(Kevin) #4

I love games that have more than one map this is great! I love the health bars it makes me think more if it would be possible to have a MOBA game on Arduboy! :smiley:

1 Like

(Stephane C) #5

Wow. This game is awesome! Thank you for sharing!

1 Like

(Pharap) #6

Doesn’t that mean you could get your editor to calculate the paths and store them in progmem?

Or maybe I’m misunderstanding how the editor works?

There’s an explanation here:

In your case it would currently be:
https://felipemanga.github.io/ProjectABE/?url=https://raw.githubusercontent.com/hartmann1301/MicroTowers/master/MicroTowers.ino.hex

Though I’d recommend using GitHub’s releases feature for releasing .hex files, it makes it easier to keep track of which .hex is from which version (.hex files tend to get out of sync with the source code if you don’t remember to update the .hex every time you update the source)

1 Like

#7

This game plays well on the real Arduboy. I did like MicroTD that drummyfish did.

And it’s pretty cool that the path of the enemy changes when you place a tower in their path. I could make a long path of towers shooting at the enemies that comes running towards the town. I managed to complete 2 map so far. Really cool TD game.

1 Like

(Pharap) #8

@hartmann1301, I have edited the emulator link into your post.

I also changed the formatting and fixed a few minor grammar/spelling problems.
If you don’t like my changes you can edit your post at any time using the pencil icon.


A small code tip, you can use EEPROM.get and EEPROM.put to load/store objects to/from EEPROM:

uint16_t get2BytesData(uint16_t address)
{
	uint16_t twoBytes;
	EEPROM.get(address, twoBytes);
	return twoBytes;
}

void set2BytesData(uint16_t address, uint16_t data)
{
	EEPROM.put(address, data);
#ifdef ESP8266
	EEPROM.commit();
#endif
}

(put uses update internally.)

Personally I use the avr-libc eeprom functions, but I suspect ESP8266 doesn’t support them.

And for the Arduboy you should avoid String and make sure to wrap your string literals in the F macro.
E.g. Serial.print(F(", "));
The F macro puts the string in progmem.

0 Likes

#9

@Pharap thanks for editing my post, I like all of your changes.

And you are right with EEPROM.put/.get, it is better to use those functions, I will change this soon.

I think I explained my pathfinding a bit too short. The following is the content of the mapCost[TILES] array formated to serial for my personal debug.

Map 1 in game

Map 1 Debug with no towers:

       00  01  02  03  04  05  06  07  08  09  10  11  12  13  14  15  16  17  18  19 

00 |  .22..21..20. 19  18  17  16  15  14  13  12  11  10  09  08  07  06  05 .04..05.|
01 |  .21..20. 19  18  17  16  15  14  13  12  11  10  09  08  07  06  05  04  03 .04.|
02 |   22  21  20  19  18  17  16  15 [__][__].10. 09  08  07  06  05  04  03  02  03 |
03 |   23  22  21  20  19  18  17 .16.[__][__].09. 08  07  06  05  04  03  02  01  02 |
04     24  23  22  21  20  19  18 .17..16.[__].08. 07  06  05  04  03  02  01 [HQ] 01 |
05 |   23  22  21  20  19  18  17 .16..15.[__][__] 08  07  06  05  04  03  02  01  02 |
06 |   22  21  20  19  18  17  16  15  14 [__][__] 09  08  07  06  05  04  03  02  03 |
07 |  .21. 20  19  18  17  16  15  14  13  12  11  10  09  08  07  06  05  04  03  04 |
08 |  .22..21. 20  19  18  17  16  15  14  13  12  11  10  09  08  07  06  05 .04..05.|

Map 1 Debug after placing two towers:

       00  01  02  03  04  05  06  07  08  09  10  11  12  13  14  15  16  17  18  19 

00 |  .22..21..20. 19  18  17  16  15  14  13  12  11  10  09  08  07  06  05 .04..05.|
01 |  .23..22. 21  20  19  18 [To][To] 13  12  11  10  09  08  07  06  05  04  03 .04.|
02 |   24  23  22  21  20  19 [To][To][__][__].10. 09  08  07  06  05  04  03  02  03 |
03 |   25  24  23  22  21  20  21 .22.[__][__].09. 08  07  06  05  04  03  02  01  02 |
04     26  25  24  23  22  21  22 .23..24.[__].08. 07  06  05  04  03  02  01 [HQ] 01 |
05 |   27  26  25  24  23  22  23 .24..25.[__][__] 08  07  06  05  04  03  02  01  02 |
06 |   28  27  26  25  24  23  24  25  26 [__][__] 09  08  07  06  05  04  03  02  03 |
07 |  .29. 28  27  26  25  24  25  26 [To][To] 11  10  09  08  07  06  05  04  03  04 |
08 |  .30..29. 28  27  26  25  26  27 [To][To] 12  11  10  09  08  07  06  05 .04..05.|

`    The Symbols and Numbers:`
    [__] is a ROCK
    [To] is a TOWER
    [HQ] the HEADQUARTER
     XX a Value is the cost in tiles to the headquarter
    .XX. points means that this is forest

TOWER, ROCK and HEADQUARTER are all the value 255.

So every enemy is looking at this array to check the tile it is currently standing and than try to go to a tile with a smaller number.

To come back to your actual question, it is not possible to calculate the path in the editor, because everytime a tower is placed or selled some cost values will change.

Thanks to all the nice feedback so far, now I will try to implement your advices.

2 Likes

(Pharap) #10

If ESP8266 supports the avr-libc eeprom functions then consider using those because they are often cheaper.

If not, put and get should be fine.

Ok, that makes sense.

I might think of something else later.

At the moment the main issue I can see is that you aren’t using any of the more modern (C++11) features.
E.g.

0 Likes

#11

I read the cppreference and thought, why not. Those scoped enumerations seem to be great.

So I replaced:

enum {
  TOWER_GATLING = 0,
  TOWER_CANNON,
  TOWER_FROST,
  TOWER_RAILGUN,
  TOWER_FLAME,
  TOWER_LASER,
  TOWER_SHOCK,
  TOWER_SUPPORT,
  TOWER_PROTOTYPE
};

with this:

enum class TowerType {
      GATLING = 0,
      CANNON,
      FROST,
      RAILGUN,
      FLAME,
      LASER,
      SHOCK,
      SUPPORT,
      PROTOTYPE
    };

after adding about 100 static_casts I got to the point where I used this enumerated integer value in the following function with reference:

// value is this TowerType enumeration
void checkUpDown(uint8_t& value, const uint8_t vMax) {

  if (isPressed(arduboy.pressed(UP_BUTTON)) && value > 0)
    value--;

  if (isPressed(arduboy.pressed(DOWN_BUTTON)) && value < vMax - 1)
    value++;
}

// I tried this overloaded function
void checkUpDown(TowerType& value, const uint8_t vMax) {
  checkUpDown(static_cast<uint8_t&>(value), vMax);
}

but I got the following error:
invalid static_cast from type ‘TowerType’ to type ‘uint8_t& {aka unsigned char&}’

and this is where I started reading stackoverflow and got really frustrated after a while because I have no Idea how to do this correct.

I now removed the reference and replaced it with a return function but that is not really what i want to do. An easy function call now looks like this:

indexBuildMenu = static_cast<TowerType>(checkUpDown(static_cast<uint8_t>(indexBuildMenu), MENU_ITEMS_BUILD));

And that´s why I usally don´t use most of the more modern (C++11) features.

But I will try to use constexpr in the future, because I think they are really easy to use.

0 Likes

(Simon) #12

You neeeded to specify the ‘base’ type in the enum:

enum class TowerType : uint8_t {
      GATLING = 0,
      CANNON,
      FROST,
      RAILGUN,
      FLAME,
      LASER,
      SHOCK,
      SUPPORT,
      PROTOTYPE
    };
1 Like

#13

Changed it, but I still got the following compiler error:

Buttons.h:59: error: in passing argument 1 of ‘void checkUpDown(uint8_t&, uint8_t)’

void checkUpDown(uint8_t &value, const uint8_t vMax) {

^

exit status 1
invalid initialization of non-const reference of type ‘uint8_t& {aka unsigned char&}’ from an rvalue of type ‘uint8_t {aka unsigned char}’

 // the global enum
TowerType indexBuildMenu = TowerType::GATLING;


    void checkUpDown(uint8_t &value, const uint8_t vMax) {

      if (isPressed(arduboy.pressed(UP_BUTTON)) && value > 0)
        value--;

      if (isPressed(arduboy.pressed(DOWN_BUTTON)) && value < vMax - 1)
        value++;
    }

checkUpDown(static_cast<uint8_t>(indexBuildMenu), MENU_ITEMS_EDITOR));
0 Likes

(Simon) #14

There is a n awesome article on scoped enums here > https://issuu.com/arduboymag/docs/arduboy_magazine_9 (page 39)

Following the article, you can overload the ++ and – operators and you could then change the

void checkUpDown(uint8_t &value, const uint8_t vMax) {

to be:

void checkUpDown(TowerType &value, const uint8_t vMax) {

and remove the cast altogether.

You should be able to pass the value by reference or you could change the checkUpDown() to something like:

TowerType indexBuildMenu = TowerType::GATLING;


    TowerType checkUpDown(TowerType value, const uint8_t vMax) {

      if (isPressed(arduboy.pressed(UP_BUTTON)) && value > 0)
         return --value;

      if (isPressed(arduboy.pressed(DOWN_BUTTON)) && value < vMax - 1)
        return ++value;
    }

indexBuildMenu = checkUpDown(indexBuildMenu, MENU_ITEMS_EDITOR));

Note: I haven’t compiled this code!

0 Likes

(Pharap) #15

Sounds to me like you’ve been trying to use enumerations as if they were just plain integers,
which kind of defeats the point of enumerations.

If you use an enumeration you’re supposed to store it in the correct type,
not try to store it/treat it as an integer type.

I.e. instead of:

struct SomeStruct
{
	uint8_t towerType = static_cast<uint8_t>(TowerType::Gatling);
};

You should do:

struct SomeStruct
{
	TowerType towerType = TowerType::Gatling;
};

Half the reason scoped enumerations were introduced is for type safety.
I.e. to make code like this illegal:

// 99 isn't a valid TowerType
TowerType towerType = 99;

If TowerType is an unscoped enumeration then that code would introduce an error and the compiler wouldn’t tell you.
But if TowerType is a scoped enumeration then that code won’t compile because 99 isn’t a valid TowerType.
As always, you can opt out of type safety through casting,
but type safety is there to prevent mistakes.

That’s correct, you can’t cast a non-reference type to a reference type.

You could do something like this:
(Basically along the lines of what @filmote was suggesting. He was just missing a few things.)

constexpr TowerType next(TowerType towerType)
{
	return static_cast<TowerType>(static_cast<uint8_t>(towerType) + 1);
}

constexpr TowerType previous(TowerType towerType)
{
	return static_cast<TowerType>(static_cast<uint8_t>(towerType) - 1);
}

TowerType & operator++(TowerType & towerType)
{
	towerType = next(towerType);
	return towerType;
}

TowerType & operator--(TowerType & towerType)
{
	towerType = previous(towerType);
	return towerType;
}

void checkUpDown(TowerType & value, TowerType vMax)
{
	if (isPressed(arduboy.pressed(UP_BUTTON)) && (value > TowerType::Gatling))
		--value;

	if (isPressed(arduboy.pressed(DOWN_BUTTON)) && (value < previous(vMax)))
		++value;
}

Then you can:

checkUpDown(indexBuildMenu, MENU_ITEMS_BUILD);

Though to be honest, I think using a reference like this is a bad idea anyway.
You’d be better off with:

TowerType checkUpDown(TowerType value, TowerType vMax)
{
	if (isPressed(arduboy.pressed(UP_BUTTON)) && (value > TowerType::Gatling))
		return previous(value);

	if (isPressed(arduboy.pressed(DOWN_BUTTON)) && (value < previous(vMax)))
		return next(value);

	return value;
}

Or maybe have something like:

TowerType previousClamped(TowerType value)
{
	return (value > TowerType::Gatling) ? previous(value) : value;
}

TowerType nextClamped(TowerType value, TowerType maxValue)
{
	return (value < maxValue) ? next(value) : value;
}

uint8_t previousClamped(uint8_t value)
{
	return (value > 0) ? (value - 1) : value;
}

uint8_t nextClamped(uint8_t value, uint8_t maxValue)
{
	return (value < maxValue) ? (value + 1) : value;
}

(Now I think about it, I’m not actually sure what isPressed is doing, arduboy.pressed already returns true or false.)

But if most of your code is already trying to treat the enumerations like integers then all of this is probably going to be too much work for this game/project.


(Some recommended reading about enums here.)

1 Like

#16

@filmote and @Pharap thanks for your help and all those code examples.

Today I had a bit time so I repaced most of the c style enums with c++ enums class enums. The replacing was no big thing (I let TowerType as it is, because of my little call by reference problem).

And then after I was done, believe it or not, my code got a litte bigger and isn´t fitting anymore on the Leonardo. I swear, no extra functionality, no other changes, only replacing a few enums. So I will leave it as it is, only change bugs and parameters.

I stored them as the c++ class enum. The problem is all over my code I threat most of my c enums like integers, so I always had to cast it back to integer. (because I did not use all those operator stuff for every enum class)

It is doing the repeat button stuff while scrolling. (not my best function name)

That´s it. My Code is filled with little hacks and tricks and messy things, but I think for an arduino game project that´s absolutly okay.

0 Likes

(Pharap) #17

That’s probably down to one of the other changes.

Scoped enumerations consume no more memory than unscoped enumerations, and neither consume more memory than using plain integers because they are in fact implemented at plain integers.

All the type checking happens at compile time, by the time the code is compiled all three approaches (if semantically equivalent) should produce the same machine code.

If you’re casting to int rather than uint8_t and/or aren’t specifying uint8_t as the underlying type for the enum then that could explain the size increase.
AVR chips are 8-bit processors and int is 16-bit (on AVR systems) so using an int(/int16_t) over a uint8_t(/unsigned char) can result in more instructions being needed to process the data correctly.

And of course, int is signed where uint8_t isn’t, and signed operations also often need more instructions depending on the operation being performed/usage.

Just a thought.

If I get chance in the next few days I might have a look myself to see if there’s something that can be done.
(I can’t look now because my internet isn’t working properly and I don’t have the time anyway.)

Wherever there’s a ‘hack’, there’s almost always a ‘clean’ way that comes at no extra memory cost.

Sometimes there’s even a ‘clean’ way that reduces memory usage.
For example, replacing a macro with a template function can sometimes save a few bytes.
(I distinctly remember replacing the abs macro with a template function saving memory in a game I helped with, but I can never remember which one it was.)


Like I say though, don’t worry too much about it now.
In the case of this game it’s probably going to be too much effort.

0 Likes