Comment détecter lorsqu'un objet a été encerclé par un geste

Vous n'êtes jamais trop vieux pour jouer à Spot the Difference. Je me souviens de l'avoir jouée quand j'étais enfant et je trouve maintenant que ma femme y joue encore de temps en temps! Dans ce didacticiel, nous verrons comment détecter le moment où un anneau est dessiné autour d'un objet, avec un algorithme pouvant être utilisé avec une entrée de souris, de stylet ou d'écran tactile..

Remarque: Bien que les démos et le code source de ce didacticiel utilisent Flash et AS3, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..


Aperçu du résultat final

Jetons un coup d'œil au résultat final sur lequel nous allons travailler. L'écran est divisé en deux images presque identiques mais pas tout à fait. Essayez de repérer les six différences et encerclez celles de l'image de gauche. Bonne chance!

Remarque: vous n'êtes pas obligé de tracer un cercle parfait! Il vous suffit de dessiner un anneau rugueux ou une boucle autour de chaque différence.

Vous n'avez pas de flash? Découvrez cette démo vidéo:


Étape 1: Le mouvement indirect

Nous allons utiliser des calculs vectoriels dans l'algorithme. Comme toujours, il est bon de comprendre les mathématiques sous-jacentes avant de les appliquer. Voici donc un bref rappel des mathématiques vectorielles..

L'image ci-dessus montre le vecteur UNE décomposé en ses composantes horizontale et verticale (Hache et Oui, respectivement).

Maintenant regardons le produit scalaire opération, illustrée dans l'image ci-dessous. Tout d'abord, vous verrez le fonctionnement du produit scalaire entre les vecteurs A et B.

Pour trouver l'angle pris en sandwich entre les deux vecteurs, nous pouvons utiliser ce produit scalaire.

| A | et | B | désignent les magnitudes des vecteurs A et B, ainsi données | A | et | B | et un point B, ce qui reste inconnu est thêta. Avec un peu d'algèbre (montré dans l'image), l'équation finale est produite, que nous pouvons utiliser pour trouver thêta.

Pour plus d'informations sur les produits vectoriels, consultez la page suivante de Wolfram..

L’autre opération utile est produit croisé. Découvrez l'opération ci-dessous:

Cette opération est utile pour déterminer si l'angle pris en sandwich est dans le sens des aiguilles d'une montre ou dans le sens contraire des aiguilles d'une montre par rapport à un vecteur spécifique..

Laissez-moi élaborer plus avant. Dans le cas du diagramme ci-dessus, la rotation de A à B se fait dans le sens des aiguilles d'une montre, de sorte que la croix B est négative. La rotation de B vers A se fait dans le sens inverse des aiguilles d'une montre, donc B croix A est positive. Notez que cette opération est sensible à la séquence. Un croisement B produira un résultat différent de B croisement A.

Ce n'est pas tout. Il se trouve que dans l’espace de coordonnées de nombreuses plates-formes de développement de jeux, l’axe des y est inversé (y augmente au fur et à mesure que nous nous déplaçons). Donc, notre analyse est inversée, et A croisera B sera positif tandis que B croisera A sera négatif.

C'est assez de révision. Passons à notre algorithme.


Étape 2: Interaction indirecte

Les joueurs devront entourer le détail correct sur l'image. Maintenant, comment faisons-nous cela? Avant de répondre à cette question, calculons l'angle entre deux vecteurs. Comme vous vous en souviendrez maintenant, nous pouvons utiliser le produit scalaire pour cela, nous allons donc implémenter cette équation ici.

Voici une démo pour illustrer ce que nous faisons. Faites glisser l'une des flèches pour voir les commentaires.

Voyons comment cela fonctionne. Dans le code ci-dessous, j'ai simplement initialisé les vecteurs et une minuterie, et mis des flèches interactives à l'écran.

fonction publique Demo1 () feedback = new TextField; addChild (commentaires); feedback.selectable = false; feedback.autoSize = TextFieldAutoSize.LEFT; a1 = nouvelle flèche; addChild (a1); a2 = nouvelle flèche; addChild (a2); a2.rotation = 90 center = new Point (stage.stageWidth >> 1, stage.stageHeight >> 1) a1.x = center.x; a1.y = center.y; a1.name = "a1"; a2.x = center.x; a2.y = center.y; a2.name = "a2"; a1.transform.colorTransform = new ColorTransform (0, 0, 0, 1, 255); a2.transform.colorTransform = new ColorTransform (0, 0, 0, 1, 0, 255); a1.addEventListener (MouseEvent.MOUSE_DOWN, handleMouse); a2.addEventListener (MouseEvent.MOUSE_DOWN, handleMouse); stage.addEventListener (MouseEvent.MOUSE_UP, handleMouse); v1 = nouveau Vector2d (1, 0); v2 = nouveau Vector2d (0, 1); curr_vec = nouveau Vector2d (1, 0); t = nouvelle minuterie (50); 

Toutes les 50 millisecondes, la fonction ci-dessous est exécutée et utilisée pour mettre à jour les commentaires graphiques et textuels:

mise à jour de fonction privée (e: TimerEvent): void var curr_angle: Number = Math.atan2 (mouseY - center.y, mouseX - center.x); curr_vec.angle = curr_angle; if (item == 1) // met à jour la rotation de la flèche visuellement a1.rotation = Math2.degreeOf (curr_angle); // mesure l'angle de a1 à b1 v1 = curr_vec.clone (); direction = v2.crossProduct (v1); feedback.text = "Vous déplacez maintenant le vecteur rouge, A \ n"; feedback.appendText ("Angle mesuré du vert au rouge:");  else if (item == 2) a2.rotation = Math2.degreeOf (curr_angle); v2 = curr_vec.clone (); direction = v1.crossProduct (v2); feedback.text = "Vous déplacez maintenant le vecteur vert, B \ n"; feedback.appendText ("Angle mesuré du rouge au vert:");  theta_rad = Math.acos (v1.dotProduct (v2)); // thêta est en radians theta_deg = Math2.degreeOf (theta_rad); si (direction < 0)  feedback.appendText("-" + theta_deg.toPrecision(4) + "\n"); feedback.appendText("rotation is anti clockwise")  else  feedback.appendText(theta_deg.toPrecision(4) + "\n"); feedback.appendText("rotation is clockwise")  drawSector(); 

Vous remarquerez que les magnitudes pour v1 et v2 sont toutes les deux égales à 1 unité dans ce scénario (consultez les lignes 52 et 53 surlignées ci-dessus). J'ai donc évité de devoir calculer la magnitude des vecteurs pour l'instant..

Si vous voulez voir le code source complet, consultez Demo1.as dans le téléchargement source.


Étape 3: détecter un cercle complet

Ok, maintenant que nous avons compris l'idée de base, nous allons maintenant l'utiliser pour vérifier si le joueur a encerclé un point..

J'espère que le diagramme parle de lui-même. Le début de l'interaction se fait lorsque le bouton de la souris est enfoncé et la fin de l'interaction se produit lorsque le bouton de la souris est relâché.

À chaque intervalle (par exemple, 0,01 seconde) au cours de l'interaction, nous calculons l'angle pris en sandwich entre les vecteurs actuels et précédents. Ces vecteurs sont construits à partir de l'emplacement du marqueur (où se trouve la différence) jusqu'à l'emplacement de la souris dans cette instance. Additionnez tous ces angles (t1, t2, t3 dans ce cas) et si l'angle fait est de 360 ​​degrés à la fin de l'interaction, alors le joueur a tracé un cercle.

Bien sûr, vous pouvez modifier la définition d’un cercle complet pour qu’elle soit comprise entre 300 et 340 degrés, ce qui laisse l’espace pour les erreurs des joueurs lors de l’exécution du geste de la souris..

Voici une démo pour cette idée. Faites glisser un geste circulaire autour du marqueur rouge au milieu. Vous pouvez déplacer la position du marqueur rouge en utilisant les touches W, A, S, D.


Étape 4: la mise en œuvre

Examinons l'implémentation de la démo. Nous allons simplement regarder les calculs importants ici.

Vérifiez le code en surbrillance ci-dessous et faites-le correspondre à l'équation mathématique de l'étape 1. Vous remarquerez que la valeur de arccos produit parfois Pas un nombre (NaN) si vous sautez la ligne 92. Aussi, valeur_ constants parfois dépasse 1 en raison d'arrondis inexacts, nous devons donc le ramener manuellement à un maximum. Tout nombre entré pour arccos supérieur à 1 produira un NaN.

mise à jour de fonction privée (e: TimerEvent): void graphics.clear (); graphics.lineStyle (1) graphics.moveTo (marqueur.x, marqueur.y); graphics.lineTo (mouseX, mouseY); prev_vec = curr_vec.clone (); curr_vec = new Vector2d (mouseX - marker.x, mouseY - marker.y); // la valeur du calcul dépasse parfois 1 besoin de gérer manuellement la précission var constants_value: Number = Math.min (1, prev_vec.dotProduct (curr_vec) / (prev_vec.magnitude * curr_vec.magnitude)); var delta_angle: Number = Math.acos (constants_value) // angle créé direction var: Number = prev_vec.crossProduct (curr_vec)> 0? 1: -1; // vérifiant la direction de la rotation total_angle + = direction * delta_angle; // ajoute à l'angle cumulé créé lors de l'interaction

La source complète de ceci peut être trouvée dans Demo2.as


Étape 5: la faille

Un problème que vous pouvez voir est que tant que je dessine un grand cercle entourant la toile, le marqueur est considéré encerclé. Je n'ai pas nécessairement besoin de savoir où se trouve le marqueur.

Eh bien, pour contrer ce problème, nous pouvons vérifier la proximité du mouvement circulaire. Si le cercle est tracé dans les limites d'une certaine plage (la valeur est sous votre contrôle), alors seulement il est considéré comme un succès.

Découvrez le code ci-dessous. Si jamais l'utilisateur dépasse MIN_DIST (avec une valeur de 60 dans ce cas), alors il est considéré comme une supposition aléatoire.

mise à jour de fonction privée (e: TimerEvent): void graphics.clear (); graphics.lineStyle (1) graphics.moveTo (marqueur.x, marqueur.y); graphics.lineTo (mouseX, mouseY); prev_vec = curr_vec.clone (); curr_vec = new Vector2d (mouseX - marker.x, mouseY - marker.y); if (curr_vec.magnitude> MIN_DIST) inside_bound = false; // la valeur du calcul dépasse parfois 1 besoin de gérer manuellement la précission var constants_value: Number = Math.min (1, prev_vec.dotProduct (curr_vec) / (prev_vec.magnitude * curr_vec.magnitude)); var delta_angle: Number = Math.acos (constants_value) // angle créé direction var: Number = prev_vec.crossProduct (curr_vec)> 0? 1: -1; // vérifiant la direction de la rotation total_angle + = direction * delta_angle; // ajoute à l'angle cumulé créé lors de l'interaction mag_box.text = "Distance du marqueur:" + curr_vec.magnitude.toPrecision (4); mag_box.x = mouseX + 10; mag_box.y = mouseY + 10; feedback.text = "N'allez pas au-delà de" + MIN_DIST

Encore une fois, essayez d'encercler le marqueur. Si vous pensez que le MIN_DIST est un peu impitoyable, il peut toujours être ajusté en fonction de l'image.


Étape 6: différentes formes

Et si la "différence" n'est pas un cercle exact? Certains peuvent être rectangulaires, triangulaires ou de toute autre forme.
Dans ces cas, au lieu d'utiliser un seul marqueur, nous pouvons en mettre quelques-uns:

Dans le diagramme ci-dessus, deux curseurs de souris sont affichés en haut. En commençant par le curseur le plus à droite, nous allons faire un mouvement circulaire dans le sens des aiguilles d'une montre jusqu'à l'autre extrémité, à gauche. Notez que le chemin entoure les trois marqueurs.

J'ai également dessiné les angles écoulés par ce chemin sur chacun des marqueurs (tirets clairs à tirets sombres). Si les trois angles ont plus de 360 ​​degrés (ou quelle que soit la valeur choisie), alors seulement nous le comptons comme un cercle..

Mais ça ne suffit pas. Rappelez-vous la faille à l'étape 4? Eh bien, la même chose se passe ici: nous devrons vérifier la proximité. Au lieu d'exiger que le geste ne dépasse pas un certain rayon d'un marqueur spécifique, nous allons simplement vérifier si le curseur de la souris s'est approché de tous les marqueurs pendant au moins une courte instance. Je vais utiliser un pseudo-code pour expliquer cette idée:

Calculez l'angle écoulé par le chemin pour marker1, marker2 et marker3 si chaque angle est supérieur à 360 si la proximité de chaque marqueur a été traversée par le curseur de la souris, puis le cercle formé entoure la zone marquée par les marqueurs endif endif

Étape 7: Démo pour l'idée

Ici, nous utilisons trois points pour représenter un triangle.

Essayez de faire le tour:

  • un point
  • deux points
  • trois points

… Dans l'image ci-dessous. Notez que le geste ne réussit que s'il contient les trois points..

Voyons le code de cette démo. J'ai souligné les lignes clés de l'idée ci-dessous; le script complet est en Demo4.as.

fonction privée handleMouse (e: MouseEvent): void if (type.e == "mouseDown") t.addEventListener (TimerEvent.TIMER, mise à jour); t.start (); update_curr_vecs ();  else if (e.type == "mouseUp") t.stop (); t.removeEventListener (TimerEvent.TIMER, mise à jour); // vérifie si les conditions sont remplies condition1 = true // tous les angles répondent à MIN_ANGLE condition2 = true // toutes les proximités répondent à MIN_DIST pour (var i: int = 0; i < markers.length; i++)  if (Math.abs(angles[i])< MIN_ANGLE)  condition1 = false; break;  if (proximity[i] == false)  condition2 = false; break   if (condition1 && condition2)  box.text="Attempt to circle the item is successful"  else  box.text="Failure"  reset_vecs(); reset_values();   private function update(e:TimerEvent):void  update_prev_vecs(); update_curr_vecs(); update_values(); 

Étape 8: Tracer les cercles

La meilleure méthode pour tracer la ligne que vous tracez dépend de votre plate-forme de développement. Je vais donc décrire ici la méthode que nous utiliserions en Flash..

Il existe deux manières de dessiner des lignes dans AS3, comme indiqué par l'image ci-dessus..

La première approche est assez simple: utiliser déménager à() pour déplacer la position de dessin à coordonner (10, 20). Ensuite, tracez une ligne pour connecter (10, 20) à (80, 70) en utilisant lineTo ().

La seconde approche consiste à stocker tous les détails dans deux tableaux., commandes [] et coords [] (avec les coordonnées stockées dans des paires (x, y) dans coords []) et plus tard tracer tous les détails graphiques sur la toile en utilisant drawPath () en un seul coup. J'ai opté pour la seconde approche dans ma démo.

Check it out: essayez de cliquer et de faire glisser la souris sur la toile pour tracer une ligne.

Et voici le code AS3 pour cette démo. Découvrez la source complète dans Dessin1.as.

Classe publique Drawing1 étend Sprite private var cmd: Vector.; coords de var privé: vecteur.; private var _thickness: Number = 2, _col: Number = 0, _alpha: Number = 1; fonction publique Drawing1 () // assigne handlerst à mouse up et mouse down stage.addEventListener (MouseEvent.MOUSE_DOWN, mouseHandler); stage.addEventListener (MouseEvent.MOUSE_UP, mouseHandler);  / ** * Gestionnaire d'événements de souris * @param e événement de souris * / fonction privée mouseHandler (e: MouseEvent): void if (e.type == "mouseDown") // randomise les propriétés de la ligne _thickness = Math.random () * 5; _col = Math.random () * 0xffffff; _alpha = Math.random () * 0.5 + 0.5 // initialiser les variables cmd = nouveau vecteur.; coords = nouveau vecteur.; // premier enregistrement de la ligne commençant cmd [0] = 1; coords [0] = mouseX; coords [1] = mouseY; // commence le dessin lorsque la souris déplace stage.addEventListener (MouseEvent.MOUSE_MOVE, mouseHandler);  else if (e.type == "mouseUp") // supprime le gestionnaire de déplacement de la souris une fois le bouton de la souris relâché stage.removeEventListener (MouseEvent.MOUSE_MOVE, mouseHandler);  else if (e.type == "mouseMove") // pousser dans la souris déplace le cmd.push (2); // dessine la commande coords.push (mouseX); // coordonnées pour tracer une ligne à coords.push (mouseY); redessiner (); // exécute la commande de dessin / ** * Méthode pour dessiner la ou les lignes comme défini par le déplacement de la souris * / fonction privée redraw (): void graphics.clear (); // efface tous les dessins précédents graphics.lineStyle (_thickness, _col, _alpha); graphics.drawPath (cmd, coords); 

En Flash, en utilisant le graphique objet pour dessiner comme celui-ci utilise rendu en mode retenu, ce qui signifie que les propriétés des lignes individuelles sont stockées séparément - par opposition à rendu en mode immédiat, où seule l'image finale est stockée. (Les mêmes concepts existent dans d'autres plates-formes de développement; par exemple, en HTML5, dessiner en SVG utilise le mode conservé, alors que dessiner sur la toile utilise le mode immédiat.)

S'il y a beaucoup de lignes à l'écran, les stocker et les restituer séparément rendra votre jeu plus lent et plus lent. La solution à cela dépend de votre plate-forme - dans Flash, vous pouvez utiliser BitmapData.draw () pour stocker chaque ligne dans un seul bitmap après son tracé..


Étape 9: Niveau d'échantillon

Ici, j'ai créé une démo pour le niveau d'échantillon d'un jeu Spot the Difference. Vérifiez-le! La source complète est en Sample2.as de la source télécharger.

Conclusion

Merci d'avoir lu cet article. J'espère que cela vous a donné une idée pour créer votre propre jeu. Laissez des commentaires s'il y a un problème avec le code et je vous recontacterai dès que possible..