4

I'm trying to create a Tower Defense game where minions move through a labyrinth and you can build towers on top of the labyrinths' walls to stop the minions. I'm personally using Python and pygame, but I'm looking for generic design answers that can be applied to any OOP language.

So, since both minions and towers have a size and a position, as well as a sprite image, I felt like it'd be appropriate to create a common base class:

class Entity:

    def __init__(self, image, size, position):
        self.image = image
        self.size = size
        self.position = position

    def draw(self, surface):
        surface.blit(self.image, self.position, self.size)

Alright so I want my game to work similar to Vector TD, where you have multiple different types of towers that can be build, but you can build multiple instances of each type, and each instance can then be upgraded individually. What this means is that I need some kind of generic way to explain how all towers of similar type work, yet I need each instance to be separated. So I began with a Tower base class with all the properties I can think of:

class Tower(Entity):

    def __init__(self, image, size, position, attack_speed, attack_damage, attack_splash_radius, attack_range, cost):
        super().__init__(image, size, position)
        self.attack_speed = attack_speed
        self.attack_damage = attack_damage
        ...  # etc

But the problem is, all turrets of the same type have same range, same attack speed, same cost, etc. but they're still individual instances, so it would feel stupid to copy all of these common properties with the same values to every instance separately.

The reason I don't just subclass Tower to create my custom tower types is because they don't really add anything to the class. Literally the only thing they would do is change the default values for the __init__'s arguments. Using subclasses like this would also lead into the common values being copied into every instance, so if my subclass GreenTower defaulted range=800 for all my turrets, it would mean that the value 800 would be stored separately into every instance (when they could just use a common value). I just feel like there's a better way.

So my question is entirely about the design/architecture/structure of the system. How should I group these properties together while still allowing individual turrets to be upgraded separately? Ideally I could be able to design these tower types in something like JSON where I can dynamically load them to the game.

7

It sounds like a use-case for a Flyweight. Essentially pull all the shared details for your turrets as a single flyweight instance and each turret then refers back to that object. Note that you can have more than one flyweight so this doesn't limit you in any way. If you end up with close to one flyweight instance for each turret, then it's probably not worth it, though.

You can also decorate your turret instances so that you get most of the properties from the flyweight but modify specific ones as needed for specific turrets. You can even have flyweights that decorate other flyweights. It all depends on your needs.

  • This does seem good! I will have to study this too a little more, but it's very similar to what I had in mind (just couldn't wrap my head around it well). Thanks! – Markus Meskanen Dec 6 '17 at 18:02
  • To be clear, the value of using flyweight is not just saving space (which I'm not sure even is the case here) but that the behavior of a single flyweight controls all the objects that refer to it. For example, lets say the player can get a temporary defense bonus in the game. Instead of looping through all the turrets and updating their specific defense values, you modify the single flyweight instance. – JimmyJames Dec 6 '17 at 18:26
  • Yeah saving memory isn't the main concern here, so this pattern looks perfect. I also found Type Object Pattern, but it seems to be the same thing but with a different name. I will be using this, thank you very much! :) – Markus Meskanen Dec 6 '17 at 21:08
5

Why on Earth would it bother you that a range value is stored in every instance of a tower? Are you trying to save the extra 2 bytes per instance? If you're severely memory constrained in your game, that's a different matter, but that doesn't sound like the problem. And if at some point in the future you want to make towers that give temporary bonuses to some other towers, wouldn't you want for each one to have their own stats fields?

You do have a valid point that making a subclass just to set some default values seems ridiculous. And it is. That's what factories are for. Just make a class that knows about the various defaults, and ask it to make whatever flavour of tower you want at the time. It would go something like this (excuse the Java syntax):

public static Tower makeGreenTower() {
    Tower newTower = new Tower();
    newTower.color = Color.Green;
    newTower.range = 800;
    return newTower;
}

It's a bit difficult to tie in with a JSON configuration, but entirely possible. You'd need to pass some identifiers around to the various classes involved, but your factory would be the one that, when requested for a particular type of tower, would look at the actual values and construct the appropriate object. The signature would then be more like:

public static Tower createBasicTower(String towerType) { ... }

You'd want the factory to also handle permanent tower upgrades, as it will be the one to know what upgrades to what and how.

  • range was just an example, all of the properties (attack_speed, attack_damage, attack_splash_radius, attack_range, cost in my example, there could be more in reality) will be duplicated across all instances. It's not about me being afraid to save memory, it's about wasting memory for no reason when there's probably a better way. Your factory pattern seems nice, I'll study it a little more. How would you say it compares to @JimmyJames's suggested Flyweight pattern? – Markus Meskanen Dec 6 '17 at 18:01
  • 3
    @MarkusMeskanen They solve different problems and can be used together or separately. I strongly feel that you are trying to optimize way too early. Will you have enough towers, critters and fields to be in the same memory footprint league as just loading game libraries? Will saving the extra bytes of memory make up for the extra CPU cycles needed to retrieve the value? Or the extra few hours of engineering per class? Premature optimization is the root of all evil, after all, see softwareengineering.stackexchange.com/questions/80084 and softwareengineering.stackexchange.com/questions/33020 – Ordous Dec 6 '17 at 18:14
  • @MarkusMeskanen Here's another thing to consider - your app will need to draw on screen. In fact, it will need to double-buffer the image, so you'll constantly have 2 copies of the screen in memory (unless you use hardware acceleration, in which case it can be in video memory, not RAM). If it's running on a 4K display (modern phones sometimes have those), that's over 8 million pixels. Each one is a byte to hold the colour, and you double it up. So simply drawing the thing smoothly at high res will take 16MB of RAM. I doubt that your entire "unoptimized" game state can take half as much. – Ordous Dec 6 '17 at 18:26
  • Again, the reason I asked this question is because I feel like it's bad design to have the values copied everywhere for no real reason at all, memory optimisation just happens to be a bonus along the design. I found Type Object pattern, which seems to be the same as Flyweight Pattern, but this article explains why it's not just for saving memory: gameprogrammingpatterns.com/type-object.html – Markus Meskanen Dec 6 '17 at 21:10
  • @MarkusMeskanen The factories are like type objects – Caleth Dec 7 '17 at 10:07
2

In your case I would just use this Tower base class and subclass it with all the towers at every single level possible that you can instantiate as startup, like BlueTowerLv1, BlueTowerLv2, GreenTowerLv1, etc, with a reference from BlueTowerLv1 to BlueTowerLv2 so that it knows what tower to use when leveling up (a null/nil for next_tower could indicate that the tower has reached a maximum level).

However, these towers are "meta towers". They don't get instantiated for every single tower instance the user creates. Instead they get pointed to (referenced) by all the towers the user creates. It is basically the flyweight pattern Jimmy describes but with a bit more detail specific to the nature of TDs.

That's in a case where the scope is small enough that you can just hard-code all the tower types and their properties at every level possible, and that there aren't an infinite number of levels possible. If you want to use a formula to generate the properties of each type of tower at every level, you can just generate the tower instances at startup based on that at which point you don't have to bother to create a unique identifier for, say, BlueTowerLv3 (it might just be stored in a list).

Here's a basic diagram illustrating the idea.

enter image description here

This is assuming a TD where the user can't do so much to make a single instance they create of a tower on the game map unique besides choose what type of tower they want to create, position it, and upgrade it. That implies very little unique data per tower the user creates, so you can just have the towers referencing read-only data (MetaTowers) you create in advance when the game starts up. You might have some other unique data like the experience points a tower accumulates before they're allowed to be upgraded if you have such a concept in your game. If so, you'd put that data in Tower, not the MetaTowers, but your MetaTowers might store how much experience points are required to upgrade.

The reason to have like MetaBlueTowerLv1 inherit from MetaTower is mainly just for polymorphism and to guarantee that all meta-towers (and the towers referencing them using composition) provide all the fields/functionality the game expects. If you have no such need whatsoever, then you can simply instantiate MetaTower instead of inheriting it.

Anyway, you avoid the issue of redundancy by separating the idea of read-only metadata from data which the user does modify. The metadata doesn't have to be read-only. It could just be referenced by all towers of the same type.

For example, you might offer the player an expensive upgrade that makes all blue towers more powerful, not a single blue tower. In that case, you can modify the blue tower meta data which all tower instances reference without looping through every single blue tower instance the user has created and having to add more code every time the user creates another blue tower, and if you ever want to add such a feature in hindsight, the flyweight pattern is especially useful. I couldn't think of a better name than "meta" in this case for the shared flyweight data, but hopefully you get the idea.

As pointed out, there isn't a very strong reason to use this design from an efficiency/memory standpoint. A TD is typically not going to instantiate millions of towers in a game session for the user to create and manage individually. However, avoiding the redundancy here by using flyweights can give you some more breathing room with the design and make it a little easier to reason about the game logic.

  • 2
    Why is Meta Blue Tower Level N a subclass, rather than an instance of MetaTower? – Caleth Dec 7 '17 at 10:01
  • You're right, it could be! I just imagined there might be some case where there's a temptation to override functionality in a subclass, but it's probably overkill. If it's always going to be just raw data/properties, I think just simply instantiating is better. – user204677 Dec 7 '17 at 10:03
  • I ended up updating the answer to suggest instantiating instead when there's no foreseen need for overriding. This is a case where my own bias leaked into the answer a bit since I made a TD ages ago where the tower "types" did have some overridden functionality here and there that wasn't so effective to model by just adding more data. – user204677 Dec 7 '17 at 10:04
  • 1
    This is exactly what I'm looking for, but to me it just seems like the normal flyweight pattern, so I already had pretty much this :) Thanks for your detailed answer anyways, it sure helped me deepen my knowledge. – Markus Meskanen Dec 7 '17 at 10:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.