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.
Every unit and building type gets its own class. There's an extremely wide and shallow hierarchy, with all of them just inheriting from
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.
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.
- 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.
- 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 my
HandleClickcode 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 a
DefenseBuildinglike a Castle or a Tower. Are not Castles and Towers similar, yet a Castle is a sort of
ResearchBuildingand so on. I think I could end up with similar amounts of code duplication if I took this route.
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.
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.
- 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:
- I can cross categories and defy any specific sorting of buildings into "production" or "military". Everything can be everything at once.
- 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.
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.
- Everything that 2a has
- 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.
Just have a
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.