Inheritance, composition, aggregation
It often happens that deciding to deal with some new topic, concept, programming tool, I read one after another articles on various sites on the Internet. And, if the topic is complex, then these articles may not take me a step closer to understanding. And suddenly there is an article that instantly gives insight and all the puzzles are put together. It is difficult to determine what distinguishes such an article from others. Correctly chosen words, optimal presentation logic, or just a more relevant example. I do not pretend that my article will turn out to be a new word in C # or the best educational article. But, perhaps for someone it will become one that will allow you to understand, remember and begin to correctly apply the concepts that will be discussed.
In object-oriented programming languages, there are three ways to organize interaction between classes. Inheritance is when an inheriting class has all the fields and methods of the parent class, and, as a rule, adds some new functionality or / and fields. Inheritance is described by the word “is”. A passenger car is a car. It is only natural if he will be his heir.
Association is when one class includes another class as one of the fields. The association is described by the word "has." The car has an engine. It is only natural that he will not be the heir to the engine (although such an architecture is also possible in some situations).
Two particular cases of association are distinguished: composition and aggregation.
Composition is when the engine does not exist separately from the car. It is created when the car is created and is completely controlled by the car. In a typical example, an engine instance will be created in the car designer.
Aggregation is when an engine instance is created somewhere else in the code, and passed to the car designer as a parameter.
Although there is debate about the benefits of a particular way of organizing interaction between classes, there is no abstract rule. The developer chooses one way or another based on elementary logic (“is” or “has”), but also takes into account the possibilities and limitations that these methods give and impose. In order to see these features and limitations, I tried to write an example. Simple enough so that the code remains compact, but also sufficiently developed so that all three methods can be applied within one program. And, most importantly, I tried to make this example as abstract as possible - all objects and instances are understandable and tangible.
Let's write a simple game - tank battle. Two tanks are playing. They alternately shoot and lose one whose health has fallen to zero. The game will have various types of shells and armor. In order to inflict damage it is necessary, firstly, to hit the enemy tank, and secondly, to break through his armor. If the armor is not broken, damage is not done. The logic of the game is based on the principle of “stone-scissors-paper”: that is, armor of one type well resists shells of a certain type, but poorly holds other shells. In addition, shells that penetrate armor well do low “armor” damage, and, on the contrary, the most “lethal” shells have less chance of penetrating armor.
Let's create a simple class for the gun. It will have two private fields: caliber and barrel length. Damage depends on the caliber, and, in part, the ability to break through armor. From barrel length - accuracy.
We will also make a constructor for the gun:
Let's make a method for getting caliber from other classes:
Remember that in order to hit a target, two things must happen: hitting the target and breaking through the armor? So, the gun will be responsible for the first of them: hit. Therefore, we make the boolean method IsOnTarget, which takes a random variable (dice) and returns the result: hit or not:
The whole gun class is as follows:
Now we make shells - this is the most obvious case for applying inheritance, but aggregation in it is also applicable. Any shell has its own characteristics. Just some hypothetical shells do not exist. Therefore, we make the class abstract. We make him a string field "type".
Shells are made for guns. For certain guns. A projectile of one caliber will not fire a cannon of another caliber. Therefore, we add the projectile field link to the instance of the gun. We make the constructor.
Here we applied aggregation. Somewhere a gun will be created. Then, shells that have a pointer to the gun will be created for this gun.
Specific types of shells will be the heirs of the abstract shell. Heirs can simply inherit the methods of the parent, but they can also be overridden, that is, work differently from the parent method. But we know for sure that any projectile must have a number of methods. Any projectile must do damage. The GetDamage method simply returns a caliber multiplied by three. In general, projectile damage depends on caliber. But this method will be redefined in child classes (remember that shells that penetrate armor well usually do less “armor” damage. To be able to redefine the method in a child class, we use the word virtual.
Any projectile must pierce (or at least try to pierce) the armor. In general, the ability to penetrate armor also depends on the caliber (well, and much more - the initial speed, for example, but we will not complicate it). Therefore, the method returns a caliber. That is, roughly speaking, a projectile can penetrate armor equal in thickness to its caliber. This method will not be overridden in child classes.
In addition, for convenient debugging and organizing console output, it makes sense to add the ToString method, which will simply allow us to see what kind of shell it is and what caliber:
Now we will make different types of shells that will inherit the abstract shell: high-explosive, cumulative, sub-caliber. High explosive does the greatest damage, cumulative - less, sub-caliber - even less. The child classes have no fields and call the constructor of the base shell, passing the gun to it, and the string type. The GetDamage () method is overridden in the child class - coefficients are added that increase or decrease the damage compared to the default.
High explosive (default damage):
Cumulative (default damage x 0.6):
Subcaliber (default damage x 0.3):
Note that the overridden GetDamage method also calls the base class method. That is, by overriding the method, we also retain the ability to access the default method using the base keyword).
So, for shells we used both aggregation (a gun in the base class) and inheritance.
Now create armor for the tank. Only inheritance applies here. Any armor has a thickness. Therefore, the abstract armor class will have a thickness field, and a type string field that will be defined when creating child classes.
The armor in our game will determine whether they are broken or not. Therefore, she will have only one method, which will be redefined in the subsidiaries, depending on the type of reservation.
And whether they are broken or not depends on which projectile arrived: in the default case of what caliber. Therefore, the method takes an instance of the projectile and returns a Boolean result: broken or not. Let's create several types of armor - heirs of abstract armor. I’ll give you only one type of code - the logic is about the same as in shells. Homogeneous armor holds a high-explosive shell well, but poorly - a sub-caliber. Therefore, if a sub-caliber projectile arrives, which has high armor penetration, then in calculations our armor becomes thinner. And so on: each type of armor has its own set of coefficients of resistance to a particular projectile.
Here we use one of the wonders that polymorphism gives. The method accepts any projectile. The signature indicates the base class, not the child ones. But inside the method, we can see what kind of shell flew - what type. And depending on this, we implement this or that logic. If we did not apply inheritance for shells, but just made three unique classes of types of shells, then we would have to arrange a different test for breaking through the armor. We would have to write as many overloaded methods as there are types of shells in our game, and call one of them depending on what kind of shell arrived. This would also be pretty elegant, but not relevant to the topic of this article.
Now we are ready to create a tank. There will be no inheritance in the tank, but there will be composition and aggregation. Of course, the tank will have a name. The tank will have a gun (aggregation). For our game, we will make the assumption that the tank can "change" the armor before each move - choose one or another type of armor. For this, the tank will have a list of types of armor. The tank will have an ammunition depot — a list of shells that will be filled with shells created in the tank’s designer (composition!). The tank will have health (decreases when it hits it), and the tank will have the currently selected armor and the currently selected projectile.
In order for the tank designer to remain more or less compact, we will make two auxiliary private methods that add three types of armor of the corresponding thickness and fill the ammunition pack with 10 shells of each of the three types:
Now the tank designer looks like this:
Please note that here we are again using the possibilities of polymorphism. Our ammunition holds shells of any type, since the list has the Ammo data type - the parent shell. If we were not inherited, but created unique types of shells, we would have to make a separate list for each type of shell.
The user interface of the tank consists of three methods: select armor, load a gun, shoot.
Select armor:
Charge the gun:
As I mentioned at the beginning, in this example I tried to get as far away from abstract concepts that I need to keep in mind all the time. Therefore, each instance of the projectile with us is equal to the physical projectile that was put in the combat pack before the battle. Consequently, shells can end at the most inopportune moment!
Fire:
Here - in more detail. Firstly, there is a check to see if the gun is loaded. Secondly, the shell that flew out of the barrel no longer exists for this tank, it is no longer in the cannon or in the ammunition depot. But physically it still exists - it flies towards the goal. And if it hits, it will participate in calculating the penetration of armor and damage to the target. Therefore, we save this shell in a new variable: Ammo firedAmmo. Since on the next line this projectile will cease to exist for this tank, you will have to use the IClonable interface for the base class of the projectile:
This interface requires the implementation of the Clone () method. Here she is:
Now everything is super realistic: when a shot is fired, dice is generated, the gun calculates the hit with its IsOnTarget method, and if there is a hit, the Shoot method will return an instance of the projectile, and if it misses, it will return null.
The last method of the tank is its behavior when an enemy shell hits:
Again polymorphism in all its glory. A shell flies to us. Any. Based on the selected armor and the type of projectile, the armor is broken through or not. If pierced, the GetDamage () method of the specific shell type is called.
All is ready. It remains only to write a console (or non-console) output, in which the user interface will be provided and in the cycle the alternate moves of the players are implemented.
To summarize. We wrote a program in which we used inheritance, composition, and aggregation, I hope we understood and remembered the differences. We actively exploited the possibilities of polymorphism, firstly, when any instances of child classes can be combined into a list that has a data type of the parent, and secondly, by creating methods that take the parent instance as a parameter, but within which the methods of the child are called. In the course of the text, I mentioned possible alternative implementations - replacing inheritance with aggregation, and there is no universal recipe. In our implementation, inheritance gave us the ease of adding new details to the game. For example, to add a new type of projectile we only need:
Similarly, to add another kind of armor, you only need to describe this type and add an item to the user interface. Modification of other classes or methods is not required.
Below is a diagram of our classes.
In the final code of the game, all the "magic numbers" that were used in the text are placed in a separate static Config class. We can access the public fields of a static class from any fragment of our code and it is not necessary (and impossible) to create an instance of it. This is how it looks:
And thanks to this class, we can make further adjustments, changing the parameters only here, without further deepening into classes and methods. If, for example, we came to the conclusion that the sub-caliber projectile turned out to be too strong, then we change one digit in Config.
All game code can be seen here .
In object-oriented programming languages, there are three ways to organize interaction between classes. Inheritance is when an inheriting class has all the fields and methods of the parent class, and, as a rule, adds some new functionality or / and fields. Inheritance is described by the word “is”. A passenger car is a car. It is only natural if he will be his heir.
```class Vehicle
{
bool hasWheels;
}
class Car : Vehicle
{
string model = "Porshe";
int numberOfWheels = 4
}```
Association is when one class includes another class as one of the fields. The association is described by the word "has." The car has an engine. It is only natural that he will not be the heir to the engine (although such an architecture is also possible in some situations).
Two particular cases of association are distinguished: composition and aggregation.
Composition is when the engine does not exist separately from the car. It is created when the car is created and is completely controlled by the car. In a typical example, an engine instance will be created in the car designer.
```
class Engine
{
int power;
public Engine(int p)
{
power = p;
}
}
class Car
{
string model = "Porshe";
Engine engine;
public Car()
{
this.engine = new Engine(360);
}
}
```
Aggregation is when an engine instance is created somewhere else in the code, and passed to the car designer as a parameter.
```
class Engine
{
int power;
public Engine(int p)
{
power = p;
}
}
class Car
{
string model = "Porshe";
Engine engine;
public Car(Engine someEngine)
{
this.engine = someEngine;
}
}
Engine goodEngine = new Engine(360);
Car porshe = new Car(goodEngine);
```
Although there is debate about the benefits of a particular way of organizing interaction between classes, there is no abstract rule. The developer chooses one way or another based on elementary logic (“is” or “has”), but also takes into account the possibilities and limitations that these methods give and impose. In order to see these features and limitations, I tried to write an example. Simple enough so that the code remains compact, but also sufficiently developed so that all three methods can be applied within one program. And, most importantly, I tried to make this example as abstract as possible - all objects and instances are understandable and tangible.
Let's write a simple game - tank battle. Two tanks are playing. They alternately shoot and lose one whose health has fallen to zero. The game will have various types of shells and armor. In order to inflict damage it is necessary, firstly, to hit the enemy tank, and secondly, to break through his armor. If the armor is not broken, damage is not done. The logic of the game is based on the principle of “stone-scissors-paper”: that is, armor of one type well resists shells of a certain type, but poorly holds other shells. In addition, shells that penetrate armor well do low “armor” damage, and, on the contrary, the most “lethal” shells have less chance of penetrating armor.
Let's create a simple class for the gun. It will have two private fields: caliber and barrel length. Damage depends on the caliber, and, in part, the ability to break through armor. From barrel length - accuracy.
```
public class Gun
{
private int caliber;
private int barrelLength;
}
```
We will also make a constructor for the gun:
```
public Gun(int cal, int length)
{
this.caliber = cal;
this.barrelLength = length;
}
```
Let's make a method for getting caliber from other classes:
```
public int GetCaliber()
{
return this.caliber;
}
```
Remember that in order to hit a target, two things must happen: hitting the target and breaking through the armor? So, the gun will be responsible for the first of them: hit. Therefore, we make the boolean method IsOnTarget, which takes a random variable (dice) and returns the result: hit or not:
```
public bool IsOnTarget(int dice)
{
return (barrelLength + dice) > 100;
}
```
The whole gun class is as follows:
```
public class Gun
{
private int caliber;
private int barrelLength;
public Gun(int cal, int length)
{
this.caliber = cal;
this.barrelLength = length;
}
public int GetCaliber()
{
return this.caliber;
}
public bool IsOnTarget(int dice)
{
return (barrelLength + dice) > 100;
}
}
```
Now we make shells - this is the most obvious case for applying inheritance, but aggregation in it is also applicable. Any shell has its own characteristics. Just some hypothetical shells do not exist. Therefore, we make the class abstract. We make him a string field "type".
Shells are made for guns. For certain guns. A projectile of one caliber will not fire a cannon of another caliber. Therefore, we add the projectile field link to the instance of the gun. We make the constructor.
```
public abstract class Ammo
{
Gun gun;
public string type;
public Ammo(Gun someGun, string type)
{
gun = someGun;
this.type = type;
}
}
```
Here we applied aggregation. Somewhere a gun will be created. Then, shells that have a pointer to the gun will be created for this gun.
Specific types of shells will be the heirs of the abstract shell. Heirs can simply inherit the methods of the parent, but they can also be overridden, that is, work differently from the parent method. But we know for sure that any projectile must have a number of methods. Any projectile must do damage. The GetDamage method simply returns a caliber multiplied by three. In general, projectile damage depends on caliber. But this method will be redefined in child classes (remember that shells that penetrate armor well usually do less “armor” damage. To be able to redefine the method in a child class, we use the word virtual.
```
public virtual int GetDamage()
{
//TO OVERRIDE: add logic of variable damage depending on Ammo type
return gun.GetCaliber()*3;
}
```
Any projectile must pierce (or at least try to pierce) the armor. In general, the ability to penetrate armor also depends on the caliber (well, and much more - the initial speed, for example, but we will not complicate it). Therefore, the method returns a caliber. That is, roughly speaking, a projectile can penetrate armor equal in thickness to its caliber. This method will not be overridden in child classes.
```
public int GetPenetration()
{
return gun.GetCaliber();
}
```
In addition, for convenient debugging and organizing console output, it makes sense to add the ToString method, which will simply allow us to see what kind of shell it is and what caliber:
```
public override string ToString()
{
return $"Снаряд " + type + " к пушке калибра " + gun.GetCaliber();
}
```
Now we will make different types of shells that will inherit the abstract shell: high-explosive, cumulative, sub-caliber. High explosive does the greatest damage, cumulative - less, sub-caliber - even less. The child classes have no fields and call the constructor of the base shell, passing the gun to it, and the string type. The GetDamage () method is overridden in the child class - coefficients are added that increase or decrease the damage compared to the default.
High explosive (default damage):
```
public class HECartridge : Ammo
{
public HECartridge(Gun someGun) : base(someGun, "фугасный") { }
public override int GetDamage()
{
return (int)(base.GetDamage());
}
}
```
Cumulative (default damage x 0.6):
```
public class HEATCartridge : Ammo
{
public HEATCartridge(Gun someGun) : base(someGun, "кумулятивный") { }
public override int GetDamage()
{
return (int)(base.GetDamage() * 0.6);
}
}
```
Subcaliber (default damage x 0.3):
```
public class APCartridge : Ammo
{
public APCartridge(Gun someGun) : base(someGun, "подкалиберный") { }
public override int GetDamage()
{
return (int)(base.GetDamage() * 0.3);
}
}
```
Note that the overridden GetDamage method also calls the base class method. That is, by overriding the method, we also retain the ability to access the default method using the base keyword).
So, for shells we used both aggregation (a gun in the base class) and inheritance.
Now create armor for the tank. Only inheritance applies here. Any armor has a thickness. Therefore, the abstract armor class will have a thickness field, and a type string field that will be defined when creating child classes.
```
public abstract class Armour
{
public int thickness;
public string type;
public Armour(int thickness, string type)
{
this.thickness = thickness;
this.type = type;
}
}
```
The armor in our game will determine whether they are broken or not. Therefore, she will have only one method, which will be redefined in the subsidiaries, depending on the type of reservation.
```
public virtual bool IsPenetrated(Ammo projectile)
{
return projectile.GetDamage() > thickness;
}
```
And whether they are broken or not depends on which projectile arrived: in the default case of what caliber. Therefore, the method takes an instance of the projectile and returns a Boolean result: broken or not. Let's create several types of armor - heirs of abstract armor. I’ll give you only one type of code - the logic is about the same as in shells. Homogeneous armor holds a high-explosive shell well, but poorly - a sub-caliber. Therefore, if a sub-caliber projectile arrives, which has high armor penetration, then in calculations our armor becomes thinner. And so on: each type of armor has its own set of coefficients of resistance to a particular projectile.
```
public class HArmour : Armour
{
public HArmour(int thickness) : base(thickness, "гомогенная") { }
public override bool IsPenetrated(Ammo projectile)
{
if (projectile is HECartridge)
{
//Если фугасный, то толщина брони считается больше
return projectile.GetPenetration() > this.thickness * 1.2;
}
else if (projectile is HEATCartridge)
{
//Если кумулятивный, то толщина брони нормальная
return projectile.GetPenetration() > this.thickness * 1;
}
else
{
//Если подкалиберный, то считаем уменьшаем толщину
return projectile.GetPenetration() > this.thickness * 0.7;
}
}
}
```
Here we use one of the wonders that polymorphism gives. The method accepts any projectile. The signature indicates the base class, not the child ones. But inside the method, we can see what kind of shell flew - what type. And depending on this, we implement this or that logic. If we did not apply inheritance for shells, but just made three unique classes of types of shells, then we would have to arrange a different test for breaking through the armor. We would have to write as many overloaded methods as there are types of shells in our game, and call one of them depending on what kind of shell arrived. This would also be pretty elegant, but not relevant to the topic of this article.
Now we are ready to create a tank. There will be no inheritance in the tank, but there will be composition and aggregation. Of course, the tank will have a name. The tank will have a gun (aggregation). For our game, we will make the assumption that the tank can "change" the armor before each move - choose one or another type of armor. For this, the tank will have a list of types of armor. The tank will have an ammunition depot — a list of shells that will be filled with shells created in the tank’s designer (composition!). The tank will have health (decreases when it hits it), and the tank will have the currently selected armor and the currently selected projectile.
```
public class Panzer
{
private string model;
private Gun gun;
private List armours;
private List ammos;
private int health;
public Ammo LoadedAmmo { get; set; }
public Armour SelectedArmour { get; set; }
}
```
In order for the tank designer to remain more or less compact, we will make two auxiliary private methods that add three types of armor of the corresponding thickness and fill the ammunition pack with 10 shells of each of the three types:
```
private void AddArmours(int armourWidth)
{
armours.Add(new SArmour(armourWidth));
armours.Add(new HArmour(armourWidth));
armours.Add(new CArmour(armourWidth));
}
private void LoadAmmos()
{
for(int i = 0; i < 10; i++)
{
ammos.Add(new APCartridge(this.gun));
ammos.Add(new HEATCartridge(this.gun));
ammos.Add(new HECartridge(this.gun));
}
}
```
Now the tank designer looks like this:
```
public Panzer(string name, Gun someGun, int armourWidth, int h)
{
model = name;
gun = someGun;
health = h;
armours = new List();
ammos = new List();
AddArmours(armourWidth);
LoadAmmos();
LoadedAmmo = null;
SelectedArmour = armours[0]; //по умолчанию - гомогенная броня
}```
Please note that here we are again using the possibilities of polymorphism. Our ammunition holds shells of any type, since the list has the Ammo data type - the parent shell. If we were not inherited, but created unique types of shells, we would have to make a separate list for each type of shell.
The user interface of the tank consists of three methods: select armor, load a gun, shoot.
Select armor:
```
public void SelectArmour(string type)
{
for (int i = 0; i < armours.Count; i++)
{
if (armours[i].type == type)
{
SelectedArmour = armours[i];
break;
}
}
}
```
Charge the gun:
```
public void LoadGun(string type)
{
for(int i = 0; i < ammos.Count; i++)
{
if(ammos[i].type == type)
{
LoadedAmmo = ammos[i];
Console.WriteLine("заряжено!");
return;
}
}
Console.WriteLine($"сорян, командир, " + type + " закончились!");
}
```
As I mentioned at the beginning, in this example I tried to get as far away from abstract concepts that I need to keep in mind all the time. Therefore, each instance of the projectile with us is equal to the physical projectile that was put in the combat pack before the battle. Consequently, shells can end at the most inopportune moment!
Fire:
```
public Ammo Shoot()
{
if (LoadedAmmo != null)
{
Ammo firedAmmo = (Ammo)LoadedAmmo.Clone();
ammos.Remove(LoadedAmmo);
LoadedAmmo = null;
Random rnd = new Random();
int dice = rnd.Next(0, 100);
bool hit = this.gun.IsOnTarget(dice);
if (this.gun.IsOnTarget(dice))
{
Console.WriteLine("Попадание!");
return firedAmmo;
}
else
{
Console.WriteLine("Промах!");
return null;
}
}
else Console.WriteLine("не заряжено");
return null;
}
```
Here - in more detail. Firstly, there is a check to see if the gun is loaded. Secondly, the shell that flew out of the barrel no longer exists for this tank, it is no longer in the cannon or in the ammunition depot. But physically it still exists - it flies towards the goal. And if it hits, it will participate in calculating the penetration of armor and damage to the target. Therefore, we save this shell in a new variable: Ammo firedAmmo. Since on the next line this projectile will cease to exist for this tank, you will have to use the IClonable interface for the base class of the projectile:
```
public abstract class Ammo : ICloneable
```
This interface requires the implementation of the Clone () method. Here she is:
```
public object Clone()
{
return this.MemberwiseClone();
}
```
Now everything is super realistic: when a shot is fired, dice is generated, the gun calculates the hit with its IsOnTarget method, and if there is a hit, the Shoot method will return an instance of the projectile, and if it misses, it will return null.
The last method of the tank is its behavior when an enemy shell hits:
```
public void HandleHit(Ammo projectile)
{
if (SelectedArmour.IsPenetrated(projectile))
{
this.health -= projectile.GetDamage();
}
else Console.WriteLine("Броня не пробита.");
}
```
Again polymorphism in all its glory. A shell flies to us. Any. Based on the selected armor and the type of projectile, the armor is broken through or not. If pierced, the GetDamage () method of the specific shell type is called.
All is ready. It remains only to write a console (or non-console) output, in which the user interface will be provided and in the cycle the alternate moves of the players are implemented.
To summarize. We wrote a program in which we used inheritance, composition, and aggregation, I hope we understood and remembered the differences. We actively exploited the possibilities of polymorphism, firstly, when any instances of child classes can be combined into a list that has a data type of the parent, and secondly, by creating methods that take the parent instance as a parameter, but within which the methods of the child are called. In the course of the text, I mentioned possible alternative implementations - replacing inheritance with aggregation, and there is no universal recipe. In our implementation, inheritance gave us the ease of adding new details to the game. For example, to add a new type of projectile we only need:
- in fact, copy one of the existing types, replacing the name and string field passed to the constructor;
- add another if to child armor classes;
- add an additional item to the shell selection menu in the user interface.
Similarly, to add another kind of armor, you only need to describe this type and add an item to the user interface. Modification of other classes or methods is not required.
Below is a diagram of our classes.
In the final code of the game, all the "magic numbers" that were used in the text are placed in a separate static Config class. We can access the public fields of a static class from any fragment of our code and it is not necessary (and impossible) to create an instance of it. This is how it looks:
```
public static class Config
{
public static List ammoTypes = new List { "фугасный", "кумулятивный", "подкалиберный" };
public static List armourTypes = new List { "гомогенная", "разнесенная", "комбинированная" };
//трешхолд для пушки - величина, выше которой будем считать, что снаряд попал в цель
public static int _gunTrashold = 100;
//дефолтный коэффициент для заброневого действия базового снаряда
public static int _defaultDamage = 3;
//коэффициенты урона для снарядов разных типов
public static double _HEDamage = 1.0;
public static double _HEATDamage = 0.6;
public static double _APDamage = 0.3;
//коэффициенты стойкости брони
//для гомогенной:
//Если в гомогенную броню прилетает фугасный, то ее толщина считается большей - коэффициент 1.2
public static double _HArmour_VS_HE = 1.2;
//Если в гомогенную броню прилетает кумулятивный, то ее толщина считается нормальной - коэффициент 1.0
public static double _HArmour_VS_HEAT = 1.0;
//Если в гомогенную броню прилетает подкалиберный, то ее толщина считается меньшей - коэффициент 0.7
public static double _HArmour_VS_AP = 0.7;
//для комбинированной брони
//Если в комбинированную броню прилетает фугасный, то ее толщина считается нормальной - коэффициент 1
public static double _СArmour_VS_HE = 1.0;
//Если в комбинированную броню прилетает фугасный, то ее толщина считается меньше - коэффициент 0.8
public static double _СArmour_VS_HEAT = 0.8;
//Если в комбинированную броню прилетает фугасный, то ее толщина считается больше - коэффициент 1.2
public static double _СArmour_VS_AP = 1.2;
//Для разнесенной брони
//Если в разнесенную броню прилетает фугасный, то ее толщина считается меньше - коэффициент 0.8
public static double _SArmour_VS_HE = 0.8;
//Если в разнесенную броню прилетает кумулятивный, то ее толщина считается больше - коэффициент 1.2
public static double _SArmour_VS_HEAT = 1.2;
//Если в разнесенную броню прилетает подкалибереый, то ее толщина считается нормальной - коэффициент 1
public static double _SArmour_VS_AP = 1.0;
}
```
And thanks to this class, we can make further adjustments, changing the parameters only here, without further deepening into classes and methods. If, for example, we came to the conclusion that the sub-caliber projectile turned out to be too strong, then we change one digit in Config.
All game code can be seen here .