Parties 1-3: maillage, couleurs et hauteurs de celluleParties 4-7: bosses, rivières et routesParties 8-11: eau, reliefs et rempartsParties 12-15: sauvegarde et chargement, textures, distancesParties 16-19: trouver le chemin, équipes de joueurs, animationsParties 20-23: Brouillard de guerre, recherche cartographique, génération procéduraleParties 24-27: cycle de l'eau, érosion, biomes, carte cylindriquePartie 16: trouver le chemin
- Mettre en surbrillance les cellules
- Sélectionnez une cible de recherche
- Trouvez le chemin le plus court
- Créer une file d'attente prioritaire
Après avoir calculé les distances entre les cellules, nous avons procédé à la recherche des chemins entre elles.
À partir de cette partie, des didacticiels de carte hexagonale seront créés dans Unity 5.6.0. Il convient de noter qu'en 5.6, il existe un bogue qui détruit les tableaux de textures dans les assemblys pour plusieurs plates-formes. Vous pouvez le contourner en incluant
Is Readable dans l'inspecteur de tableau de textures.
Planifier un voyageCellules en surbrillance
Pour rechercher le chemin entre deux cellules, nous devons d'abord sélectionner ces cellules. C’est plus que de choisir une cellule et de surveiller la recherche sur la carte. Par exemple, nous allons d'abord sélectionner la cellule initiale, puis la dernière. Dans ce cas, il serait pratique qu'ils soient mis en évidence. Par conséquent, ajoutons une telle fonctionnalité. Jusqu'à ce que nous créons un moyen complexe ou efficace de mettre en évidence, nous créons juste quelque chose pour nous aider dans le développement.
Texture de contour
Une façon simple de sélectionner des cellules consiste à leur ajouter un chemin. Pour ce faire, la méthode la plus simple consiste à utiliser une texture contenant un contour hexagonal.
Ici, vous pouvez télécharger une telle texture. Il est transparent à l'exception du contour blanc de l'hexagone. Après l'avoir rendu blanc, nous pourrons à l'avenir le coloriser selon nos besoins.
Contour de cellule sur fond noirImportez la texture et définissez son
type de texture sur
Sprite . Son
mode Sprite sera défini sur
Single avec les paramètres par défaut. Comme il s'agit d'une texture exceptionnellement blanche, nous n'avons pas besoin de convertir en
sRGB . Le canal alpha indique la transparence, donc activer
Alpha est Transparence . J'ai également défini la texture du
mode de filtre sur
Trilinéaire , car sinon les transitions de mip pour les chemins peuvent devenir trop visibles.
Options d'importation de textureUn sprite par cellule
Le moyen le plus rapide consiste à ajouter un contour possible aux cellules, en ajoutant chaque sprite. Créez un nouvel objet de jeu, ajoutez-y le composant Image (
Component / UI / Image ) et affectez-lui notre sprite de contour. Insérez ensuite l'instance de préfabriqué
Hex Cell Label dans la scène, faites-en un objet sprite, appliquez les modifications au préfabriqué, puis supprimez-le.
Élément de sélection enfant préfabriquéMaintenant, chaque cellule a un sprite, mais il sera trop grand. Pour que les contours correspondent aux centres des cellules, modifiez la
largeur et la
hauteur du composant de transformation de l'image-objet à 17.
Sprites de sélection partiellement cachés par un reliefDessiner par dessus tout
Le contour étant superposé à la surface des bords des alvéoles, il apparaît souvent sous la géométrie du relief. Pour cette raison, une partie du circuit disparaît. Cela peut être évité en élevant légèrement les sprites verticalement, mais pas en cas de pauses. Au lieu de cela, nous pouvons faire ce qui suit: toujours dessiner des sprites au-dessus de tout le reste. Pour ce faire, créez votre propre shader de sprite. Il nous suffira de copier le shader de sprite Unity standard et d'y apporter quelques modifications.
Shader "Custom/Highlight" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0 [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1) [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1) [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {} [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Cull Off ZWrite Off Blend One OneMinusSrcAlpha Pass { CGPROGRAM
Le premier changement est que nous ignorons le tampon de profondeur, ce qui fait que le test Z réussit toujours.
ZWrite Off ZTest Always
Le deuxième changement est que nous effectuons le rendu après le reste de la géométrie transparente. De quoi ajouter 10 à la file d'attente de transparence.
"Queue"="Transparent+10"
Créez un nouveau matériau que ce shader utilisera. Nous pouvons ignorer toutes ses propriétés, en respectant les valeurs par défaut. Faites ensuite le préfabriqué sprite utiliser ce matériau.
Nous utilisons notre propre matériau spriteDésormais, les contours de la sélection sont toujours visibles. Même si la cellule est cachée sous un relief plus élevé, son contour sera toujours dessiné au-dessus de tout le reste. Cela peut ne pas être beau, mais les cellules sélectionnées seront toujours visibles, ce qui est utile pour nous.
Ignorer le tampon de profondeurContrôle de sélection
Nous ne voulons pas que toutes les cellules soient mises en évidence en même temps. En fait, au départ, ils devraient tous être désélectionnés. Nous pouvons implémenter cela en désactivant le composant Image de l'objet préfabriqué
Highlight .
Composant d'image désactivéPour activer la sélection de cellules, ajoutez la méthode
EnableHighlight
à
EnableHighlight
. Il doit prendre le seul enfant de son
uiRect
et inclure son composant Image. Nous allons également créer la méthode
DisableHighlight
.
public void DisableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = false; } public void EnableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = true; }
Enfin, nous pouvons spécifier la couleur de sorte que lorsqu'il est allumé, donner au rétro-éclairage une teinte.
public void EnableHighlight (Color color) { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.color = color; highlight.enabled = true; }
paquet d'unitéTrouver le chemin
Maintenant que nous pouvons sélectionner les cellules, nous devons avancer et sélectionner deux cellules, puis trouver le chemin entre elles. Nous devons d'abord sélectionner les cellules, puis restreindre la recherche à un chemin entre elles, et enfin montrer ce chemin.
Début de la recherche
Nous devons sélectionner deux cellules différentes, les points de début et de fin de la recherche. Supposons que pour sélectionner la cellule de recherche initiale, maintenez la touche Maj enfoncée tout en cliquant avec la souris. Dans ce cas, la cellule est surlignée en bleu. Nous devons enregistrer le lien vers cette cellule pour une recherche plus approfondie. De plus, lors du choix d'une nouvelle cellule de départ, la sélection de l'ancienne doit être désactivée. Par conséquent, nous ajoutons le champ
searchFromCell
à
searchFromCell
.
HexCell previousCell, searchFromCell;
À l'intérieur de
HandleInput
nous pouvons utiliser
Input.GetKey(KeyCode.LeftShift)
pour tester la touche Maj enfoncée.
if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); } else { hexGrid.FindDistancesTo(currentCell); }
Où chercherPoint de terminaison de recherche
Au lieu de rechercher toutes les distances d'une cellule, nous recherchons maintenant un chemin entre deux cellules spécifiques. Par conséquent, renommez
HexGrid.FindDistancesTo
en
HexGrid.FindPath
et attribuez-lui le deuxième paramètre
HexCell
. Modifiez également la méthode
Search
.
public void FindPath (HexCell fromCell, HexCell toCell) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell)); } IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } WaitForSeconds delay = new WaitForSeconds(1 / 60f); List<HexCell> frontier = new List<HexCell>(); fromCell.Distance = 0; frontier.Add(fromCell); … }
Désormais,
HexMapEditor.HandleInput
doit appeler la méthode modifiée, en utilisant
searchFromCell
et
currentCell
comme arguments. De plus, nous ne pouvons rechercher que lorsque nous savons à partir de quelle cellule effectuer la recherche. Et nous n'avons pas à nous soucier de rechercher si les points de début et de fin coïncident.
if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { … } else if (searchFromCell && searchFromCell != currentCell) { hexGrid.FindPath(searchFromCell, currentCell); }
En ce qui concerne la recherche, nous devons d'abord nous débarrasser de toutes les sélections précédentes. Par conséquent, faites
HexGrid.Search
désactiver la sélection lors de la réinitialisation des distances. Comme cela désactive également l'éclairage de la cellule initiale, puis rallumez-le. À ce stade, nous pouvons également mettre en évidence le point final. Faisons-la rouge.
IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].DisableHighlight(); } fromCell.EnableHighlight(Color.blue); toCell.EnableHighlight(Color.red); … }
Points finaux d'un chemin potentielLimiter la recherche
À ce stade, notre algorithme de recherche calcule toujours les distances de toutes les cellules accessibles depuis la cellule de départ. Mais nous n'en avons plus besoin. Nous pouvons nous arrêter dès que nous trouvons la distance finale à la cellule finale. Autrement dit, lorsque la cellule actuelle est finie, nous pouvons quitter la boucle d'algorithme.
while (frontier.Count > 0) { yield return delay; HexCell current = frontier[0]; frontier.RemoveAt(0); if (current == toCell) { break; } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … } }
Arrêtez-vous au point finalQue se passe-t-il si le point final ne peut pas être atteint?Ensuite, l'algorithme continuera de fonctionner jusqu'à ce qu'il trouve toutes les cellules accessibles. Sans la possibilité d'une sortie prématurée, cela fonctionnera comme l'ancienne méthode FindDistancesTo
.
Affichage du chemin
Nous pouvons trouver la distance entre le début et la fin du chemin, mais nous ne savons pas encore quel sera le vrai chemin. Pour le trouver, vous devez suivre la façon dont chaque cellule est atteinte. Mais comment faire?
Lors de l'ajout d'une cellule à la bordure, nous le faisons car c'est un voisin de la cellule actuelle. La seule exception est la cellule de départ. Toutes les autres cellules ont été atteintes via la cellule actuelle. Si nous gardons une trace de la cellule à partir de laquelle chaque cellule a été atteinte, nous obtenons un réseau de cellules en conséquence. Plus précisément, un réseau arborescent dont la racine est le point de départ. Nous pouvons l'utiliser pour construire le chemin après avoir atteint le point final.
Réseau d'arbres décrivant les chemins vers le centreNous pouvons enregistrer ces informations en ajoutant un lien vers une autre cellule dans
HexCell
. Nous n'avons pas besoin de sérialiser ces données, nous utilisons donc la propriété standard pour cela.
public HexCell PathFrom { get; set; }
Dans
HexGrid.Search
définissez la valeur
PathFrom
du voisin sur la cellule actuelle lors de son ajout à la bordure. De plus, nous devons changer ce lien lorsque nous trouvons un chemin plus court vers le voisin.
if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; frontier.Add(neighbor); } else if (distance < neighbor.Distance) { neighbor.Distance = distance; neighbor.PathFrom = current; }
Après avoir atteint le point final, nous pouvons visualiser le chemin en suivant ces liens vers la cellule de départ et en les sélectionnant.
if (current == toCell) { current = current.PathFrom; while (current != fromCell) { current.EnableHighlight(Color.white); current = current.PathFrom; } break; }
Chemin trouvéIl convient de noter qu'il existe souvent plusieurs chemins les plus courts. Celui trouvé dépend de l'ordre de traitement des cellules. Certains chemins peuvent sembler bons, d'autres peuvent être mauvais, mais il n'y a jamais de chemin plus court. Nous y reviendrons plus tard.
Modifier le début de la recherche
Après avoir sélectionné le point de départ, la modification du point de fin déclenchera une nouvelle recherche. La même chose devrait se produire lors du choix d'une nouvelle cellule de départ. Pour rendre cela possible,
HexMapEditor
doit également se souvenir du point de terminaison.
HexCell previousCell, searchFromCell, searchToCell;
En utilisant ce champ, nous pouvons également lancer une nouvelle recherche lors du choix d'un nouveau départ.
else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell); }
De plus, nous devons éviter des points de départ et d'arrivée égaux.
if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { … }
paquet d'unitéRecherche plus intelligente
Bien que notre algorithme trouve le chemin le plus court, il passe beaucoup de temps à explorer des points qui ne feront évidemment pas partie de ce chemin. Au moins, c'est évident pour nous. L'algorithme ne peut pas regarder vers le bas sur la carte; il ne peut pas voir qu'une recherche dans certaines directions n'aura aucun sens. Il préfère se déplacer sur les routes, malgré le fait qu'elles se dirigent dans la direction opposée au point final. Est-il possible de rendre la recherche plus intelligente?
Pour le moment, lors du choix de la cellule à traiter ensuite, nous considérons uniquement la distance entre la cellule et le début. Si nous voulons faire plus intelligemment, nous devons également tenir compte de la distance jusqu'au point final. Malheureusement, nous ne le connaissons pas encore. Mais nous pouvons créer une estimation de la distance restante. L'ajout de cette estimation à la distance de la cellule nous donne une compréhension de la longueur totale du chemin passant par cette cellule. Ensuite, nous pouvons l'utiliser pour prioriser les recherches de cellules.
Recherche heuristique
Lorsque nous utilisons une estimation ou une conjecture au lieu de données exactement connues, cela s'appelle utiliser une heuristique de recherche. Cette heuristique représente la meilleure estimation de la distance restante. Nous devons déterminer cette valeur pour chaque cellule que nous recherchons, nous allons donc lui ajouter une propriété entière
HexCell
. Nous n'avons pas besoin de le sérialiser, donc une autre propriété standard suffira.
public int SearchHeuristic { get; set; }
Comment faire une hypothèse sur la distance restante? Dans le cas le plus idéal, nous aurons une route menant directement au point d'arrivée. Si c'est le cas, alors la distance est égale à la distance inchangée entre les coordonnées de cette cellule et la cellule finale. Profitons de cela dans notre heuristique.
Étant donné que l'heuristique ne dépend pas d'un chemin déjà parcouru, elle est constante dans le processus de recherche. Par conséquent, nous devons le calculer une seule fois lorsque
HexGrid.Search
ajoute une cellule à la bordure.
if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); frontier.Add(neighbor); }
Priorité de recherche
Désormais, nous déterminerons la priorité de la recherche en fonction de la distance à la cellule plus son heuristique. Ajoutons une propriété pour cette valeur dans
HexCell
.
public int SearchPriority { get { return distance + SearchHeuristic; } }
Pour que cela fonctionne,
HexGrid.Search
afin qu'il utilise cette propriété pour trier la bordure.
frontier.Sort( (x, y) => x.SearchPriority.CompareTo(y.SearchPriority) );
Recherche sans heuristique et avec heuristiqueHeuristique valide
Grâce aux nouvelles priorités de recherche, nous allons en fait visiter moins de cellules. Cependant, sur une carte uniforme, l'algorithme traite toujours les cellules qui sont dans la mauvaise direction. En effet, par défaut, les coûts pour chaque étape de déplacement sont de 5, et l'heuristique n'ajoute que 1 par étape, c'est-à-dire que l'influence de l'heuristique n'est pas très forte.
Si les coûts de déplacement sur toutes les cartes sont les mêmes, alors nous pouvons utiliser les mêmes coûts lors de la détermination de l'heuristique. Dans notre cas, ce sera l'heuristique actuelle multipliée par 5. Cela réduira considérablement le nombre de cellules traitées.
Utiliser l'heuristique × 5Cependant, s'il y a des routes sur la carte, nous pouvons surestimer la distance restante. Par conséquent, l'algorithme peut faire des erreurs et créer un chemin qui n'est en fait pas le plus court.
Heuristique surévaluée et validePour s'assurer que le chemin le plus court est trouvé, nous devons nous assurer de ne jamais surestimer la distance restante. Cette approche est appelée heuristique valide. Le coût minimum de déplacement étant de 1, nous n'avons d'autre choix que d'utiliser les mêmes coûts pour déterminer l'heuristique.
À strictement parler, il est tout à fait normal d'utiliser des coûts encore plus bas, mais cela ne fera que rendre l'heuristique plus faible. L'heuristique minimale possible est zéro, ce qui nous donne juste l'algorithme de Dijkstra. Avec une heuristique non nulle, l'algorithme est appelé A
* (prononcé "A star").
Pourquoi est-il appelé A *?L'idée d'ajouter une heuristique à l'algorithme de Dijkstra a été proposée pour la première fois par Niels Nilsson. Il a nommé sa version A1. Bertram Rafael a proposé plus tard la meilleure version qu'il a appelée A2. Ensuite, Peter Hart a prouvé qu'avec une bonne heuristique, A2 est optimal, c'est-à-dire qu'il ne peut y avoir de meilleure version. Cela l'a forcé à appeler l'algorithme A * pour montrer qu'il ne pouvait pas être amélioré, c'est-à-dire que A3 ou A4 n'apparaîtraient pas. Alors oui, l'algorithme A * est le meilleur que nous puissions obtenir, mais il est aussi bon que son heuristique.
paquet d'unitéFile d'attente prioritaire
Bien que A
* soit un bon algorithme, notre implémentation n'est pas aussi efficace, car pour stocker la bordure, nous utilisons une liste qui doit être triée à chaque itération. Comme mentionné dans la partie précédente, nous avons besoin d'une file d'attente prioritaire, mais son implémentation standard n'existe pas. Par conséquent, créons-le vous-même.
Notre tour doit prendre en charge l'opération de définition et d'exclusion de la file d'attente en fonction de la priorité. Il doit également prendre en charge la modification de la priorité d'une cellule déjà dans la file d'attente. Idéalement, nous l'implémentons en minimisant la recherche de tri et la mémoire allouée. De plus, cela doit rester simple.
Créez votre propre file d'attente
Créez une nouvelle classe
HexCellPriorityQueue
avec les méthodes courantes requises. Nous utilisons une simple liste pour suivre le contenu d'une file d'attente. De plus, nous y ajouterons la méthode
Clear
pour effacer la file d'attente afin qu'elle puisse être utilisée à plusieurs reprises.
using System.Collections.Generic; public class HexCellPriorityQueue { List<HexCell> list = new List<HexCell>(); public void Enqueue (HexCell cell) { } public HexCell Dequeue () { return null; } public void Change (HexCell cell) { } public void Clear () { list.Clear(); } }
Nous stockons les priorités des cellules dans les cellules elles-mêmes. Autrement dit, avant d'ajouter une cellule à la file d'attente, sa priorité doit être définie. Mais en cas de changement de priorité, il sera probablement utile de savoir quelle était l'ancienne priorité. Ajoutons donc ceci à
Change
comme paramètre.
public void Change (HexCell cell, int oldPriority) { }
Il est également utile de connaître le nombre de cellules dans la file d'attente, ajoutons donc la propriété
Count
pour cela. Utilisez simplement le champ pour lequel nous allons effectuer l'incrémentation et la décrémentation correspondantes.
int count = 0; public int Count { get { return count; } } public void Enqueue (HexCell cell) { count += 1; } public HexCell Dequeue () { count -= 1; return null; } … public void Clear () { list.Clear(); count = 0; }
Ajouter à la file d'attente
Lorsqu'une cellule est ajoutée à la file d'attente, utilisons d'abord sa priorité comme index, en traitant la liste comme un simple tableau.
public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; list[priority] = cell; }
Cependant, cela ne fonctionne que si la liste est suffisamment longue, sinon nous dépasserons les frontières. Vous pouvez éviter cela en ajoutant des éléments vides à la liste jusqu'à ce qu'elle atteigne la longueur requise. Ces éléments vides ne référencent pas la cellule, vous pouvez donc les créer en ajoutant
null
à la liste.
int priority = cell.SearchPriority; while (priority >= list.Count) { list.Add(null); } list[priority] = cell;
Liste avec trousMais c'est ainsi que nous stockons une seule cellule par priorité, et il y en aura probablement plusieurs. Pour suivre toutes les cellules avec la même priorité, nous devons utiliser une autre liste. Bien que nous puissions utiliser une vraie liste pour chaque priorité, nous pouvons également ajouter une propriété à
HexCell
pour les lier ensemble. Cela nous permet de créer une chaîne de cellules appelée liste chaînée.
public HexCell NextWithSamePriority { get; set; }
Pour créer une chaîne, laissez
HexCellPriorityQueue.Enqueue
forcer la cellule nouvellement ajoutée à se référer à la valeur actuelle avec la même priorité, avant de la supprimer.
cell.NextWithSamePriority = list[priority]; list[priority] = cell;
Liste des listes liéesSupprimer de la file d'attente
Pour obtenir une cellule d'une file d'attente prioritaire, nous devons accéder à la liste liée à l'index non vide le plus bas. Par conséquent, nous allons parcourir la liste en boucle jusqu'à ce que nous la trouvions. Si nous ne trouvons pas, alors la file d'attente est vide et nous retournons
null
.
De la chaîne trouvée, nous pouvons renvoyer n'importe quelle cellule, car elles ont toutes la même priorité. Le moyen le plus simple consiste à renvoyer la cellule depuis le début de la chaîne.
public HexCell Dequeue () { count -= 1; for (int i = 0; i < list.Count; i++) { HexCell cell = list[i]; if (cell != null) { return cell; } } return null; }
Pour conserver le lien vers la chaîne restante, utilisez la cellule suivante avec la même priorité que le nouveau départ. S'il n'y avait qu'une seule cellule à ce niveau de priorité, alors l'élément devient
null
et sera ignoré à l'avenir.
if (cell != null) { list[i] = cell.NextWithSamePriority; return cell; }
Suivi minimum
Cette approche fonctionne, mais parcourt la liste chaque fois qu'une cellule est reçue. Nous ne pouvons pas éviter de trouver le plus petit indice non vide, mais nous ne sommes pas tenus de recommencer à zéro à chaque fois. Au lieu de cela, nous pouvons suivre la priorité minimale et commencer la recherche avec elle. Initialement, le minimum est essentiellement égal à l'infini.
int minimum = int.MaxValue; … public void Clear () { list.Clear(); count = 0; minimum = int.MaxValue; }
Lors de l'ajout d'une cellule à la file d'attente, nous modifions le minimum si nécessaire.
public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; if (priority < minimum) { minimum = priority; } … }
Et lors du retrait de la file d'attente, nous utilisons au moins la liste pour les itérations, et ne partons pas de zéro.
public HexCell Dequeue () { count -= 1; for (; minimum < list.Count; minimum++) { HexCell cell = list[minimum]; if (cell != null) { list[minimum] = cell.NextWithSamePriority; return cell; } } return null; }
Cela réduit considérablement le temps nécessaire pour parcourir la boucle de liste de priorités.
Changer les priorités
Lors de la modification de la priorité d'une cellule, elle doit être supprimée de la liste chaînée dont elle fait partie. Pour ce faire, nous devons suivre la chaîne jusqu'à ce que nous la trouvions.
Commençons par déclarer que la tête de l'ancienne liste de priorité sera la cellule actuelle, et nous suivrons également la cellule suivante. Nous pouvons immédiatement prendre la cellule suivante, car nous savons qu'il y a au moins une cellule par cet indice.
public void Change (HexCell cell, int oldPriority) { HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; }
Si la cellule actuelle est une cellule modifiée, il s'agit de la cellule principale et nous pouvons la couper comme si nous l'avions retirée de la file d'attente.
HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; if (current == cell) { list[oldPriority] = next; }
Si ce n'est pas le cas, alors nous devons suivre la chaîne jusqu'à ce que nous soyons dans la cellule en face de la cellule modifiée. Il contient un lien vers la cellule qui a été modifiée.
if (current == cell) { list[oldPriority] = next; } else { while (next != cell) { current = next; next = current.NextWithSamePriority; } }
À ce stade, nous pouvons supprimer la cellule modifiée de la liste liée, en la sautant.
while (next != cell) { current = next; next = current.NextWithSamePriority; } current.NextWithSamePriority = cell.NextWithSamePriority;
Après avoir supprimé une cellule, vous devez l'ajouter à nouveau pour qu'elle apparaisse dans la liste de sa nouvelle priorité.
public void Change (HexCell cell, int oldPriority) { … Enqueue(cell); }
La méthode
Enqueue
incrémente le compteur, mais en réalité nous
Enqueue
pas de nouvelle cellule. Par conséquent, afin de compenser cela, nous devrons décrémenter le compteur.
Enqueue(cell); count -= 1;
Utilisation de la file d'attente
Nous pouvons maintenant profiter de notre file d'attente prioritaire chez
HexGrid
. Cela peut être fait avec une seule instance, réutilisable pour toutes les opérations de recherche.
HexCellPriorityQueue searchFrontier; … IEnumerator Search (HexCell fromCell, HexCell toCell) { if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } … }
Avant de démarrer la boucle, la méthode Search
doit d'abord être ajoutée à la file d'attente fromCell
et chaque itération commence par la sortie de la cellule de la file d'attente. Cela remplacera l'ancien code frontalier. WaitForSeconds delay = new WaitForSeconds(1 / 60f);
Modifiez le code afin qu'il ajoute et modifie le voisin. Avant le changement, nous nous souviendrons de l'ancienne priorité. if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates);
De plus, nous n'avons plus besoin de trier la frontière.
Recherche à l'aide d'une file d'attente prioritaireComme mentionné précédemment, le chemin le plus court trouvé dépend de l'ordre de traitement des cellules. Notre tour crée un ordre différent de l'ordre de la liste triée, nous pouvons donc obtenir d'autres moyens. Puisque nous ajoutons et supprimons de la tête de la liste liée pour chaque priorité, ils ressemblent plus à des piles qu'à des files d'attente. Les cellules ajoutées en dernier sont traitées en premier. Un effet secondaire de cette approche est que l'algorithme est sujet au zigzag. Par conséquent, la probabilité de chemins en zigzag augmente également. Heureusement, ces chemins semblent généralement meilleurs, donc cet effet secondaire est en notre faveur.Liste triée et file d'attente avec priorité aupaquet unitairePartie 17: mouvement limité
- Nous trouvons des moyens pour un mouvement pas à pas.
- Affichez immédiatement le chemin.
- Nous créons une recherche plus efficace.
- Nous visualisons uniquement le chemin.
Dans cette partie, nous allons diviser le mouvement en mouvements et accélérer autant que possible la recherche.Voyagez à partir de plusieurs mouvementsMouvement pas à pas
Les jeux de stratégie qui utilisent des filets hexagonaux sont presque toujours au tour par tour. Les unités se déplaçant sur la carte ont une vitesse limitée, ce qui limite la distance parcourue en un tour.La vitesse
Pour fournir un support pour un mouvement limité, nous ajoutons dans HexGrid.FindPath
et dans le HexGrid.Search
paramètre entier speed
. Il détermine l'amplitude de mouvement pour un mouvement. public void FindPath (HexCell fromCell, HexCell toCell, int speed) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell, speed)); } IEnumerator Search (HexCell fromCell, HexCell toCell, int speed) { … }
Différents types d'unités dans le jeu utilisent des vitesses différentes. La cavalerie est rapide, l'infanterie est lente, etc. Nous n'avons pas encore d'unités, donc pour l'instant nous allons utiliser une vitesse constante. Prenons une valeur de 24. Il s'agit d'une valeur assez grande, non divisible par 5 (le coût par défaut du déménagement). Ajouter un argument pour FindPath
à HexMapEditor.HandleInput
une vitesse constante. if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); }
Se déplace
En plus de suivre le coût total du déplacement le long du chemin, nous devons maintenant également savoir combien de mouvements il faudra pour se déplacer le long de celui-ci. Mais nous n'avons pas besoin de stocker ces informations dans chaque cellule. Il peut être obtenu en divisant la distance parcourue par la vitesse. Puisque ce sont des entiers, nous utiliserons la division entière. C'est-à-dire que les distances totales ne dépassant pas 24 correspondent au parcours 0. Cela signifie que l'ensemble du chemin peut être complété dans le parcours actuel. Si le point final est à une distance de 30, alors ce doit être le tour 1. Pour arriver au point final, l'unité devra dépenser tout son mouvement dans le tour en cours et en partie dans le tour suivant.Déterminons le cours de la cellule actuelle et de tous ses voisins à l'intérieurHexGrid.Search
. Le parcours de la cellule courante ne peut être calculé qu'une seule fois, juste avant de faire le tour du cycle voisin. Le mouvement du voisin peut être déterminé dès que nous trouvons la distance qui le sépare. int currentTurn = current.Distance / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else if (current.Walled != neighbor.Walled) { continue; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; } int turn = distance / speed; … }
Mouvement perdu
Si le mouvement du voisin est supérieur au mouvement actuel, alors nous avons traversé la limite du mouvement. Si le mouvement nécessaire pour atteindre un voisin était de 1, alors tout va bien. Mais si passer à la cellule suivante coûte plus cher, alors tout devient plus compliqué.Supposons que nous nous déplacions le long d'une carte homogène, c'est-à-dire que pour entrer dans chaque cellule, vous avez besoin de 5 unités de mouvement. Notre vitesse est de 24. Après quatre étapes, nous avons dépensé 20 unités de notre stock de mouvement, et il en reste 4. Dans l'étape suivante, 5 unités sont à nouveau nécessaires, c'est-à-dire une de plus que celles disponibles. Que devons-nous faire à ce stade?Il existe deux approches à cette situation. La première consiste à permettre à l'unité d'entrer dans la cinquième cellule du tour en cours, même si nous n'avons pas assez de mouvement. La seconde consiste à interdire le mouvement pendant le mouvement en cours, c'est-à-dire que les points de mouvement restants ne peuvent pas être utilisés et ils seront perdus.Le choix de l'option dépend du jeu. En général, la première approche est plus appropriée pour les jeux dans lesquels les unités ne peuvent se déplacer que de quelques pas par tour, par exemple, pour les jeux de la série Civilization. Cela garantit que les unités peuvent toujours se déplacer d'au moins une cellule par tour. Si les unités peuvent déplacer plusieurs cellules par tour, comme dans Age of Wonders ou Battle for Wesnoth, alors la deuxième option est meilleure.Puisque nous utilisons la vitesse 24, choisissons la deuxième approche. Pour qu'il commence à fonctionner, nous devons isoler les coûts de l'accès à la cellule suivante avant de l'ajouter à la distance actuelle.
Si, par conséquent, nous traversons la frontière du mouvement, nous utilisons d'abord tous les points de mouvement du mouvement en cours. Nous pouvons le faire en multipliant simplement le mouvement par la vitesse. Après cela, nous ajoutons le coût du déménagement. int distance = current.Distance + moveCost; int turn = distance / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; }
À la suite de cela, nous terminerons le premier mouvement dans la quatrième cellule avec 4 points de mouvement inutilisés. Ces points perdus sont ajoutés aux coûts de la cinquième cellule, donc sa distance devient 29, et non 25. En conséquence, les distances sont plus grandes qu'auparavant. Par exemple, la dixième cellule avait une distance de 50. Mais maintenant, pour y entrer, nous devons traverser les frontières de deux mouvements, perdant 8 points de mouvement, c'est-à-dire que la distance à celle-ci devient maintenant 58.Plus long que prévuÉtant donné que les points de mouvement inutilisés sont ajoutés aux distances aux cellules, ils sont pris en compte lors de la détermination du chemin le plus court. Le moyen le plus efficace est de perdre le moins de points possible. Par conséquent, à différentes vitesses, nous pouvons obtenir différents chemins.Affichage des mouvements au lieu des distances
Lorsque nous jouons au jeu, nous ne sommes pas très intéressés par les valeurs de distance utilisées pour trouver le chemin le plus court. Nous sommes intéressés par le nombre de mouvements nécessaires pour atteindre le point final. Par conséquent, au lieu des distances, affichons les mouvements.Tout d'abord, débarrassez-vous de UpdateDistanceLabel
son appel HexCell
. public int Distance { get { return distance; } set { distance = value;
Au lieu de cela, nous ajouterons à la HexCell
méthode générale SetLabel
qui reçoit une chaîne arbitraire. public void SetLabel (string text) { UnityEngine.UI.Text label = uiRect.GetComponent<Text>(); label.text = text; }
Nous utilisons cette nouvelle méthode pour HexGrid.Search
nettoyer les cellules. Pour masquer les cellules, affectez-les simplement null
. for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].SetLabel(null); cells[i].DisableHighlight(); }
Ensuite, nous attribuons à la marque du voisin la valeur de son mouvement. Après cela, nous pourrons voir combien de mouvements supplémentaires il faudra pour aller jusqu'au bout. if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); }
Le nombre de mouvements requis pour se déplacer le long du chemin dupaquet d'unitéChemins instantanés
De plus, lorsque nous jouons au jeu, peu nous importe comment l'algorithme de recherche de chemin trouve le chemin. Nous voulons voir le chemin demandé immédiatement. Pour le moment, nous pouvons être sûrs que l'algorithme fonctionne, alors débarrassons-nous de la visualisation de la recherche.Sans corutine
Pour un passage lent dans l'algorithme, nous avons utilisé la corutine. Nous n'avons plus besoin de le faire, nous allons donc nous débarrasser des appels StartCoroutine
et StopAllCoroutines
c HexGrid
. Au lieu de cela, nous l'invoquons simplement Search
comme méthode régulière. public void Load (BinaryReader reader, int header) {
Comme nous ne l'utilisons plus Search
comme coroutine, il n'a pas besoin de rendement, nous allons donc nous débarrasser de cet opérateur. Cela signifie que nous supprimerons également la déclaration WaitForSeconds
et changerons le type de retour de la méthode en void
. void Search (HexCell fromCell, HexCell toCell, int speed) { …
Résultats instantanésDéfinition du temps de recherche
Maintenant, nous pouvons obtenir les chemins instantanément, mais à quelle vitesse sont-ils calculés? Les chemins courts apparaissent presque immédiatement, mais les chemins longs sur les grandes cartes peuvent sembler un peu lents.Mesurons le temps qu'il faut pour rechercher et afficher le chemin. Nous pouvons utiliser un profileur pour déterminer le temps de recherche, mais c'est un peu trop et crée des coûts supplémentaires. Utilisons à la place Stopwatch
, qui se trouve dans l'espace de noms System.Diagnostics
. Comme nous ne l'utilisons que temporairement, je n'ajouterai pas la construction using
au début du script.Juste avant la recherche, créez un nouveau chronomètre et démarrez-le. Une fois la recherche terminée, arrêtez le chronomètre et affichez le temps écoulé dans la console. public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); }
Choisissons le pire cas pour notre algorithme - une recherche du coin inférieur gauche au coin supérieur droit d'une grande carte. Le pire est une carte uniforme, car l'algorithme devra traiter les 4 800 cellules de la carte.Rechercher dans le pire des cas Le tempspassé à le rechercher peut être différent, car l'éditeur Unity n'est pas le seul processus en cours d'exécution sur votre machine. Alors testez-le plusieurs fois pour comprendre la durée moyenne. Dans mon cas, la recherche prend environ 45 millisecondes. Ce n'est pas beaucoup et correspond à 22,22 trajets par seconde; dénotons cela comme 22 pps (chemins par seconde). Cela signifie que la fréquence d'images du jeu diminuera également d'un maximum de 22 ips dans cette image lorsque ce chemin sera calculé. Et cela sans tenir compte de tous les autres travaux, par exemple, le rendu du cadre lui-même. Autrement dit, nous obtenons une diminution assez importante de la fréquence d'images, elle tombera à 20 ips.Lorsque vous effectuez un tel test de performances, vous devez tenir compte du fait que les performances de l'éditeur Unity ne seront pas aussi élevées que les performances de l'application terminée. Si je fais le même test avec l'assemblage, cela ne prendra en moyenne que 15 ms. C'est 66 pps, ce qui est beaucoup mieux. Néanmoins, cela représente toujours une grande partie des ressources allouées par trame, de sorte que la fréquence d'images deviendra inférieure à 60 ips.Où puis-je voir le journal de débogage de l'assembly?Unity , . . , , Unity
Log Files .
Recherchez uniquement si nécessaire.
Nous pouvons faire une optimisation simple - effectuer une recherche uniquement lorsque cela est nécessaire. Pendant que nous lançons une nouvelle recherche dans chaque image dans laquelle le bouton de la souris est maintenu enfoncé. Par conséquent, la fréquence d'images sera constamment sous-estimée lors du glisser-déposer. Nous pouvons éviter cela en n'initiant une nouvelle recherche HexMapEditor.HandleInput
que lorsque nous avons vraiment affaire à un nouveau point de terminaison. Sinon, le chemin visible actuel est toujours valide. if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell != currentCell) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } } else if (searchFromCell && searchFromCell != currentCell) { if (searchToCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); } }
Afficher les étiquettes uniquement pour le chemin
L'affichage des marques de déplacement est une opération assez coûteuse, notamment parce que nous utilisons une approche non optimisée. L'exécution de cette opération pour toutes les cellules ralentira certainement l'exécution. Alors sautons l'étiquetage HexGrid.Search
. if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance;
Nous devons voir ces informations uniquement pour le chemin trouvé. Par conséquent, après avoir atteint le point final, nous calculerons le parcours et définirons les étiquettes des seules cellules en cours de route. if (current == toCell) { current = current.PathFrom; while (current != fromCell) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } break; }
Affichage des étiquettes pour les cellules de chemin uniquementNous n'incluons maintenant que les étiquettes de cellule entre le début et la fin. Mais le point final est la chose la plus importante, nous devons également lui attribuer une étiquette. Vous pouvez le faire en démarrant le cycle de chemin à partir de la cellule de destination, et non à partir de la cellule en face d'elle. Dans ce cas, l'éclairage du point final du rouge passera au blanc, nous supprimerons donc son rétro-éclairage sous le cycle. fromCell.EnableHighlight(Color.blue);
Les informations de progression sont les plus importantes pour le noeud final.Après ces modifications, le pire des cas est réduit à 23 millisecondes dans l'éditeur et à 6 millisecondes dans l'assemblage terminé. Ce sont 43 pps et 166 pps - beaucoup mieux.paquet d'unitéLa recherche la plus intelligente
Dans la partie précédente, nous avons rendu la procédure de recherche plus intelligente en implémentant l'algorithme A * . Cependant, en réalité, nous n'effectuons toujours pas la recherche de la manière la plus optimale. À chaque itération, nous calculons les distances de la cellule actuelle à tous ses voisins. Cela est vrai pour les cellules qui ne sont pas encore ou font actuellement partie de la bordure de recherche. Mais les cellules qui ont déjà été retirées de la frontière, n'ont plus besoin d'être prises en compte, car nous avons déjà trouvé le chemin le plus court vers ces cellules. L'implémentation correcte de A * ignore ces cellules, nous pouvons donc faire de même.Phase de recherche de cellules
Comment savoir si une cellule a déjà quitté la frontière? Bien que nous ne pouvons pas déterminer cela. Par conséquent, vous devez suivre dans quelle phase de la recherche se trouve la cellule. Elle n'est pas encore à la frontière, elle y est maintenant ou est à l'étranger. Nous pouvons suivre cela en ajoutant à une HexCell
simple propriété entière. public int SearchPhase { get; set; }
Par exemple, 0 signifie que les cellules n'ont pas encore atteint, 1 - que la cellule est maintenant dans la bordure et 2 - qu'elle a déjà été supprimée de la bordure.Frapper la frontière
Dans HexGrid.Search
nous pouvons réinitialiser toutes les cellules à 0 et toujours utiliser 1 pour la bordure. Ou nous pouvons augmenter le nombre de frontières à chaque nouvelle recherche. Grâce à cela, nous n'aurons pas à faire face au déversement de cellules si nous augmentons à chaque fois le nombre de frontières de deux. int searchFrontierPhase; … void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; … }
Nous devons maintenant définir la phase de la recherche de cellules lors de leur ajout à la bordure. Le processus commence par une cellule initiale, qui est ajoutée à la bordure. fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell);
Et aussi chaque fois que nous ajoutons un voisin à la frontière. if (neighbor.Distance == int.MaxValue) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); }
Contrôle des frontières
Jusqu'à présent, pour vérifier que la cellule n'a pas encore été ajoutée à la bordure, nous avons utilisé une distance égale à int.MaxValue
. Maintenant, nous pouvons comparer la phase de la recherche de cellules avec la bordure actuelle.
Cela signifie que nous n'avons plus besoin de réinitialiser les distances des cellules avant de chercher, c'est-à-dire que nous devrons faire moins de travail, ce qui est bien. for (int i = 0; i < cells.Length; i++) {
Quitter la frontière
Lorsqu'une cellule est retirée de la frontière, nous désignons cela par une augmentation de sa phase de recherche. Cela la place au-delà de la frontière actuelle et avant la suivante. while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; … }
Nous pouvons maintenant ignorer les cellules retirées de la frontière, en évitant le calcul inutile et la comparaison des distances. for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } … }
À ce stade, notre algorithme produit toujours les mêmes résultats, mais plus efficacement. Sur ma machine, la recherche du pire des cas prend 20 ms dans l'éditeur et 5 ms dans l'assemblage.Nous pouvons également calculer combien de fois la cellule a été traitée par l'algorithme, augmentant le compteur lors du calcul de la distance à la cellule. Auparavant, notre algorithme dans le pire des cas calculait 28 239 distances. Dans l'algorithme A * prêt à l'emploi, nous calculons ses 14 120 distances. Le montant a diminué de 50%. Le degré d'impact de ces indicateurs sur la productivité dépend des coûts de calcul du coût du déménagement. Dans notre cas, il n'y a pas beaucoup de travail ici, donc l'amélioration de l'assemblage n'est pas très importante, mais elle est très perceptible dans l'éditeur.paquet d'unitéOuvrir la voie
Lorsque vous lancez une nouvelle recherche, nous devons d'abord effacer la visualisation du chemin précédent. Pendant ce temps, désactivez la sélection et supprimez les étiquettes de chaque cellule de la grille. Il s'agit d'une approche très difficile. Idéalement, nous devons éliminer uniquement les cellules qui faisaient partie du chemin précédent.Rechercher uniquement
Commençons par supprimer complètement le code de visualisation de Search
. Il n'a qu'à effectuer une recherche de chemin et n'a pas à savoir ce que nous ferons de ces informations. void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); }
Pour signaler que Search
nous avons trouvé un moyen, nous reviendrons booléen. bool Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; if (current == toCell) { return true; } … } return false; }
Rappelez-vous le chemin
Lorsque le chemin est trouvé, nous devons nous en souvenir. Grâce à cela, nous pourrons le nettoyer à l'avenir. Par conséquent, nous allons suivre les points d'extrémité et voir s'il existe un chemin entre eux. HexCell currentPathFrom, currentPathTo; bool currentPathExists; … public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); }
Afficher à nouveau le chemin
Nous pouvons utiliser les données de recherche que nous avons enregistrées pour visualiser à nouveau le chemin. Créons une nouvelle méthode pour cela ShowPath
. Il parcourra le cycle de la fin au début du chemin, mettant en évidence les cellules et attribuant une valeur de trait à leurs étiquettes. Pour ce faire, nous devons connaître la vitesse, alors faites-en un paramètre. Si nous n'avons pas de chemin, la méthode sélectionne simplement les points de terminaison. void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } } currentPathFrom.EnableHighlight(Color.blue); currentPathTo.EnableHighlight(Color.red); }
Appelez cette méthode FindPath
après la recherche. currentPathExists = Search(fromCell, toCell, speed); ShowPath(speed);
Balayer
Nous revoyons le chemin, mais maintenant il ne s'éloigne pas. Pour l'effacer, créez une méthode ClearPath
. En fait, c'est une copie ShowPath
, sauf qu'elle désactive la sélection et les étiquettes, mais ne les inclut pas. Cela fait, il doit effacer les données de chemin enregistrées qui ne sont plus valides. void ClearPath () { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { current.SetLabel(null); current.DisableHighlight(); current = current.PathFrom; } current.DisableHighlight(); currentPathExists = false; } currentPathFrom = currentPathTo = null; }
En utilisant cette méthode, nous pouvons effacer la visualisation de l'ancien chemin en visitant uniquement les cellules nécessaires, la taille de la carte n'est plus importante. Nous l'appellerons FindPath
avant de lancer une nouvelle recherche. sw.Start(); ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); if (currentPathExists) { ShowPath(speed); } sw.Stop();
De plus, nous effacerons le chemin lors de la création d'une nouvelle carte. public bool CreateMap (int x, int z) { … ClearPath(); if (chunks != null) { for (int i = 0; i < chunks.Length; i++) { Destroy(chunks[i].gameObject); } } … }
Et aussi avant de charger une autre carte. public void Load (BinaryReader reader, int header) { ClearPath(); … }
La visualisation du chemin est à nouveau effacée, comme avant ce changement. Mais maintenant, nous utilisons une approche plus efficace, et dans le pire des cas, le temps est passé à 14 millisecondes. Assez d'améliorations sérieuses uniquement grâce à un nettoyage plus intelligent. Le temps d'assemblage a été réduit à 3 ms, ce qui représente 333 points par seconde. Grâce à cela, la recherche de chemins est exactement applicable en temps réel.Maintenant que nous avons effectué une recherche rapide des chemins, nous pouvons supprimer le code de débogage temporaire. public void FindPath (HexCell fromCell, HexCell toCell, int speed) {
paquet d'unitéPartie 18: unités
- Nous plaçons les escouades sur la carte.
- Enregistrez et chargez les escouades.
- Nous trouvons des moyens pour les troupes.
- Nous déplaçons les unités.
Maintenant que nous avons compris comment rechercher un chemin, plaçons les escouades sur la carte.Des renforts sont arrivésCréer des escouades
Jusqu'à présent, nous n'avons traité que des cellules et de leurs objets fixes. Les unités diffèrent d'eux en ce qu'elles sont mobiles. Une escouade peut signifier n'importe quelle échelle, d'une personne ou d'un véhicule à une armée entière. Dans ce tutoriel, nous nous limitons à un type d'unité généralisé simple. Après cela, nous passerons à la prise en charge de combinaisons de plusieurs types d'unités.Escouade préfabriquée
Pour travailler avec des escouades, créez un nouveau type de composant HexUnit
. Pour l'instant, commençons par un vide MonoBehaviour
et ajoutons plus tard des fonctionnalités. using UnityEngine; public class HexUnit : MonoBehaviour { }
Créez un objet de jeu vide avec ce composant, qui devrait devenir un préfabriqué. Ce sera l'objet racine de l'équipe.Escouade préfabriquée.Ajoutez un modèle 3D symbolisant le détachement en tant qu'objet enfant. J'ai utilisé un simple cube à l'échelle pour lequel j'ai créé un matériau bleu. L'objet racine détermine le niveau du sol du détachement, par conséquent, nous déplaçons en conséquence l'élément enfant.Élément de cube enfantAjoutez un collisionneur à l'équipe afin qu'il soit plus facile de le sélectionner à l'avenir. Le collisionneur du cube standard nous convient tout à fait, il suffit de le placer dans une seule cellule.Création d'instances d'escouade
Comme nous n'avons pas encore de gameplay, la création d'unités se fait en mode édition. Par conséquent, cela devrait être résolu HexMapEditor
. Pour ce faire, il a besoin d'un préfabriqué, alors ajoutez un champ HexUnit unitPrefab
et connectez-le. public HexUnit unitPrefab;
Connexion du préfabriquéLors de la création d'unités, nous les placerons sur la cellule sous le curseur. Il HandleInput
existe un code pour trouver cette cellule lors de la modification d'un terrain. Maintenant, nous en avons également besoin pour les escouades, nous allons donc déplacer le code correspondant vers une méthode distincte. HexCell GetCellUnderCursor () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { return hexGrid.GetCell(hit.point); } return null; }
Nous pouvons maintenant utiliser cette méthode pour la HandleInput
simplifier. void HandleInput () {
Ensuite, ajoutez une nouvelle méthode CreateUnit
qui utilise également GetCellUnderCursor
. S'il y a une cellule, nous créerons une nouvelle équipe. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { Instantiate(unitPrefab); } }
Pour garder la hiérarchie propre, utilisons la grille comme parent pour tous les objets de jeu dans les escouades. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); } }
La façon la plus simple d'ajouter la HexMapEditor
prise en charge de la création d'unités consiste à appuyer sur une touche. Modifiez la méthode Update
afin qu'elle appelle CreateUnit
lorsque vous appuyez sur la touche U. Comme avec c HandleInput
, cela doit se produire si le curseur n'est pas au-dessus de l'élément GUI. Tout d'abord, nous vérifierons si nous devons modifier la carte, et sinon, nous vérifierons si nous devons ajouter une équipe. Si oui, appelez CreateUnit
. void Update () {
Instance créée de l'escouadePlacement des troupes
Nous pouvons maintenant créer des unités, mais elles apparaissent à l'origine de la carte. Nous devons les mettre au bon endroit. Pour cela, il faut que les troupes soient conscientes de leur position. Par conséquent, nous ajoutons à la HexUnit
propriété Location
indiquant la cellule qu'ils occupent. Lors de la définition de la propriété, nous modifierons la position de l'escouade afin qu'elle corresponde à la position de la cellule. public HexCell Location { get { return location; } set { location = value; transform.localPosition = value.Position; } } HexCell location;
Maintenant, je HexMapEditor.CreateUnit
dois attribuer la position de la cellule de l'escouade sous le curseur. Ensuite, les unités seront là où elles devraient. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; } }
Escouades sur la carteOrientation de l'unité
Jusqu'à présent, toutes les unités ont la même orientation, ce qui semble peu naturel. Pour les faire revivre, ajoutez à la HexUnit
propriété Orientation
. Il s'agit d'une valeur flottante qui indique la rotation de l'escouade le long de l'axe Y en degrés. Lors de sa configuration, nous modifierons en conséquence la rotation de l'objet de jeu lui-même. public float Orientation { get { return orientation; } set { orientation = value; transform.localRotation = Quaternion.Euler(0f, value, 0f); } } float orientation;
En HexMapEditor.CreateUnit
assigner une rotation aléatoire de 0 à 360 degrés. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } }
Différentes orientations d'unitéUne escouade par cellule
Les unités semblent bonnes si elles ne sont pas créées dans une cellule. Dans ce cas, nous obtenons un groupe de cubes étranges.Unités superposéesCertains jeux permettent de placer plusieurs unités au même endroit, d'autres non. Puisqu'il est plus facile de travailler avec une escouade par cellule, je choisirai cette option. Cela signifie que nous ne devons créer une nouvelle escouade que lorsque la cellule actuelle n'est pas occupée. Pour que vous puissiez le découvrir, ajoutez-le à la HexCell
propriété standard Unit
. public HexUnit Unit { get; set; }
Nous utilisons cette propriété HexUnit.Location
pour faire savoir à la cellule si l'unité s'y trouve. public HexCell Location { get { return location; } set { location = value; value.Unit = this; transform.localPosition = value.Position; } }
Il HexMapEditor.CreateUnit
peut maintenant vérifier si la cellule actuelle est libre. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { HexUnit unit = Instantiate(unitPrefab); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } }
Modification des cellules occupées
Initialement, les unités sont placées correctement, mais tout peut changer si leurs cellules sont modifiées ultérieurement. Si la hauteur de la cellule change, alors l'unité qui l'occupe sera suspendue au-dessus d'elle ou y plongera.Escouades suspendues et noyéesLa solution consiste à vérifier la position de l'escouade après avoir effectué des modifications. Pour ce faire, ajoutez la méthode à HexUnit
. Jusqu'à présent, nous ne sommes intéressés que par la position de l'équipe, alors posez-la à nouveau. public void ValidateLocation () { transform.localPosition = location.Position; }
Nous devons coordonner la position du détachement lors de la mise à jour de la cellule, ce qui se passe lorsque les méthodes Refresh
ou l' RefreshSelfOnly
objet sont HexCell
appelés. Bien sûr, cela n'est nécessaire que lorsqu'il y a vraiment un détachement dans la cellule. void Refresh () { if (chunk) { chunk.Refresh(); … if (Unit) { Unit.ValidateLocation(); } } } void RefreshSelfOnly () { chunk.Refresh(); if (Unit) { Unit.ValidateLocation(); } }
Supprimer des escouades
En plus de créer des unités, il serait utile de les détruire. Par conséquent, ajoutez à la HexMapEditor
méthode DestroyUnit
. Il doit vérifier s'il y a un détachement dans la cellule sous le curseur, et si c'est le cas, détruire l'objet de jeu du détachement. void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { Destroy(cell.Unit.gameObject); } }
Veuillez noter que pour arriver dans l'équipe, nous traversons la cellule. Pour interagir avec l'équipe, déplacez simplement la souris sur sa cellule. Par conséquent, pour que cela fonctionne, l'équipe n'a pas besoin d'avoir un collisionneur. Cependant, l'ajout d'un collisionneur facilite la sélection car il bloque les rayons qui autrement entreraient en collision avec la cellule derrière l'escouade.Utilisons Update
une combinaison de Shift gauche + U pour détruire l'équipe . if (Input.GetKeyDown(KeyCode.U)) { if (Input.GetKey(KeyCode.LeftShift)) { DestroyUnit(); } else { CreateUnit(); } return; }
Dans le cas où nous créons et détruisons plusieurs unités, soyons prudents et effaçons la propriété lors du retrait de l'unité. Autrement dit, nous effaçons explicitement le lien cellulaire vers l'équipe. Ajoutez à la HexUnit
méthode Die
qui traite de cela, ainsi que la destruction de votre propre objet de jeu. public void Die () { location.Unit = null; Destroy(gameObject); }
Nous appellerons cette méthode HexMapEditor.DestroyUnit
et ne détruirons pas directement l'équipe. void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) {
paquet d'unitéSauvegarde et chargement des escouades
Maintenant que nous pouvons avoir des unités sur la carte, nous devons les inclure dans le processus d'enregistrement et de chargement. Nous pouvons aborder cette tâche de deux manières. La première consiste à enregistrer les données de l'escouade lors de l'enregistrement d'une cellule afin que les données de la cellule et de l'escouade soient mélangées. La deuxième façon consiste à enregistrer les données de cellule et d'escouade séparément. Bien qu'il puisse sembler que la première approche soit plus facile à mettre en œuvre, la seconde nous donne des données plus structurées. Si nous partageons les données, il sera plus facile de travailler avec elles à l'avenir.Suivi des unités
Pour garder toutes les unités ensemble, nous devons les suivre. Nous le ferons en ajoutant à la HexGrid
liste des unités. Cette liste doit contenir toutes les unités sur la carte. List<HexUnit> units = new List<HexUnit>();
Lors de la création ou du chargement d'une nouvelle carte, nous devons nous débarrasser de toutes les unités sur la carte. Pour simplifier ce processus, créez une méthode ClearUnits
qui tue tout le monde sur la liste et l'efface. void ClearUnits () { for (int i = 0; i < units.Count; i++) { units[i].Die(); } units.Clear(); }
Nous appelons cette méthode in CreateMap
et in Load
. Faisons-le après avoir nettoyé le chemin. public bool CreateMap (int x, int z) { … ClearPath(); ClearUnits(); … } … public void Load (BinaryReader reader, int header) { ClearPath(); ClearUnits(); … }
Ajout d'escouades à la grille
Maintenant, lors de la création de nouvelles unités, nous devons les ajouter à la liste. Définissons une méthode pour cela AddUnit
, qui traitera également de l'emplacement de l'escouade et des paramètres de son objet parent. public void AddUnit (HexUnit unit, HexCell location, float orientation) { units.Add(unit); unit.transform.SetParent(transform, false); unit.Location = location; unit.Orientation = orientation; }
Maintenant, il HexMapEditor.CreatUnit
suffira d'appeler AddUnit
avec une nouvelle instance du détachement, son emplacement et son orientation aléatoire. void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) {
Supprimer des escouades de la grille
Ajoutez une méthode pour supprimer l'escouade et c HexGrid
. Il suffit de retirer l'équipe de la liste et de lui ordonner de mourir. public void RemoveUnit (HexUnit unit) { units.Remove(unit); unit.Die(); }
Nous appelons cette méthode au HexMapEditor.DestroyUnit
lieu de détruire directement l'équipe. void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) {
Enregistrement des unités
Puisque nous allons garder toutes les unités ensemble, nous devons nous rappeler quelles cellules elles occupent. Le moyen le plus fiable consiste à enregistrer les coordonnées de leur emplacement. Pour rendre cela possible, nous ajoutons les champs X et Z à la HexCoordinates
méthode Save
qui l'écrit. using UnityEngine; using System.IO; [System.Serializable] public struct HexCoordinates { … public void Save (BinaryWriter writer) { writer.Write(x); writer.Write(z); } }
La méthode Save
pour HexUnit
peut maintenant enregistrer les coordonnées et l'orientation de l'équipe. Ce sont toutes les données des unités que nous avons en ce moment. using UnityEngine; using System.IO; public class HexUnit : MonoBehaviour { … public void Save (BinaryWriter writer) { location.coordinates.Save(writer); writer.Write(orientation); } }
Puisqu'il HexGrid
suit les unités, sa méthode Save
enregistrera les données des unités. Tout d'abord, notez le nombre total d'unités, puis contournez-les toutes en boucle. public void Save (BinaryWriter writer) { writer.Write(cellCountX); writer.Write(cellCountZ); for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } writer.Write(units.Count); for (int i = 0; i < units.Count; i++) { units[i].Save(writer); } }
Nous avons modifié les données stockées, nous allons donc augmenter le numéro de version SaveLoadMenu.Save
à 2. L'ancien code de démarrage fonctionnera toujours, car il ne lira tout simplement pas les données de l'équipe. Cependant, vous devez augmenter le numéro de version pour indiquer que le fichier contient des informations sur l'unité. void Save (string path) { using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(2); hexGrid.Save(writer); } }
Escouades de chargement
Puisqu'il HexCoordinates
s'agit d'une structure, cela n'a pas beaucoup de sens d'y ajouter la méthode habituelle Load
. Faisons-en une méthode statique qui lit et renvoie les coordonnées stockées. public static HexCoordinates Load (BinaryReader reader) { HexCoordinates c; cx = reader.ReadInt32(); cz = reader.ReadInt32(); return c; }
Comme le nombre d'unités est variable, nous n'avons pas d'unités préexistantes dans lesquelles les données peuvent être chargées. Nous pouvons créer de nouvelles instances d'unités avant de charger leurs données, mais cela nécessitera que nous HexGrid
créons des instances de nouvelles unités au démarrage. Il vaut donc mieux le laisser HexUnit
. Nous utilisons également la méthode statique HexUnit.Load
. Commençons par lire simplement ces escouades. Pour lire la valeur du flotteur d'orientation, nous utilisons la méthode BinaryReader.ReadSingle
.Pourquoi célibataire?float
, . , double
, . Unity .
public static void Load (BinaryReader reader) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); }
L'étape suivante consiste à créer une instance d'une nouvelle équipe. Cependant, pour cela, nous avons besoin d'un lien vers le préfabriqué de l'unité. Afin de ne pas le compliquer encore, ajoutons une HexUnit
méthode statique pour cela . public static HexUnit unitPrefab;
Pour définir ce lien, utilisons-le à nouveau HexGrid
, comme nous l'avons fait avec la texture du bruit. Lorsque nous devons prendre en charge de nombreux types d'unités, nous allons passer à une meilleure solution. public HexUnit unitPrefab; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; CreateMap(cellCountX, cellCountZ); } … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; } }
Nous passons le préfabriqué de l'unité.Après avoir connecté le terrain, nous n'avons plus besoin d'un lien direct vers HexMapEditor
. Au lieu de cela, il peut utiliser HexUnit.unitPrefab
.
Nous pouvons maintenant créer une instance de la nouvelle équipe dans HexUnit.Load
. Au lieu de le renvoyer, nous pouvons utiliser les coordonnées et l'orientation chargées pour l'ajouter à la grille. Pour rendre cela possible, ajoutez un paramètre HexGrid
. public static void Load (BinaryReader reader, HexGrid grid) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); grid.AddUnit( Instantiate(unitPrefab), grid.GetCell(coordinates), orientation ); }
En fin de compte, HexGrid.Load
nous comptons le nombre d'unités et l'utilisons pour charger toutes les unités stockées, en nous passant comme argument supplémentaire. public void Load (BinaryReader reader, int header) { … int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } }
Bien sûr, cela ne fonctionnera que pour les fichiers de sauvegarde dont la version n'est pas inférieure à 2, dans les versions plus récentes, il n'y a pas d'unités à charger. if (header >= 2) { int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } }
Nous pouvons maintenant télécharger correctement les fichiers de la version 2, SaveLoadMenu.Load
augmentez donc le nombre de versions prises en charge à 2. void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header <= 2) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
paquet d'unitéMouvement des troupes
Les escouades sont mobiles, nous devons donc pouvoir les déplacer sur la carte. Nous avons déjà un code de recherche de chemin, mais jusqu'à présent, nous l'avons testé uniquement pour des endroits arbitraires. Nous devons maintenant supprimer l'ancienne interface utilisateur de test et créer une nouvelle interface utilisateur pour la gestion des effectifs.Nettoyage de l'éditeur de carte
Déplacer des unités le long des chemins fait partie du gameplay, cela ne s'applique pas à l'éditeur de carte. Par conséquent, nous nous débarrasserons HexMapEditor
de tout le code associé à la recherche du chemin.
Après avoir supprimé ce code, il n'est plus logique de laisser l'éditeur actif lorsque nous ne sommes pas en mode édition. Par conséquent, au lieu d'un champ de suivi de mode, nous pouvons simplement activer ou désactiver le composant HexMapEditor
. De plus, l'éditeur n'a plus à gérer les étiquettes d'interface utilisateur.
Puisque par défaut, nous ne sommes pas en mode d'édition de carte, dans Awake, nous désactiverons l'éditeur. void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); SetEditMode(false); }
Utiliser raycast pour rechercher la cellule actuelle sous le curseur est nécessaire lors de la modification de la carte et pour gérer les unités. Peut-être qu'à l'avenir, il nous sera utile pour autre chose. Passons de la logique de diffusion de rayons HexGrid
à une nouvelle méthode GetCell
avec un paramètre de faisceau. public HexCell GetCell (Ray ray) { RaycastHit hit; if (Physics.Raycast(ray, out hit)) { return GetCell(hit.point); } return null; }
HexMapEditor.GetCellUniderCursor
peut simplement appeler cette méthode avec le faisceau du curseur. HexCell GetCellUnderCursor () { return hexGrid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); }
Interface utilisateur du jeu
Pour contrôler l'interface utilisateur du mode de jeu, nous utiliserons un nouveau composant. Bien qu'il ne s'occupe que de la sélection et du mouvement des unités. Créez-lui un nouveau type de composant HexGameUI
. Un lien vers la grille lui suffit pour faire son travail. using UnityEngine; using UnityEngine.EventSystems; public class HexGameUI : MonoBehaviour { public HexGrid grid; }
Ajoutez ce composant au nouvel objet de jeu dans la hiérarchie de l'interface utilisateur. Il n'a pas besoin d'avoir son propre objet, mais il sera évident pour nous qu'il existe une interface utilisateur distincte pour le jeu.Objet d'interface utilisateur de jeuAjoutez une HexGameUI
méthode SetEditMode
, comme dans HexMapEditor
. L'interface utilisateur du jeu doit être activée lorsque nous ne sommes pas en mode édition. De plus, les étiquettes doivent être incluses ici car l'interface utilisateur du jeu fonctionne avec des chemins. public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); }
Ajoutez la méthode d'interface utilisateur du jeu avec la liste des événements du commutateur de mode d'édition. Cela signifie que lorsque le joueur change de mode, les deux méthodes sont appelées.Plusieurs méthodes d'événement.Suivre la cellule actuelle
Selon la situation, HexGameUI
vous devez savoir quelle cellule se trouve actuellement sous le curseur. Par conséquent, nous y ajoutons un champ currentCell
. HexCell currentCell;
Créez une méthode UpdateCurrentCell
qui utilise le HexGrid.GetCell
faisceau du curseur pour mettre à jour ce champ. void UpdateCurrentCell () { currentCell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); }
Lors de la mise à jour de la cellule actuelle, nous devrons peut-être savoir si elle a changé. Forcer à UpdateCurrentCell
renvoyer ces informations. bool UpdateCurrentCell () { HexCell cell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); if (cell != currentCell) { currentCell = cell; return true; } return false; }
Sélection d'unité
Avant de déplacer une équipe, elle doit être sélectionnée et suivie. Par conséquent, ajoutez un champ selectedUnit
. HexUnit selectedUnit;
Lorsque nous essayons de faire une sélection, nous devons commencer par mettre à jour la cellule actuelle. Si la cellule actuelle est alors l'unité occupant cette cellule devient l'unité sélectionnée. S'il n'y a pas d'unité dans la cellule, aucune unité n'est sélectionnée. Créons une méthode pour cela DoSelection
. void DoSelection () { UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } }
Nous réalisons le choix des unités d'un simple clic de souris. Par conséquent, nous ajoutons une méthode Update
qui effectue une sélection lorsque le bouton de la souris est activé. Bien sûr, nous devons l'exécuter uniquement lorsque le curseur n'est pas au-dessus de l'élément GUI. void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } } }
À ce stade, nous avons appris à sélectionner une unité à la fois d'un simple clic de souris. Lorsque vous cliquez sur une cellule vide, la sélection d'une unité est supprimée. Mais nous n'en recevons aucune confirmation visuelle.Recherche d'escouade
Lorsqu'une unité est sélectionnée, nous pouvons utiliser son emplacement comme point de départ pour trouver un chemin. Pour l'activer, nous n'aurons pas besoin d'un autre clic du bouton de la souris. Au lieu de cela, nous trouverons et afficherons automatiquement le chemin entre la position de l'escouade et la cellule actuelle. Nous le ferons toujours en Update
, sauf lorsque le choix est fait. Pour ce faire, lorsque nous avons un détachement, nous appelons la méthode DoPathfinding
. void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { DoPathfinding(); } } }
DoPathfinding
met simplement à jour la cellule actuelle et appelle HexGrid.FindPath
s'il y a un point de terminaison. Nous utilisons à nouveau une vitesse constante de 24. void DoPathfinding () { UpdateCurrentCell(); grid.FindPath(selectedUnit.Location, currentCell, 24); }
Veuillez noter que nous ne devons pas trouver de nouveau chemin à chaque mise à jour, mais uniquement lorsque la cellule actuelle change. void DoPathfinding () { if (UpdateCurrentCell()) { grid.FindPath(selectedUnit.Location, currentCell, 24); } }
Trouver un chemin pour une escouadeNous voyons maintenant les chemins qui apparaissent lorsque vous déplacez le curseur après avoir sélectionné une escouade. Grâce à cela, il est évident quelle unité est sélectionnée. Cependant, les chemins ne sont pas toujours effacés correctement. Tout d'abord, effaçons l'ancien chemin si le curseur est en dehors de la carte. void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell) { grid.FindPath(selectedUnit.Location, currentCell, 24); } else { grid.ClearPath(); } } }
Bien sûr, cela nécessite que ce HexGrid.ClearPath
soit commun, donc nous faisons un tel changement. public void ClearPath () { … }
Deuxièmement, nous effacerons l'ancienne voie lors du choix d'un détachement. void DoSelection () { grid.ClearPath(); UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } }
Enfin, nous effacerons le chemin lors du changement de mode d'édition. public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); grid.ClearPath(); }
Rechercher uniquement les points de terminaison valides
Nous ne pouvons pas toujours trouver le chemin, car il est parfois impossible d'atteindre la cellule finale. C'est normal.
Mais parfois, la cellule finale elle-même est inacceptable. Par exemple, nous avons décidé que les chemins ne pouvaient pas inclure de cellules sous-marines. Mais cela peut dépendre de l'unité. Ajoutons à une HexUnit
méthode qui nous indique si une cellule est un point de terminaison valide. Les cellules sous-marines ne le sont pas. public bool IsValidDestination (HexCell cell) { return !cell.IsUnderwater; }
De plus, nous n'avons autorisé qu'une seule unité à rester dans la cellule. Par conséquent, la cellule finale ne sera pas valide si elle est occupée. public bool IsValidDestination (HexCell cell) { return !cell.IsUnderwater && !cell.Unit; }
Nous utilisons cette méthode HexGameUI.DoPathfinding
pour ignorer les points de terminaison non valides. void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell && selectedUnit.IsValidDestination(currentCell)) { grid.FindPath(selectedUnit.Location, currentCell, 24); } else { grid.ClearPath(); } } }
Déplacer vers le point final
Si nous avons un chemin valide, nous pouvons déplacer l'équipe au point final. HexGrid
sait quand cela peut être fait. Nous lui faisons passer ces informations dans une nouvelle propriété en lecture seule HasPath
. public bool HasPath { get { return currentPathExists; } }
Pour déplacer une escouade, ajoutez à la HexGameUI
méthode DoMove
. Cette méthode sera appelée lorsqu'une commande est émise et si une unité est sélectionnée. Par conséquent, il doit vérifier s'il existe un moyen et, dans l'affirmative, changer l'emplacement du détachement. Pendant que nous téléportons immédiatement l'équipe au point final. Dans l'un des didacticiels suivants, nous allons faire en sorte que l'équipe continue jusqu'au bout. void DoMove () { if (grid.HasPath) { selectedUnit.Location = currentCell; grid.ClearPath(); } }
Utilisons le bouton 1 de la souris (clic droit) pour soumettre la commande. Nous vérifierons cela si un détachement est sélectionné. Si le bouton n'est pas enfoncé, alors nous recherchons le chemin. void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { if (Input.GetMouseButtonDown(1)) { DoMove(); } else { DoPathfinding(); } } } }
Maintenant, nous pouvons déplacer des unités! Mais parfois, ils refusent de trouver un moyen d'accéder à certaines cellules. En particulier, aux cellules dans lesquelles se trouvait le détachement. Cela se produit car il HexUnit
ne met pas à jour l'ancien emplacement lors de la définition d'un nouveau. Pour résoudre ce problème, nous effacerons le lien vers l'équipe à son ancien emplacement. public HexCell Location { get { return location; } set { if (location) { location.Unit = null; } location = value; value.Unit = this; transform.localPosition = value.Position; } }
Évitez les escouades
Trouver le chemin fonctionne désormais correctement et les unités peuvent se téléporter sur la carte. Bien qu'ils ne puissent pas se déplacer vers des cellules qui ont déjà une escouade, les détachements qui se dressent sur le chemin sont ignorés.Les unités en cours de route sont ignorées. Lesunités d'une même faction peuvent généralement se déplacer entre elles, mais jusqu'à présent, nous n'avons pas de factions. Par conséquent, considérons toutes les unités comme déconnectées les unes des autres et bloquant les chemins. Cela peut être implémenté en ignorant les cellules occupées HexGrid.Search
. if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } if (neighbor.IsUnderwater || neighbor.Unit) { continue; }
Évitez les détachementsunitypackagePartie 19: Animation de mouvement
- Nous déplaçons les unités entre les cellules.
- Visualisez le chemin parcouru.
- Nous déplaçons les troupes le long des courbes.
- Nous forçons les troupes à regarder dans le sens du mouvement.
Dans cette partie, nous forcerons les unités au lieu de la téléportation à se déplacer le long des pistes.Des escouades en routeMouvement le long du chemin
Dans la partie précédente, nous avons ajouté des unités et la possibilité de les déplacer. Bien que nous ayons utilisé la recherche du chemin pour déterminer les points de terminaison valides, après avoir donné l'ordre, les troupes se sont simplement téléportées vers la cellule finale. Pour suivre réellement le chemin trouvé, nous devons suivre ce chemin et créer un processus d'animation qui force l'équipe à se déplacer de cellule en cellule. Étant donné qu'en regardant les animations, il est difficile de remarquer comment l'équipe s'est déplacée, nous visualisons également le chemin parcouru à l'aide de gadgets. Mais avant de continuer, nous devons corriger l'erreur.Erreur avec virages
En raison d'un oubli, nous calculons incorrectement le cap auquel la cellule sera atteinte. Maintenant, nous déterminons le parcours en divisant la distance totale par la vitesse de l'équipet = d / s , et en éliminant le reste. L'erreur se produit lorsque pour entrer dans la cellule, vous devez dépenser exactement tous les points de mouvement restants par mouvement. Par exemple, lorsque chaque étape coûte 1 et que la vitesse est de 3, nous pouvons déplacer trois cellules par tour. Cependant, avec les calculs existants, nous ne pouvons prendre que deux étapes au premier coup, car pour la troisième étapet = d / s = 3 / 3 = 1 .
Les coûts totaux du déplacement avec des mouvements mal définis, vitesse 3Pour le calcul correct des mouvements, nous devons déplacer la bordure d'un pas depuis la cellule initiale. Nous pouvons le faire en réduisant la distance de 1 avant de calculer le mouvement. Ensuite, le mouvement pour la troisième étape serat = deux / trois = 0Mouvements correctsNous pouvons le faire en changeant la formule de calcul ent = ( d - 1 ) / s .
Nous allons apporter cette modification à HexGrid.Search
. bool Search (HexCell fromCell, HexCell toCell, int speed) { … while (searchFrontier.Count > 0) { … int currentTurn = (current.Distance - 1) / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance + moveCost; int turn = (distance - 1) / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } … } } return false; }
Nous changeons également les marques des mouvements. void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = (current.Distance - 1) / speed; … } } … }
Notez qu'avec cette approche, le chemin de cellule initial est -1. Ceci est normal, car nous ne l'afficheons pas, et l'algorithme de recherche reste opérationnel.Comment arriver
Se déplacer le long du chemin est la tâche de l'équipe. Pour ce faire, il doit connaître le chemin. Nous avons ces informations HexGrid
, alors ajoutons-y une méthode pour obtenir le chemin actuel sous la forme d'une liste de cellules. Il peut le prendre dans le pool de listes et revenir s'il y a vraiment un chemin. public List<HexCell> GetPath () { if (!currentPathExists) { return null; } List<HexCell> path = ListPool<HexCell>.Get(); return path; }
La liste est remplie en suivant le chemin du lien de la cellule finale à la cellule initiale, comme pour la visualisation du chemin. List<HexCell> path = ListPool<HexCell>.Get(); for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } return path;
Dans ce cas, nous avons besoin du chemin complet, qui inclut la cellule initiale. for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } path.Add(currentPathFrom); return path;
Maintenant, nous avons le chemin dans l'ordre inverse. On peut travailler avec lui, mais ce ne sera pas très intuitif. Faisons basculer la liste pour qu'elle passe du début à la fin. path.Add(currentPathFrom); path.Reverse(); return path;
Demande de motion
Maintenant, nous pouvons ajouter à la HexUnit
méthode, lui ordonnant de suivre le chemin. Au départ, nous l'avons simplement laissé se téléporter vers la cellule finale. Nous ne retournerons pas immédiatement la liste à la piscine, car elle nous sera utile pendant un certain temps. using UnityEngine; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; } … }
Pour demander un mouvement, nous le modifions HexGameUI.DoMove
pour qu'il appelle une nouvelle méthode avec le chemin courant, et pas seulement pour définir l'emplacement de l'unité. void DoMove () { if (grid.HasPath) {
Visualisation du chemin
Avant de commencer à animer l'équipe, vérifions que les chemins sont corrects. Nous le ferons en ordonnant de se HexUnit
souvenir du chemin le long duquel il doit se déplacer, afin qu'il puisse être visualisé à l'aide de gadgets. List<HexCell> pathToTravel; … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; }
Ajoutez une méthode OnDrawGizmos
pour afficher le dernier chemin à parcourir (s'il existe). Si l'unité n'a pas encore bougé, le chemin doit être égal null
. Mais en raison de la sérialisation d'Unity lors de l'édition après recompilation en mode Lecture, il peut également s'agir d'une liste vide. void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } }
Le moyen le plus simple de montrer le chemin consiste à dessiner une sphère de gizmo pour chaque cellule du chemin. Une sphère d'un rayon de 2 unités nous convient. void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } for (int i = 0; i < pathToTravel.Count; i++) { Gizmos.DrawSphere(pathToTravel[i].Position, 2f); } }
Puisque nous montrerons les chemins du détachement, nous pourrons voir simultanément tous ses derniers chemins.Les gizmos affichent les derniers chemins parcourus.Afin de mieux montrer les connexions des cellules, nous dessinons plusieurs sphères en boucle sur une ligne entre les cellules précédentes et actuelles. Pour ce faire, nous devons démarrer le processus à partir de la deuxième cellule. Les sphères peuvent être arrangées en utilisant une interpolation linéaire avec un incrément de 0,1 unité, de sorte que nous obtenons dix sphères par segment. for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } }
Des moyens plus évidentsGlissez le long du chemin
Vous pouvez utiliser la même méthode pour déplacer des unités. Créons une coroutine pour cela. Au lieu de dessiner un gizmo, nous allons définir la position de l'équipe. Au lieu d'incrémenter, nous utiliserons un delta temporel de 0,1 et nous effectuerons le rendement pour chaque itération. Dans ce cas, l'escouade passera d'une cellule à l'autre en une seconde. using UnityEngine; using System.Collections; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } } … }
Commençons coroutine à la fin de la méthode Travel
. Mais d'abord, nous arrêterons toutes les coroutines existantes. Nous garantissons donc que deux coroutines ne démarreront pas en même temps, sinon cela conduirait à des résultats très étranges. public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; StopAllCoroutines(); StartCoroutine(TravelPath()); }
Déplacer une cellule par seconde est assez lent. Le joueur pendant le jeu ne voudra pas attendre aussi longtemps. Vous pouvez faire de la vitesse de déplacement de l'escouade une option de configuration, mais pour l'instant, utilisons une constante. Je lui ai attribué une valeur de 4 cellules par seconde; c'est assez rapide, mais permet de remarquer ce qui se passe. const float travelSpeed = 4f; … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } }
Tout comme nous pouvons visualiser plusieurs chemins simultanément, nous pouvons faire voyager plusieurs unités en même temps. Du point de vue de l'état du jeu, le mouvement est toujours de la téléportation, les animations sont exclusivement visuelles. Les unités occupent instantanément la cellule finale. Vous pouvez même trouver des moyens et commencer un nouveau mouvement avant leur arrivée. Dans ce cas, ils sont téléportés visuellement au début d'un nouveau chemin. Cela peut être évité en bloquant des unités ou même toute l'interface utilisateur pendant leur déplacement, mais une telle réaction rapide est très pratique lors du développement et du test de mouvements.Unités mobiles.Et la différence de hauteur?, . , . , . , . , Endless Legend, , . , .
Position après compilation
L'un des inconvénients de la corutine est qu'elle ne «survit» pas lorsqu'elle est recompilée en mode Play. Bien que l'état du jeu soit toujours vrai, cela peut entraîner le blocage des escouades quelque part sur leur dernier chemin si la recompilation est lancée alors qu'elles sont toujours en mouvement. Pour atténuer les conséquences, assurons-nous qu'après la recompilation, les unités sont toujours dans la bonne position. Cela peut être fait en mettant à jour leur position dans OnEnable
. void OnEnable () { if (location) { transform.localPosition = location.Position; } }
paquet d'unitéMouvement fluide
Le mouvement du centre vers le centre de la cellule semble trop mécaniste et crée de brusques changements de direction. Pour de nombreux jeux, ce sera normal, mais inacceptable si vous avez besoin d'au moins un mouvement légèrement réaliste. Modifions donc le mouvement pour lui donner un aspect un peu plus organique.Passer d'une côte à l'autre
L'escouade commence son voyage depuis le centre de la cellule. Il passe au milieu du bord de la cellule, puis entre dans la cellule suivante. Au lieu de se déplacer vers le centre, il peut se diriger tout droit vers le bord suivant qu'il doit traverser. En fait, l'unité coupera le chemin lorsqu'elle devra changer de direction. Cela est possible pour toutes les cellules, à l'exception des extrémités du chemin.Trois façons de se déplacer d'un bord à l'autre Adaptons-nous OnDrawGizmos
à l'affichage des chemins générés de cette façon. Il doit interpoler entre les bords des cellules, ce qui peut être trouvé en faisant la moyenne des positions des cellules voisines. Il nous suffit de calculer une arête par itération, en réutilisant la valeur de l'itération précédente. Ainsi, nous pouvons faire fonctionner la méthode pour la cellule initiale, mais au lieu du bord, nous prenons sa position. void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) {
Pour atteindre le centre de la cellule d'extrémité, nous devons utiliser la position de la cellule comme dernier point, pas le bord. Vous pouvez ajouter la vérification de ce cas à la boucle, mais c'est un code si simple qu'il sera plus évident de simplement dupliquer le code et de le modifier légèrement. void OnDrawGizmos () { … for (int i = 1; i < pathToTravel.Count; i++) { … } a = b; b = pathToTravel[pathToTravel.Count - 1].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } }
Trajectoires basées sur lesnervures Les trajectoires résultantes ressemblent moins à des zigzags et l'angle de braquage maximal est réduit de 120 ° à 90 °. Cela peut être considéré comme une amélioration, nous appliquons donc les mêmes modifications dans la coroutine TravelPath
pour voir à quoi cela ressemble dans l'animation. IEnumerator TravelPath () { Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) {
Déplacement avec une vitesse variableAprès avoir coupé les angles, la longueur des segments de trajectoire est devenue dépendante du changement de direction. Mais nous définissons la vitesse en cellules par seconde. Par conséquent, la vitesse de détachement change de façon aléatoire.Courbes suivantes
Les changements instantanés de direction et de vitesse lors du franchissement des limites des cellules semblent laids. Mieux vaut utiliser un changement progressif de direction. Nous pouvons ajouter un soutien à cela en forçant les troupes à suivre des courbes plutôt que des lignes droites. Vous pouvez utiliser des courbes de Bézier pour cela. En particulier, nous pouvons prendre des courbes de Bézier quadratiques où le centre des cellules sera le point de contrôle central. Dans ce cas, les tangentes des courbes adjacentes seront des images miroir les unes des autres, c'est-à-dire que le chemin entier se transformera en une courbe lisse continue.Courbes d'un bord à l'autreCréez une classe auxiliaire Bezier
avec une méthode pour obtenir des points sur une courbe de Bézier quadratique. Comme expliqué dans le didacticiel Courbes et splines , la formule est utilisée pour( 1 - t ) 2 A + 2 ( 1 - t ) t B + t 2 C où
Un ,
B et
C sont les points de contrôle et t est l'interpolateur. using UnityEngine; public static class Bezier { public static Vector3 GetPoint (Vector3 a, Vector3 b, Vector3 c, float t) { float r = 1f - t; return r * r * a + 2f * r * t * b + t * t * c; } }
GetPoint ne devrait-il pas être limité à 0-1?0-1, . . , GetPointClamped
, t
. , GetPointUnclamped
.
Pour afficher le chemin de la courbe OnDrawGizmos
, nous devons suivre non pas deux, mais trois points. Un point supplémentaire est le centre de la cellule avec laquelle nous travaillons à l'itération actuelle, qui a un index i - 1
, car le cycle commence par 1. Après avoir reçu tous les points, nous pouvons le remplacer Vector3.Lerp
par Bezier.GetPoint
.Dans les cellules de début et de fin, au lieu des points de fin et de milieu, nous pouvons simplement utiliser le centre de la cellule. void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } }
Chemins créés à l'aide de courbes de Bézier Unchemin courbe est beaucoup plus esthétique. Nous appliquons les mêmes changements TravelPath
et voyons comment les unités sont animées avec cette approche. IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } }
Nous nous déplaçons le long des courbes. L'animation est également devenue fluide, même lorsque la vitesse du détachement est instable. Comme les tangentes de la courbe des segments adjacents coïncident, la vitesse est continue. Le changement de vitesse se produit progressivement et se produit lorsqu'un détachement traverse la cellule, ralentissant lors d'un changement de direction. S'il va tout droit, alors la vitesse reste constante. De plus, l'équipe commence et termine son voyage à vitesse nulle. Cela imite le mouvement naturel, alors laissez-le comme ça.Suivi du temps
Jusqu'à ce point, nous avons commencé à itérer sur chacun des segments à partir de 0, en continuant jusqu'à ce que nous atteignions 1. Cela fonctionne bien lorsque nous augmentons d'une valeur constante, mais notre itération dépend du delta temporel. Lorsque l'itération sur un segment est terminée, nous sommes susceptibles de dépasser 1 d'un certain montant, selon le delta de temps. Ceci est invisible à des fréquences d'images élevées, mais peut entraîner des à-coups à des fréquences d'images faibles.Pour éviter la perte de temps, nous devons transférer le temps restant d'un segment au suivant. Cela peut être fait en suivant t
tout le chemin, et pas seulement dans chaque segment. Ensuite, à la fin de chaque segment, nous en soustrayons 1. IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; float t = 0f; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } t -= 1f; } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (; t < 1f; t += Time.deltaTime * traveSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } }
Si nous le faisons déjà, assurons-nous que le delta temporel est pris en compte au début du chemin. Cela signifie que nous commencerons à bouger immédiatement et ne resterons pas inactifs pendant une image. float t = Time.deltaTime * travelSpeed;
De plus, nous ne terminons pas exactement au moment où le chemin doit se terminer, mais quelques instants avant. Ici, la différence peut également dépendre de la fréquence d'images. Par conséquent, faisons en sorte que l'équipe termine le chemin exactement au point final. IEnumerator TravelPath () { … transform.localPosition = location.Position; }
paquet d'unitéAnimation d'orientation
Les unités ont commencé à se déplacer le long d'une courbe lisse, mais elles n'ont pas changé d'orientation en fonction de la direction du mouvement. En conséquence, ils semblent glisser. Pour que le mouvement ressemble à un vrai mouvement, nous devons les faire pivoter.Pour l'avenir
Comme dans le didacticiel Courbes et splines , nous pouvons utiliser la dérivée de la courbe pour déterminer l'orientation de l'unité. La formule pour la dérivée d'une courbe de Bézier quadratique:2 ( ( 1 - t ) ( B - A ) + t ( C - B ) ) .
Ajoutez à la Bezier
méthode pour le calculer. public static Vector3 GetDerivative ( Vector3 a, Vector3 b, Vector3 c, float t ) { return 2f * ((1f - t) * (b - a) + t * (c - b)); }
Le vecteur dérivé est situé sur une ligne droite avec la direction du mouvement. Nous pouvons utiliser la méthode Quaternion.LookRotation
pour la convertir en tour d'équipe. Nous le réaliserons à chaque étape HexUnit.TravelPath
. transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null; … transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null;
N'y a-t-il pas d'erreur au début du chemin?, . A et B , . , t=0 , , Quaternion.LookRotation
. , , t=0 . . , t>0 .
, t<1 .
Contrairement à la position du détachement, la non-idéalité de son orientation en fin de parcours n'est pas importante. cependant, nous devons nous assurer que son orientation correspond à la rotation finale. Pour ce faire, après achèvement, nous assimilons son orientation à sa rotation en Y. transform.localPosition = location.Position; orientation = transform.localRotation.eulerAngles.y;
Maintenant, les unités regardent exactement dans la direction du mouvement, à la fois horizontalement et verticalement. Cela signifie qu'ils se pencheront en avant et en arrière, descendant des pentes et les escaladant. Pour s'assurer qu'ils sont toujours droits, nous forçons la composante Y du vecteur de direction à zéro avant de l'utiliser pour déterminer la rotation de l'unité. Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); … Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d);
Regarder vers l'avant tout en se déplaçantNous regardons le point
Tout au long du chemin, les unités regardent vers l'avant, mais avant de commencer à se déplacer, elles peuvent regarder dans l'autre direction. Dans ce cas, ils changent instantanément leur orientation. Ce sera mieux s'ils tournent en direction du chemin avant le début du mouvement.Regarder dans la bonne direction peut être utile dans d'autres situations, alors créons une méthode LookAt
qui force l'équipe à changer d'orientation afin de regarder un certain point. La rotation requise peut être définie à l'aide de la méthode Transform.LookAt
, tout d'abord en plaçant le point dans la même position verticale que le détachement. Après cela, nous pouvons récupérer l'orientation de l'équipe. void LookAt (Vector3 point) { point.y = transform.localPosition.y; transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; }
Pour que le détachement tourne réellement, nous transformerons la méthode en une autre corutine qui la fera tourner à vitesse constante. La vitesse de rotation peut également être ajustée, mais nous utiliserons à nouveau la constante. La rotation doit être rapide, environ 180 ° par seconde. const float rotationSpeed = 180f; … IEnumerator LookAt (Vector3 point) { … }
Il n'est pas nécessaire de bricoler avec une accélération de virage car elle est imperceptible. Il nous suffira d'interpoler simplement entre les deux orientations. Malheureusement, ce n'est pas aussi simple que dans le cas de deux nombres, car les angles sont circulaires. Par exemple, une transition de 350 ° à 10 ° devrait entraîner une rotation de 20 ° dans le sens horaire, mais une simple interpolation forcera une rotation de 340 ° dans le sens antihoraire.La façon la plus simple de créer une rotation correcte consiste à interpoler entre deux quaternions à l'aide d'une interpolation sphérique. Cela conduira au virage le plus court. Pour ce faire, nous obtenons les quaternions du début et de la fin, puis effectuons une transition entre eux en utilisant Quaternion.Slerp
. IEnumerator LookAt (Vector3 point) { point.y = transform.localPosition.y; Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); for (float t = Time.deltaTime; t < 1f; t += Time.deltaTime) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; } transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; }
Cela fonctionnera, mais l'interpolation passe toujours de 0 à 1, quel que soit l'angle de rotation. Pour assurer une vitesse angulaire uniforme, nous devons ralentir l'interpolation à mesure que l'angle de rotation augmente. Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); float angle = Quaternion.Angle(fromRotation, toRotation); float speed = rotationSpeed / angle; for ( float t = Time.deltaTime * speed; t < 1f; t += Time.deltaTime * speed ) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; }
Connaissant l'angle, nous pouvons sauter complètement le virage s'il s'avère être nul. float angle = Quaternion.Angle(fromRotation, toRotation); if (angle > 0f) { float speed = rotationSpeed / angle; for ( … ) { … } }
Nous pouvons maintenant ajouter la rotation de l'unité en TravelPath
effectuant simplement le rendement avant de déplacer la LookAt
position de la deuxième cellule. Unity lancera automatiquement coroutine LookAt
et TravelPath
attendra sa fin. IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); float t = Time.deltaTime * travelSpeed; … }
Si vous vérifiez le code, l'escouade se téléporte dans la dernière cellule, y retourne, puis se téléporte au début du chemin et commence à se déplacer à partir de là. Cela se produit car nous attribuons une valeur à une propriété Location
avant le début de la coroutine TravelPath
. Pour se débarrasser de la téléportation, on peut au départ TravelPath
ramener la position du détachement à la cellule initiale. Vector3 a, b, c = pathToTravel[0].Position; transform.localPosition = c; yield return LookAt(pathToTravel[1].Position);
Tournez avant de bougerBalayer
Ayant reçu le mouvement dont nous avons besoin, nous pouvons nous débarrasser de la méthode OnDrawGizmos
. Supprimez-le ou commentez-le au cas où nous aurions besoin de voir des chemins à l'avenir.
Comme nous n'avons plus besoin de nous rappeler dans quelle direction nous nous déplacions, TravelPath
vous pouvez finalement libérer la liste des cellules. IEnumerator TravelPath () { … ListPool<HexCell>.Add(pathToTravel); pathToTravel = null; }
Qu'en est-il des vraies animations d'équipe?, . 3D- . . , . Mecanim, TravelPath
.
paquet d'unité