Correctly programmable. We use polymorphism. General logic of game characters
Unity3D. USE OOP ADVANTAGES. POLYMORPHISM
ATTENTION!
I want to warn you right away , read the comments on the article, there KumoKairo suggested a more perfect approach to solving the problems posed in the article.
Good day. In this lesson, we will look at some important programming tricks on the Unity 3D engine. I think the article will help both beginners and experienced game-makers.
The aim of the article is to recognize the power and unlimited usability of all the charms of OOP. In particular, POLYMORPHISM. In addition, we will touch upon several other important issues.
We’ll also talk a little about the concept of creating your own motors, the general logic of game characters and the methods of their interaction with each other and with the outside world. We will talk about how to do this simply and logically, easily editable (modernizable). How to make sure that when adding new elements to the game, you do not have to rewrite (supplement) what has already been written.
I think in the future there will be a continuation of the article. There we will examine in detail the writing of our own Motors, talk about vectors and the operations we need on them, create a full-fledged inventory model. A very good inventory in which it will be easy to add new items without changing the old ones.
The article is intended for people with an understanding of Unity3D. If you do not have this idea, I highly recommend reading these articles:
1) Very good articles: habrahabr.ru/post/112287 , habrahabr.ru/post/113859
The second article contains examples of scripts in JS. I think that there will be no problems translating to C #, even if you are new to both JS and C #. The only thing I will say is that the line in JS:
bull_clone = Instantiate(bullet, transform.position, transform.rotation);
In C # it will look like this:
bull_clone = (GameObject)Instantiate(bullet, transform.position, transform.rotation);
And such a line in JS:
var ml = GameObject.Find("Player").GetComponent(MouseLook);
In C # it will look like this:
GameObject ml = GameObject.Find("Player").GetComponent();
2) Article: habrahabr.ru/post/141362 There is also a continuation of this article on the site. They are called "Unity3d. Lessons from Unity 3D Student (B00-B16) »Only 4 articles. At the moment, at least. There is a search engine on the site at the top right. I think you can handle it.
3) habrahabr.ru/post/148410 It is also necessary to read and understand. There is another part of this article. The search engine on the site will help you again.
4) This is something like the directory habrahabr.ru/post/128711 , habrahabr.ru/post/128948 .
5) There are many interesting articles on this forum. Look for yourself if you want.
So. You have learned everything you read, experimented, you may already have made a small game. Maybe not a little one. It’s bad if you just read these articles, but didn’t write anything yourself. Practice and practice again
But first, consider a couple of secondary issues. (Just in case)
The first half of the article is for demonstration purposes. Rewriting the code is not necessary.
I use the following words as synonyms: script class
CLASSES, CLASSES
We are engaged in object-oriented programming (OOP). Classes and class instances exist. A class is like a form or drawing of an object. A molded figure on a mold is an instance of a class. All buses are instances of the Bus class. Bus Vasya, your neighbor, is an instance of the class Bus. Vasya bus has a sticker on the left side, scratches all over the body, the right front wheel is lowered, the floor of the gas tank, one glass is broken, one of the doors does not work. This bus has many individual properties. Your other neighbor also has a bus. It also has scratches, stickers, but there are no broken windows, the wheels are not flat and many more individual features of this particular bus. Both buses are instances of the class Bus. The class bus is a drawing. A drawing is a bus that has not yet been created and you cannot ride it. It has no scratches and flat tires. Without a drawing, you cannot create a single bus. Without a description of the class, we cannot create a single instance of the class. The class serves not only to create instances, for example, you can find out the length of the bus by it. But you can’t ride a drawing. In Unity, class names are capitalized (GameObject, Transform, Animation, etc.), and class instance names are capitalized.
It makes sense to start all variable names with a small letter to distinguish them from class names. This is what Unity dictates to us.
Caching
From wikipedia: Caching work results -
Many programs write somewhere intermediate or auxiliary results of work, so as not to calculate them every time they are needed. This speeds up the work, but requires additional memory (RAM or disk).
Consider this script. What is bad about him? Everything seems to be fine. An object moves to the Left at a constant speed of 10 units per second. Thanks to Time.deltaTime, there is a uniform movement that does not depend on the current performance on a particular computer.
The bad thing about this script is that every frame is called transform.position, or rather the bad thing is that transform is called. Actually, the transform written in Update () here means gameObject.transform. And this, in turn, is the same as gameObject.GetComponent (). But this already means that every frame your script is looking for among all that is hung on the gameObject is transform. Why do you need to look every time, when you can find it once and remember (cache) (note the gameObject is written with a small letter. That is, it is an instance of the GameObject class)
It would seem that nothing has changed. But in fact, it uses the resources of the machine much more rationally.
Do not believe me. Open the Unity documentation and it says Caching NECESSARY !!!
Let's look at a couple more examples of what needs to be cached:
Read the comments in the code carefully
Of course, it makes sense to cache only if what we are going to cache will be used many times. (Often this is more than once). In addition, it is worthwhile to understand if we remove the component that was previously cached into the variable “A” from our gameObject, and then added it again, nothing will happen in “A”. Therefore, it makes no sense to cache something that is constantly deleted and re-added. Although you can cache immediately after the next addition. Only instances of the class can be cached (maybe I won’t lie yet), but you don’t need to cache data types and structures.
For example, you can cache it.
It is written on the right that this is a class, and the icon on the left is also special. (Red circled)
But this can not be cached
Since position is not an instance of a class. This is a Vector3 structure.
And again, Visual Studio shows us that Vector3 is a structure. And her icon is also different.
So. Cache classes only. All kinds of variables like float, byte, vector3, etc. do not cache! Well, you understand that:
Public Vector3 aaa;
aaa = transform.position; //просто сохранит текущее значение transform.position. А когда оно измениться в aaa останется старое значение transform.position. А кэширование класса (экземпляра класса) обеспечивает постоянный доступ к нему, свеженькому. Мы как бы сохраняем ссылку на нужный нам объект.
You need to cache not only scripts hung on objects, but also the objects themselves.
In further examples, everything will be cached, so you will understand the examples.
GENERAL CONCEPT OF MOBA (CHARACTER)
If you analyze the standard projects that come with Unity and put your brains a little, it becomes clear that the correct concept of the character is as follows: There is a main script that turns on and off behavior scripts. For example, behavior scripts may be:
- Standby behavior (There is no player nearby and the mob does nothing (or does. It depends on the specific idea of the game)). You can also call this mode "Target Search"
- Behavior in the mode “I see the goal, I can kill, get closer” (Run to a new target)
- Behavior in the "target nearby must be attacked" mode (Attack)
Of course, you can complicate the scheme. For example, adding a Behavior in the mode “I see the target, I can’t kill, I run away” (Running away from the target)
Implementation of each Behavior in your script. The main script has no idea how a particular behavior is implemented. Its task is to turn it on at the right time. For example, the Behavior script in the mode “I see the target, I can kill, get close” can be turned on when the player approached the mob at a certain distance or attacked the mob first. If we need two almost identical mobs, then we do not redo the entire script, but change one of the Behavior scripts. This can be done not only at the development stage, but also at runtime. (this requires polymorphism)
All these scripts can control the movement of the character (mob). They do not manage it themselves, but through an intermediate script called MOTOR. Motor-in-law has several methods necessary to control the character. For instance:
Why is this necessary? There are two main reasons. Firstly, it’s just logical and if you want to change something in the Motor you don’t have to change the code in different scripts that move the character. They all do it through one script. Change the code in the motor and all. In addition, the start of the animation during movement is also carried out from the Motor. This is especially important if the project is developed by a team. The person who makes the motor has no idea where the motor methods will be called from. And the person making the Behavior script has no idea how the MobMotor.MoveLeft () method is implemented, the main thing is that he moves the character to the left. That’s all he needs. Thus, for coordinated work, they need to know only two simple facts. The mob can walk left and right. All! Nothing extra. Secondly ... ... the most important thing is secondly. But we do not yet know what polymorphism is.
We continue. We have a main script that monitors the situation and includes the necessary scripts when necessary. And those move the character only through the motor. There is a script that is responsible for the character's health. For example:
By the way, I remind you Public - means that the declared variable (method, etc.) will be available to all scripts in general. Private-available only inside the script itself
Consider the MobHp script in detail. The character appears and immediately has a health of 100 units. When a character receives damage (receiving damage is monitored by the main script and not MobHp itself), the main script calls the MobHp.SetDamage (float damage) method. This method returns either true or false. If true is returned, then the mob survived the damage. And the main script continues to function. If false returns, the mob is dead. What happens to the mob after death is the main script anyway. He does not know this. In our case, upon death, a sound file is played (the sound of the character dying) and after 5 seconds the character disappears (see picture). The role of the main script is simple. If false, it disables all Behavior scripts. Disables the character’s motor. He will turn everything off. That is, the mob will freeze, the sound of the dying mob will play, and then it will disappear. Of course, you can add the generation of fragments to the MobHp script, playing the animation of the character falling to the ground. Anything you want. The beauty is that the MobHp script is completely independent. If different people program the main script and MobHp, then all they need to know for coordinated work is that the character receives damage through the MobHp.SetDamage (float dfamage) method. And if the Method returns false, then the mob is dead. All! No unnecessary information. And if the creator of MobHp wants to change the contents of the script, this does not apply to the main script! that the character receives damage through the MobHp.SetDamage (float dfamage) method. And if the Method returns false, then the mob is dead. All! No unnecessary information. And if the creator of MobHp wants to change the contents of the script, this does not apply to the main script! that the character receives damage through the MobHp.SetDamage (float dfamage) method. And if the Method returns false, then the mob is dead. All! No unnecessary information. And if the creator of MobHp wants to change the contents of the script, this does not apply to the main script!
His Majesty POLYMORPHISM
So we got there. I congratulate those who have not quit reading. The story of the fable about polymorphism will begin with a few simple life examples, and then we will extend them to our general concept of a mob (character).
Imagine a switch. Normal switch. Which turns on (off) the light in your room. Or does he turn on the fan? Or maybe he starts a lathe. But can a conventional switch launch a nuclear bomb. Perhaps it’s obvious that the switch does not care what to turn on (off). It just opens the contact. He lets energy out somewhere. He doesn’t care where.
Now imagine a tank shell. It flies, falls and explodes, causing damage to everything around. He doesn’t care what is around. He doesn’t just care. He has no idea what's around him, but, nevertheless, he does damage.
Let's get back to our scripts. We have a MobHp.SetDamage (float damage) method. A hypothetical tank shell that fell next to the character must call this method to damage the character. However, according to our concept, the MobHp.SetDamage (float damage) method should be called from the Main script. Thus, the projectile from the tank should interact precisely with the main script. Suppose we create a global method in the Main script. And the projectile during the fall will check if the character (mob) is within the radius of the explosion and call this method if necessary. Let us have a lot of absolutely identical mobs (characters) on the map. When falling, the projectile must go through all of them, select those in the radius of damage and inflict damage on them (call the corresponding method in the main script of each of the mobs). In Unity, this can be implemented like this: All mobs are assigned the Mob tag
Получить массив всех объектов с определенным тегом можно так
GameObject[] allMobs= GameObject.FindGameObjectsWithTag ("Mob" ); //получили массив всех мобов
//пока не зависимо от расстояния
Для определенности пусть наш Главный скрипт называется MyBehaviour
И теперь перебираем всех мобов и наносим им дамаг(Обратите внимание. Мы представили что у главного скрипта(MyBehaviour) есть метод MyBehaviour.SetDamage(float damage), который в свою очередь вызывает метод MobHp.SetDamage(float damage) у скрипта MobHp. Оба скрипта висят на персонаже(мобе))
Код в скрипте на снаряде.
foreach ( GameObject mob in allMob ) //перебираем по одному из массива мобов
{
mob.GetComponent().SetDamage(50); //у каждого моба вызываем метод SetDamage
}
In this example, the projectile deals 50 damage.
Everything is fine! This will work. But if we want many mobs of TWO types? In the second type, the Main script can be implemented differently. For example, a robot and a gun. They have very different behaviors. They have differently implemented Behavior in the mode “I see the goal, I can kill, get closer” (We run to a new target). The gun doesn’t have one at all. Thus, at the tower, the main script will be called MyBehaviour2 (different scripts must be named differently), and all Behavior scripts are called differently. And even MobHp has a different gun. MobHp2, for example, implements “gun dying” in its own way (the matter may not be limited to replacing the sound and animation of dying). How should a tank shell behave now to damage all mobs and towers?
We need to introduce a new tag for the tower. And the shell script itself will take the form:
//РАБОТАЕМ С МОБАМИ
GameObject[] allMobs= GameObject.FindGameObjectsWithTag ("Mob" ); //получили массив всех мобов
//пока не зависимо от расстояния
foreach ( GameObject mob in allMob )//перебираем по одному всех роботов
{
mob.GetComponent().SetDamage(50); //у каждого вызываем нужный метод
}
//РАБОТАЕМ С БАШНЯМИ
GameObject[] allTower= GameObject.FindGameObjectsWithTag ("Tower" );
foreach ( GameObject tower in allTower )//перебираем по одному все башни
{
tower.GetComponent().SetDamage(50); //у каждого вызываем нужный метод
}
What are the disadvantages of this approach. Obviously, the Shell script depends on the number of types of mobs. Thus, if you want to add one kind of mob, you will have to get into the Shell script and upgrade it. Now imagine that the game has 500 types of mobs. The shell script will have about 1,500 lines of code. It is worth considering that the game will surely have several dozen types of shells. Count for yourself. If the shell script will be made by one person, and the main scripts of mobs and guns and other characters (MyBehaviour, MyBehaviour2, MyBehaviour3, etc.) is another person, then for coordinated work they will need to know the number of mobs and the exact names (and maybe the content) their main scripts (imagine that the names will not be of the form MyBehaviourN, but RobotBehaviour, TowerBehaviour, MonsterBehaviour, TigerBehaviour, etc.)
Let's go back to the switch, which does not care what it turns on. If you have 5 types of doors in the game, then each one will need its own switch (button). And if you want to add one type of door, then you have to add a button or upgrade existing ones. And if different people program buttons and doors, then they will have to know all the names (and maybe the contents) of all scripts of all doors and buttons.
The situation is exactly the same with weapons of characters, armor (if their influence on the game is not limited to changes in strength, speed, etc.), by any complexly interacting objects.
Of course, there are ways to solve this problem. But the most elegant and easy to understand, in my opinion, is POLYMORPHISM. I will not write strict definitions, you yourself can find them.
Inheritance, encapsulation and polymorphism are the three OOP elephants.
The following are no definitions! Rather, rough explanations
Encapsulation is the ability of a class to hide its guts from external code. We cannot refer to variables (methods, procedures, etc.) declared with the Private, Protected keyword from the external, relative to the code class, and this is good.
Inheritance - the ability of a class to inherit (copy) methods, variables, etc. from its parent.
You see! MobHp is the heir to MonoBehaviour. MonoBehaviour is the parent of MobHp. The parent is written through the colon.
And now the trick. Create a new empty script, name it NewMobHp and instead of MonoBehaviour (in the script itself) write MobHp (That is, we indicated that the parent of this new NewMobHp will not be MonoBehaviour, but MobHp) (See the picture below).
In the NewMobHp script, declare a variable of type NewMobHp with the name newMobHp (With a lowercase letter). Do not be alarmed that in the NewMobHp script (“script” and “class” are synonyms for us) we declared a variable of the same type as the class itself. Yes, a class can contain variables of the same type as the class itself. Write “newMobHp” in Start () and press the dot. If you have a normal code editor, then it will show all the methods, properties, etc. available to you in this place of the code. Let's start writing SetDa ... Magic
And the magic !!! Our NewMobHp class has a NewMobHp .SetDamage (float damage) method, just like the MobHp class. Although the script has just been created and there is no declaration of the SetDamage (float damage) method in it, but this method is declared in the parent class. This is the HERITAGE. The NewMobHp class inherited the SetDamage (float damage) method since it was declared with the keyword Public in MobHp.
By the way, what is the difference between Protected and Private? All that Private is not inherited. All that Protected inherits. So, if we want to inherit the globally declared (Public) method or variable, etc., then nothing needs to be done. It is already inherited. If we want to inherit a local variable for this class, then instead of Private we need to write Protected in the parent class.
So from this moment I give the command, CODE! All that will be written by me should be written by you. Experiment, understand, stuff pens.
NewMobHp and other scripts will come in handy for me; we will delete them (if you have done). By the way, the NewMobHp class is the heir not only of MobHp, but also of MonoBehaviour. Since MobHp is the descendant of the MonoBehaviour class.
Like this: MonoBehaviour -> MobHp -> NewMobHp.
MobHp is the immediate parent for NewMobHp.
Open Unity, we start coding! Maybe something will not be clear, but when we get to the creation of a small game everything will become clear.
Create a daddy MyScript. In it, create a script MyBehaviour. This will be the parent for all the main scripts of all mobs (in general, everything that knows how to receive damage). We can create the SetDamage method in it. And in the future, new main scripts will inherit it. And all mobs will have this method. But for each type of mob, the process of getting damage can be implemented in different ways. How to make the method implemented differently for each script?
POLYMORPHISM - I do not know how to give it a definition. Maybe you yourself will give it later. We’ll take a closer look at everything with an example. Let's write this in MyBehaviour:
An empty SetDamage method. Why is it needed? Yes, we do not need him. Pay attention to the Virtual keyword (see picture). This means that this method can be overridden in other scripts that will be inheritors of the MyBehaviour script. That is, they inherit it, but implement it in their own way. Now we’ll make a MobBehaviour script:
And so we’ll analyze what happens here:
1- We have clearly indicated that MobBehaviour is an inheritor of the MyBehaviour class
2- I write the word Override, put a space and Visual Studio itself suggests that we can REDEFINE. As you can see there is SetDamage (float damage), then we need it. All other methods are methods declared in Parent scripts farther than MyBehaviour. For example ToString (). We can override ToString () and program how this method works for our MobBehaviour class. But we don’t need it. We select SetDamage (float damage) and we see (figure 3) that this method is initially defined in MyBehaviour (that is, it is inherited from the MyBehaviou class). You can see where ToString () and others are defined. Now select the line SetDamage (float damage), press Enter and this is what Visual Studio drew itself:
Base.SetDamage (damage); - here Base is like the parent (class) in which the SetDamage (float damage) method was defined. Or to put it another way, a reference to the class from which this method was inherited. We have it defined in MyBehaviour. It is empty if you have not forgotten. That is, you can add anything there. And then Base.SetDamage (damage); will call this code, and after that you can add something for a specific heir. This is necessary if the milestones of MyBehaviour heirs in the SetDamage (float damage) method have something in common. But we don’t need it. For now, remove all unnecessary from MobBehaviour. Leave it so
Take a break from MobBehaviour. Let's create a new script MyHp
This is a script from which all the scripts responsible for the character’s health will be inherited
Let's create another script. Let's call it MobHp. We examined it above.
Just like last time, there is only the word Override in the redefinition of the SetDamage (float damage) method. And it is indicated that the class is an inheritor of the MyHp class. A little more changed the dying mob. There are no sounds, the mob will simply increase by death 3 times.
Again, think about MobBehaviour. Let's make it like this:
We re-declared the SetDamfge (float damage) method. Please note that thisHp is declared as MyHp, although the health script is called MobHp. Here it is POLYMORPHISM. We can work with MobHp as well as with MyHp. We can assign a variable of type MyHp to an instance of the MobHp class, since it is an inheritor of MyHp. Thus, the MobBehaviour script doesn’t care which inheritor of the MyHp class lies in the thisHp variable. The main thing is that thisHp has a SetDamfge (float damage) method. And it can be used! We can change the MobHp script to another (also an inheritor of MyHp). And everything will work.
Now we’ll make our mob in the form of a ball (double its size from the initial one). Let's make the earth (I make a flat rectangle), and some kind of light. It happened to me like this:
This ball will be our first mob. We’ll hang MobBehaviour and MobHp scripts on it.
Pay attention to This Hp in the MobBehaviour script (See the picture). Now nothing is assigned there (None). However, at the start (in the Start () procedure in MobBehaviour) MyHp (or its successor) will be found
thisHp = GetComponent ();
So, we have a mob which you can kill. True, it is called "Sphere", but it does not matter. It would be necessary to create a player who will shoot balls at mobs. Let's do it.
Delete the existing camera. Let's create First Person Controller (it’s there in the folders, remember the lessons that are referenced at the beginning of the article). Create a ball (small), attach Rigibody to it. Let's call it Bullet and drag it into a new folder called MyPrefab
This is our future patron. By the way, so that he does not fly through the walls at high speeds, we will do so
Now the physical engine will interpolate the movement of the cartridge. This is probably a rather complicated algorithm, so it’s not worth everything to install Interpolate. Only fast moving objects (Very fast. Those that can fly from a half meter or more in one frame). Read on Wikipedia what interpolation and extrapolation are.
Create a new BulletScript script. This is a cartridge script.
When you hit something, this script gets the MyBehaviour component (or any of its successors) from this something. And calls (if this something has MyBehaviour) the SetDamfge (float damage) method with damage = 30. So, one cartridge will do 30 points of damage. Thanks to the POLYMORPHISM, the cartridge doesn’t care what exactly, and which particular heir of MyBehavior he wants to attack. Even if after writing this script we add a new type of mob to the game, with our heir MyBehaviour (for example, TowerBehaviour), the cartridge will still do damage when it gets into this new mob. Remember the example of a tank shell. Do not forget to drag this script to the cartridge prefab.
It remains only to force the player to shoot these cartridges. Create a new PlayerShoot script
A similar script was created and described in detail in one of the articles, a link to which is at the beginning of the article. Only there he was at JS. Let us dwell only on the main points:
1-prefab of the cartridge we are going to shoot,
2-initial cartridge speed,
3-pause between shots,
4-camera transforms, because it rotates when we move the mouse,
5-time of the last shot,
6- cache the camera transform. Pay attention to how we did it. This script will hang on our FirstPersonController. Therefore, by transform (if you write this word in a script) we mean the FirstPersonController transform, not the camera.
Pay attention to the scale. First Person Controller I have a scale of 1,1,1
If you have 100,100,100 there, then you may not notice either the mob or its future movement.
We will search among sub transforms (one might say child transforms. But not in the sense of heirs as with class) camera transforms using the transform.FindChild (“Main Camera”) method. This method will find father-in-law among the FirstPersonController subobjects an object with the name “Main Camera” and get Transform from it. And we cache this Transform into the cameraTransform variable.
7-if more than shootPause has passed since the last shot, then you can shoot.
8-Create a copy of the cartridge.
9-Get rigiBody cartridge.
10-Add speed to the cartridge.
11-Remember the time of the shot.
We throw this script on the player. Drag the prefab of the cartridge where necessary
We start the game. We find the mob. We shoot at him using the left mouse button. Counting the shots. It should swell with 4 hits. In the console appeared "Mob is dead !!!". The mob swells and disappears after 5 seconds.
The mob is bloated because in the MobHp script we wrote thisTransform.localScale = new Vector3 (3, 3, 3). If your mob isn’t swollen enough, change the three points to large values. Well, here we have done what you probably could have done. What is the beauty of polymorphism? But here's the thing. Let's create one more mob. Also a ball. We’ll hang a new MonsterHp script on it:
As you can see, it is almost no different from the MobHp script. All differences are underlined in red. We cached the material, and change its color depending on the health. If you don’t know how to create the color you want, check out the internet about the RGB color model.
Let's make the “Player” tag for our protagonist.
Now create the MonsterBehaviour script:
This is an analogue of the MobBehaviour script. But a mob with this script can also run after the player, if he is close enough. In general, the methods of movement of the mob should be in the Motor (we already discussed why), but in order not to litter the example and not blow your brain, we will do it in MonsterBehaviour itself. Please note that the SetDamage (float damage) method is not implemented as in MobHp. It added IsLive = false; and the text in Debud.Log (); has changed. Of course, you can add some more code. I want to draw your attention to the fact that the FindGameObjectWithTag ("Player") method returns only the first one gameObject with the "Player" tag. But with us, and so he is alone. If we need to get all the objects, we will need the FindGameObjectsWithTag method.
FindGameObjectWithTag("Tag") //Вернет один gameObject
FindGameObjectsWithTag("Tag")// Вернет массив gameObject[] Всего одной буковкой отличаются. А какая большая разница.
Note that we cached the player’s transform, not the entire gameObject. We will refer to the transform, and through gameObject it would look like this: player.transform which is equivalent to player.getComponent (). But why do this every time, when you can cache once and immediately. Of course, from a couple of dozen extra getComponent nothing much will change. But if the project is very large. And every programmer will poke where getComponent and GameObject.Find (...) and GameObject.FindGameObjectWithTag (...) hit, etc. This together can slow down the program. Lower FPS. Do you need it?
We’ll hang the MonsterBehaviour and MonsterHp scripts on our new mob (New Ball). I got the following scene:
The small ball is the cartridge (Bullet). It can be deleted, anyway, it is already in prefabs (MyPrefab folder). Please note that I have both mobs called the same (Shape). You can change. Call the new mob Monster, and the old Tower.
We start the game. We approach the new mob closer. He begins to move in our direction. Run back away, he stops. We shoot at the mob, it changes color. We shoot again, he blushes and no longer runs after us. After 5 seconds it disappears. You can shoot in the old mob and make sure that he is still able to receive damage and die.
So, what is the beauty of POLYMORPHISM. We added a mob and wrote him a script. But we did not write anything in the player’s script and in the cartridge’s script.
Again, recall our tank shell. What could our script look like in our case? Very simple. But first, we’ll add our “Mob” tag to
both mobs! Now we will make a tank explosive shell from our cartridge. We will not make splinters, explosions, etc. this is not relevant to our lesson. Remove the hanging cartridge from the scene (if not already removed). It should remain only in the MyPrefab folder. Create a new BigBulletScript script
This is an analogue of the BulletScript script. This is a tank cartridge script. When colliding with something, it uses GameObject.FindGameObjectsWithTag (“Mob”) to get an array of all game objects with the “Mob” tag. Checks if this object has a MyBehaviour component (or its successor) and, if so, checks the distance to this object. And if this distance is less than atacDistanse, then this object is subjected to damage via the interface (SetDamage method) of MyBehaviour (or any of its heirs).
Now we’ll remove the BulletScript script from the cartridge’s prefab, and instead, hang up the BigBulletScript script.
Make your mobs on the stage not far from each other. We start the game. We shoot so that the cartridge falls between the mobs. Four shots and both mobs are ready. We completely replaced the script on the cartridge, but we did not affect the scripts of the mobs and the player. The BigBullrtScript script does not depend on the mob it gets into. If mobs will be made by one person, and cartridges by another, then all they need to know for coordinated work is that all mobs (in general, anything that can receive damage) carry any (we don’t know which, that is, any ) the heir of the class MyBehaviour. And that this heir has a SetDamage (float damage) method. It is very comfortable. Thus, 100500 types of mobs can be riveted without thinking about the rest of the project code.
To summarize
In this simple project, we used Polymorphism in two places.
1) You can remove MyHp from any mob and hang a new one (another MyHp successor). This is especially convenient for collective development. Suppose a certain project head selects the best behavior script (or the script responsible for health in our case). He changes the options offered to him. And they immediately work without alterations with the Main script. The head of the project does not know how these scripts work, and he will not be able to quickly find the declaration of the desired variable in them and change its type. But if we use polymorphism, this is not necessary. You can edit and add new descendants of the MyHp script and they will perfectly fit into the project. Most importantly, these scripts can be changed not only at the development stage, but also at runtime.
2) The cartridge can attack any mob. Even created after the creation of the cartridge itself. You can create a new cartridge, and do not change the scripts of mobs.
Here is a link to a polymorphism lesson from the creators of Unity unity3d.com/learn/tutorials/modules/intermediate/scripting/polymorphism . In my opinion, it is disgusting and not understandable. I'm not talking about the fact that he is in English. You can read more about polymorphism here sharp-generation.narod.ru/C_Sharp/methods.html . But first, read the first couple of chapters of this tutorial (to which a link is given), otherwise it will not be clear.
We rethink the team approach to creating large projects.
Design can start from different ends. But it’s better to start from above. That is, first decide what will generally be in the game. What mobs, what weapons will be available to the player, etc. We break everything that we thought up into classes and subclasses. For example:
In the figure, the ovals do not mean the objects themselves, but scripts hung on them. for instance
A player is a script that will hang on a player. So, in the picture there is the familiar MyHp and several of its heirs. As you can see, destructible items also have their own MyHp heir, as they react to the damage done. There is a class “Dynamic” in the scheme (they participate in one way or another). If you find all the heirs of this class in the scene (except those tied to the player) and turn them off (the disconnection method will be publicly declared as "Dynamic" (participate in one way or another)), then everything in the game will freeze, except for the player himself. So you can simply realize the effect of stopping time. Although you can do it differently and not create the “Dynamic” class - start with lower classes. “Able to receive damage” is a script (class) that will hang on everything that knows how to receive damage. We have already implemented it and named MyBehavior, but here, MyBehaviour is the heir to MonoBehaviour, and not “Dynamic” because we do not need it ("Dynamic"). Our scheme looks like this:
We have all the scripts (classes) are the heirs of MonoBehaviour. This is what Unity dictates to us.
Let's get back to the big scheme. An “action” is a class whose descendants receive several methods needed to implement events. For example, it can be StartAction (), Open (), Close () methods. The heirs of the “Action” class are all plot scripts, scripts of doors, hatches, etc. This approach allows you to create a universal trigger (button, etc.) that can call any of these methods (or several methods at once) in the Action script array. It is very comfortable. Unity even has a standard script for such a trigger. Only it is slightly improved. Thanks to polymorphism, each descendant of the Action class implements its own methods StartAction (), Open (), Close (). Different doors open differently, different story scripts implement StartAction () in different ways. Door example: a player enters a universal trigger, a door is attached to the trigger (more precisely, the heir to the "Action" class from this door). The trigger calls the Open () method. Setting the trigger consists in checking the box, which method should be called on the specified successor of the "Action" class. That is, it is not necessary to make a button class for each door class. The button is universal. And in it a public variable is declared:
Public ActionScript action; //Переменная для хранения наследника класса ActionScript
Public bool callStartAction=true; //Флаги означающие, что именно надо вызвать
Public bool callOpen =false;
Public bool callClose =false;
Further, in the code of the button (trigger, etc.) under certain circumstances (pressing the button or entering the trigger of the desired object), the desired method of the action object is called
If(callStartAction) { action. StartAction(); }
If(callOpen) { action. Open(); }
If(callClose) { action. Close (); }
Again, look at the large diagram. We find there the player’s motors, zombies, and guns. We have already discussed the concept of the motor. But now there is the Main motor, from which everyone else inherits methods. It will be very good if all motors have the same methods. For example, there may be such methods Jump (), MoveDirection (Vector3 direction), Run (bool run), Stop (), LookTo (Vector3 direction), GoToPoint (Vector3 point). The player needs all methods except the last. And here is the last one for mobs. It may contain a path search algorithm, etc. Thanks to polymorphism, each heir to the main motor will be able to implement these methods in his own way, and it will also be possible to change the motor from one to another. They are completely interchangeable.
Now consider scripts of behavior. “Zombie is not busy”, “Zombie attack”, etc. They are also the heirs of one script. We can distinguish a general method. For example, SetTarget (Transform target). Through this method, you can pass the current target of the mob to the behavior script. After all, it is the heir of the MyBehaviour class who is engaged in the analysis of the situation, therefore, when turning on the desired behavior script, he must transmit the necessary information to him. In a real game, there will probably be more of these methods.
3) After drawing up a similar scheme, we proceed to its implementation. First we make the scripts “parents”. All others will be inherited from them. For example, the script "parent" of the motor can be made as follows:
Red emphasizes a keyword that will allow the heirs of this class to redefine these methods in their own way. The announcement will look like this:
Then, in MonsterBehaviour, we will create a variable of the MyMotor type, in which any descendant of the MyMotor script hung on this mob will be stored. Of course, the mob does not jump in our game. This is just an example. For mobs, you can come up with another method for the MyMotor class and call it SetTarget (Transform target). And when you need to move after the player (or another mob) use this method. And the Mob Mobility will itself decide on the direction of movement of jumps, etc.
The last example clearly shows: all that is needed to implement polymorphism is the Virtual and Override keywords, as well as an explicit indication of the class parent
That's all. A few "words" in the code will make our scripts independent and interchangeable. In general, all scripts must be made independent of each other.
We continue to use polymorphism and other OOP charms in the next article.