I'm currently at the start of a project, and I'm following the wisdoms of my day-to-day career as a C# developer for large web applications.
Currently I have set up a bunch of interfaces. Notable of these are IActor
, IHuman
, ISelectable
, IUIAction
& IHealth
.
I also have a Person
class. Person
implements IHuman
(which implements IActor
), ISelectable
and IHealth
and also inherits from MonoBehaviour
.
ISelectable
has a property of IEnumerable
. Actions have a property of Action click
. Im sure you can see where I am going with this...
The intended goal is that if a class inheriting from MonoBehaviour
(person
in this case) implements ISelectable
and is attached to a GameObject
that is clicked, a list of buttons representing ISelectable.Actions
is displayed on the screen. If a class implements IHealth
, a healthbar will be displayed on the screen etc etc...
Now, the problem I see is I'm going to end up with huge classes. The classes are going to need properties for their health, theyre going to need properties for their actions, theyre going to need properties for what the UI panels title should be when selected, and anything else that comes up as development progresses... My other thought is to remove the interfaces like IHealth
from the class itself and have them be their own class, such that IHealth
will only mean a class has a property that is of the class Health
and Health
has all the properties relating to an objects health levels... but this removes authority from the class as to what control it has over its health.
Whats the industry standard for this sort of thing, and how can I implement it effectively in my game?
Answer
Unity emphasizes a philosophy called Composition Over Inheritance.
The idea is that rather than thinking of what a particular entity "is":
A soldier is a unit which is selectable and which is damageable.
You think of the set of behaviours/features it "has":
A soldier has a selection behaviour and a taking-damage behaviour
So then, as you suggest in the question, you can break these responsibilities off into their own MonoBehaviour
components:
Selectable (watch out for naming conflicts with
UnityEngine.UI.Selectable
) can respond to click events and hold information needed to draw a selection highlight.Damageable can track the current health, resistances, and handle firing off low-health state change events or destruction animations.
etc.
Your "soldier" might not exist as a class anywhere in code - it's just a particular combination of components and parameter values that gives the set of behaviours you want, ie. an entity with both Selectable and Damageable components attached (among others).
This move from defining each entity in code to doing it in data gives us a number of outcomes that are useful in game development:
Non-programmers on the team can take a more active role in developing game behaviour. If your coders provide a good set of building-block components, the level designers can go to town experimenting & combining them in new ways, without waiting on a coder to write a class that glues-together all the bits they need in a new standalone class.
Occasionally you'll get new behaviour "for free" out of this combinatoric play. For example, take an enemy and remove the Damageable component - boom, now you have invulnerable enemies, without a coder creating a special mode or flag. ;)
We can create and modify these compound entities on the fly while the game is running, without recompiling code or relying on edit-and-continue functionality.
This style helps encourage small modular components, each easy to understand and modify on their own, rather than massive sprawling classes with many responsibilities as you describe - a common pitfall in game development, with player classes especially!
A more modular, data-driven architecture tends to have fewer bottlenecks for exclusive checkouts & merging when multiple developers are collaborating on a cluster of features.
This pattern can also make it easier to develop tools like level editors or support user generated content, since all the game entities are just data files that can be modified without code changes.
That's not to say you can't or shouldn't use interfaces - they're still a great tool to have in your toolbox, especially when you have some kind of well-defined need in your game and multiple ways you might want to serve that need.
For example, maybe your base AI movement logic knows it wants to move into firing range for whatever weapon it's using - but that weapon might vary from one unit archetype to another. You could introduce an IAttackBehaviour
interface exposing methods to select a target, locate a good attack position for the movement behaviour to reach, and to execute an attack. Then you could implement this in concrete types likeMeleeAttackBehaviour
, SniperAttackBehaviour
, GrenadierAttackBehaviour
, etc. Your movement behaviour just needs to know it "has" an IAttackBehaviour attached, but doesn't need to implement these details itself for every archetype flavour. This allows AI designers to mix & match components as they need, while maintaining a clear contract for how the movement & attack behaviours communicate.
No comments:
Post a Comment