OOP: Inheritance or Components for an Item System

Started by
7 comments, last by frob 4 years, 5 months ago

My question is about OOP, and best practice writing in C# for Unity.

Say I'm writing components for an item system.

 

Consumables have quantity.

Weapons have durability, damage, block and stats.

Armour has durability, armour and stats.

Shield has durability, block, armour and stats.

 

In this situation, should I...

  1. write a Consumable component, Weapon component, Shield component and Armour component that each extend an Item class?
  2. write a Quantity component, Durability component, Damage component, Block component, Armour component and Stats component, then add as required?
  3. write an Item component with all the possible fields plus an enum that decides which ones go unused?

I like 1) because it defines the items by what they do, but there's repetition of fields horizontally in the inheritance tree.

I like 2) because there's no repetition, but it allows for the creation of items that don't make sense. There's no strict definition going on, just the existence or non-existence of components, and some might need to be mutually exclusive.

I like 3) because it's the solution to the problem with 1), and the enum is kind of a solution to the problem with 2), but it seems wasteful to deal with a larger data structure than required for every item.

Any thoughts?

Advertisement

A fourth option would be to just maintain a list of properties which you can query at runtime. This is often more versatile than hardcoding all variations into your class system. It allows you for example to invent new properties and rules on-the-go or make new combinations in a level editor. There's many possibilities that you can't have with hardcoded classes.

Not saying this is what you should use here, but consider the concept behind it.

 

I don't think there can be a single best practice here. I've seen all three of your approaches in practice. You've correctly identified the tradeoffs involved in your choices, so the next thing to do is weigh up how much those tradeoffs apply to your game. For example, if there are only a handful of fields that are not needed for every item (e.g. durability not needed on consumables) then you are not wasting much, and that might be a good approach.

Thanks for your input and advice.

I find myself procrastinating a lot on how to best approach problems like this. I'd actually started doing 1) but it got more messy than I'd like it to be.

@Prototype's fourth option sounds the same as 2), unless I'm misunderstanding something?

I think I was expecting to be told to pursue 2), but I might try 3) as suggested by @Kylotan.

If you google this sort of problem, it returns a lot of articles saying you should try to favour composition over inheritance. Unity also seems to encourage an 'Entity-Component' system. I think maybe I have a tendency to overuse inheritance. I guess I'll try the other approaches and see how it goes.

In general it is often best to prefer composition, but sometimes you find that an object doesn't break down into clean parts - such as your example where Armour and Shields both have 'armour' values but only one of them has a 'block' value. You could end up decomposing the object into smaller and smaller bits, and then you potentially end up with microcomponents where each one is an expensive wrapper around 1 or 2 variables - and that's not worthwhile.

I would probably just try to make one generic Item object. If I spot parts of the data that are only relevant for certain subtypes, I can always factor that out later if absolutely necessary, and it doesn't even need to be a separate MonoBehaviour or anything like that.

@Kylotan I'm going to follow your advice.

The "expensive wrapper around 1 or 2 variables" is exactly how some sources will suggest you work - for example I've seen articles which say you should have a "Health" component, consisting of a max and current health value, for everything with a health pool. I thought it was kind of interesting.

Keep in mind, that the inventory (wich is a part of the item system) can be handled in multuple ways as well. On the one hand, you could store

  • a list of Items (and consumables would store their quantity)
  • a list of Item quantity pairs
  • a list of item-identifier (numerical id, string, combination of type and id, ...) quantity pairs

And here again, there are certain benefits and drawbacks for all of them. The last one e. g. requires additional lookups, but it's independent on how items are handled, if they have a shared baseclass/interface etc.

Inheritance says "I am one of these", and the objects must be completely interchangeable. Inheritance is great for systems where you don't care about implementation details, only the interface, and you can program against an interface. Both of those things --- the ability to substitute objects and the ability to program against an interface instead of a concrete type -- are part of the SOLID principles.

Are they truly substitutable? In some designs you can attach as many components you want since components themselves all follow a shared interface, but often that's as far as it goes.  While a wooden shield, a metal shield, and a riot shield may all be designed to be substitutable, it is harder to imagine shields being substituted with an assault rifle or a sword but that doesn't mean people haven't build designs that do exactly that.

Personally I see composition in your description, grouping together a bunch of properties in a container.

An item may have a property that says where the mounts or slots or attaches, a property that says if it stacks and if so, how many are allowed and the size of the current stack, a property for durability, a property for damage, a property for what actions it supports, and so on.

Many of those properties might be made out of interchangeable objects that inherit from a common interface if you choose to implement them that way, so those properties might be able to live inside an inheritance hierarchy if you can follow the dependency inversion principle.

This topic is closed to new replies.

Advertisement