Le terrain
- Création d'un champ de tuiles.
- Chemins de recherche utilisant une recherche en largeur.
- Implémentez la prise en charge des tuiles vides et finales, ainsi que des tuiles murales.
- Modification du contenu en mode jeu.
- Affichage facultatif des champs et des chemins de la grille.
Ceci est la première partie d'une série de tutoriels sur la création d'un jeu de
tower defense simple. Dans cette partie, nous envisagerons de créer un terrain de jeu, de trouver un chemin et de placer les tuiles et les murs finaux.
Le didacticiel a été créé dans Unity 2018.3.0f2.
Un champ prêt à l'emploi dans un jeu de tuiles de genre tower defense.Jeu de Tower Defense
Le Tower Defense est un genre dans lequel le but du joueur est de détruire des foules d'ennemis jusqu'à ce qu'ils atteignent leur point final. Le joueur atteint son objectif en construisant des tours qui attaquent les ennemis. Ce genre a beaucoup de variations. Nous allons créer un jeu avec un champ de tuiles. Les ennemis se déplaceront à travers le champ vers leur point final, et le joueur leur créera des obstacles.
Je suppose que vous avez déjà étudié une série de tutoriels sur la
gestion des objets .
Le terrain
Le terrain de jeu est la partie la plus importante du jeu, nous allons donc le créer en premier. Ce sera un objet de jeu avec son propre composant
GameBoard
, qui peut être initialisé en définissant la taille en deux dimensions, pour laquelle nous pouvons utiliser la valeur de
Vector2Int
. Le champ devrait fonctionner avec n'importe quelle taille, mais nous choisirons la taille ailleurs, nous allons donc créer une méthode
Initialize
commune pour cela.
De plus, nous visualisons le champ avec un quadrilatère, qui désignera la terre. Nous ne ferons pas de l'objet champ lui-même un quadrilatère, mais nous y ajouterons un objet enfant quad. Lors de l'initialisation, nous rendrons l'échelle XY de la terre égale à la taille du champ. Autrement dit, chaque tuile aura une taille d'une unité de mesure carrée pour le moteur.
using UnityEngine; public class GameBoard : MonoBehaviour { [SerializeField] Transform ground = default; Vector2Int size; public void Initialize (Vector2Int size) { this.size = size; ground.localScale = new Vector3(size.x, size.y, 1f); } }
Pourquoi définir explicitement Ground à la valeur par défaut?L'idée est que tout ce qui est personnalisable via l'éditeur Unity est accessible via des champs cachés sérialisés. Il est nécessaire que ces champs ne puissent être modifiés que dans l'inspecteur. Malheureusement, l'éditeur Unity affichera constamment un compilateur avertissant que la valeur n'est jamais affectée. Nous pouvons supprimer cet avertissement en définissant explicitement la valeur par défaut du champ. Vous pouvez également attribuer null
, mais je l'ai fait de manière à montrer explicitement que nous utilisons simplement la valeur par défaut, qui n'est pas une véritable référence à la terre, nous utilisons donc la default
.
Créez un objet champ dans une nouvelle scène et ajoutez un quad enfant avec un matériau qui ressemble à la terre. Puisque nous créons un jeu prototype simple, un matériau vert uniforme sera suffisant. Faites pivoter le quad de 90 ° le long de l'axe X afin qu'il se trouve sur le plan XZ.
Terrain de jeu.Pourquoi ne pas positionner le jeu sur l'avion XY?Bien que le jeu se déroule dans un espace 2D, nous le rendrons en 3D, avec des ennemis 3D et une caméra pouvant être déplacée par rapport à un certain point. Le plan XZ est plus pratique pour cela et correspond à l'orientation skybox standard utilisée pour l'éclairage ambiant.
Le jeu
Ensuite, créez un composant de
Game
qui sera responsable de l'ensemble du jeu. A ce stade, cela signifie qu'il initialise le champ. Nous rendons simplement la taille personnalisable via l'inspecteur et forçons le composant à initialiser le champ lorsqu'il se réveille. Utilisons la taille par défaut de 11 × 11.
using UnityEngine; public class Game : MonoBehaviour { [SerializeField] Vector2Int boardSize = new Vector2Int(11, 11); [SerializeField] GameBoard board = default; void Awake () { board.Initialize(boardSize); } }
La taille des champs ne peut être que positive et il est peu logique de créer un champ avec une seule tuile. Limitons donc le minimum à 2 × 2. Cela peut être fait en ajoutant la méthode
OnValidate
, en limitant de force les valeurs minimales.
void OnValidate () { if (boardSize.x < 2) { boardSize.x = 2; } if (boardSize.y < 2) { boardSize.y = 2; } }
Quand on appelle Onvalidate?S'il existe, l'éditeur Unity l'appelle pour les composants après les avoir modifiés. Y compris lors de leur ajout à l'objet de jeu, après le chargement de la scène, après la recompilation, après le changement dans l'éditeur, après l'annulation / la nouvelle tentative et après la réinitialisation du composant.
OnValidate
est le seul endroit du code où vous pouvez affecter des valeurs aux champs de configuration des composants.
Objet de jeu.Maintenant, lorsque vous démarrez le mode de jeu, nous recevrons un champ avec la bonne taille. Pendant le jeu, positionnez la caméra de manière à ce que tout le plateau soit visible, copiez son composant de transformation, quittez le mode de jeu et collez les valeurs du composant. Dans le cas d'un champ 11 × 11 à l'origine, pour obtenir une vue pratique d'en haut, vous pouvez positionner la caméra en position (0.10.0) et la faire pivoter de 90 ° le long de l'axe X. Nous laisserons la caméra dans cette position fixe, mais c'est possible changer à l'avenir.
Caméra sur le terrain.Comment copier et coller des valeurs de composants?Grâce au menu déroulant qui apparaît lorsque vous cliquez sur le bouton avec l'engrenage dans le coin supérieur droit du composant.
Tuile préfabriquée
Le champ se compose de carreaux carrés. Les ennemis pourront se déplacer d'une tuile à l'autre, en traversant les bords, mais pas en diagonale. Le mouvement se produira toujours vers le point final le plus proche. Désignons graphiquement la direction du mouvement le long de la tuile avec une flèche. Vous pouvez télécharger la texture de la flèche
ici .
Flèche sur fond noir.Placez la texture de la flèche dans votre projet et activez l'option
Alpha comme transparence . Créez ensuite un matériau pour la flèche, qui peut être le matériau par défaut pour lequel le mode de découpe est sélectionné, puis sélectionnez la flèche comme texture principale.
Matériel de flèche.Pourquoi utiliser le mode de rendu découpé?Il vous permet d'obscurcir la flèche à l'aide du pipeline de rendu Unity standard.
Pour désigner chaque tuile du jeu, nous utiliserons l'objet du jeu. Chacun d'eux aura son propre quadruple avec un matériau de flèche, tout comme le champ a un quadruple de terre. Nous ajouterons également des tuiles au composant GameTile avec un lien vers leur flèche.
using UnityEngine; public class GameTile : MonoBehaviour { [SerializeField] Transform arrow = default; }
Créez un objet tuile et transformez-le en préfabriqué. Les tuiles seront au ras du sol, alors relevez un peu la flèche pour éviter les problèmes de profondeur lors du rendu. Faites également un zoom arrière un peu, de sorte qu'il y ait peu d'espace entre les flèches adjacentes. Un décalage Y de 0,001 et une échelle de 0,8 identique pour tous les axes feront l'affaire.
Tuile préfabriquée.Où est la hiérarchie de tuiles préfabriquées?Vous pouvez ouvrir le mode d'édition de préfabriqué en double-cliquant sur l'élément préfabriqué ou en sélectionnant le préfabriqué et en cliquant sur le bouton Ouvrir le préfabriqué dans l'inspecteur. Vous pouvez quitter le mode d'édition préfabriqué en cliquant sur le bouton avec une flèche dans le coin supérieur gauche de son en-tête de hiérarchie.
Notez que les tuiles elles-mêmes ne doivent pas nécessairement être des objets de jeu. Ils ne sont nécessaires que pour suivre l'état du champ. Nous pourrions utiliser la même approche que pour le comportement dans la série de didacticiels sur la
gestion d'objets . Mais dans les premiers stades de jeux simples ou de prototypes d'objets de jeu, nous sommes très heureux. Cela peut être changé à l'avenir.
Nous avons des tuiles
Pour créer des tuiles, le
GameBoard
doit avoir un lien vers le préfabriqué de tuiles.
[SerializeField] GameTile tilePrefab = default;
Lien vers la tuile préfabriquée.Il peut ensuite créer ses instances à l'aide d'une double boucle sur deux dimensions de la grille. Bien que la taille soit exprimée en X et Y, nous disposerons les tuiles sur le plan XZ, ainsi que le champ lui-même. Puisque le champ est centré par rapport à l'origine, nous devons soustraire la taille correspondante moins une divisée par deux des composants de la position de la tuile. Veuillez noter que ce doit être une division en virgule flottante, sinon cela ne fonctionnera pas pour des tailles égales.
public void Initialize (Vector2Int size) { this.size = size; ground.localScale = new Vector3(size.x, size.y, 1f); Vector2 offset = new Vector2( (size.x - 1) * 0.5f, (size.y - 1) * 0.5f ); for (int y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++) { GameTile tile = Instantiate(tilePrefab); tile.transform.SetParent(transform, false); tile.transform.localPosition = new Vector3( x - offset.x, 0f, y - offset.y ); } } }
Création d'instances de tuiles.Plus tard, nous aurons besoin d'accéder à ces tuiles, nous allons donc les suivre dans un tableau. Nous n'avons pas besoin d'une liste, car après l'initialisation, la taille du champ ne changera pas.
GameTile[] tiles; public void Initialize (Vector2Int size) { … tiles = new GameTile[size.x * size.y]; for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { GameTile tile = tiles[i] = Instantiate(tilePrefab); … } } }
Comment fonctionne cette mission?Il s'agit d'une affectation liée. Dans ce cas, cela signifie que nous attribuons un lien à l'instance de tuile à la fois à l'élément de tableau et à la variable locale. Ces opérations fonctionnent de la même manière que le code ci-dessous.
GameTile t = Instantiate(tilePrefab); tiles[i] = t; GameTile tile = t;
Rechercher un moyen
À ce stade, chaque tuile a une flèche, mais elles pointent toutes dans la direction positive de l'axe Z, que nous interpréterons comme nord. L'étape suivante consiste à déterminer la bonne direction pour la tuile. Nous le faisons en trouvant le chemin que les ennemis doivent suivre jusqu'au point final.
Voisins de tuile
Les chemins vont de tuile en tuile, au nord, à l'est, au sud ou à l'ouest. Pour simplifier la recherche,
GameTile
des liens de suivi
GameTile
vers ses quatre voisins.
GameTile north, east, south, west;
Les relations entre voisins sont symétriques. Si la tuile est le voisin oriental de la deuxième tuile, alors la seconde est le voisin occidental de la première. Ajoutez une méthode statique générale à
GameTile
pour définir cette relation entre deux tuiles.
public static void MakeEastWestNeighbors (GameTile east, GameTile west) { west.east = east; east.west = west; }
Pourquoi utiliser une méthode statique?Nous pouvons en faire une méthode d'instance avec un seul paramètre, et dans ce cas, nous l'appellerons eastTile.MakeEastWestNeighbors(westTile)
ou quelque chose comme ça. Mais dans les cas où il n'est pas clair sur laquelle des tuiles la méthode doit être appelée, il est préférable d'utiliser des méthodes statiques. Les exemples sont les méthodes Distance
et Dot
de la classe Vector3
.
Une fois connecté, il ne devrait jamais changer. Si cela se produit, nous avons fait une erreur dans le code. Vous pouvez le vérifier en comparant les deux liens avant d'attribuer des valeurs à
null
et en affichant une erreur si elle est incorrecte. Vous pouvez utiliser la méthode
Debug.Assert
pour
Debug.Assert
.
public static void MakeEastWestNeighbors (GameTile east, GameTile west) { Debug.Assert( west.east == null && east.west == null, "Redefined neighbors!" ); west.east = east; east.west = west; }
Que fait Debug.Assert?Si le premier argument est false
, il affiche une erreur de condition, en utilisant le deuxième argument s'il est spécifié. Un tel appel est inclus uniquement dans les versions de test, mais pas dans les versions. Par conséquent, c'est un bon moyen d'ajouter des vérifications au cours du processus de développement qui n'affecteront pas la version finale.
Ajoutez une méthode similaire pour créer des relations entre les voisins du nord et du sud.
public static void MakeNorthSouthNeighbors (GameTile north, GameTile south) { Debug.Assert( south.north == null && north.south == null, "Redefined neighbors!" ); south.north = north; north.south = south; }
Nous pouvons établir cette relation lors de la création de tuiles dans
GameBoard.Initialize
. Si la coordonnée X est supérieure à zéro, alors nous pouvons créer une relation est-ouest entre les tuiles actuelles et précédentes. Si la coordonnée Y est supérieure à zéro, alors nous pouvons créer une relation nord-sud entre la tuile actuelle et la tuile de la ligne précédente.
for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … if (x > 0) { GameTile.MakeEastWestNeighbors(tile, tiles[i - 1]); } if (y > 0) { GameTile.MakeNorthSouthNeighbors(tile, tiles[i - size.x]); } } }
Notez que les tuiles sur les bords du champ n'ont pas quatre voisins. Une ou deux références voisines resteront
null
.
Distance et direction
Nous n'obligerons pas tous les ennemis à chercher constamment le chemin. Cela ne doit être fait qu'une seule fois par tuile. Ensuite, les ennemis pourront demander à la tuile dans laquelle ils se trouvent où aller. Nous
GameTile
ces informations dans
GameTile
en ajoutant un lien vers la
GameTile
de chemin suivante. En outre, nous enregistrerons également la distance jusqu'au point final, exprimée en nombre de tuiles qui doivent être visitées avant que l'ennemi n'atteigne le point final. Pour les ennemis, ces informations sont inutiles, mais nous les utiliserons pour trouver les chemins les plus courts.
GameTile north, east, south, west, nextOnPath; int distance;
Chaque fois que nous décidons que nous devons rechercher des chemins, nous devons initialiser les données de chemin. Jusqu'à ce que le chemin soit trouvé, il n'y a pas de tuile suivante et la distance peut être considérée comme infinie. Nous pouvons imaginer cela comme la valeur entière maximale possible de
int.MaxValue
. Ajoutez une méthode générique
ClearPath
pour réinitialiser le
GameTile
à cet état.
public void ClearPath () { distance = int.MaxValue; nextOnPath = null; }
Les chemins ne peuvent être recherchés que si nous avons un point de terminaison. Cela signifie que la tuile doit devenir le point de terminaison. Une telle tuile a une distance de zéro, et elle n'a pas la dernière tuile, car le chemin se termine dessus. Ajoutez une méthode générique qui transforme une tuile en point de terminaison.
public void BecomeDestination () { distance = 0; nextOnPath = null; }
En fin de compte, toutes les tuiles devraient se transformer en un chemin, de sorte que leur distance ne sera plus égale à
int.MaxValue
. Ajoutez une propriété getter pratique pour vérifier si la tuile a actuellement un chemin.
public bool HasPath => distance != int.MaxValue;
Comment fonctionne cette propriété?Il s'agit d'une entrée raccourcie pour une propriété getter contenant une seule expression. Il fait la même chose que le code ci-dessous.
public bool HasPath { get { return distance != int.MaxValue; } }
L'opérateur de flèche
=>
peut également être utilisé individuellement pour le getter et le setter de propriétés, pour les corps de méthodes, les constructeurs et dans d'autres endroits.
Nous grandissons
Si nous avons une tuile avec un chemin, nous pouvons la laisser pousser un chemin vers l'un de ses voisins. Initialement, la seule tuile avec le chemin est le point final, donc nous partons de la distance zéro et l'augmentons à partir d'ici, en nous déplaçant dans la direction opposée au mouvement des ennemis. Autrement dit, tous les voisins immédiats du point de terminaison auront une distance de 1, et tous les voisins de ces tuiles auront une distance de 2, et ainsi de suite.
Ajoutez une méthode masquée
GameTile
pour développer le chemin vers l'un de ses voisins, spécifié via le paramètre. La distance par rapport au voisin est une de plus que la tuile actuelle, et le chemin du voisin indique la tuile actuelle. Cette méthode ne doit être appelée que pour les tuiles qui ont déjà un chemin, vérifions donc cela avec assert.
void GrowPathTo (GameTile neighbor) { Debug.Assert(HasPath, "No path!"); neighbor.distance = distance + 1; neighbor.nextOnPath = this; }
L'idée est que nous appelons cette méthode une fois pour chacun des quatre voisins de la tuile. Étant donné que certains de ces liens seront
null
, nous allons vérifier cela et arrêter l'exécution, le cas échéant. De plus, si un voisin a déjà un chemin, nous ne devons rien faire et cesser de le faire.
void GrowPathTo (GameTile neighbor) { Debug.Assert(HasPath, "No path!"); if (neighbor == null || neighbor.HasPath) { return; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; }
La façon dont
GameTile
suit ses voisins est inconnue du reste du code. Par conséquent,
GrowPathTo
est masqué. Nous ajouterons des méthodes générales qui indiquent à la tuile de se développer dans une certaine direction, en appelant indirectement
GrowPathTo
. Mais le code qui recherche dans tout le champ doit garder une trace des tuiles visitées. Par conséquent, nous lui ferons retourner un voisin ou
null
si l'exécution est terminée.
GameTile GrowPathTo (GameTile neighbor) { if (!HasPath || neighbor == null || neighbor.HasPath) { return null; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; return neighbor; }
Ajoutez maintenant des méthodes pour développer des chemins dans des directions spécifiques.
public GameTile GrowPathNorth () => GrowPathTo(north); public GameTile GrowPathEast () => GrowPathTo(east); public GameTile GrowPathSouth () => GrowPathTo(south); public GameTile GrowPathWest () => GrowPathTo(west);
Recherche large
GameBoard
doit
GameBoard
que toutes les tuiles contiennent les données de chemin correctes. Pour ce faire, nous effectuons une recherche en premier. Commençons par la tuile de point de terminaison, puis développons le chemin vers ses voisins, puis vers les voisins de ces tuiles, etc. À chaque étape, la distance augmente d'une unité et les chemins ne croissent jamais en direction des tuiles qui ont déjà des chemins. Cela garantit que toutes les tuiles pointeront le long du chemin le plus court vers le point final.
Que diriez-vous de trouver un chemin en utilisant A *?L'algorithme A
* est le développement évolutif de la recherche en largeur d'abord. Il est utile lorsque nous recherchons le seul chemin le plus court. Mais nous avons besoin de tous les chemins les plus courts, donc A
* ne donne aucun avantage. Pour des exemples de recherche en largeur et A
* sur une grille d'hexagones avec animation, voir la série de tutoriels sur les
cartes d'hexagones .
Pour effectuer la recherche, nous devons suivre les tuiles que nous avons ajoutées au chemin, mais à partir desquelles nous n'avons pas encore développé le chemin. Cette collection de tuiles est souvent appelée la frontière de recherche. Il est important que les tuiles soient traitées dans le même ordre dans lequel elles sont ajoutées à la bordure, utilisons donc la
Queue
. Plus tard, nous devrons effectuer la recherche plusieurs fois, nous allons donc le définir comme le champ du
GameBoard
.
using UnityEngine; using System.Collections.Generic; public class GameBoard : MonoBehaviour { … Queue<GameTile> searchFrontier = new Queue<GameTile>(); … }
Pour que l'état du terrain de jeu soit toujours vrai, nous devons trouver les chemins à la fin de
Initialize
, mais placer le code dans une méthode
FindPaths
distincte. Tout d'abord, vous devez effacer le chemin de toutes les tuiles, puis faire d'une tuile le point final et l'ajouter à la bordure. Sélectionnons d'abord la première tuile. Comme les
tiles
sont un tableau, nous pouvons utiliser la
foreach
sans crainte de pollution de la mémoire. Si nous passons plus tard d'un tableau à une liste, nous devrons également remplacer les boucles
foreach
par
for
boucles
for
.
public void Initialize (Vector2Int size) { … FindPaths(); } void FindPaths () { foreach (GameTile tile in tiles) { tile.ClearPath(); } tiles[0].BecomeDestination(); searchFrontier.Enqueue(tiles[0]); }
Ensuite, nous devons prendre une tuile de la frontière et développer un chemin vers tous ses voisins, en les ajoutant tous à la frontière. Nous allons d'abord nous déplacer vers le nord, puis vers l'est, le sud et enfin vers l'ouest.
public void FindPaths () { foreach (GameTile tile in tiles) { tile.ClearPath(); } tiles[0].BecomeDestination(); searchFrontier.Enqueue(tiles[0]); GameTile tile = searchFrontier.Dequeue(); searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); }
Nous répétons cette étape, alors qu'il y a des tuiles à la frontière.
while (searchFrontier.Count > 0) { GameTile tile = searchFrontier.Dequeue(); searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); }
La croissance d'un chemin ne nous mène pas toujours à une nouvelle tuile. Avant d'ajouter à la file d'attente, nous devons vérifier la valeur de
null
, mais nous pouvons reporter la vérification de
null
jusqu'à la sortie de la file d'attente.
GameTile tile = searchFrontier.Dequeue(); if (tile != null) { searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); }
Afficher les chemins
Nous avons maintenant un champ contenant les bons chemins, mais jusqu'à présent, nous ne voyons pas cela. Vous devez configurer les flèches afin qu'elles pointent le long du chemin à travers leurs tuiles. Cela peut être fait en les tournant. Comme ces virages sont toujours les mêmes, nous ajoutons au
GameTile
un champ
Quaternion
statique pour chacune des directions.
static Quaternion northRotation = Quaternion.Euler(90f, 0f, 0f), eastRotation = Quaternion.Euler(90f, 90f, 0f), southRotation = Quaternion.Euler(90f, 180f, 0f), westRotation = Quaternion.Euler(90f, 270f, 0f);
Ajoutez également la méthode générale
ShowPath
. Si la distance est nulle, alors la tuile est le point final et il n'y a rien vers quoi pointer, alors désactivez sa flèche. Sinon, activez la flèche et réglez sa rotation. La direction souhaitée peut être déterminée en comparant
nextOnPath
avec ses voisins.
public void ShowPath () { if (distance == 0) { arrow.gameObject.SetActive(false); return; } arrow.gameObject.SetActive(true); arrow.localRotation = nextOnPath == north ? northRotation : nextOnPath == east ? eastRotation : nextOnPath == south ? southRotation : westRotation; }
Appelez cette méthode pour toutes les tuiles à la fin GameBoard.FindPaths
. public void FindPaths () { … foreach (GameTile tile in tiles) { tile.ShowPath(); } }
Trouve des moyens.Pourquoi ne transformons-nous pas la flèche directement en GrowPathTo?. . , FindPaths
.
Modifier la priorité de recherche
Il s'avère que lorsque le point final est le coin sud-ouest, tous les chemins vont exactement vers l'ouest jusqu'à ce qu'ils atteignent le bord du champ, après quoi ils tournent vers le sud. Tout est vrai ici, car il n'y a vraiment pas de chemins plus courts vers le point final, car les mouvements diagonaux sont impossibles. Cependant, il existe de nombreux autres chemins les plus courts qui peuvent sembler plus beaux.Pour mieux comprendre pourquoi de tels chemins sont trouvés, déplacez le point final au centre de la carte. Avec une taille de champ impaire, ce n'est qu'une tuile au milieu du tableau. tiles[tiles.Length / 2].BecomeDestination(); searchFrontier.Enqueue(tiles[tiles.Length / 2]);
Point d'arrivée au centre.Le résultat semble logique si vous vous souvenez du fonctionnement de la recherche. Puisque nous ajoutons des voisins dans l'ordre nord-est-sud-ouest, le nord a la plus haute priorité. Puisque nous effectuons la recherche dans l'ordre inverse, cela signifie que la dernière direction que nous avons parcourue est le sud. C'est pourquoi seules quelques flèches pointent vers le sud et beaucoup pointent vers l'est.Vous pouvez modifier le résultat en définissant les priorités des directions. Échangeons l'est et le sud. Nous devons donc obtenir la symétrie nord-sud et est-ouest. searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathWest())
L'ordre de recherche est nord-sud-est-ouest.Il semble plus joli, mais il vaut mieux que les chemins changent de direction, s'approchant du mouvement diagonal où il aura l'air naturel. Nous pouvons le faire en inversant les priorités de recherche des tuiles voisines dans un motif en damier.Au lieu de déterminer quel type de mosaïque nous traitons pendant la recherche, nous ajoutons à la GameTile
propriété générale qui indique si la mosaïque actuelle est une alternative. public bool IsAlternative { get; set; }
Nous allons définir cette propriété dans GameBoard.Initialize
. Tout d'abord, marquez les tuiles comme alternative si leur coordonnée X est paire. for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … tile.IsAlternative = (x & 1) == 0; } }
Que fait l'opération (x & 1) == 0?— (AND). . 1, 1. 10101010 00001111 00001010.
. 0 1. 1, 2, 3, 4 1, 10, 11, 100. , .
AND , , . , .
Deuxièmement, nous changeons le signe du résultat si leur coordonnée Y est paire. Nous allons donc créer un modèle d'échecs. tile.IsAlternative = (x & 1) == 0; if ((y & 1) == 0) { tile.IsAlternative = !tile.IsAlternative; }
Comme FindPaths
nous gardons le même ordre que la recherche de tuiles alternative, mais de le faire revenir à toutes les autres tuiles. Cela forcera le chemin vers le mouvement diagonal et créera des zigzags. if (tile != null) { if (tile.IsAlternative) { searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathWest()); } else { searchFrontier.Enqueue(tile.GrowPathWest()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathNorth()); } }
Ordre de recherche variable.Changer les tuiles
À ce stade, toutes les tuiles sont vides. Une tuile est utilisée comme point de terminaison, mais en plus de l'absence d'une flèche visible, elle ressemble à tout le monde. Nous ajouterons la possibilité de changer les tuiles en plaçant des objets dessus.Contenu des tuiles
Les objets en mosaïque eux-mêmes sont simplement un moyen de suivre les informations sur les mosaïques. Nous ne modifions pas ces objets directement. Ajoutez plutôt du contenu séparé et placez-le sur le terrain. Pour l'instant, nous pouvons distinguer entre les tuiles vides et les tuiles de point de terminaison. Pour indiquer ces cas, créez une énumération GameTileContentType
. public enum GameTileContentType { Empty, Destination }
Ensuite, créez un type de composant GameTileContent
qui vous permet de définir le type de son contenu via l'inspecteur, et l'accès à celui-ci se fera via une propriété getter commune. using UnityEngine; public class GameTileContent : MonoBehaviour { [SerializeField] GameTileContentType type = default; public GameTileContentType Type => type; }
Ensuite, nous créerons des préfabriqués pour deux types de contenu, chacun ayant un composant GameTileContent
avec le type spécifié correspondant. Utilisons un cube bleu aplati pour désigner les tuiles d'extrémité. Comme il est presque plat, il n'a pas besoin de collisionneur. Pour préfabriquer du contenu vide, utilisez un objet de jeu vide.Préfabriqués du point de terminaison et du contenu vide.Nous donnerons l'objet de contenu aux tuiles vides, car alors toutes les tuiles auront toujours le contenu, ce qui signifie que nous n'aurons pas besoin de vérifier l'égalité des liens vers les contenus null
.Content Factory
Pour rendre le contenu modifiable, nous allons également créer une fabrique pour cela, en utilisant la même approche que dans le didacticiel de gestion des objets . Cela signifie que vous GameTileContent
devez garder une trace de votre usine d'origine, qui ne doit être définie qu'une seule fois, et vous renvoyer à l'usine dans la méthode Recycle
. GameTileContentFactory originFactory; … public GameTileContentFactory OriginFactory { get => originFactory; set { Debug.Assert(originFactory == null, "Redefined origin factory!"); originFactory = value; } } public void Recycle () { originFactory.Reclaim(this); }
Cela suppose l'existence GameTileContentFactory
, par conséquent, nous allons créer un type d'objet scriptable pour cela avec la méthode requise Recycle
. À ce stade, nous ne nous occuperons pas de la création d'une usine entièrement fonctionnelle qui utilise le contenu, nous allons donc la faire simplement détruire le contenu. Plus tard, il sera possible d'ajouter la réutilisation des objets à l'usine sans changer le reste du code. using UnityEngine; using UnityEngine.SceneManagement; [CreateAssetMenu] public class GameTileContentFactory : ScriptableObject { public void Reclaim (GameTileContent content) { Debug.Assert(content.OriginFactory == this, "Wrong factory reclaimed!"); Destroy(content.gameObject); } }
Ajoutez une méthode cachée Get
à l' usine avec un préfabriqué comme paramètre. Ici, nous sautons à nouveau la réutilisation des objets. Il crée une instance de l'objet, définit son usine d'origine, le déplace vers la scène d'usine et le renvoie. GameTileContent Get (GameTileContent prefab) { GameTileContent instance = Instantiate(prefab); instance.OriginFactory = this; MoveToFactoryScene(instance.gameObject); return instance; }
L'instance a été déplacée vers la scène de contenu d'usine, qui peut être créée selon les besoins. Si nous sommes dans l'éditeur, avant de créer une scène, nous devons vérifier si elle existe, au cas où nous la perdrions de vue lors d'un redémarrage à chaud. Scene contentScene; … void MoveToFactoryScene (GameObject o) { if (!contentScene.isLoaded) { if (Application.isEditor) { contentScene = SceneManager.GetSceneByName(name); if (!contentScene.isLoaded) { contentScene = SceneManager.CreateScene(name); } } else { contentScene = SceneManager.CreateScene(name); } } SceneManager.MoveGameObjectToScene(o, contentScene); }
Nous n'avons que deux types de contenu, il suffit donc d'ajouter deux champs de configuration préfabriqués pour eux. [SerializeField] GameTileContent destinationPrefab = default; [SerializeField] GameTileContent emptyPrefab = default;
La dernière chose à faire pour que l'usine fonctionne est de créer une méthode générale Get
avec un paramètre GameTileContentType
qui reçoit une instance du préfabriqué correspondant. public GameTileContent Get (GameTileContentType type) { switch (type) { case GameTileContentType.Destination: return Get(destinationPrefab); case GameTileContentType.Empty: return Get(emptyPrefab); } Debug.Assert(false, "Unsupported type: " + type); return null; }
Est-il obligatoire d'ajouter une instance distincte de contenu vide à chaque tuile?, . . , - , , , , . , . , , .
Créons un actif d'usine et configurons ses liens vers les préfabriqués.Content FactoryEt puis passez le Game
lien à l'usine. [SerializeField] GameTileContentFactory tileContentFactory = default;
Jeu avec une usine.Taper sur une tuile
Pour changer de champ, nous devons pouvoir sélectionner une tuile. Nous le rendrons possible en mode jeu. Nous émettrons un faisceau dans la scène à l'endroit où le joueur a cliqué sur la fenêtre de jeu. Si la poutre croise la tuile, le joueur la touche, c'est-à-dire qu'elle doit être changée. Game
gérera l'entrée du joueur, mais sera responsable de déterminer la tuile que le joueur a touchée GameBoard
.Tous les rayons ne se croisent pas avec la tuile, donc parfois nous ne recevrons rien. Par conséquent, nous ajoutons à la GameBoard
méthode GetTile
, qui renvoie toujours toujours initialement null
(cela signifie que la tuile n'a pas été trouvée). public GameTile GetTile (Ray ray) { return null; }
Pour déterminer si un rayon a traversé une tuile, nous devons appeler Physics.Raycast
en spécifiant le rayon comme argument. Il renvoie des informations sur la présence d'une intersection. Si c'est le cas, nous pouvons retourner la tuile, bien que nous ne sachions pas encore laquelle, donc pour le moment nous la retournerons null
. public GameTile TryGetTile (Ray ray) { if (Physics.Raycast(ray) { return null; } return null; }
Pour savoir s'il y avait une intersection avec une tuile, nous avons besoin de plus d'informations sur l'intersection. Physics.Raycast
peut fournir ces informations à l'aide du deuxième paramètre RaycastHit
. Il s'agit du paramètre de sortie, qui est indiqué par le mot out
devant lui. Cela signifie qu'un appel de méthode peut affecter une valeur à la variable que nous lui transmettons. RaycastHit hit; if (Physics.Raycast(ray, out hit) { return null; }
Nous pouvons incorporer la déclaration des variables utilisées pour les paramètres de sortie, alors faisons-le. if (Physics.Raycast(ray, out RaycastHit hit) { return null; }
Nous ne nous soucions pas avec quel collisionneur l'intersection s'est produite, nous utilisons simplement la position de l'intersection XZ pour déterminer la tuile. Nous obtenons les coordonnées de la tuile en ajoutant la moitié de la taille du champ aux coordonnées du point d'intersection, puis en convertissant les résultats en valeurs entières. En conséquence, l'index de tuile final sera sa coordonnée X plus la coordonnée Y multipliée par la largeur du champ. if (Physics.Raycast(ray, out RaycastHit hit)) { int x = (int)(hit.point.x + size.x * 0.5f); int y = (int)(hit.point.z + size.y * 0.5f); return tiles[x + y * size.x]; }
Mais cela n'est possible que lorsque les coordonnées de la tuile sont dans le champ, nous allons donc vérifier cela. Si ce n'est pas le cas, la tuile ne sera pas retournée. int x = (int)(hit.point.x + size.x * 0.5f); int y = (int)(hit.point.z + size.y * 0.5f); if (x >= 0 && x < size.x && y >= 0 && y < size.y) { return tiles[x + y * size.x]; }
Changement de contenu
Pour pouvoir modifier le contenu de la tuile, ajoutez-la à la GameTile
propriété générale Content
. Son getter renvoie simplement le contenu et le setter rejette le contenu précédent, le cas échéant, et place le nouveau contenu. GameTileContent content; public GameTileContent Content { get => content; set { if (content != null) { content.Recycle(); } content = value; content.transform.localPosition = transform.localPosition; } }
C'est le seul endroit où vous devez vérifier le contenu null
, car au départ, nous n'avons pas de contenu. Pour garantir, nous exécutons assert afin que le passeur ne soit pas appelé avec null
. set { Debug.Assert(value != null, "Null assigned to content!"); … }
Et enfin, nous avons besoin d'une entrée de joueur. La conversion d'un clic de souris en rayon peut être effectuée en appelant ScreenPointToRay
avec Input.mousePosition
comme argument. L'appel doit être effectué pour la caméra principale, accessible via Camera.main
. Ajoutez la propriété c pour cela Game
. Ray TouchRay => Camera.main.ScreenPointToRay(Input.mousePosition);
Ensuite, nous ajoutons une méthode Update
qui vérifie si le bouton principal de la souris a été enfoncé pendant la mise à niveau. Pour ce faire, appelez Input.GetMouseButtonDown
avec zéro comme argument. Si la touche a été enfoncée, nous traitons le toucher du joueur, c'est-à-dire que nous prenons la tuile sur le terrain et définissons le point final comme son contenu, en le prenant de l'usine. void Update () { if (Input.GetMouseButtonDown(0)) { HandleTouch(); } } void HandleTouch () { GameTile tile = GetTile(TouchRay); if (tile != null) { tile.Content = tileContentFactory.Get(GameTileContentType.Destination); } }
Maintenant, nous pouvons transformer n'importe quelle tuile en point final en appuyant sur le curseur.Plusieurs points de terminaison.Faire le bon terrain
Bien que nous puissions transformer des tuiles en points de terminaison, cela n'affecte pas les chemins jusqu'à présent. De plus, nous n'avons pas encore défini de contenu vide pour les tuiles. Le maintien de l'exactitude et de l'intégrité du champ est une tâche GameBoard
, nous allons donc lui donner la responsabilité de définir le contenu de la tuile. Pour l'implémenter, nous allons lui donner un lien vers la fabrique de contenu via sa méthode Intialize
, et l'utiliser pour donner à toutes les tuiles une instance de contenu vide. GameTileContentFactory contentFactory; public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { this.size = size; this.contentFactory = contentFactory; ground.localScale = new Vector3(size.x, size.y, 1f); tiles = new GameTile[size.x * size.y]; for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … tile.Content = contentFactory.Get(GameTileContentType.Empty); } } FindPaths(); }
Maintenant, je Game
dois transférer mon usine sur le terrain. void Awake () { board.Initialize(boardSize, tileContentFactory); }
Pourquoi ne pas ajouter un champ de configuration d'usine au GameBoard?, , . , .
Puisque nous avons maintenant plusieurs points de terminaison, nous le modifions GameBoard.FindPaths
pour qu'il appelle BecomeDestination
chacun d'eux et les ajoute tous à la frontière. Et c'est tout ce qu'il faut pour prendre en charge plusieurs points de terminaison. Toutes les autres tuiles sont effacées comme d'habitude. Ensuite, nous supprimons le point de terminaison défini au centre. void FindPaths () { foreach (GameTile tile in tiles) { if (tile.Content.Type == GameTileContentType.Destination) { tile.BecomeDestination(); searchFrontier.Enqueue(tile); } else { tile.ClearPath(); } }
Mais si nous pouvons transformer des tuiles en points de terminaison, alors nous devrions être en mesure d'effectuer l'opération inverse, transformer les points de terminaison en tuiles vides. Mais alors nous pouvons obtenir un champ sans aucun point final. Dans ce cas, FindPaths
ne pourra pas effectuer sa tâche. Cela se produit lorsque la bordure est vide après l'initialisation du chemin pour toutes les cellules. Nous désignons cela comme un état non valide du champ, renvoyant false
et terminant l'exécution; sinon retournez à la fin true
. bool FindPaths () { foreach (GameTile tile in tiles) { … } if (searchFrontier.Count == 0) { return false; } … return true; }
Le moyen le plus simple de mettre en œuvre la prise en charge de la suppression des points de terminaison, ce qui en fait une opération de commutation. En cliquant sur les tuiles vides, nous les transformerons en points de terminaison, et en cliquant sur les points de terminaison, nous les supprimerons. Mais maintenant, il est engagé dans la modification du contenu GameBoard
, nous allons donc lui donner une méthode générale ToggleDestination
dont le paramètre est la vignette. Si la tuile est le point de terminaison, faites-la vide et appelez FindPaths
. Sinon, nous en faisons le point final et nous l'appelons également FindPaths
. public void ToggleDestination (GameTile tile) { if (tile.Content.Type == GameTileContentType.Destination) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } }
L'ajout d'un point de terminaison ne peut jamais créer un état de champ non valide, et la suppression d'un point de terminaison peut le faire. Par conséquent, nous vérifierons s'il a réussi à s'exécuter FindPaths
après avoir rendu la tuile vide. Si ce n'est pas le cas, annulez la modification, ramenez la tuile au point de terminaison et appelez à nouveau FindPaths
pour revenir à l'état correct précédent. if (tile.Content.Type == GameTileContentType.Destination) { tile.Content = contentFactory.Get(GameTileContentType.Empty); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } }
La validation peut-elle être rendue plus efficace?, . , . , . FindPaths
, .
Maintenant, à la fin, Initialize
nous pouvons appeler ToggleDestination
avec la tuile centrale comme argument, au lieu d'appeler explicitement FindPaths
. C'est la seule fois où nous commençons avec un état de champ non valide, mais nous sommes garantis de terminer avec l'état correct. public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { …
Enfin, nous forçons à Game
appeler ToggleDestination
au lieu de définir le contenu de la tuile elle-même. void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) {
Plusieurs points de terminaison avec des chemins corrects.Ne devrions-nous pas interdire à Game de définir directement le contenu de la vignette?. . , Game
. , .
Les murs
Le but de la défense de tour est d'empêcher les ennemis d'atteindre le point final. Cet objectif est atteint de deux manières. Premièrement, nous les tuons, et deuxièmement, nous les ralentissons afin qu'il y ait plus de temps pour les tuer. Sur le champ de tuiles, le temps peut être allongé, augmentant la distance que les ennemis doivent parcourir. Ceci peut être réalisé en plaçant des obstacles sur le terrain. Ce sont généralement des tours qui tuent également les ennemis, mais dans ce didacticiel, nous nous limiterons aux murs.Le contenu
Les murs sont un autre type de contenu, alors ajoutons-y GameTileContentType
un élément. public enum GameTileContentType { Empty, Destination, Wall }
Créez ensuite le préfabriqué mural. Cette fois, nous allons créer un objet de jeu du contenu de la tuile et y ajouter un cube enfant, qui sera au-dessus du champ et remplira la tuile entière. Faites-le à une demi-unité de hauteur et sauvez le collisionneur, car les murs peuvent chevaucher visuellement une partie des tuiles derrière lui. Par conséquent, lorsqu'un joueur touche un mur, il influence la tuile correspondante.Mur préfabriqué.Ajoutez le préfabriqué mural à l'usine, à la fois dans le code et dans l'inspecteur. [SerializeField] GameTileContent wallPrefab = default; … public GameTileContent Get (GameTileContentType type) { switch (type) { case GameTileContentType.Destination: return Get(destinationPrefab); case GameTileContentType.Empty: return Get(emptyPrefab); case GameTileContentType.Wall: return Get(wallPrefab); } Debug.Assert(false, "Unsupported type: " + type); return null; }
Usine avec mur préfabriqué.Activer et désactiver les murs
Ajoutez à GameBoard
la méthode marche / arrêt des murs, comme nous l'avons fait pour le point final. Initialement, nous ne vérifierons pas l'état incorrect du champ. public void ToggleWall (GameTile tile) { if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else { tile.Content = contentFactory.Get(GameTileContentType.Wall); FindPaths(); } }
Nous prendrons en charge la commutation uniquement entre les carreaux vides et les carreaux muraux, sans permettre aux murs de remplacer directement les points de terminaison. Par conséquent, nous ne créerons un mur que lorsque la tuile sera vide. De plus, les murs doivent bloquer la recherche du chemin. Mais chaque tuile doit avoir un chemin vers le point final, sinon les ennemis se coincent. Pour ce faire, nous devons à nouveau utiliser la validation FindPaths
et ignorer les modifications si elles ont créé un état de champ incorrect. else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Wall); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } }
L'activation et la désactivation des murs seront utilisées beaucoup plus souvent que l'activation et la désactivation des points de terminaison, nous allons donc effectuer la commutation des murs dans la Game
touche principale. Les points d'extrémité peuvent être commutés par une touche supplémentaire (généralement le bouton droit de la souris), qui peut être reconnue en passant à une Input.GetMouseButtonDown
valeur de 1. void Update () { if (Input.GetMouseButtonDown(0)) { HandleTouch(); } else if (Input.GetMouseButtonDown(1)) { HandleAlternativeTouch(); } } void HandleAlternativeTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { board.ToggleDestination(tile); } } void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { board.ToggleWall(tile); } }
Maintenant, nous avons les murs.Pourquoi ai-je de grands écarts entre les ombres des murs adjacents en diagonale?, , , . , , far clipping plane . , far plane 20 . , MSAA, .
Veillons également à ce que les extrémités ne puissent pas remplacer directement les murs. public void ToggleDestination (GameTile tile) { if (tile.Content.Type == GameTileContentType.Destination) { … } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } }
Verrou de recherche de chemin
Pour que les murs bloquent la recherche du chemin, il nous suffit de ne pas ajouter de tuiles avec des murs à la bordure de recherche. Cela peut être fait en forçant à GameTile.GrowPathTo
ne pas retourner les carreaux avec des murs. Mais le chemin doit toujours croître en direction du mur, afin que toutes les tuiles du terrain aient un chemin. Cela est nécessaire car il est possible qu'une tuile avec des ennemis se transforme soudainement en mur. GameTile GrowPathTo (GameTile neighbor) { if (!HasPath || neighbor == null || neighbor.HasPath) { return null; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; return neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; }
Pour s'assurer que toutes les tuiles ont un chemin, elles GameBoard.FindPaths
doivent le vérifier une fois la recherche terminée. Si ce n'est pas le cas, l'état du champ n'est pas valide et doit être renvoyé false
. Il n'est pas nécessaire de mettre à jour la visualisation du chemin pour les états non valides, car le champ reviendra à l'état précédent. bool FindPaths () { … foreach (GameTile tile in tiles) { if (!tile.HasPath) { return false; } } foreach (GameTile tile in tiles) { tile.ShowPath(); } return true; }
Les murs affectent le chemin.Pour vous assurer que les murs ont réellement les bons chemins, vous devez rendre les cubes translucides.Murs transparents.Notez que l'exigence d'exactitude de tous les chemins ne permet pas aux murs d'enfermer une partie du champ dans laquelle il n'y a pas de point final. Nous pouvons diviser la carte, mais seulement s'il y a au moins un point de terminaison dans chaque partie. De plus, chaque mur doit être adjacent à une tuile ou à un point de terminaison vide, sinon il ne pourra pas avoir de chemin. Par exemple, il est impossible de faire un bloc solide de murs 3 × 3.Cachez le chemin
La visualisation des chemins nous permet de voir comment fonctionne la recherche de chemin et de nous assurer qu'elle est bien correcte. Mais il n'a pas besoin d'être montré au joueur, ou du moins pas nécessairement. Par conséquent, fournissons la possibilité de désactiver les flèches. Cela peut être fait en ajoutant à la GameTile
méthode générale HidePath
, qui désactive simplement sa flèche. public void HidePath () { arrow.gameObject.SetActive(false); }
L'état de mappage de chemin fait partie de l'état du champ. Ajoutez un GameBoard
champ booléen à la valeur par défaut false
pour suivre son état, ainsi qu'une propriété commune comme getter et setter. Le passeur doit afficher ou masquer les chemins sur toutes les tuiles. bool showPaths; public bool ShowPaths { get => showPaths; set { showPaths = value; if (showPaths) { foreach (GameTile tile in tiles) { tile.ShowPath(); } } else { foreach (GameTile tile in tiles) { tile.HidePath(); } } } }
Désormais, la méthode FindPaths
ne doit afficher les chemins mis à jour que si le rendu est activé. bool FindPaths () { … if (showPaths) { foreach (GameTile tile in tiles) { tile.ShowPath(); } } return true; }
Par défaut, la visualisation du chemin est désactivée. Désactivez la flèche dans le préfabriqué de tuiles.La flèche préfabriquée est inactive par défaut.Nous faisons en sorte qu'il Game
permute l'état de visualisation lorsqu'une touche est enfoncée. Il serait logique d'utiliser la touche P, mais c'est aussi un raccourci clavier pour activer / désactiver le mode de jeu dans l'éditeur Unity. En conséquence, la visualisation changera lorsque le raccourci clavier pour quitter le mode de jeu est utilisé, ce qui n'est pas très joli. Utilisons donc la touche V (abréviation de visualisation).Pas de flèches.Affichage de la grille
Lorsque les flèches sont cachées, il devient difficile de discerner l'emplacement de chaque tuile. Ajoutons les lignes de la grille. Téléchargez ici une texture de maillage de bordure carrée qui peut être utilisée comme contour de tuile distinct.Texture de maille.Nous n'ajouterons pas cette texture individuellement à chaque carreau, mais l'appliquerons au sol. Mais nous rendrons cette grille facultative, ainsi que la visualisation des chemins. Par conséquent, nous allons ajouter au GameBoard
champ de configuration Texture2D
et sélectionner une texture de maillage pour celui-ci. [SerializeField] Texture2D gridTexture = default;
Champ avec texture de maille.Ajoutez un autre champ booléen et une propriété pour contrôler l'état de la visualisation de la grille. Dans ce cas, le passeur doit changer le matériau de la terre, ce qui peut être mis en œuvre en appelant la GetComponent<MeshRenderer>
terre et en accédant à la propriété du material
résultat. Si la grille doit être affichée, nous attribuerons la mainTexture
texture de la grille à la propriété du matériau. Sinon, attribuez-le-lui null
. Notez que lorsque vous modifiez la texture du matériau, des doublons de l'instance de matériau seront créés, de sorte qu'elle deviendra indépendante de l'actif matériel. bool showGrid, showPaths; public bool ShowGrid { get => showGrid; set { showGrid = value; Material m = ground.GetComponent<MeshRenderer>().material; if (showGrid) { m.mainTexture = gridTexture; } else { m.mainTexture = null; } } }
Faites - le Game
commutées en appuyant sur la visualisation de la grille G. void Update () { … if (Input.GetKeyDown(KeyCode.G)) { board.ShowGrid = !board.ShowGrid; } }
Ajoutez également la visualisation de maillage par défaut à Awake
. void Awake () { board.Initialize(boardSize, tileContentFactory); board.ShowGrid = true; }
Grille non mise à l'échelle.Jusqu'à présent, nous avons une frontière autour de tout le champ. Cela correspond à la texture, mais ce n'est pas ce dont nous avons besoin. Nous devons mettre à l'échelle la texture principale du matériau afin qu'elle corresponde à la taille de la grille. Vous pouvez le faire en appelant la méthode SetTextureScale
matérielle avec le nom de la propriété de texture ( _MainTex ) et la taille bidimensionnelle. Nous pouvons utiliser directement la taille du champ, qui est indirectement convertie en valeur Vector2
. if (showGrid) { m.mainTexture = gridTexture; m.SetTextureScale("_MainTex", size); }
Grille à l'échelle avec visualisation du chemin activée et désactivée.Donc, à ce stade, nous avons obtenu un champ fonctionnel pour un jeu de tuiles du genre tower defense. Dans le prochain tutoriel, nous ajouterons des ennemis.DépôtPdf