📜 ⬆️ ⬇️

Introduction to the component-oriented approach to programming

The Unity Engine itself (hereinafter Unity), like many other game engines, is most suited to component-oriented programming (hereinafter referred to as the 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, the component is the basic unit for the implementation of business logic in Unity. In this article I will talk about how to apply the CPC in Unity.

In general, CPC can be viewed as the development of OOP principles with the elimination of a problem area known as the fragile base class. In turn, the development of CPC can be considered Service-oriented programming. But back to the topic. It should be immediately noted that KOP-KOP, but never forget the principles of GRASP, SOLID and others.

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

Now pay attention to GRASP. Programming should be at the level of abstractions, not specific implementations (Low Coupling). And even seemingly simple gameplay requires the creation of UML interface diagrams. What for? Is it not excessive, the principle of “YAGNI” (You Ain't Gonna Need It) does not give rest. Not superfluous when justified. Who comes up with the game? Game designers, these insidious people of civilian appearance. And it does not happen that they do not change anything. Because you need to be ready for change, but more often you need to be ready to expand the game logic. Moreover, all these changes will not be made immediately, but then, when the developers themselves already forget why it was done this way and not otherwise. Therefore, UML diagrams, even abstractions, should always be done: this is the project documentation. I will develop the UML diagrams in Visual Studio in order to later generate C # code from them.
So, let's start developing the game; for example, create core gameplay of the tower defense game. There are different approaches to the development steps, I will use the abbreviated version.
')

First step: problem statement


No matter how many multipage documentation is created for the game, you should always be able to select the main thing. A rough description of the gameplay, of course, is better divided into use case:
The player must not allow enemies to destroy the House. For this, he must place the towers that will destroy the enemies that fell within the radius of action. 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 are moving from the point of appearance along the road to the House. Going to the house begin to destroy it. When the house is destroyed - the game is over.

Second step: problem analysis


Let's start with the study of the main thing: towers, creeps and houses. So, having created a scene with already placed game objects (towers and a house), we should get the result: creeps appear, move to the house, dying on the way from the damage of the towers, and reaching the house, cause it damage. At death, the creep disappears from the scene. With the destruction of the house or the installation of all creeps - the game is over.

Select the main game entities, we indicate their properties and behavior:

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

The third step: decomposition


I believe that the correct decomposition of tasks is very important. Most of the time it is worth spending on the elaboration of abstractions and the logical connection between them.

Let's start with the generalization of behavior, to identify what should be the components.

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



Stand still, 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 component properties in an intuitive way. Therefore, all properties will only get to display information in the UI, and the set will not be needed. In addition, Unity will not be able to display properties in the inspector with the specified get / set construct, and you will need 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 launch of this behavior and stop it, indicate the target, change the target. But who?

2) Since the logic of target selection will be different for different types of towers, and not only the tower, but the creep will use this behavior, it is necessary to distinguish two logical components.

The first will work with physics, as soon as a creep enters the collider trigger, it adds it to the target queue, creating an event that the targets are said to have increased. As soon as the creep leaves the affected area, remove this target from the queue, create an event that deletes the creep from the queue.

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



The second logical component will react to these events and use the IDamageDealer itself to inflict damage on the selected target and stop the infliction of damage.



But how then can this second component choose a target differently, depending on the type of tower and, if at all, a creep? A simple option is a certain universal method SelectTarget (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. Here, Interface Segregation Principle is recalled: several specific are better than one universal one. Therefore, there will be different components for different behavior of target selection, united by one ITargetSelector interface.



KeepSelector: selects a home as a 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 of the mechanics of target selection (the basic mechanics, since only the towers differ from it). However, there is another option to do with inheritance. There will be a basic TargetSelector component with default logic for the tower. And the KeepSelector and WeakCreepSelector classes will override the method of adding to the list of targets (to check the house or creep) and the method of selecting a target.

3) The game has two entities that have the following behavior: when taking damage, the number of hit points decreases until it dies.



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

In order to easily extend the method of applying damage in the future, let's give this behavior to another auxiliary component:



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

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

5) Now we need to identify the component that will handle the logic of loss and winnings: 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 check how many creeps are dead from those that were spawned, and, taking into account the number of waves and the current wave, spun or say that the player won. Also check whether the house is destroyed: if so, the player has lost.

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 that are configured both visually (towers of different types, creeps, houses) 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. For this purpose, Unity has tags. Some are frustrated that only one game object can have one tag, but this is normal: do not make universal objects.

We start the implementation with the generation of UML code diagrams, getting a dummy skeleton. Then create an 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: https://github.com/sountex/COPTD

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

Next step: testing


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

Source: https://habr.com/ru/post/243479/


All Articles