So stellen Sie fest, wenn ein Objekt von einer Geste eingekreist wurde

Sie sind nie zu alt für ein Spiel von Spot the Difference. Ich erinnere mich, dass ich es als Kind gespielt habe, und ich finde, dass meine Frau es immer noch gelegentlich spielt. In diesem Lernprogramm wird beschrieben, wie Sie feststellen können, wann ein Ring um ein Objekt gezeichnet wurde, und zwar mit einem Algorithmus, der mit der Maus, dem Stift oder der Touchscreen-Eingabe verwendet werden kann.

Hinweis: Obwohl die Demos und der Quellcode dieses Lernprogramms Flash und AS3 verwenden, sollten Sie in der Lage sein, in nahezu jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte zu verwenden.


Endergebnisvorschau

Werfen wir einen Blick auf das Endergebnis, auf das wir hinarbeiten werden. Der Bildschirm ist in zwei Bilder unterteilt, die fast identisch sind, jedoch nicht ganz. Versuchen Sie, die sechs Unterschiede zu erkennen, und kreisen Sie die auf dem linken Bild ein. Viel Glück!

Hinweis: Sie müssen keinen perfekten Kreis zeichnen! Sie müssen nur einen groben Ring oder eine Schleife um jeden Unterschied zeichnen.

Kein Flash? Schauen Sie sich diese Video-Demo an:


Schritt 1: Die kreisende Bewegung

Wir werden im Algorithmus einige Vektorberechnungen verwenden. Wie immer ist es gut, die zugrunde liegende Mathematik zu verstehen, bevor Sie sie anwenden. Hier ist eine kurze Auffrischung der Vektor-Mathematik.

Das Bild oben zeigt den Vektor EIN nach horizontalen und vertikalen Komponenten zerlegt (Axt und Ay, beziehungsweise).

Nun schauen wir uns das an Skalarprodukt Bedienung, wie im Bild unten dargestellt. Zuerst sehen Sie die Punktproduktoperation zwischen den Vektoren A und B.

Um den Winkel zwischen den beiden Vektoren zu finden, können wir dieses Punktprodukt verwenden.

| A | und | B | bezeichnen die Größen der Vektoren A und B, so dass | A | gegeben wird und | B | und A Punkt B, was noch unbekannt ist, ist Theta. Mit einer kleinen Algebra (im Bild gezeigt) wird die endgültige Gleichung erstellt, mit der wir Theta finden können.

Weitere Informationen zu Vector Dot-Produkten finden Sie auf der folgenden Wolfram-Seite.

Die andere nützliche Operation ist Kreuzprodukt. Schauen Sie sich die Operation unten an:

Diese Operation ist nützlich, um herauszufinden, ob der Sandwichwinkel relativ zu einem bestimmten Vektor im oder gegen den Uhrzeigersinn ist.

Lassen Sie mich weiter ausarbeiten. Für den Fall des obigen Diagramms ist die Drehung von A nach B im Uhrzeigersinn, also ist A-Kreuz B negativ. Die Drehung von B zu A ist gegen den Uhrzeigersinn, also ist B-Kreuz A positiv. Beachten Sie, dass diese Operation sequenzabhängig ist. Ein Kreuz B führt zu einem anderen Ergebnis als B Kreuz A.

Das ist nicht alles. Es kommt vor, dass im Koordinatenraum vieler Spieleentwicklungsplattformen die y-Achse invertiert ist (y steigt mit abwärts). Daher ist unsere Analyse umgekehrt und A-Kreuz B ist positiv, während B-Kreuz A negativ ist.

Das ist genug Revision. Kommen wir zu unserem Algorithmus.


Schritt 2: Einkreisen der Interaktion

Die Spieler müssen die korrekten Details im Bild umkreisen. Nun, wie machen wir das? Bevor wir diese Frage beantworten, sollten wir den Winkel zwischen zwei Vektoren berechnen. Wie Sie sich jetzt erinnern, können wir das Dot-Produkt dafür verwenden, also werden wir diese Gleichung hier implementieren.

Hier ist eine Demo, die veranschaulicht, was wir tun. Ziehen Sie einen der Pfeil um, um das Feedback zu sehen.

Mal sehen, wie das funktioniert. Im folgenden Code habe ich einfach die Vektoren und einen Timer initialisiert und einige interaktive Pfeile auf dem Bildschirm angezeigt.

öffentliche Funktion Demo1 () feedback = new TextField; addChild (feedback); feedback.selectable = false; feedback.autoSize = TextFieldAutoSize.LEFT; a1 = neuer Pfeil; addChild (a1); a2 = neuer Pfeil; addChild (a2); a2.rotation = 90 center = neuer Punkt (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 = neue ColorTransform (0, 0, 0, 1, 255); a2.transform.colorTransform = neue 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 = neuer Vector2d (1, 0); v2 = neuer Vector2d (0, 1); curr_vec = new Vector2d (1, 0); t = neuer Timer (50); 

Alle 50 Millisekunden wird die folgende Funktion ausgeführt und zum Aktualisieren der grafischen und textuellen Rückmeldung verwendet:

Aktualisierung der privaten Funktion (e: TimerEvent): void var curr_angle: Number = Math.atan2 (mouseY - center.y, mouseX - center.x); curr_vec.angle = curr_angle; if (item == 1) // Rotation des Pfeils visuell aktualisieren a1.rotation = Math2 ° Grad (curr_angle); // Messen des Winkels von a1 nach b1 v1 = curr_vec.clone (); direction = v2.crossProduct (v1); feedback.text = "Sie bewegen jetzt den roten Vektor A \ n"; feedback.appendText ("Winkel gemessen von grün nach rot:");  else if (item == 2) a2.rotation = Math2 ° OF (curr_angle); v2 = curr_vec.clone (); direction = v1.crossProduct (v2); feedback.text = "Sie bewegen jetzt den grünen Vektor, B \ n"; feedback.appendText ("Winkel gemessen von rot nach grün:");  theta_rad = Math.acos (v1.dotProduct (v2)); // Theta ist im Bogenmaß theta_deg = Math2 ° Of (theta_rad); wenn (Richtung < 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(); 

Sie werden feststellen, dass die Beträge für v1 und v2 in diesem Szenario beide eine Einheit sind (siehe Zeile 52 und 53, die oben hervorgehoben sind). Daher habe ich die Notwendigkeit der Berechnung der Größe der Vektoren zunächst übersprungen.

Wenn Sie den vollständigen Quellcode sehen möchten, checken Sie aus Demo1.as im Quelldownload.


Schritt 3: Einen vollständigen Kreis erkennen

Ok, jetzt, da wir die Grundidee verstanden haben, prüfen wir nun, ob der Spieler einen Punkt erfolgreich umrundet hat.

Ich hoffe das Diagramm spricht für sich. Der Beginn der Interaktion beginnt mit dem Drücken der Maustaste und das Ende der Interaktion endet mit dem Loslassen der Maustaste.

In jedem Intervall (beispielsweise 0,01 Sekunden) während der Interaktion berechnen wir den Winkel zwischen aktuellen und vorherigen Vektoren. Diese Vektoren werden von der Markerposition (wo sich der Unterschied befindet) zur Mausposition in dieser Instanz konstruiert. Addiere alle diese Winkel (t1, t2, t3 in diesem Fall) und wenn der Winkel am Ende der Interaktion 360 Grad beträgt, hat der Spieler einen Kreis gezeichnet.

Natürlich können Sie die Definition eines vollen Kreises auf 300 bis 340 Grad einstellen, um bei der Mausbewegung Spielefehler zuzulassen.

Hier ist eine Demo für diese Idee. Ziehen Sie eine kreisförmige Geste um die rote Markierung in der Mitte. Sie können die Position der roten Markierung mit den Tasten W, A, S, D verschieben.


Schritt 4: Die Implementierung

Sehen wir uns die Implementierung für die Demo an. Wir werden uns hier nur die wichtigen Berechnungen ansehen.

Sehen Sie sich den hervorgehobenen Code unten an und ordnen Sie ihn der mathematischen Gleichung in Schritt 1 zu. Sie werden feststellen, dass der Wert für Arccos manchmal produziert Keine Nummer (NaN), wenn Sie die Zeile 92 überspringen, constants_value manchmal wegen der Ungenauigkeiten der Rundung größer als 1, so dass wir ihn manuell auf maximal 1 zurückstellen müssen. Jede Eingabe von Arccos, die größer als 1 ist, erzeugt eine NaN.

Aktualisierung der privaten Funktion (e: TimerEvent): void graphics.clear (); graphics.lineStyle (1) graphics.moveTo (marker.x, marker.y); graphics.lineTo (mouseX, mouseY); prev_vec = curr_vec.clone (); curr_vec = new Vector2d (mouseX - marker.x, mouseY - marker.y); // Der Wert der Berechnung überschreitet manchmal den Wert 1, der für die manuelle Bearbeitung der Genauigkeit erforderlich ist. 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) // Winkel aus var Richtung: Number = prev_vec.crossProduct (curr_vec)> 0? 1: -1; // Überprüfung der Drehrichtung total_angle + = direction * delta_angle; // zu dem während der Interaktion gemachten kumulativen Winkel hinzufügen

Die vollständige Quelle dafür finden Sie in Demo2.as


Schritt 5: Der Fehler

Wenn Sie einen großen Kreis zeichnen, der die Leinwand umgibt, wird die Markierung als eingekreist betrachtet. Ich muss nicht unbedingt wissen, wo der Marker ist.

Um diesem Problem zu begegnen, können wir die Nähe der Kreisbewegung überprüfen. Wenn der Kreis innerhalb der Grenzen eines bestimmten Bereichs (dessen Wert unter Ihrer Kontrolle steht) gezeichnet wird, gilt dies nur als Erfolg.

Schauen Sie sich den Code unten an. Wenn überhaupt, überschreitet der Benutzer MIN_DIST (mit einem Wert von 60 in diesem Fall), gilt dies als Zufallsschätzung.

Aktualisierung der privaten Funktion (e: TimerEvent): void graphics.clear (); graphics.lineStyle (1) graphics.moveTo (marker.x, marker.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) within_bound = false; // Der Wert der Berechnung überschreitet manchmal den Wert 1, der für die manuelle Bearbeitung der Genauigkeit erforderlich ist. 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) // Winkel aus var Richtung: Number = prev_vec.crossProduct (curr_vec)> 0? 1: -1; // Überprüfung der Drehrichtung total_angle + = direction * delta_angle; // zum kumulativen Winkel hinzufügen, der während der Interaktion erstellt wurde mag_box.text = "Entfernung vom Marker:" + curr_vec.magnitude.toPrecision (4); mag_box.x = mouseX + 10; mag_box.y = mouseY + 10; feedback.text = "Gehen Sie nicht weiter" + MIN_DIST

Versuchen Sie erneut, die Markierung einzukreisen. Wenn du das denkst MIN_DIST ist ein wenig unversöhnlich, es kann immer an das Bild angepasst werden.


Schritt 6: Unterschiedliche Formen

Was ist, wenn der "Unterschied" kein exakter Kreis ist? Einige können rechteckig oder dreieckig sein oder eine andere Form haben.
In diesen Fällen können wir nicht nur einen Marker verwenden, sondern auch einige:

Im obigen Diagramm sind oben zwei Mauszeiger dargestellt. Wir beginnen mit dem Cursor ganz rechts und bewegen uns im Uhrzeigersinn auf das linke Ende. Beachten Sie, dass der Pfad alle drei Markierungen umgibt.

Ich habe auch die Winkel, die durch diesen Pfad vergangen sind, auf jeden der Marker gezeichnet (helle Striche zu dunklen Strichen). Wenn alle drei Winkel mehr als 360 Grad betragen (oder den von Ihnen gewählten Wert), zählen wir nur dann als Kreis.

Aber das reicht nicht aus. Erinnern Sie sich an den Fehler in Schritt 4? Nun, das gleiche gilt auch hier: Wir müssen die Nähe prüfen. Anstatt zu verlangen, dass die Geste einen bestimmten Radius eines bestimmten Markers nicht überschreitet, wird lediglich geprüft, ob der Mauszeiger zumindest für eine kurze Instanz nahe an alle Markierungen kam. Ich werde Pseudo-Code verwenden, um diese Idee zu erklären:

Berechnen Sie den durch den Pfad für marker1, marker2 und marker3 verstrichenen Winkel. Wenn jeder Winkel mehr als 360 beträgt, wenn die Nähe eines jeden Markers mit dem Mauszeiger gekreuzt wurde, umgibt der Kreis den Bereich, der durch den Marker endif endif markiert ist

Schritt 7: Demo für die Idee

Hier verwenden wir drei Punkte, um ein Dreieck darzustellen.

Versuchen Sie zu umkreisen:

  • ein Punkt
  • zwei punkte
  • drei Punkte

… Im Bild unten. Beachten Sie, dass die Geste nur erfolgreich ist, wenn sie alle drei Punkte enthält.

Schauen wir uns den Code für diese Demo an. Ich habe die wichtigsten Zeilen für die Idee unten hervorgehoben. das vollständige Skript ist in Demo4.as.

private Funktion handleMouse (e: MouseEvent): void if (e.type == "mouseDown") t.addEventListener (TimerEvent.TIMER, Update); t.start (); update_curr_vecs ();  else if (e.type == "mouseUp") t.stop (); t.removeEventListener (TimerEvent.TIMER, Update); // prüfe, ob die Bedingungen erfüllt wurden condition1 = true // alle Winkel erfüllen die MIN_ANGLE-Bedingung2 = true // alle Proximitäten erfüllen MIN_DIST für (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(); 

Schritt 8: Zeichnen der Kreise

Die beste Methode zum Zeichnen der Linie, die Sie verfolgen, hängt von Ihrer Entwicklungsplattform ab. Deshalb werde ich hier nur die Methode beschreiben, die wir in Flash verwenden würden.

Es gibt zwei Möglichkeiten, Linien in AS3 zu zeichnen, wie in der Abbildung oben dargestellt.

Der erste Ansatz ist ziemlich einfach: Verwendung ziehen nach() um die Zeichnungsposition zu verschieben (10, 20). Ziehen Sie dann eine Linie, um (10, 20) mit (80, 70) zu verbinden lineTo ().

Der zweite Ansatz besteht darin, alle Details in zwei Arrays zu speichern, Befehle [] und Koordinaten [] (mit Koordinaten, die in (x, y) Paaren gespeichert sind Koordinaten []) und zeichnen später alle grafischen Details mit drawPath () in einem einzigen Schuss. Ich habe mich für den zweiten Ansatz in meiner Demo entschieden.

Check it out: Versuchen Sie, mit der Maus auf die Leinwand zu klicken, um eine Linie zu zeichnen.

Und hier ist der AS3-Code für diese Demo. Überprüfen Sie die vollständige Quelle Drawing1.as.

public class Drawing1 erweitert Sprite private var cmd: Vector.; Private Variabeln: Vektor.; private var _thickness: Number = 2, _col: Number = 0, _alpha: Number = 1; public function Drawing1 () // Event Handlerst der Maus nach oben und nach unten zuweisen stage.addEventListener (MouseEvent.MOUSE_DOWN, mouseHandler); stage.addEventListener (MouseEvent.MOUSE_UP, mouseHandler);  / ** * Mausereignis-Handler * @param e-Mausereignis * / private Funktion mouseHandler (e: MouseEvent): void if (e.type == "mouseDown") // randomisiert die Linieneigenschaften _thickness = Math.random () * 5; _col = Math.random () * 0xffffff; _alpha = Math.random () * 0.5 + 0.5 // leiten Sie die Variablen cmd = new Vector ein.; Koordinaten = neuer Vektor.; // erste Registrierung des Zeilenanfangs cmd [0] = 1; Koordinaten [0] = MausX; Koordinaten [1] = mouseY; // die Zeichnung beim Verschieben mit der Maus starten stage.addEventListener (MouseEvent.MOUSE_MOVE, mouseHandler);  else if (e.type == "mouseUp") // Entferne den Mausbewegungs-Handler, sobald die Maustaste losgelassen wird. stage.removeEventListener (MouseEvent.MOUSE_MOVE, mouseHandler);  else if (e.type == "mouseMove") // Drücken in die Maus Bewegen Sie den Befehl cmd.push (2); // Befehl zeichnen coords.push (mouseX); // Koordinaten zum Zeichnen einer Linie in Koordinaten.push (mouseY); neu zeichnen (); // führe den Zeichenbefehl aus / ** * Methode zum Zeichnen der Linie (n) wie durch die Mausbewegung definiert * / private Funktion redraw (): void graphics.clear (); // alle vorherigen Zeichnungen löschen graphics.lineStyle (_thickness, _col, _alpha); graphics.drawPath (cmd, coords); 

In Flash mit der Grafik Objekt zum Zeichnen wie dieses verwendet Rendering im beibehaltenen Modus, Das bedeutet, dass die Eigenschaften der einzelnen Zeilen getrennt gespeichert werden sofortiges Rendern im Modus, wo nur das endgültige Bild gespeichert wird. (Dieselben Konzepte gelten auch für andere Entwicklungsplattformen. In HTML5 wird zum Beispiel beim Zeichnen in SVG der beibehaltene Modus verwendet, während beim Zeichnen in die Zeichenfläche der unmittelbare Modus verwendet wird.)

Wenn viele Zeilen auf dem Bildschirm angezeigt werden, kann das Spiel langsam und verzögert werden, wenn Sie alle Zeilen einzeln speichern und erneut darstellen. Die Lösung hierfür hängt von Ihrer Plattform ab. In Flash können Sie BitmapData.draw () verwenden, um jede Zeile nach dem Zeichnen in einer einzelnen Bitmap zu speichern.


Schritt 9: Probenebene

Hier habe ich eine Demo für das Beispiellevel eines Spot the Difference-Spiels erstellt. Hör zu! Die vollständige Quelle ist in Sample2.as des Quelldownloads.

Fazit

Vielen Dank, dass Sie diesen Artikel gelesen haben. Ich hoffe, es hat Ihnen eine Idee gegeben, wie Sie Ihr eigenes Spiel entwickeln können. Bitte hinterlassen Sie einige Kommentare, wenn es Probleme mit dem Code gibt und ich werde mich so schnell wie möglich bei Ihnen melden.