Tireur de zombies simple sur Unity

Bonjour à tous! Bientôt, les cours commenceront dans le premier groupe du cours Unity Games Developer . En prévision du début du cours, une leçon ouverte sur la création d'un jeu de tir zombie sur Unity a eu lieu. Le webinaire a été organisé par Nikolai Zapolnov , développeur de jeux senior de Rovio Entertainment Corporation. Il a également écrit un article détaillé que nous portons à votre attention.



Dans cet article, je voudrais montrer à quel point il est facile de créer des jeux dans Unity. Si vous avez des connaissances de base en programmation, vous pouvez rapidement commencer à travailler avec ce moteur et créer votre premier jeu.



Avertissement # 1: Cet article est destiné aux débutants. Si vous avez mangé un chien dans Unity, cela peut vous sembler ennuyeux.

Disclaimer # 2: Pour lire cet article, vous devez avoir au moins des connaissances de base en programmation. Au minimum, les mots «classe» et «méthode» ne devraient pas vous faire peur.

Attention, le trafic sous la coupe!

Introduction à l'unité


Si vous connaissez déjà l'éditeur Unity, vous pouvez ignorer l'introduction et passer directement à la section «Création d'un monde de jeu».

L'unité structurelle de base dans Unity est la «scène». Une scène est généralement un niveau du jeu, bien que dans certains cas, il puisse y avoir plusieurs niveaux à la fois dans une scène ou, inversement, un grand niveau puisse être divisé en plusieurs scènes chargées dynamiquement. Les scènes sont remplies d'objets de jeu et, à leur tour, sont remplis de composants. Ce sont les composants qui mettent en œuvre diverses fonctions de jeu: dessin d'objets, animation, physique, etc. Ce modèle vous permet d'assembler des fonctionnalités à partir de blocs simples, comme un jouet du constructeur Lego.

Vous pouvez écrire des composants vous-même, en utilisant le langage de programmation C # pour cela. C'est ainsi que la logique du jeu est écrite. Ci-dessous, nous verrons comment cela se fait, mais pour l'instant, jetons un coup d'œil au moteur lui-même.

Lorsque vous démarrez le moteur et créez un nouveau projet, vous verrez une fenêtre devant vous où vous pouvez sélectionner quatre éléments principaux:



Dans le coin supérieur gauche de la capture d'écran se trouve la fenêtre «Hiérarchie». Ici, nous pouvons voir la hiérarchie des objets de jeu dans la scène ouverte actuelle. Unity a créé deux objets de jeu pour nous: une caméra («caméra principale») à travers laquelle le joueur verra notre monde de jeu et une «lumière directionnelle» qui illuminera notre scène. Sans cela, nous ne verrions qu'un carré noir.

Au centre se trouve la fenêtre d'édition de scène («Scene»). Ici, nous voyons notre niveau et nous pouvons le modifier visuellement - déplacez et faites pivoter les objets avec la souris et voyez ce qui se passe. À proximité, vous pouvez voir l'onglet «Jeu», qui est actuellement inactif; si vous passez à celui-ci, vous pouvez voir à quoi ressemble le jeu depuis la caméra. Et si vous démarrez le jeu (en utilisant le bouton avec l'icône de lecture dans la barre d'outils), Unity passera à cet onglet, où nous jouerons le jeu lancé.

Dans la partie supérieure droite se trouve la fenêtre «Inspecteur». Dans cette fenêtre, Unity affiche les paramètres de l'objet sélectionné et nous pouvons les modifier. En particulier, nous pouvons voir que la caméra sélectionnée a deux composants - "Transform", qui définit la position de la caméra dans le monde du jeu, et, en fait, "Camera", qui implémente les fonctionnalités de la caméra.

Soit dit en passant, le composant Transformer est sous une forme ou une autre dans tous les objets de jeu dans Unity.

Et enfin, en bas, il y a l'onglet "Projet", où nous pouvons voir tous les soi-disant actifs qui sont dans notre projet. Les actifs sont des fichiers de données tels que des textures, des sprites, des modèles 3D, des animations, des sons et de la musique, des fichiers de configuration. Autrement dit, toutes les données que nous pouvons utiliser pour créer des niveaux ou l'interface utilisateur. Unity comprend un grand nombre de formats standard (par exemple, png et jpg pour les images ou fbx pour les modèles 3D), il n'y aura donc aucun problème de chargement des données dans un projet. Et si vous, comme moi, ne savez pas comment dessiner, les actifs peuvent être téléchargés à partir de l'Unity Asset Store, qui contient une énorme collection de toutes sortes de ressources: gratuites et vendues pour de l'argent.

À droite de l'onglet «Projet», l'onglet «Console» inactif est visible. Unity écrit des avertissements et des messages d'erreur sur la console, assurez-vous donc de vérifier régulièrement. Surtout si quelque chose ne fonctionne pas - très probablement, la console fera allusion à la cause du problème. De plus, la console peut afficher des messages du code du jeu, pour le débogage.

Créez un monde de jeu


Étant donné que je suis un programmeur et que je dessine pire que la patte de poulet, pour les graphismes, j'ai pris des actifs gratuits du Unity Asset Store. Vous pouvez trouver des liens vers eux à la fin de cet article.

A partir de ces atouts, j'ai rassemblé un niveau simple avec lequel nous travaillerons:



Pas de magie, je viens de faire glisser les objets que j'aimais depuis la fenêtre Projet et à l'aide de la souris, je les ai arrangés comme j'aime:



Soit dit en passant, Unity vous permet d'ajouter des objets standard à la scène en un seul clic, comme un cube, une sphère ou un plan. Pour ce faire, cliquez avec le bouton droit de la souris dans la fenêtre Hiérarchie et sélectionnez, par exemple, 3D Object⇨Plane. Donc, l'asphalte de mon niveau est simplement assemblé à partir d'un ensemble d'avions sur lesquels j'ai «tiré» une texture d'un ensemble d'actifs.

NB Si vous vous demandez pourquoi j'ai utilisé beaucoup d'avions, et pas un avec des valeurs à grande échelle, la réponse est assez simple: un avion à grande échelle aura une texture considérablement agrandie, qui ne semblera pas naturelle par rapport aux autres objets de la scène (cela peut être corrigé avec les paramètres matériel, mais nous essayons de faire tout aussi simple que possible, non?)

Zombies à la recherche d'un moyen


Donc, nous avons un niveau de jeu, mais rien ne s'y passe encore. Dans notre jeu, les zombies chassent le joueur et l'attaquent, et pour cela, ils doivent être capables de se déplacer vers le joueur et de contourner les obstacles.

Pour l'implémenter, nous utiliserons l'outil «Navigation Mesh». Sur la base des données de la scène, cet outil calcule les zones dans lesquelles vous pouvez vous déplacer et génère un ensemble de données qui peuvent être utilisées pour rechercher l'itinéraire optimal de n'importe quel point du niveau à n'importe quel autre pendant le jeu. Ces données sont stockées dans l'actif et ne peuvent pas être modifiées à l'avenir - ce processus est appelé «cuisson». Si vous avez besoin d'obstacles à changement dynamique, vous pouvez utiliser le composant NavMeshObstacle, mais ce n'est pas nécessaire pour notre jeu.

Un point important: pour qu'Unity sache quels objets doivent être inclus dans le calcul, dans l'inspecteur de chaque objet (vous pouvez tout sélectionner en même temps dans la fenêtre Hiérarchie), cliquez sur la flèche vers le bas à côté de l'option «Statique» et cochez «Navigation statique»:



En général, les points restants sont également utiles et aident Unity à optimiser le rendu des scènes. Nous ne nous attarderons pas sur eux aujourd'hui, mais lorsque vous aurez fini d'apprendre les bases du moteur, je vous recommande vivement de traiter également d'autres paramètres. Parfois, une seule coche peut augmenter considérablement la fréquence d'images.

Nous allons maintenant utiliser l'élément de menu Fenêtre⇨AI⇨Navigation et dans la fenêtre qui s'ouvre, sélectionnez l'onglet «Bake». Ici, Unity nous proposera de définir des paramètres tels que la hauteur et le rayon du personnage, l'angle d'inclinaison maximum de la terre sur laquelle vous pouvez toujours marcher, la hauteur maximale des marches, etc. Nous ne changerons rien encore et appuyez simplement sur le bouton "Bake".



Unity fera les calculs nécessaires et nous montrera le résultat:



Ici, le bleu indique la zone où vous pouvez marcher. Comme vous pouvez le voir, Unity a laissé un petit côté autour des obstacles - la largeur de ce côté dépend du rayon du personnage. Ainsi, si le centre du personnage se trouve dans la zone bleue, il ne "passera" pas par les obstacles.

Ayant une grille de navigation calculée, nous pouvons utiliser le composant NavMeshAgent pour rechercher l'itinéraire de mouvement et contrôler le mouvement des objets de jeu à notre niveau.

Créons un objet de jeu "Zombie", ajoutons-y un modèle 3D de zombies à partir des ressources, ainsi que le composant NavMeshAgent:



Si vous démarrez le jeu maintenant, rien ne se passera. Nous devons indiquer au composant NavMeshAgent où aller. Pour ce faire, nous allons créer notre premier composant en C #.

Dans la fenêtre du projet, sélectionnez le répertoire racine (il s'appelle «Assets») et dans la liste des fichiers, faites un clic droit pour créer le répertoire «Scripts». Nous y stockerons tous nos scripts afin que le projet soit en ordre. Maintenant, à l'intérieur des "Scripts", créons un script "Zombie" et ajoutons-le à l'objet du jeu zombie:



Double-cliquez sur le script pour l'ouvrir dans l'éditeur. Voyons ce que Unity a créé pour nous.

using System.Collections; using System.Collections.Generic; using UnityEngine; public class Zombie : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } } 

Il s'agit d'un blanc de composant standard. Comme nous pouvons le voir, Unity nous a connecté les bibliothèques System.Collections et System.Collections.Generic (maintenant elles ne sont pas nécessaires, mais elles sont souvent nécessaires dans le code des jeux Unity, elles sont donc incluses dans le modèle standard), ainsi que la bibliothèque UnityEngine, qui contient tous les API du moteur principal.

De plus, Unity a créé la classe Zombie pour nous (le nom correspond au nom du fichier; c'est important: s'ils ne correspondent pas, Unity ne pourra pas faire correspondre le script avec le composant de la scène). La classe est héritée de MonoBehaviour - il s'agit de la classe de base pour les composants créés par l'utilisateur.

Dans la classe, Unity a créé deux méthodes pour nous: Démarrer et Mettre à jour. Le moteur appellera ces méthodes lui-même: Démarrer - immédiatement après le chargement de la scène, et Mettre à jour - chaque image. En fait, il y a beaucoup de ces fonctions appelées par le moteur, mais la plupart d'entre elles ne seront pas nécessaires aujourd'hui. La liste complète, ainsi que la séquence de leur appel, se trouvent toujours dans la documentation: https://docs.unity3d.com/Manual/ExecutionOrder.html

Faisons bouger les zombies sur la carte!

Tout d'abord, nous devons connecter la bibliothèque UnityEngine.AI. Il contient la classe NavMeshAgent et d'autres classes liées à la grille de navigation. Pour ce faire, ajoutez la directive using UnityEngine.AI au début du fichier.

Ensuite, nous devons accéder au composant NavMeshAgent. Pour ce faire, nous pouvons utiliser la méthode standard GetComponent. Il vous permet d'obtenir un lien vers n'importe quel composant du même objet de jeu dans lequel se trouve le composant à partir duquel nous appelons cette méthode (dans notre cas, c'est l'objet de jeu «Zombie»). Nous allons créer le champ NavMeshAgent navMeshAgent dans la classe, dans la méthode Start, nous obtiendrons un lien vers NavMeshAgent et lui demanderons de se déplacer vers le point (0, 0, 0). Nous devrions obtenir ce script:

 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Zombie : MonoBehaviour { NavMeshAgent navMeshAgent; // Start is called before the first frame update void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); navMeshAgent.SetDestination(Vector3.zero); } // Update is called once per frame void Update() { } } 

En commençant le jeu, nous verrons comment le zombie se déplace au centre de la carte:



Zombies pourchassant une victime


Super Mais nos zombies s'ennuient et se sentent seuls, ajoutons la victime d'un joueur au jeu pour lui.

Par analogie avec les zombies, nous allons créer un objet de jeu "Player" (cette fois nous sélectionnerons un modèle 3D d'un officier de police), nous y ajouterons également le composant NavMeshAgent et le script Player fraîchement créé. Nous ne toucherons pas encore au contenu du script Player, mais nous devrons apporter des modifications au script Zombie. En outre, je recommande de définir la valeur de la propriété Priority du lecteur sur 10 dans le composant NavMeshAgent (ou toute autre valeur inférieure à 50 standard, c'est-à-dire en accordant au joueur une priorité plus élevée). Dans ce cas, si le joueur et les zombies se rencontrent sur la carte, les zombies ne pourront pas déplacer le joueur, tandis que le joueur pourra repousser les zombies.

Pour chasser un joueur, un zombie a besoin de connaître sa position. Et pour cela, nous devons obtenir un lien vers celui-ci dans notre classe Zombie en utilisant la méthode standard FindObjectOfType. Après avoir mémorisé le lien, nous pouvons nous tourner vers le composant de transformation du joueur et lui demander la valeur de position. Et pour que le zombie poursuive toujours le joueur, et pas seulement au début du jeu, nous fixerons un objectif pour NavMeshAgent dans la méthode de mise à jour. Vous obtenez le script suivant:

 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Zombie : MonoBehaviour { NavMeshAgent navMeshAgent; Player player; // Start is called before the first frame update void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); player = FindObjectOfType<Player>(); } // Update is called once per frame void Update() { navMeshAgent.SetDestination(player.transform.position); } } 

Exécutez le jeu et assurez-vous que le zombie a trouvé sa victime:



Échapper échapper


Notre joueur se tient comme une idole. Cela ne l'aidera clairement pas à survivre dans un monde aussi agressif, vous devez donc lui apprendre à se déplacer sur la carte.

Pour ce faire, nous devons obtenir des informations sur les touches enfoncées dans Unity. La méthode GetKey de la classe Input standard fournit simplement ces informations!

NB En général, cette façon d'obtenir des entrées n'est pas entièrement canonique. Il est préférable d'utiliser Input.GetAxis et la liaison via les paramètres du projet ⇨ Input Manager. Mieux encore, nouveau système d'entrée . Mais cet article s'est avéré trop long, et donc, faisons-le plus simplement.

Ouvrez le script Player et modifiez-le comme suit:

 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Player : MonoBehaviour { NavMeshAgent navMeshAgent; public float moveSpeed; // Start is called before the first frame update void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); } // Update is called once per frame void Update() { Vector3 dir = Vector3.zero; if (Input.GetKey(KeyCode.LeftArrow)) dir.z = -1.0f; if (Input.GetKey(KeyCode.RightArrow)) dir.z = 1.0f; if (Input.GetKey(KeyCode.UpArrow)) dir.x = -1.0f; if (Input.GetKey(KeyCode.DownArrow)) dir.x = 1.0f; navMeshAgent.velocity = dir.normalized * moveSpeed; } } 

Comme dans le cas des zombies, dans la méthode Start, nous obtenons un lien vers le composant NavMeshAgent du joueur et le stockons dans le champ de classe. Mais maintenant, nous avons également ajouté le champ moveSpeed.
Étant donné que ce champ est public, sa valeur peut être modifiée directement dans l'inspecteur dans Unity! Si vous avez un game designer dans votre équipe, il sera très content de ne pas avoir à entrer dans le code pour éditer les paramètres du joueur.

Définissez 10 comme vitesse:



Dans la méthode de mise à jour, nous utiliserons Input.GetKey pour vérifier si l'une des flèches du clavier est enfoncée et former un vecteur de direction pour le joueur. Notez que nous utilisons les coordonnées X et Z. Cela est dû au fait que dans Unity, l'axe Y regarde vers le ciel, et la terre est située dans le plan XZ.

Après avoir formé un vecteur de direction pour la direction du mouvement dir, nous le normalisons (sinon, si le joueur veut se déplacer en diagonale, le vecteur sera légèrement plus long qu'un seul et ce mouvement sera plus rapide que de se déplacer directement) et multiplier par la vitesse de mouvement donnée. Le résultat est transmis à navMeshAgent.velocity et l'agent fera le reste.

En lançant le jeu, on peut enfin tenter de s'échapper des zombies vers un endroit sûr:



Pour faire bouger la caméra avec le lecteur, écrivons un autre script simple. Appelons cela "PlayerCamera":

 using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerCamera : MonoBehaviour { Player player; Vector3 offset; // Start is called before the first frame update void Start() { player = FindObjectOfType<Player>(); offset = transform.position - player.transform.position; } // Update is called once per frame void LateUpdate() { transform.position = player.transform.position + offset; } } 

La signification de ce script doit être largement comprise. À partir des fonctionnalités - ici, au lieu de Update, nous utilisons LateUpdate. Cette méthode est similaire à Update, mais elle est toujours appelée strictement après la fin de Update pour tous les scripts de la scène. Dans ce cas, nous utilisons LateUpdate, car il est important pour nous que NavMeshAgent calcule la nouvelle position du lecteur avant de déplacer la caméra. Sinon, un effet de «secousses» désagréable peut se produire.

Si vous attachez maintenant ce composant à l'objet de jeu "Appareil photo principal" et démarrez le jeu, le personnage du joueur sera toujours à l'honneur!

Moment d'animation


Pendant un moment, nous nous éloignons des problèmes de survie dans les conditions d'une apocalypse zombie et pensons à l'éternel - à l'art. Nos personnages ressemblent maintenant à des statues animées, mises en mouvement par une force inconnue (peut-être des aimants sous l'asphalte). Et je voudrais qu'ils ressemblent à de vraies personnes vivantes (et pas très) - ils ont bougé leurs bras et leurs jambes. Le composant Animator et un outil appelé Contrôleur Animator nous aideront avec cela.

Animator Controller est une machine à états finis (machine à états), où nous définissons certains états (le personnage est debout, le personnage est activé, le personnage est mourant, etc.), nous leur attachons des animations et définissons les règles de transition d'un état à un autre. Unity passera automatiquement d'une animation à une autre dès que la règle correspondante fonctionnera.

Créons un contrôleur d'animateur pour les zombies. Pour ce faire, créez le répertoire Animations dans le projet (rappelez-vous l'ordre dans le projet), et dans celui-ci - à l'aide du bouton droit - Animator Controller. Et appelons-le "Zombie". Double-cliquez - et l'éditeur apparaîtra devant nous:



Jusqu'à présent, il n'y a aucun État, mais il y a deux points d'entrée («Entrée» et «Tout État») et un point de sortie («Sortie»). Faites glisser quelques animations depuis les ressources:



Comme vous pouvez le voir, dès que nous avons fait glisser la première animation, Unity l'a automatiquement liée au point d'entrée Entry. Il s'agit de la soi-disant animation par défaut. Il sera joué immédiatement après le début du niveau.

Pour passer à un état différent (et lire une autre animation), nous devons créer des règles de transition. Et pour cela, tout d'abord, nous devrons ajouter un paramètre que nous allons définir à partir du code de gestion des animations.

Il y a deux boutons dans le coin supérieur gauche de la fenêtre de l'éditeur: «Calques» et «Paramètres». Par défaut, l'onglet «Couches» est sélectionné, mais nous devons passer à «Paramètres». Maintenant, nous pouvons ajouter un nouveau paramètre de type float en utilisant le bouton "+". Appelons cela «vitesse»:



Maintenant, nous devons dire à Unity que l'animation "Z_run" doit être jouée lorsque la vitesse est supérieure à 0 et "Z_idle_A" lorsque la vitesse est nulle. Pour ce faire, nous devons créer deux transitions: l'une de "Z_idle_A" à "Z_run", et l'autre dans le sens opposé.

Commençons par la transition de l'inactif à l'exécution. Faites un clic droit sur le rectangle "Z_idle_A" et sélectionnez "Effectuer la transition". Une flèche apparaîtra, en cliquant sur laquelle vous pourrez configurer ses paramètres. Tout d'abord, vous devez décocher «A l'heure de sortie». Si cela n'est pas fait, l'animation ne changera pas selon notre condition, mais lorsque la précédente aura fini de jouer. Nous n'en avons pas du tout besoin, alors nous la décochons. Deuxièmement, en bas, dans la liste des conditions («Conditions»), vous devez cliquer sur «+» et Unity nous ajoutera une condition. Les valeurs par défaut dans ce cas sont exactement ce dont nous avons besoin: le paramètre «speed» doit être supérieur à zéro pour passer du ralenti au run.



Par analogie, nous créons une transition dans la direction opposée, mais comme condition nous spécifions maintenant une «vitesse» inférieure à 0,0001. Il n'y a pas de contrôle d'égalité pour les paramètres de type float, ils ne peuvent être comparés que pour plus / moins:



Vous devez maintenant lier le contrôleur à l'objet de jeu. Nous allons sélectionner le modèle 3D du zombie dans la scène (il s'agit d'un enfant de l'objet "Zombie") et faire glisser le contrôleur avec la souris dans le champ correspondant du composant Animator:



Il ne reste plus qu'à écrire un script qui contrôlera le paramètre de vitesse!

Créez le script MovementAnimator avec le contenu suivant:

 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class MovementAnimator : MonoBehaviour { NavMeshAgent navMeshAgent; Animator animator; // Start is called before the first frame update void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); animator = GetComponentInChildren<Animator>(); } // Update is called once per frame void Update() { animator.SetFloat("speed", navMeshAgent.velocity.magnitude); } } 

Ici, comme dans d'autres scripts, dans la méthode Start, nous avons accès à NavMeshAgent. Nous avons également accès au composant Animator, mais comme nous allons attacher le composant "MovementAnimator" à l'objet de jeu "Zombie" et que l'animateur se trouve dans l'objet enfant, au lieu de GetComponent, nous devons utiliser la méthode standard GetComponentInChildren.

Dans la méthode Update, nous demandons à NavMeshAgent son vecteur de vitesse, calculons sa longueur et le transmettons à l'animateur comme paramètre de vitesse. Pas de magie, tout en science!

Ajoutez maintenant le composant MovementAnimator à l'objet de jeu Zombie et, si le jeu démarre, nous voyons que les zombies sont maintenant animés:



Notez que puisque nous avons placé le code de contrôle de l'animateur dans un composant MovementAnimation séparé, il peut être facilement ajouté pour le joueur. Nous n'avons même pas besoin de créer un contrôleur à partir de zéro - vous pouvez copier un contrôleur zombie (cela peut être fait en sélectionnant le fichier "Zombie" et en appuyant sur Ctrl + D) et remplacer les animations dans les rectangles d'état par "m_idle_" et "m_run". Tout le reste est comme un zombie. Je vous laisse cela comme un exercice (enfin, ou téléchargez le code à la fin de l'article).

Un petit ajout utile à faire consiste à ajouter les lignes suivantes à la classe Zombie:

Dans la méthode Start:

 navMeshAgent.updateRotation = false; 

Dans la méthode Update:

 transform.rotation = Quaternion.LookRotation(navMeshAgent.velocity.normalized); 

La première ligne indique à NavMeshAgent qu'il ne doit pas contrôler la rotation du personnage, nous le ferons nous-mêmes. La deuxième ligne définit le tour du personnage dans la même direction où son mouvement est dirigé. NavMeshAgent interpole par défaut l'angle de rotation du personnage et cela n'a pas l'air très joli (le zombie tourne plus lentement que change la direction du mouvement). L'ajout de ces lignes supprime cet effet.

NB Nous utilisons le quaternion pour spécifier la rotation. Dans les graphiques tridimensionnels, les principaux moyens de spécifier la rotation d'un objet sont les angles d'Euler, les matrices de rotation et les quaternions. Les deux premiers ne sont pas toujours pratiques à utiliser et sont également soumis à un effet désagréable tel que «Gimbal Lock». Les quaternions sont privés de cet inconvénient et sont maintenant utilisés presque universellement. Unity fournit des outils pratiques pour travailler avec des quaternions (ainsi qu'avec des matrices et des angles d'Euler), ce qui vous permet de ne pas entrer dans les détails de l'appareil de cet appareil mathématique.

Je vois le but


Génial, maintenant nous pouvons échapper aux zombies. Mais cela ne suffit pas, tôt ou tard un deuxième zombie apparaîtra, puis un troisième, cinquième, dixième ... mais vous ne pouvez pas simplement fuir la foule. Pour survivre, vous devez tuer. De plus, le joueur a déjà un pistolet dans sa main.

Pour que le joueur puisse tirer, vous devez lui donner la possibilité de choisir une cible. Pour ce faire, placez le curseur contrôlé par la souris sur le sol.

Sur l'écran, le curseur de la souris se déplace dans un espace à deux dimensions - la surface du moniteur. En même temps, notre scène de jeu est tridimensionnelle. L'observateur voit la scène à travers son œil, où tous les rayons de lumière convergent en un point. En combinant tous ces rayons, nous obtenons une pyramide de visibilité:



L'œil de l'observateur ne voit que ce qui tombe dans cette pyramide. De plus, le moteur tronque spécifiquement cette pyramide de deux côtés: premièrement, du côté de l'observateur, il y a un écran de contrôle, le soi-disant «plan proche» (sur la figure, il est peint en jaune). Le moniteur ne peut pas afficher physiquement des objets plus proches que l'écran, donc le moteur les coupe. Deuxièmement, étant donné que l'ordinateur a une quantité limitée de ressources, le moteur ne peut pas étendre les rayons à l'infini (par exemple, une certaine plage de valeurs possibles doit être définie pour le tampon de profondeur; de plus, plus il est large, plus la précision est faible), de sorte que la pyramide est coupée derrière ce que l'on appelle «Avion lointain».

Puisque le curseur de la souris se déplace le long du plan proche, nous pouvons libérer le rayon du point où il se trouve profondément dans la scène. Le premier objet avec lequel il se croise sera l'objet vers lequel pointe le curseur de la souris du point de vue de l'observateur.



Pour construire un tel rayon et trouver son intersection avec des objets dans la scène, vous pouvez utiliser la méthode Raycast standard de la classe Physique. Mais si nous utilisons cette méthode, elle trouvera l'intersection avec tous les objets de la scène - terre, murs, zombies ... Mais nous voulons que le curseur se déplace uniquement sur le sol, nous devons donc expliquer à Unity que la recherche d'intersection ne doit être limitée que un ensemble donné d'objets (dans notre cas, uniquement les plans de la terre).

Si vous sélectionnez un objet de jeu dans la scène, dans la partie supérieure de l'inspecteur, vous pouvez voir la liste déroulante "Couche". Par défaut, il y aura une valeur de «Default». En ouvrant la liste déroulante, vous pouvez y trouver l'élément «Ajouter un calque ...», ce qui ouvrira la fenêtre de l'éditeur de calque. Dans l'éditeur, vous devez ajouter une nouvelle couche (appelons-la «Ground»):



Vous pouvez maintenant sélectionner tous les plans au sol de la scène et utiliser cette liste déroulante pour leur affecter la couche au sol. Cela nous permettra d'indiquer dans le script à la méthode Physics.Raycast qu'il est nécessaire de vérifier l'intersection du faisceau uniquement avec ces objets.

Maintenant, glissons le sprite du curseur des actifs vers la scène (j'utilise Spags Assets⇨Textures⇨Demo⇨white_hip⇨white_hip_14):



J'ai ajouté une rotation de 90 degrés autour de l'axe X au curseur afin qu'il repose horizontalement sur le sol, je règle l'échelle à 0,25 pour qu'elle ne soit pas si grande et je règle la coordonnée Y à 0,01. Ce dernier est important pour qu'il n'y ait aucun effet appelé «combat Z». La carte vidéo utilise des calculs en virgule flottante pour déterminer quels objets sont plus proches de la caméra. Si vous placez le curseur sur 0 (c'est-à-dire le même que celui du plan de masse), puis en raison d'erreurs dans ces calculs, pour certains pixels, la carte vidéo décidera que le curseur est plus proche, et pour d'autres, que la terre. De plus, dans différentes images, les ensembles de pixels seront différents, ce qui créera un effet désagréable de faire briller des morceaux de curseur à travers le sol et de "scintiller" lorsqu'il se déplace. La valeur de 0,01 est suffisamment grande pour compenser les erreurs de calcul de la carte vidéo, mais pas si grande que l'œil a remarqué que le curseur est suspendu dans les airs.

Renommez maintenant l'objet jeu en Cursor et créez un script avec le même nom et le contenu suivant:

 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Cursor : MonoBehaviour { SpriteRenderer spriteRenderer; int layerMask; // Start is called before the first frame update void Start() { spriteRenderer = GetComponent<SpriteRenderer>(); layerMask = LayerMask.GetMask("Ground"); } // Update is called once per frame void Update() { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (!Physics.Raycast(ray, out hit, 1000, layerMask)) spriteRenderer.enabled = false; else { transform.position = new Vector3(hit.point.x, transform.position.y, hit.point.z); spriteRenderer.enabled = true; } } } 

Le curseur étant un sprite (dessin en deux dimensions), Unity utilise le composant SpriteRenderer pour le rendre. Nous obtenons un lien vers ce composant dans la méthode Start afin de pouvoir l'activer / le désactiver selon les besoins.

Toujours dans la méthode Start, nous convertissons le nom de la couche «Ground» que nous avons créée précédemment en masque de bits. Unity utilise des opérations au niveau du bit pour filtrer les objets lors de la recherche d'intersections, et la méthode LayerMask.GetMask renvoie le masque de bits correspondant au calque spécifié.

Dans la méthode Update, nous accédons à la caméra principale de la scène à l'aide de Camera.main et lui demandons de recalculer les coordonnées bidimensionnelles de la souris (obtenues à l'aide de Input.mousePosition) en un rayon tridimensionnel. Ensuite, nous transmettons ce rayon à la méthode Physics.Raycast et vérifions s'il recoupe un objet de la scène. Une valeur de 1000 est la distance maximale. En mathématiques, les rayons sont infinis, mais pas les ressources informatiques et la mémoire d'un ordinateur. Par conséquent, Unity nous demande de déterminer une distance maximale raisonnable.

S'il n'y avait pas d'intersection, nous désactivons SpriteRenderer et l'image du curseur disparaît de l'écran. Si l'intersection a été trouvée, nous déplaçons le curseur sur le point d'intersection.Veuillez noter que nous ne modifions pas la coordonnée Y, car le point d'intersection du rayon avec le sol aura Y égal à zéro et en l'affectant à notre curseur, nous obtenons à nouveau l'effet de combat Z, dont nous avons essayé de nous débarrasser ci-dessus. Par conséquent, nous prenons uniquement les coordonnées X et Z du point d'intersection, et Y reste le même.

Ajoutez le composant Cursor à l'objet de jeu Cursor.

Maintenant, finalisons le script Player: tout d'abord, ajoutez le champ curseur Curseur. Ensuite, dans la méthode Start, ajoutez les lignes suivantes:

 cursor = FindObjectOfType<Cursor>(); navMeshAgent.updateRotation = false; 

Et enfin, pour que le joueur se tourne toujours vers le curseur, dans la méthode Update, ajoutez:

 Vector3 forward = cursor.transform.position - transform.position; transform.rotation = Quaternion.LookRotation(new Vector3(forward.x, 0, forward.z)); 

Ici aussi, nous ne prenons pas en compte la coordonnée Y.

Tirez pour survivre


Le simple fait de se tourner vers le curseur ne nous protégera pas des zombies, mais ne fera que soulager le personnage du joueur de l'effet de surprise - maintenant vous ne pouvez plus vous faufiler derrière lui. Pour qu'il puisse vraiment survivre dans les dures réalités de notre jeu, vous devez lui apprendre à tirer. Et quel genre de cliché est-ce s'il n'est pas visible? Tout le monde sait que tout tireur respectable tire toujours des balles traçantes.

Créez un objet de jeu Shot et ajoutez-y le composant LineRenderer standard. En utilisant le champ «Largeur» dans l'éditeur, donnez-lui une petite largeur, par exemple 0,04. Comme nous pouvons le voir, Unity le peint avec une couleur violet vif - de cette façon, les objets sans matériau sont mis en évidence.

Les matériaux sont un élément important de tout moteur tridimensionnel. L'utilisation de matériaux décrit l'apparence de l'objet. Tous les paramètres d'éclairage, textures, shaders - tout cela est décrit par le matériau.

Créons le répertoire Matériaux dans le projet et à l'intérieur, le matériau, appelons-le Jaune. En tant que shader, sélectionnez Non éclairé / Couleur. Ce shader standard n'inclut pas d'éclairage, donc notre balle sera visible même dans l'obscurité. Sélectionnez la couleur jaune:



Maintenant que le matériau est créé, vous pouvez l'affecter à LineRenderer:



Créer un script de prise de vue:

 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Shot : MonoBehaviour { LineRenderer lineRenderer; bool visible; // Start is called before the first frame update void Start() { lineRenderer = GetComponent<LineRenderer>(); } // Update is called once per frame void FixedUpdate() { if (visible) visible = false; else gameObject.SetActive(false); } public void Show(Vector3 from, Vector3 to) { lineRenderer.SetPositions(new Vector3[]{ from, to }); visible = true; gameObject.SetActive(true); } } 

Ce script, comme vous l'avez probablement déjà deviné, doit être ajouté à l'objet de jeu Shot.

Ici, j'ai utilisé une petite astuce pour afficher une photo à l'écran pour exactement une image avec un minimum de code. Tout d'abord, j'utilise FixedUpdate au lieu de Update. La méthode FixedUpdate est appelée à la fréquence spécifiée (par défaut - 60 images par seconde), même si la fréquence d'images réelle est instable. Deuxièmement, j'ai défini la variable visible, que j'ai définie sur true lorsque j'affiche la photo à l'écran. Dans la prochaine mise à jour fixe, je la réinitialise sur false et ce n'est que dans la prochaine image que je désactive l'objet de jeu du tir. Essentiellement, j'utilise une variable booléenne comme compteur de 1 à 0.

La méthode gameObject.SetActive active ou désactive tout l'objet de jeu sur lequel se trouve notre composant. Les objets de jeu désactivés ne sont pas dessinés à l'écran et leurs composants n'appellent pas les méthodes Update, FixedUpdate, etc. Cette méthode vous permet de rendre le plan invisible lorsque le joueur ne tire pas.

Il existe également une méthode Show publique dans le script, que nous utiliserons dans le script Player pour afficher la balle lors du tir.

Mais vous devez d'abord être en mesure d'obtenir les coordonnées du canon du pistolet afin que le tir provienne du bon trou. Pour ce faire, recherchez l'objet Bip001⇨Bip001 Pelvis⇨Bip001 Spine⇨Bip001 R Clavicle⇨Bip001 R UpperArm⇨Bip001 R Forearm⇨Bip001 R Hand⇨R_hand_container⇨w_handgun dans le modèle 3D du joueur et ajoutez-y l'objet enfant GunBarrel. Placez-le de manière à ce qu'il soit juste à côté du canon:



maintenant dans le script Player, ajoutez les champs:

 Shot shot; public Transform gunBarrel; 


Ajoutez à la méthode Start du script Player:

 shot = FindObjectOfType<Shot>(); 

Et dans la méthode Update:

 if (Input.GetMouseButtonDown(0)) { var from = gunBarrel.position; var target = cursor.transform.position; var to = new Vector3(target.x, from.y, target.z); shot.Show(from, to); } 

Comme vous pouvez le deviner, le champ public gunBarrel ajouté, comme moveSpeed ​​plus tôt, sera disponible dans l'inspecteur. Attribuons-lui le véritable objet de jeu que nous avons créé:



si nous commençons maintenant le jeu, nous pouvons enfin tirer sur les zombies!



Quelque chose ne va pas ici! Il semble que les tirs ne tuent pas les zombies, mais volent simplement à travers!

Eh bien, bien sûr, si vous regardez notre code de tir, nous ne suivons en aucune façon si notre tir a frappé l'ennemi ou non. Tracez simplement une ligne jusqu'au curseur.

C'est assez facile à corriger. Dans le code de traitement des clics de souris dans la classe Player, après la ligne var to = ... et avant la ligne shot.Show (...), ajoutez les lignes suivantes:

 var direction = (to - from).normalized; RaycastHit hit; if (Physics.Raycast(from, to - from, out hit, 100)) to = new Vector3(hit.point.x, from.y, hit.point.z); else to = from + direction * 100; 

Ici, nous utilisons le Physics.Raycast familier, pour laisser le faisceau sortir du canon d'un pistolet et déterminer s'il intersecte avec un objet de jeu.

Ici, cependant, il y a une mise en garde: la balle volera toujours à travers les zombies. Le fait est que l'auteur de l'actif a ajouté un collisionneur aux objets du niveau (bâtiments, boîtes, etc.). Et l'auteur de l'actif avec les personnages ne l'a pas fait. Corrigeons ce malentendu ennuyeux.

Un collisionneur est un composant avec lequel le moteur physique détermine les collisions entre les objets. Des formes géométriques généralement simples sont utilisées comme collisionneurs - cubes, sphères, etc. Bien que cette approche fournisse des collisions moins précises, les formules d'intersection entre de tels objets sont assez simples et ne nécessitent pas de grandes ressources de calcul. Bien sûr, si vous avez besoin d'une précision maximale, vous pouvez toujours sacrifier les performances et utiliser le MeshCollider. Mais nous n'avons pas besoin d'une grande précision, nous allons donc utiliser le composant CapsuleCollider:



maintenant la balle ne volera pas à travers les zombies. Cependant, les zombies sont toujours immortels.

Zombies - Zombie Death!


Ajoutons d'abord une animation de mort au contrôleur d'animation zombie. Pour ce faire, faites-y glisser l'animation AssetPacks⇨ToonyTinyPeople⇨TT_demo⇨animation⇨zombie⇨Z_death_A. Pour l'activer, créez un nouveau paramètre mort avec le type de déclencheur. Contrairement à d'autres paramètres (bool, float, etc.), les déclencheurs ne se souviennent pas de leur état et ressemblent plus à un appel de fonction: ils ont activé un déclencheur - la transition a fonctionné et le déclencheur a été réinitialisé. Et puisqu'un zombie peut mourir dans n'importe quel état - et s'il reste immobile et s'il est en cours d'exécution, nous ajouterons la transition de l'état Any State:



Ajoutez les champs suivants au script Zombie:

 CapsuleCollider capsuleCollider; Animator animator; MovementAnimator movementAnimator; bool dead; 

Dans la méthode Start de la classe Zombie, insérez:

 capsuleCollider = GetComponent<CapsuleCollider>(); animator = GetComponentInChildren<Animator>(); movementAnimator = GetComponent<MovementAnimator>(); 

Au tout début de la méthode Update, vous devez ajouter une vérification:

 if (dead) return; 

Et enfin, ajoutez la méthode publique Kill à la classe Zombie:

 public void Kill() { if (!dead) { dead = true; Destroy(capsuleCollider); Destroy(movementAnimator); Destroy(navMeshAgent); animator.SetTrigger("died"); } } 

L'attribution de nouveaux domaines, je pense, est assez évidente. Quant à la méthode Kill - nous y définissons (si nous ne sommes pas morts) le drapeau de mort zombie et supprimons les composants CapsuleCollider, MovementAnimator et NavMeshAgent de notre objet de jeu, après quoi nous activons la lecture de l'animation de la mort à partir du contrôleur d'animation.

Pourquoi retirer des composants? De sorte que dès qu'un zombie meurt, il cesse de se déplacer sur la carte et n'est plus un obstacle aux balles. Pour de bon, vous devez toujours vous débarrasser du corps d'une manière ou d'une autre d'une manière magnifique après la lecture de l'animation de la mort. Sinon, les zombies morts continueront de ronger les ressources et, s'il y a trop de cadavres, le jeu ralentira sensiblement. Le moyen le plus simple consiste à ajouter ici l'appel Destroy (gameObject, 3). Cela entraînera la suppression de cet objet de jeu par Unity 3 secondes après cet appel.

Pour que tout cela fonctionne finalement, la dernière touche est restée. Dans la classe Player, dans la méthode Update, où nous appelons Physics.Raycast, dans la branche pour le cas où une intersection a été trouvée, nous ajoutons une vérification:

 if (hit.transform != null) { var zombie = hit.transform.GetComponent<Zombie>(); if (zombie != null) zombie.Kill(); } 

Physics.Raycast appelle les informations d'intersection dans la variable de hit. En particulier, dans le champ de transformation, il y aura un lien vers le composant Transformer de l'objet de jeu avec lequel le rayon s'est croisé. Si cet objet de jeu a un composant Zombie, alors c'est un zombie et nous le tuons. Élémentaire!

Eh bien, pour que la mort de l'ennemi soit spectaculaire, nous ajoutons un système de particules simple aux zombies.

Les systèmes de particules vous permettent de contrôler un grand nombre de petits objets (généralement des sprites) selon une sorte de loi physique ou de formule mathématique. Par exemple, vous pouvez les faire voler à part ou voler directement vers le bas à une certaine vitesse. Avec l'aide de systèmes de particules dans les jeux, toutes sortes d'effets sont faits: feu, fumée, étincelles, pluie, neige, saleté sous les roues, etc. Nous utiliserons un système de particules pour qu'au moment de la mort, du sang gicle d'un zombie.

Ajoutez un système de particules à l'objet de jeu Zombie (faites un clic droit dessus et sélectionnez Effets ⇨ Système de particules):

Je suggère les options suivantes:
Transformer:

  • Position: Y 0,5
  • Rotation: X -90

Système de particules
  • Durée: 0,2
  • Boucle: faux
  • Durée de vie initiale: 0,8
  • Taille de départ: 0,5
  • Couleur de départ: vert
  • Modificateur de gravité: 1
  • Jouer sur éveillé: faux
  • Émission:
  • Taux au fil du temps: 100
  • Forme:
  • Rayon: 0,25

Cela devrait ressembler à ceci: Il



reste à l'activer dans la méthode Kill de la classe Zombie:

 GetComponentInChildren<ParticleSystem>().Play(); 

Et maintenant une toute autre affaire!



Des zombies attaquent en bande


En fait, combattre un seul zombie est ennuyeux. Tu l'as tué et c'est tout. Où est le drame? Où est la peur de mourir jeune? Pour créer une véritable atmosphère d'apocalypse et de désespoir, il devrait y avoir beaucoup de zombies.

Heureusement, c'est assez simple. Comme vous l'avez peut-être deviné, nous avons besoin d'un autre script. Appelez-le EnemySpawner et remplissez-le avec le contenu suivant:

 using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemySpawner : MonoBehaviour { public float Period; public GameObject Enemy; float TimeUntilNextSpawn; // Start is called before the first frame update void Start() { TimeUntilNextSpawn = Random.Range(0, Period); } // Update is called once per frame void Update() { TimeUntilNextSpawn -= Time.deltaTime; if (TimeUntilNextSpawn <= 0.0f) { TimeUntilNextSpawn = Period; Instantiate(Enemy, transform.position, transform.rotation); } } } 

À l'aide du champ public Période, le concepteur de jeu peut définir dans l'inspecteur la fréquence à laquelle un nouvel ennemi doit être créé. Dans le champ Ennemi, nous indiquons quel ennemi créer (jusqu'à présent, nous n'avons qu'un seul ennemi, mais à l'avenir, nous pourrons en ajouter d'autres). Eh bien, alors tout est simple - en utilisant TimeUntilNextSpawn, nous comptons combien de temps il reste jusqu'à la prochaine apparition de l'ennemi et, dès que le moment est venu, nous ajoutons un nouveau zombie à la scène en utilisant la méthode standard Instantiate. Oh oui, dans la méthode Start, nous attribuons une valeur aléatoire au champ TimeUntilNextSpawn, de sorte que si nous avons plusieurs générateurs avec le même retard dans le niveau, ils n'ajouteront pas de zombies en même temps.

Une question demeure - comment demander à l'ennemi dans le champ ennemi? Pour ce faire, nous utiliserons un outil Unity tel que «Prefabs». En fait, un préfabriqué est un morceau de la scène enregistré dans un fichier séparé. Ensuite, nous pouvons insérer ce fichier dans d'autres scènes (ou dans la même) et nous n'avons plus besoin de le collecter à partir de morceaux à chaque fois. Par exemple, nous avons collecté, à partir des objets des murs, du sol, du plafond, des fenêtres et des portes, une belle maison et l'avons sauvegardée comme préfabriquée. Vous pouvez maintenant insérer cette maison dans d'autres cartes d'un simple mouvement de poignet. Dans le même temps, si vous modifiez le fichier préfabriqué (par exemple, ajoutez une porte dérobée à la maison), l'objet changera dans toutes les scènes. Parfois, c'est très pratique. Nous pouvons également utiliser des préfabriqués comme modèles pour Instantiate - et nous utiliserons cette opportunité dès maintenant.

Pour créer un préfabriqué, faites simplement glisser l'objet de jeu de la fenêtre de hiérarchie vers la fenêtre de projet, Unity fera le reste. Créons un préfabriqué à partir de zombies, puis ajoutons un générateur ennemi à la scène:



j'ai ajouté trois autres générateurs dans le projet pour un changement (donc, au final, j'en ai 4). Et alors, que s'est-il passé:



ici! Cela ressemble déjà à une apocalypse zombie!

Conclusion


Bien sûr, c'est loin d'être un jeu complet. Nous n'avons pas pris en compte de nombreux problèmes, tels que la création d'une interface utilisateur, les sons, la vie et la mort d'un joueur - tout cela est exclu du champ d'application de cet article. Mais il me semble que cet article sera une bonne introduction à Unity pour ceux qui ne connaissent pas cet outil. Ou peut-être qu'une personne expérimentée pourra en tirer quelque chose?

En général, mes amis, j'espère que vous avez apprécié mon article. Écrivez vos questions dans les commentaires, j'essaierai d'y répondre. Le code source du projet peut être téléchargé sur le github: https://github.com/zapolnov/otus_zombies . Vous aurez besoin d'Unity 2019.3.0f3 ou supérieur, il peut être téléchargé entièrement gratuitement et sans SMS depuis le site officiel: https://store.unity.com/download .

Liens vers les actifs utilisés dans l'article:

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


All Articles