Introduction to Component-Oriented Programming Approach

The Unity Engine itself (hereinafter Unity), like many other game engines, is most adapted to component-oriented programming (hereinafter referred to as CPC), since the Behavioral Pattern is one of the basic patterns of the engine architecture, along with the “Component” pattern from the Decoupling Patterns classification. Therefore, it is the component that is the basic unit for implementing business logic in Unity. In this article I will talk about how to use CPC in Unity.

In general, CPC can be seen as the development of the principles of OOP with the elimination of a problem place known as a fragile base class. In turn, the development of CPC can be considered Service-Oriented Programming. But back to the topic. It should be noted right away that KOP-KOPom, but you should never forget about the principles of GRASP, SOLID and others.

We will call the component a class inherited from MonoBehaviour. Here, the name of the base class itself is very well chosen, it makes it clear that this behavior, and says that it is executed by the .mono platform. But it’s very convenient to think that MONO is one, * one * behavior. And the first thing you need to pay attention to when developing components: one component - one behavior. The sole responsibility principle from SOLID and the high cohesion of methods within the class by GRASP in action.

Now let's pay attention to GRASP. Programming should be at the level of abstractions, and not specific implementations (Low Coupling). And even a seemingly simple gameplay requires the creation of UML interface diagrams. What for? Is it not unnecessary, the principle of “YAGNI” (You Ain't Gonna Need It) is haunting. Not redundant when justified. Who comes up with the game? Game designers, these insidious people of civilian appearance. And it doesn’t happen that they don’t change anything. Therefore, you must be prepared for changes, but more often you need to be prepared to expand the game logic. Moreover, all these changes will not be made immediately, but then, when the developers themselves forget, why this is done, and not otherwise. Therefore, UML diagrams, at least abstractions, should always be done: this is the documentation for the project. UML diagrams I will develop in Visual Studio,
So, let's start developing the game; for example, create a core gameplay of the Tower Defense genre. There are different approaches to the development steps, I will use a shortened version.

First step: statement of the problem

No matter how many-page documentation was created for the game, you should always be able to highlight the main thing. An approximate description of the gameplay, of course, is better divided into use case:
The player must prevent the enemies from destroying the House. To do this, he must set up towers that will destroy enemies in range. At the same time, one tower can attack only one enemy. When he dies or goes out of range of the tower, the next available one is selected. Enemies appear in waves, with each wave their number increases. Enemies move from the point of appearance along the road to the House. Approaching the house they begin to destroy it. When the house is destroyed - the game is over.

Second step: task analysis

Let's start with the study of the main thing: towers, creeps and houses. So, creating a scene with already placed game objects (towers and a house), we should get the result: creeps appear, move to the house, dying along the path from the damage of the towers, and when they reach the house, they cause damage. At death, the creep disappears from the scene. When the house is destroyed or all creeps are destroyed, the game is over.

We single out the main game entities, indicate their properties and behavior:

1) Tower. Properties: damage, cooldown between shots, target selection radius, target selection logic.
Behavior: target selection and target attack.
Also, for a variety of gameplay, there will be different types of towers: in terms of damage, radius of destruction and the logic of target selection.
2) Creep. Properties: HP, damage, cooldown between attacks, movement speed.
Behavior: Moving along the route to the House. Attack at home when approaching it closely. When HP = 0, it is considered killed.
Different types of creeps - for example, by HP.
3) House. Properties: HP.
Behavior: When HP = 0, the player loses.

Third step: decomposition

I believe that the proper decomposition of tasks is very important. Most of the time should be spent on the study of abstractions and the logical connection between them.

We start by summarizing the behavior to identify which components should be.

1) There are two entities in the game that have damage behavior: tower and creep. We isolate this behavior into a component, here is its interface:

Wait, wait, someone will say: “What about encapsulation? It turns out that anyone can change the cooldown and damage? ”No, only the game designer, setting up the properties of the component in an intuitive way. Therefore, all properties will only have get to display information in the UI, and set is not needed. In addition, Unity will not be able to display properties with the specified get / set construct in the inspector, and it will be necessary to create fields marked with [SerializeField]. And rightly so, other classes from the code will not be able to change the value of the property.

Let's go back to the component. Someone will trigger the start of this behavior and stop it, indicate the goal, change the goal. But who?

2) Since the logic of target selection will be different for different types of towers, as well as not only the tower, but also the creep will use this behavior, two logical components must be distinguished.

The first will work with physics, as soon as creep enters the collider trigger, it adds it to the target queue, creating an event that the target supposedly has increased. As soon as the creep leaves the affected area - remove this target from the queue, create an event to remove the creep from the queue.

Of course, this behavior is excessive for the house: the house will never run away. Anyway. Call this interface the future component of ITrigger. With two Unity methods: OnTriggerEnter / OnTriggerExit.

Secondthe logical component will respond to these events and use IDamageDealer itself to damage the selected target and stop the damage.

But how then to make the second component choose the target differently, depending on the type of tower and if it is a creep at all? A simple option is some universal SelectTarget method (type of target selection logic (tower type, creep)), depending on the type of target selection logic, select it. But versatility is not always good, especially when it comes to components. It recalls the Interface Segregation Principle: it’s better to have a few concrete ones than one universal one. Therefore, there will be different components for different target selection behavior, united by one ITargetSelector interface.

KeepSelector: selects a house as the target.
SimpleCreepSelector: selects the first target in the list.
WeakCreepSelector: selects the weakest target to add it.

Thus, it is easy to expand the core-gameplay mechanics of target selection (the mechanics are basic, since the towers differ only in it). However, you can do another option, with inheritance. There will be a basic component TargetSelector with default logic for the tower. And the KeepSelector class and WeakCreepSelector will override the method of adding to the list of targets (to check if this is a house or creep) and the method of choosing a target.

3) There are two entities in the game with the following behavior: when taking damage, the number of hit points decreases until he dies.

Why is IsDead necessary when you can just check the condition HP <= 0? Even in the current implementation, in two places it is necessary to check the condition if the creep has died. Therefore, following the simple principle of DRY (Do not repeat yourself), we will not duplicate the logic. So it will be easier to change the logic if any more conditions are added.

In order to easily expand the method of applying damage in the future, we will give this behavior to another auxiliary component:

So we can combine two components in the future, IHittable and IDamageDealer, without inheriting and overriding the method of applying damage. It is also easy to extend the behavior by assigning other components that implement IDamageDealer.

4) One entity has a move behavior: IRouteFollower. Properties: WayPoints [], Speed;

5) Now we need to identify the component that will handle the logic of losing and winning: check IsDead at home and all creeps, taking into account the wave number.

In the implementation logic, at Start we will call the crypt spawn method, in Update we will check how many creeps are dead from those that were spawned, and, taking into account the number of waves and the current wave, spawn or say that the player won. Also check if the house is destroyed: if so, the player has lost.

The fourth step: implementation

Now you can make the implementation of the first version of the game. However, someone will ask: “Where are the Tower and Creep classes themselves?”
In Unity, we will create prefabs configured visually (towers of different types, creeps, house), and logically - that is, with added components. Also, the properties of the components must be configured, so there is no need to create separate classes. But we need to understand (for the reaction in OnTriggerEnter / OnTriggerExit) what is creep and what is home. Unity has tags for this purpose. It is frustrating for some that only one game object can have only one tag, but this is normal: you should not make universal objects.

Let's start the implementation by generating the UML diagram code, getting a dummy skeleton. Then we will create the implementation of the behavior of the components. Do not forget that it is better to use abstractions rather than specific implementations, although this will increase connectivity. An example of my implementation can be found here:

So done. And how now to expand the gameplay - for example, adding an economy? Now you need to get money for killing the crypt, and spend it on building towers. Do not rush to add new properties to a class that implements one of the creep behaviors. Recall the principle of class sole responsibility and isolate this new behavior into a new component. By creating a new component whose behavior will include storing data on the amount of money earned for killing, we will make a better solution suitable for reuse. A bit about reuse. For example, in RTS - there are also units there that inflict damage and take it, buildings. Thanks to the component-oriented approach and abstraction, all created components are easy to use in games of a different genre.

Next step: testing

To test the behavior of a game object as a set of components, it is most convenient to use BDD (Behavior-driven development). And for testing the components of Unit Test separately. But this is a separate issue.

Also popular now: