(Tower Defense) Template metaprogramming or Factory functions to avoid class hierarchy?

Started by
8 comments, last by NightCreature83 9 years, 7 months ago

Hello!

Currently i am working in a Tower Defense Game (C++, SFML), which i started like 2 years ago, and i stopped developing it since i have a job.

Now i started again to teach a friend about C++, and i should rewrite alot of part.

A key question is how to store the Tower (and Minion) hierarchy.

Now i have a TowerBase class, which have the Derived types, like ArcherTower, MageTower etc.

This is pretty bad, because TowerBase have the values like Range, Damage etc, which should be initialized there. Now i reassign it in the derived constructor, and it seems like a terrible pattern.

It's also bad, because in the game i use all towers as a TowerBase, so it doesn't make much sense to derive them just because they can attack differently.

Okay, so the attack part i will use lambdas as template parameters, which will define how should it fire. It's okay.

I like the idea to template the Tower class instead of the class hierarchy (it's also faster, because there will be no virtual calls etc).

The problem come when i should define the towers. They have different damage, range etc.

Solition 1.: I would like the idea to somehow make it possible to typedef the towers with different values.

So when i say:


ArcherTower archerTower;

and


MageTower mageTower;

it would call a constructor, which will set different base values (like for archerTower, 30 damage, for mage, 40), while ArcherTower and MageTower are just different typedefs for the same templated class.

Okay so, that's the first solution, writing a template metaprogrammed class, and typedef it for different towers.

In this case, i must use a TowerBase class, which will let me use containers that store Towers.

This TowerBase should have at least a virtual Attack() function, which is again, problematic. This is because different Towers attack differently, but i just store TowerBase ptr-s, so i can't call the derived functions. How can i solve this?

Solution 2.: The other solution that come to my mind is to make factory functions, like "MakeArcherTower ()", which will do the things for me. It's slower and a bit harder to use, but i am sure i can do this, the solution one is something i don't exactly know, because i don't really have experience in metaprogramming. In this case, i shouldn't use a base class.

What do you think, which is the better? (or is it a 3rd option?)

I would also use this pattern to minions, so it would possible used instead of more than a hundred class.

Thank you for help! smile.png

Advertisement

Why even have a templated/base class? Whats wrong with


class Tower
{
     Tower(int attack, float range);

private:

     int m_attack;
     float m_range;
}

And then instead of typedefing you could in the simpliest case use an enum and factory function:


enum class TowerType
{
    ARROW, MAGIC, BOMB
};

Tower& createTower(TowerType type)
{
    switch(type)
	{
	case TowerType::ARROW:
		return *new Tower(100, 50);
	case TowerType::MAGIC:
		return *new Tower(50, 100);
	case TowerType::BOMB:
		return *new Tower(300, 25);
	default:
		assert(false);
	}
}

And now for the attacking part, you can use composition, to derive different attack components:


class IAttack
{
	virtual void Attack() = 0;
}

class DefaultAttack : public IAttack
{
	void Attack() override;
};

class SplashAttack : public IAttack
{
	void Attack() override;
};

class SlowAttack : public IAttack
{
	void Attack() override;
};

Which you can then add to the tower class via dependency injection:


class Tower
{
     Tower(int attack, float range, IAttack& attack);

private:

     int m_attack;
     float m_range;
     IAttack* m_pAttack;
}

And your pretty much done. Now your tower can have a non-virtual "Attack"-method which calls the "components" virtual Attack(). You can (and probably will want to) grant access to the tower to the attack-component, eigther by adding "Tower& tower" to the IAttack::Attack()-method declaration, in which case you can share the attack-component with all towers that use the same type. Or you make another "IAttack::SetTower(Tower& tower)"-method and create a attacker-instance per tower.

in modern software design it's rather a bad solution in general to create those fat classes, no matter whether it's with hierarchies or templates or... it makes the software
- rigid (just what you've designed is implemented, adding fundamental new thing requires often a refactoring)
- harder to maintain (e.g. you have those hardcoded class names and now you make the 'Christmas version", every piece of art is by something from the holiday theme replaced but your code still has the hardcoded names and now the "mega santa" has a bug.. wait, was it CDestroyer or CBattleship...
- harder to design, you need upfront to define how you'll split and share code, what parts will be used in several classes, what parts are specialized, which part will you design more flexible than needed... just in case..

in modern design (and RTS games are especially well suited for it, to which I count TDs too), you go the data driven way. You could compose objects outside your applications, just with a file. e.g.


name: LaserTower
GunType: laser
speed: 0
FireDelay: 100ms
DamagePerLevel: 50, 100, 120, 150
Range: 50
...
imagin your game designer gets the awesome idea that the waves should also have a unit that fires laser -> just copy a unit, rename it, add a fire rate, damage..
imagin you could add towers that can slowly move their position, sure, you need to add ui/controls, but you don't need to add yet another attribute, extend the interface, you just pass the event to the objects and based on their configuration, they move
imagin you get want to have power ups or other ways to level up tower... you just change some values.

when you process your objects, you actually don't really care what kind of object it is, it could be a tower, an enemy or just a wall or rock or..., you process it based on the attributes.


for_each(..)
  object->render();
for_each(..)
  GunTypes[object->GunType()]->render();
now your game designer has also the possibility to change the game completely without asking you every time "could you make a new build, with those laser towers of class 1 to have a 3% higher fire rate, I think that would be more fun.. yes, sure, whenever you have time, we can try it tomorrow".

that's the basic idea of component systems, which might be a bit too much of design, unless you're doing it for learning purposes.

http://www.insomniacgames.com/a-dynamic-component-architecture-for-high-performance-gameplay/

Well, actually that's the point of templating it.

I want to be able to easily add new things to the game.

For example, i imagine a header like this:


using MageTower = Tower<MageTowerDamage, MageTowerRange, MageTowerAttackFunction, MageTowerSpecialAbilityFunction> ;

That's it, i create a new tower in 1 line, the base stats (Damage, Range, Attack Speed etc) are defined in a different header next to each other, and the complicated parts, the lambdas (how to attack, special abilities etc) are defined in another header.

I am not done with that design, it's pretty much just an idea, but the point of it is actually to avoid things like this:


enum class TowerType
{
    ARROW, MAGIC, BOMB
};

Tower& createTower(TowerType type)
{
    switch(type)
	{
	case TowerType::ARROW:
		return *new Tower(100, 50);
	case TowerType::MAGIC:
		return *new Tower(50, 100);
	case TowerType::BOMB:
		return *new Tower(300, 25);
	default:
		assert(false);
	}
}

Imagine if i have 50 tower in your enum, and a 50 case in the switch statement. Even worse, there could be like 200, or even 500 minion, with the same design. It's pretty hard to read and develop further. I think the programmer just loose himself easily in that.


Imagine if i have 50 tower in your enum, and a 50 case in the switch statement. Even worse, there could be like 200, or even 500 minion, with the same design. It's pretty hard to read and develop further. I think the programmer just loose himself easily in that.

As I said, thats the simpliest case. If you want to support more towers, use a config file like Krypt0n suggested. Templating does nothing really here, having 200 typedef is just as bad, if not even worse than such a switch statement. Having 200 different towers in code is going to suck, no matter what. Break out the common components - attacking, target selection, etc... and make those compositable via config files, again in the easiest case via a switch-statement, in the more complex case via a lexicon/registry.

Regarding further why templating is a bad idea: Range and Damage are stats/variables. Those are not something you template. They do not imply behaviour - their behaviour is dictated by the attack-component and probably some resistance-formula. Furthermore, you might want to adjust the attack based one e.g. a tower level or upgrade, and templates do not scale well with such things.

(1) Give your base class a protected constructor that takes parameters. Use that constructore in the initializer list of derived classes. That way, the derived classes control the initialization values and they only get initialized once.

(2) Read up on virtual functions. The whole point is that you store pointers to the base class and call the base class virtual function, and the most-derived virtual function actually gets called.

(3) Better yet, read up on NVI. You would have a non-virtual Attack() function in the base class and a virtual CustomAttack() function that gets overridden by derived classes, and by default does nothing. The base class does some setup/terdown (logging for example), then invokes the virtual function to implement derived-tower-specific attack modes.

Stephen M. Webb
Professional Free Software Developer

This is a good read. You might find it useful.

http://www.gameprogrammingpatterns.com/

In particular, these might be relevant:

http://www.gameprogrammingpatterns.com/type-object.html

http://www.gameprogrammingpatterns.com/subclass-sandbox.html

Thank you very much, this is simplify alot of thing. :)
(i am sad, metaprogramming were more interesting. :'( )

You can still do fun things, just for the fun of doing them :)

Well, actually that's the point of templating it.

I want to be able to easily add new things to the game.

For example, i imagine a header like this:


using MageTower = Tower<MageTowerDamage, MageTowerRange, MageTowerAttackFunction, MageTowerSpecialAbilityFunction> ;

That's it, i create a new tower in 1 line, the base stats (Damage, Range, Attack Speed etc) are defined in a different header next to each other, and the complicated parts, the lambdas (how to attack, special abilities etc) are defined in another header.

I am not done with that design, it's pretty much just an idea, but the point of it is actually to avoid things like this:


enum class TowerType
{
    ARROW, MAGIC, BOMB
};

Tower& createTower(TowerType type)
{
    switch(type)
	{
	case TowerType::ARROW:
		return *new Tower(100, 50);
	case TowerType::MAGIC:
		return *new Tower(50, 100);
	case TowerType::BOMB:
		return *new Tower(300, 25);
	default:
		assert(false);
	}
}

Imagine if i have 50 tower in your enum, and a 50 case in the switch statement. Even worse, there could be like 200, or even 500 minion, with the same design. It's pretty hard to read and develop further. I think the programmer just loose himself easily in that.

This looks like a Mixin pattern if you want to achieve this through C++ templates, it is similar to component based approach to the constructing game entities, but does this at compile time. And is probably better suited for behavior driven applications, where the behaviour is not dynamically constructed from several components.

Worked on titles: CMR:DiRT2, DiRT 3, DiRT: Showdown, GRID 2, theHunter, theHunter: Primal, Mad Max, Watch Dogs: Legion

This topic is closed to new replies.

Advertisement