2
\$\begingroup\$

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 my HandleClick 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 a DefenseBuilding like a Castle or a Tower. Are not Castles and Towers similar, yet a Castle is a sort of ProductionBuilding and ResearchBuilding 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.

\$\endgroup\$
3
  • \$\begingroup\$ Isn't unity designed around the component pattern (solution 2b) and that's what folks use generally? \$\endgroup\$
    – Vaillancourt
    Commented Oct 2, 2021 at 3:43
  • 1
    \$\begingroup\$ "feel unsafe and expensive" Why unsafe? How do you know they are expensive? \$\endgroup\$
    – Almo
    Commented Oct 2, 2021 at 7:46
  • \$\begingroup\$ I know they aren’t that expensive, but since a lot of functionality would be contained in them my code on the actual high level object (e.g. the literal WagonryBuilding) would need to call it often to use it or else load references to every component into memory which are more expensive than not having to at all. It’s not much but it adds up with thousands of units/buildings on the screen. In terms of OOP what I’m most used to, it feels unsafe because it could just return a null since no component is guaranteed to be on any one GameObject \$\endgroup\$
    – Sam Bishop
    Commented Oct 2, 2021 at 14:56

2 Answers 2

3
\$\begingroup\$

For the type of problem you're describing, I'd recommend shifting your focus away from "identity" (is-a) toward "affordance" (has-a). This is part of the classic principle of composition over inheritance, with which Unity's component-based structure tends to work well (and it has a great write-up in the Game Programming Patterns book too).

Here, my trading wagon doesn't need to know whether the thing I clicked on was a "Market Building" or a "Town Center" or a "Storehouse" etc. It just needs to know that it offers the "Goods Receiver" affordance - it has the capability to receive goods. We could express this as a GoodsReceiver component, which we could attach to multiple different kinds of buildings, or even units (like a nomadic trader unit).

This way I can change the particular set of buildings and units that offer this affordance dozens of times during development, without needing to re-wire this trade wagon behaviour every time. As long as I attach the component my trade wagon looks for, it will know that this target is valid for processing a trade order.

Note that this also neatly solves the Town Center problem you describe, where multiple "kinds" of entity intersect. Instead of needing multiple inheritance from each of these distinct "kind" classes, we can compose all of the affordances of ranged attack, unit production, research, and goods receiving as separate components living side-by-side on one parent object

Similarly, each unit or building can expose a set of components that give affordances for the kinds of orders that entity can act on. Let's make an IOrderReceiver interface for that:

public interface IOrderReceiver {

    // Handle orders issued by clicking a point on the map.
    bool TryProcessOrder(Vector3 target);

    // Handle orders issued by clicking on a unit or building.
    bool TryProcessOrder(GameObject target, AllianceState relationship);
}

Now when we click to issue an order, our interface system can iterate over the order-receiving affordances on the selected entity and execute the first one that matches. By stacking our components in priority order, we can decide which orders we want to take precedence over which, independently for each unit/building we might issue orders to. So for instance, by putting our movement component(s) at the bottom of the list, we can the ensure "move to / follow" order responses it exposes will act as a fallback behaviour if no more specialized behaviours match from the components higher-up on the object.

This gives us a fully generic way to issue orders, without the if-else pyramid of doom you describe. And the unit/building designers retain full control to adjust the order selection rules in a data-driven fashion, rather than hard-coding it.

Such an order-issuing routine could look like this:

List<IOrderReceiver> _receivers = new List<IOrderReceiver>();

void IssueOrder(GameObject clickedTarget) {
    AllianceState relationship = AllianceState.Neutral;
    if (clickedTarget.TryGetComponent(out Allegiance allegiance)) {
        relationship = allegiance.GetRelationshipTo(localPlayer);
    }

    selectedEntity.GetComponentsInChildren(_receivers);
    foreach (var receiver in _receivers) {
        if (receiver.TryProcessOrder(clickedTarget, relationship))
            break;
    }
}

Note here that we can use the TryGetComponent method to address the safety concern you raised - it neatly handles checking whether the component is present at the same time we try to fetch it, kind of like Dictionary.TryGetValue. It also does so without creating any garbage in editor mode, unlike GetComponent.

Now our trade wagon can have a component a bit like this on it:

public class GoodsTransport : MonoBehaviour, IOrderReceiver {

    [SerializeField]
    GoodsType _goodsType;

    // Goods transports don't afford any specific behaviour
    // when directed to a position.
    public bool TryProcessOrder(Vector3 position) {
        return false;
    }
    
    // Goods transports have no special behaviour toward enemies,
    // or GoodReceivers that do not process their own carried goods.
    public bool TryProcessOrder(GameObject target, AllianceState relationship) {
        if (relationship == AllianceState.enemy
            || !target.TryGetComponent(out GoodsReceiver receiver)
            || !receiver.CanReceive(_goodsType))
            return false;

        SetTradeRoute(receiver);
        return true;
    }

    public void SetTradeRoute(GoodsReceiver receiver) {
        // TODO: talk to movement component to execute delivery routine.
    }
}

The component lookup and iteration does have a cost, but for the case of responding to a user's click (ie. work that you'll do at most once per frame) it's quite reasonable, and won't create a performance bottleneck.

For interactions that can trigger more frequently - like say units on standby/patrol reacting to enemies who enter their line of sight - you can use things like physics layers to pre-filter the items to those that will matter.

\$\endgroup\$
2
\$\begingroup\$

I still use interfaces, inheritance and all the rest traditional OOP stuff but far less often. What I gradually began to value most is fast to create and modify while not being too "fragile". Without a big team all the rest is secondary. (honest translator: I gave up on being amazing so I can at least be effective)

I came to an approach that works surprisingly well for me. I began to make extremely generalized behaviours of 2 types - event based or pure behaviours. Event ones are like OnCollision, OnButton, OnRepeat, OnDelay, etc. They react to an event and then trigger UnityEvent. Behaviour ones (say Spawn, AssignMovement, Destroy, Damageable, etc) usually have one or more public methods as input and UnityEvent as output. UnityEvents pass mostly the same types like GameObject, float, Vector3, Collider, that way I can wire components in any combination.

This allows me to create all kinds of game entities with the same set of components hard wired mostly through the inspector (then saved as prefabs). And because all game entities use the same components there is rarely a need for interfaces to achieve the non-hard wired/dynamic comunication between them.

To put it from a different angle: instead of insisting on OOP way went to do things Unity way. Prefab variants as inheritance, component composition as polymorphism, UnityEvents as hard wiring interfaces, event components as dynamic wiring interfaces. Of course this doesn't replace the use of OOP it just adds more options that often work well.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .