TL; DR: How do I give unit and building types a unique "identity" without turning my codebase into a mess?
I've been reading the genius Game Programing Patterns and it's made me very (painfully) aware of bad code design. I'm currently building a somewhat unconventional RTS that still abides by the basic conventions of the genre (i.e lots of different types of units and buildings). I want selectable objects to be able to intelligently know the nature of other selectable objects. For instance, if I were to click on a trading wagon and then click some allied selling destination, it would intelligently know to begin moving goods and selling them there, whereas if I clicked it and then a repair shop, it would know to go and get repaired.
My question is how to give every independent building and unit type its badge of "type-ness" that can be read by other units.
Class Inheritance?
Solution 1a:
Every unit and building type gets its own class. There's an extremely wide and shallow hierarchy, with all of them just inheriting from Building
or Unit
which inherits from SelectableObject
. The issue that makes this a non-starter for me is the amount of code duplication. I don't want to give every individual combat unit the same combat code.
Solution 1b:
The objects are divided into very specific groups and subgroups. WagonryBuilding
inherits from ProductionBuilding
which inherits from EconomyBuilding
which inherits from Building
. I get an extremely dense and vertical hierarchy that one can traverse as far as they need to understand what sort of object they're dealing with.
Pros:
- Everything is neatly categorized
- I can serve UI views based on what general type of building/unit I'm dealing with
- Object interactions can deal with most abstract type needed, allowing for some powerful polymorphism/dynamic dispatch.
Cons:
- The only way I can think of enabling a unit to divine exactly what I'm clicking on is a big if-else tree using
typeof()
. I suppose I could maintain an enum of all the object types to upgrade to a switch statement, but it sounds like myHandleClick
code could get extremely messy. - The nature of the game is such that some units and buildings defy easy categorization. Take Age of Empires for instance. You may say the Town Center is an
EconomyBuilding
, but it shoots arrows at nearby enemies! That sounds like aDefenseBuilding
like a Castle or a Tower. Are not Castles and Towers similar, yet a Castle is a sort ofProductionBuilding
andResearchBuilding
and so on. I think I could end up with similar amounts of code duplication if I took this route.
Interfaces?
Solution 2a:
Instead of trying to describe an identity when interacting between units, I describe a behavior. I go back to the wide and flat hierarchy, but this time I define an interface for every unit behavior. ISelectable
, ICombat
, IMovement
, IInventory
, etc. So then units can inspect each other for certain behaviors and prioritize some over the other. For example, a trading unit would look for an IInventory
where they could deposit their goods. Secondarily, they might engage in some other behavior like garrisoning, transporting, etc, all depending on which interfaces they find on the target.
Pros:
- Far fewer types to check for. I can only think of a dozen or so behaviors to turn into interfaces
- Abstraction away from the unit/building divide so I can treat a unitInventory and buildingInventory the same way:
IInventory
. - I can cross categories and defy any specific sorting of buildings into "production" or "military". Everything can be everything at once.
Cons:
- Still need big control flow trees. I'm trying to figure out some way to do a dynamic dispatch of an
InteractionHandler()
method or something but I can't see how to avoid the if-else pyramid of doom. - Interface implementation seems somewhat more complex and expensive to check for than typeof() (at least in C#).
- LOTS of code duplication unless I use default implementations with the interfaces, but I'm not sure if I can call
base.method()
if I want to extend on it.
Solution 2b
I use the Unity Component system to get a similar outcome. Instead of interfaces, I load GameObjects with different MonoBehaviors
that represent different abilities/attributes.
Pros:
- Everything that 2a has
Cons:
- No extensibility beyond the default implementation of a behavior. Lots of
GetComponent<>()
calls that feel unsafe and expensive. - Navigating the Component structure can make doing simple stuff very convoluted if I want to store any actual functionality within the different components.
Prefabs?
Solution 3:
Just have a Unit
and Building
class. Configure them using public fields to shape unique instances of the Unit with different sprites, abilities, etc. This one also feels like a non-starter because every unit has to carry all of the code and data for every other type of unit with it, which is extremely non-optimal.
Is one of these solutions a clear winner? I'm leaning toward 2a but I don't want to pull the trigger and start coding it all up if it's the wrong way. What have RTSes historically used? Is there something I haven't even mentioned that is a better way to think about this? Thank you for reading my novel.