Auparavant, nous avions exploré l'approche consistant à utiliser des régions vectorielles pour implémenter le champ de vision d'une tourelle. Les troupes se sont approchées de la tourelle à découvert et aucun obstacle ne les séparait. Supposons maintenant qu’il existe un obstacle, disons un mur, qui masque la visibilité des troupes depuis la tourelle; Comment devrions-nous implémenter cela? Ce tutoriel suggère une approche pour s'attaquer à ce problème.
Jetons un coup d'œil au résultat final sur lequel nous allons travailler. Cliquez sur la tourelle au bas de la scène pour lancer la simulation..
Alors voici ce que nous essayons de réaliser dans ce tutoriel. Observez l'image ci-dessus. La tourelle peut voir l'unité de soldat si elle se trouve dans le champ de vision de la tourelle (en haut). Une fois que nous avons placé un mur entre la tourelle et le soldat, la visibilité du soldat est protégée de la tourelle..
Tout d'abord, faisons une petite révision. Dites que le vecteur de la ligne de mire de la tourelle est P et que le vecteur de tourelle à soldat est Q. Le soldat est visible pour la tourelle si:
Ci-dessus, le pseudo-code de la démarche à suivre. Déterminer si le soldat se trouve dans le champ de vision de la tourelle est expliqué à l'étape 2. Passons maintenant à la question de savoir si le soldat est derrière un mur..
Nous allons utiliser des opérations vectorielles pour y parvenir. Je suis sûr que par cette mention, le produit scalaire et le produit croisé me viennent rapidement à l’esprit. Nous ferons un petit détour pour réviser ces deux opérations de vecteurs juste pour nous assurer que tout le monde peut suivre.
Revenons sur les opérations vectorielles: produit scalaire et produit croisé. Ce n'est pas un cours de maths, et nous en avons déjà traité plus en détail auparavant, mais il est néanmoins bon de rafraîchir notre mémoire sur le fonctionnement, de sorte que j'ai inclus l'image ci-dessus. Le diagramme montre une opération "B point A" (coin supérieur droit) et une opération "B croix A" (coin inférieur droit).
Plus importants sont les équations de ces opérations. Regardez l'image ci-dessous. | A |
et | B |
se référer au magnitude scalaire de chaque vecteur - la longueur de la flèche. Notez que le produit scalaire concerne le cosinus de l'angle entre les vecteurs et le produit croisé concerne le sinus de l'angle entre les vecteurs.
En approfondissant le sujet, la trigonométrie vient jouer: le sinus et le cosinus. Je suis sûr que ces graphiques ravivent de bons souvenirs (ou agonies). Cliquez sur les boutons de la présentation Flash ci-dessous pour afficher les graphiques avec différentes unités (degrés ou radians)..
Notez que ces formes d'onde sont continues et répétitives. Par exemple, vous pouvez couper et coller l'onde sinusoïdale dans la plage négative pour obtenir quelque chose comme ci-dessous.
Diplôme | Sinus de degré | Cosinus de degré |
-180 | 0 | -1 |
-90 | -1 | 0 |
0 | 0 | 1 |
90 | 1 | 0 |
180 | 0 | -1 |
Le tableau ci-dessus montre les valeurs de cosinus et sinus correspondant à des degrés spécifiques. Vous remarquerez que le graphe sinus positif couvre la plage 0 ° à 180 ° et le graphe cosinus positif couvre la plage de -90 ° à 90 °. Nous relierons ces valeurs au produit scalaire et au produit croisé ultérieurement.
Alors, comment tout cela peut-il être utile? Pour aller droit au but, le produit scalaire est une mesure de la parallèle les vecteurs sont alors que le produit croisé est une mesure de la orthogonal les vecteurs sont.
Permet de traiter d'abord le produit scalaire. Rappelez-vous la formule du produit scalaire, mentionnée à l'étape 4. Nous pouvons déterminer si le résultat est positif ou négatif simplement en regardant le cosinus de l'angle pris en sandwich entre les deux vecteurs. Pourquoi? Parce que la magnitude d'un vecteur est toujours positive. Le seul paramètre qui reste à dicter le signe du résultat est le cosinus de l'angle.
De nouveau, rappelez-vous que le graphe en cosinus positif couvre une plage de -90 ° à 90 °, comme à l'étape 6. Par conséquent, le produit scalaire de A avec l'un des vecteurs L, M, N, O ci-dessus produira une valeur positive, car l'angle calé entre A et n'importe lequel de ces vecteurs est compris entre -90 ° et 90 °! (Pour être précis, la plage positive se situe plutôt entre -89 ° et 89 ° car -90 ° et 90 ° produisent des valeurs de cosinus de 0, ce qui nous amène au point suivant.) Le produit scalaire entre A et P (étant donné P est perpendiculaire à A) produira 0. Le reste, je pense que vous pouvez déjà le deviner: le produit scalaire de A avec K, R ou Q produira une valeur négative.
En utilisant le produit scalaire, nous pouvons diviser la zone de notre scène en deux régions. Le produit scalaire du vecteur ci-dessous avec un point quelconque situé à l'intérieur de la région marquée «x» produira une valeur positive, tandis que le produit scalaire comportant ceux de la région marquée «o» produira des valeurs négatives..
Passons au produit croisé. Rappelez-vous que le produit croisé concerne la sinus d'angle pris en sandwich entre les deux vecteurs. Le graphe sinusoïdal positif couvre une plage de 0 ° à 180 °; la plage négative couvre 0 ° à -180 °. L'image ci-dessous résume ces points.
Ainsi, en regardant à nouveau le diagramme de l'étape 7, le produit croisé entre A et K, L ou M produira des valeurs positives, tandis que le produit croisé entre A et N, O, P ou Q produira des valeurs négatives. Le produit croisé entre A et R produira 0, car le sinus de 180 ° est 0.
Pour clarifier davantage, le produit croisé du vecteur entre tout point situé dans la région marquée «o» ci-dessous sera positif, tandis que ceux situés dans la région marquée «x» seront négatifs..
Un point à noter est que, contrairement au produit scalaire, le produit croisé est sensible à la séquence. Cela signifie des résultats de AxB
et BxA
sera différent en termes de direction. Alors que nous écrivons notre programme, nous devons être précis lors du choix du vecteur à comparer.
(Remarque: ces concepts expliqués s'appliquent à l'espace cartésien 2D.)
Pour renforcer votre compréhension, j'ai placé ici une petite application vous permettant de jouer avec. Cliquez sur la boule bleue en haut de la scène et faites-la glisser. Au fur et à mesure que vous vous déplacez, la valeur de la zone de texte sera mise à jour en fonction de l’opération choisie (produit point ou croisé entre la flèche statique et celle que vous contrôlez).
Vous pouvez observer une bizarrerie avec la direction inversée du produit croisé. La région du haut est négative et le bas est positif, contrairement à notre explication de l'étape précédente. Cela est dû au fait que l'axe des y est inversé dans l'espace de coordonnées Flash par rapport à l'espace de coordonnées cartésien. il pointe vers le bas, alors que traditionnellement les mathématiciens le prennent comme pointant vers le haut.
Maintenant que vous avez compris le concept de régions, faisons un peu de pratique. Nous allons diviser notre espace en quatre quadrants: A1, A2, B1, B2.
J'ai compilé les résultats pour vérifier ci-dessous. "Vecteur" se réfère ici à la flèche dans l'image ci-dessus. "Point" fait référence à n'importe quelle coordonnée dans la région spécifiée. Le vecteur divise la scène en quatre zones principales, où les séparateurs (lignes en pointillés) s'étendent à l'infini.
Région | Vecteur sur le diagramme croisé produit avec point | Vecteur, produit point, diagramme, à, point |
A1 | (+), à cause de l'espace de coordonnées Flash | (+) |
A2 | (+) | (-) |
B1 | (-), à cause de l'espace de coordonnées Flash | (+) |
B2 | (-) | (-) |
Voici la présentation Flash présentant les idées comme expliqué à l'étape 10. Cliquez avec le bouton droit sur la scène pour afficher le menu contextuel et sélectionner la région que vous souhaitez voir mise en surbrillance..
Voici l'implémentation ActionScript du concept expliqué à l'étape 10. N'hésitez pas à afficher l'intégralité du code dans le téléchargement du code source, comme suit: AppLine.as
.
// surlignant la couleur en fonction de la sélection de l 'utilisateur fonction privée color (): void // chaque balle sur scène est vérifiée par rapport aux conditions du cas sélectionné pour chaque (var item: balle dans sp) var vec1: Vector2D = new Vector2D (item. x - stage.stageWidth * 0.5, item.y - stage.stageHeight * 0.5); if (select == 0) if (vec.vectorProduct (vec1)> 0) item.col = 0xFF9933; else item.col = 0x334455; else if (select == 1) if (vec.dotProduct (vec1)> 0) item.col = 0xFF9933; else item.col = 0x334455; else if (select == 2) if (vec.vectorProduct (vec1)> 0 && vec.dotProduct (vec1)> 0) item.col = 0xFF9933; else item.col = 0x334455; else if (select == 3) if (vec.vectorProduct (vec1)> 0 && vec.dotProduct (vec1) <0) item.col = 0xFF9933; else item.col = 0x334455; else if (select == 4) if (vec.vectorProduct(vec1) < 0 &&vec.dotProduct(vec1) > 0) item.col = 0xFF9933; else item.col = 0x334455; else if (select == 5) if (vec.vectorProduct (vec1) < 0 &&vec.dotProduct(vec1) < 0) item.col = 0xFF9933; else item.col = 0x334455; item.draw(); //swapping case according to user selction private function swap(e:ContextMenuEvent):void if (e.target.caption == "VectorProduct") select = 0; else if (e.target.caption == "DotProduct") select = 1; else if (e.target.caption == "RegionA1") select = 2; else if (e.target.caption == "RegionA2") select = 3; else if (e.target.caption == "RegionB1") select = 4; else if (e.target.caption == "RegionB2") select = 5;
Après avoir compris les interprétations géométriques du produit scalaire et du produit croisé, nous l’appliquerons à notre scénario. La présentation Flash ci-dessus montre des variantes du même scénario et résume les conditions appliquées à un soldat protégé par un mur mais à l’intérieur du FOV de la tourelle. Vous pouvez faire défiler les cadres en utilisant les boutons fléchés.
Les explications suivantes sont basées sur l’espace de coordonnées Flash 2D. Dans l'image 1, un mur est placé entre la tourelle et le cavalier. Soit A et B les vecteurs de la tourelle à la queue et à la tête du vecteur du mur, respectivement. Soit C le vecteur du mur et D le vecteur de la queue du mur au cavalier. Enfin, que Q soit le vecteur de la tourelle au cavalier.
J'ai compilé les conditions résultantes ci-dessous.
Emplacement | Produit croisé |
La troupe est devant le mur | C x D> 0 |
La troupe est derrière le mur | C x D |
Ce n'est pas la seule condition applicable, car nous devons également limiter le soldat aux lignes pointillées des deux côtés. Découvrez les images 2 à 4 pour voir la prochaine série de conditions..
Emplacement | Produit croisé |
La troupe est dans les côtés du mur. | Q x A 0 |
La troupe est à gauche du mur | Q x A> 0, Q x B> 0 |
La troupe est à droite du mur | Q x A |
Je pense que mes collègues lecteurs peuvent maintenant choisir les conditions appropriées pour déterminer si le soldat est caché ou non. N'oubliez pas que cet ensemble de conditions est évalué une fois que nous avons constaté que la troupe se trouvait dans le champ de vision de la tourelle (reportez-vous à l'étape 3)..
Voici l'implémentation ActionScript des concepts expliqués à l'étape 13. L'image ci-dessus montre le vecteur initial du mur, C. Cliquez sur le bouton rouge ci-dessous, faites-le glisser et déplacez-le pour afficher la zone blindée. Vous pouvez voir le code source complet dans HiddenSector.as
.
D'accord, j'espère que vous avez expérimenté la boule rouge, et si vous êtes suffisamment observateur, vous remarquerez peut-être une erreur. Notez qu'il n'y a pas de zone blindée lorsque le bouton rouge se déplace vers la gauche de l'autre extrémité du mur, inversant ainsi le vecteur de mur pour qu'il pointe vers la gauche plutôt que vers la droite. La solution est à l'étape suivante.
Cependant, avant cela, examinons un extrait de code ActionScript important ici. HiddenSector.as
:
surbrillance de la fonction privée (): void var lineOfSight: Vector2D = new Vector2D (0, -50) var secteur: Number = Math2.radianOf (30); pour chaque élément (var: balle dans sp) var turret_sp: Vector2D = new Vector2D (item.x - turret.x, item.y - turret.y); // Q if (Math.abs (lineOfSight.angleBetween (turret_sp)) < sector) var wall:Vector2D = new Vector2D(wall2.x - wall1.x, wall2.y - wall1.y); //C var turret_wall1:Vector2D = new Vector2D(wall1.x - turret.x, wall1.y - turret.y); //A var turret_wall2:Vector2D = new Vector2D(wall2.x - turret.x, wall2.y - turret.y); //B var wall_sp:Vector2D = new Vector2D (item.x - wall1.x, item.y - wall1.y); //D if ( wall.vectorProduct (wall_sp) < 0 // C x D && turret_sp.vectorProduct(turret_wall1) < 0 // Q x A && turret_sp.vectorProduct(turret_wall2) > 0 // Q x B) item.col = 0xcccccc sinon item.col = 0; item.draw ();
Pour résoudre ce problème, nous devons savoir si le vecteur de mur pointe vers la gauche ou vers la droite. Disons que nous avons un vecteur de référence, R, qui pointe toujours vers la droite.
Direction du vecteur | Produit scalaire |
Le mur pointe vers la droite (même côté que R) | w. R> 0 |
Le mur est dirigé vers la gauche (côté opposé de R) | w. R |
Bien sûr, il existe d'autres moyens de contourner ce problème, mais je pense que c'est une opportunité d'utiliser les concepts exprimés dans ce tutoriel..
Vous trouverez ci-dessous une présentation Flash qui implémente la correction expliquée à l'étape 15. Une fois que vous avez joué, faites défiler vers le bas pour vérifier les réglages ActionScript.
Les modifications de la mise en œuvre précédente sont mises en évidence. De plus, les ensembles de conditions sont redéfinis en fonction de la direction du mur:
fonction privée mettre en évidence (): void var lineOfSight: Vector2D = new Vector2D (0, -50); secteur var: Number = Math2.radianOf (30); var pointToRight: Vector2D = nouveau Vector2D (10, 0); // ajouté dans la deuxième version pour chaque élément (var item: Ball in sp) var turret_sp: Vector2D = new Vector2D (item.x - turret.x, item.y - turret.y); // Q if (Math.abs (lineOfSight.angleBetween (turret_sp)) < sector) var wall:Vector2D = new Vector2D(wall2.x - wall1.x, wall2.y - wall1.y); //C var turret_wall1:Vector2D = new Vector2D(wall1.x - turret.x, wall1.y - turret.y); //A var turret_wall2:Vector2D = new Vector2D(wall2.x - turret.x, wall2.y - turret.y); //B var wall_sp:Vector2D = new Vector2D (item.x - wall1.x, item.y - wall1.y); //D var sides: Boolean; //switches according to wall direction if (pointToRight.dotProduct(wall) > 0) sides = wall.vectorProduct (wall_sp) < 0 // C x D && turret_sp.vectorProduct(turret_wall1) < 0 // Q x A && turret_sp.vectorProduct(turret_wall2) > 0 // Q x B else sides = wall.vectorProduct (wall_sp)> 0 // C x D && touret_sp.vectorProduct (touret_wall1)> 0 // Q x A && touret_sp.vectorProduct (touret_wall2) < 0 // Q x B if (sides) item.col = 0xcccccc else item.col = 0; item.draw();
Découvrez la source complète dans HiddenSector2.as
.
Maintenant, nous allons patcher notre travail sur Scene1.as
du tutoriel précédent. Tout d'abord, nous allons installer notre mur.
Nous initions les variables,
Classe publique Scene1_2 étend Sprite private var river: Sprite; var privé wall_origin: Vector2D, mur: Vector2D; // ajouté au deuxième tutoriel private var troupes: Vector.; troopVelo var privé: vecteur. ;
… Puis dessine le mur pour la première fois,
fonction publique Scene1_2 () makeTroops (); makeRiver (); makeWall (); // ajouté au 2ème tutoriel makeTurret (); turret.addEventListener (MouseEvent.MOUSE_DOWN, début); function start (): void stage.addEventListener (Event.ENTER_FRAME, déplacer);
fonction privée makeWall (): void wall_origin = new Vector2D (200, 260); mur = nouveau Vector2D (80, -40); graphics.lineStyle (2, 0); graphics.moveTo (wall_origin.x, wall_origin.y); graphics.lineTo (wall_origin.x + wall.x, wall_origin.y + wall.y);
… Et redessiner sur chaque image, parce que le graphics.clear ()
appel est quelque part dans comportement Tourelle ()
:
// ajouté à la deuxième fonction privée du tutoriel move (e: Event): void behavioriourTroops (); comportement Tourelle (); redrawWall ();
// ajouté au second tutoriel, fonction privée redrawWall (): void graphics.lineStyle (2, 0); graphics.moveTo (wall_origin.x, wall_origin.y); graphics.lineTo (wall_origin.x + wall.x, wall_origin.y + wall.y);
Les troupes interagiront également avec le mur. Lorsqu'ils entrent en collision avec le mur, ils vont glisser le long du mur. Je n'essaierai pas d'entrer dans les détails, comme cela a été décrit de manière exhaustive dans Réaction de collision entre un cercle et un segment de ligne. J'encourage les lecteurs à vérifier cela pour plus d'explications.
L'extrait suivant vit dans la fonction comportementTroops ()
.
// Version 2 // si vous marchez dans une rivière, ralentissez // si vous entrez en collision avec un mur, faites glisser // autre vitesse normale var collideWithRiver: Boolean = river.hitTestObject (troupes [i]) var wall_norm: Vector2D = wall.rotate ( Math2.radianOf (-90)); var wall12Troop: Vector2D = nouveau Vector2D (troupes [i] .x - wall_origin.x, troupes [i] .y - wall_origin.y); var collideWithWall: Boolean = troupes [i] .rad> Math.abs (wall12Troop.projectionOn (wall_norm)) && wall12Troop.getMagnitude () < wall.getMagnitude() && wall12Troop.dotProduct(wall) > 0; si (collideWithRiver) troupes [i] .y + = troopVelo [i] .y * 0,3; else if (collideWithWall) // repositionne la troupe var projOnNorm: Vector2D = wall_norm.normalise (); projOnNorm.scale (troupes [i] .rad -1); var projOnWall: Vector2D = wall.normalise (); projOnWall.scale (wall12Troop.projectionOn (mur)); repositionnement var: Vector2D = projOnNorm.add (projOnWall); troupes [i] .x = wall_origin.x + reposition.x; troupes [i] .y = wall_origin.y + reposition.y; // glisse à travers le mur ajustement de la variable: Nombre = Math.abs (troopVelo [i] .projectionOn (wall_norm)); var slideVelo: Vector2D = wall_norm.normalise (); slideVelo.scale (ajustement); slideVelo = slideVelo.add (troopVelo [i]) troupes [i] .x + = slideVelo.x; troupes [i] .y + = slideVelo.y; else troupes [i] .y + = troopVelo [i] .y
Enfin, nous arrivons à la fin de ce tutoriel: définir la condition et vérifier si les soldats sont derrière le mur et donc protégés de la visibilité de la tourelle. J'ai mis en évidence les codes de patch importants:
// vérifie si l'ennemi est visible // 1. Dans le secteur de vue // 2. À portée de vue // 3. Plus proche que l'ennemi le plus proche actuel var c1: Boolean = Math.abs (lineOfSight.angleBetween (turret2Item)) < Math2.radianOf(sectorOfSight) ; var c2:Boolean = turret2Item.getMagnitude() < lineOfSight.getMagnitude(); var c3:Boolean = turret2Item.getMagnitude() < closestDistance; //Checking whether troop is shielded by wall var withinLeft:Boolean = turret2Item.vectorProduct(turret2wall1) < 0 var withinRight:Boolean = turret2Item.vectorProduct(turret2wall2) > 0 var behindWall: Boolean = wall.vectorProduct (wall12troop) < 0; var shielded:Boolean = withinLeft && withinRight && behindWall //if all conditions fulfilled, update closestEnemy if (c1 && c2&& c3 && !shielded) closestDistance = turret2Item.getMagnitude(); closestEnemy = item;
Découvrez le code complet dans Scene1_2.as
.
Enfin, nous pouvons nous asseoir et regarder le correctif en action. Appuyez sur Ctrl + Entrée pour voir les résultats de votre travail. J'ai inclus une copie de la présentation Flash de travail ci-dessous. Cliquez sur la tourelle au bas de la scène pour lancer la simulation..