So erkennen Sie, wann ein Objekt von einer Geste eingekreist wurde
() translation by (you can also view the original English article)
Du bist nie zu alt für eine Partie Spot the Difference - ich erinnere mich, dass ich es als Kind gespielt habe, und jetzt finde ich, dass meine Frau es immer noch gelegentlich spielt! In diesem Tutorial sehen wir uns an, wie Sie mit einem Algorithmus, der für die Eingabe von Maus, Stift oder Touchscreen verwendet werden kann, erkennen, wann ein Ring um ein Objekt gezogen wurde.
Hinweis: Obwohl die Demos und der Quellcode dieses Tutorials Flash und AS3 verwenden, sollten Sie in fast jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte verwenden können.
Endergebnis Vorschau
Werfen wir einen Blick auf das Endergebnis, auf das wir hinarbeiten werden. Der Bildschirm ist in zwei Bilder unterteilt, die fast identisch, aber nicht ganz sind. Versuchen Sie, die sechs Unterschiede zu erkennen und diese auf dem linken Bild zu kreisen. Viel Glück!
Hinweis: Sie müssen keinen perfekten Kreis zeichnen! Sie müssen nur einen groben Ring oder eine Schleife um jeden Unterschied ziehen.
Sie haben keinen Flash? Schauen Sie sich diese Video-Demo an:
Schritt 1: Die Kreisbewegung
Wir werden einige Vektorberechnungen im Algorithmus verwenden. Wie immer ist es gut, die zugrunde liegende Mathematik zu verstehen, bevor Sie sie anwenden. Hier ist eine kurze Auffrischung der Vektormathematik.



Das Bild oben zeigt den Vektor A, zerlegt in seine horizontalen und vertikalen Komponenten (Ax und Ay).
Schauen wir uns nun die Punktprodukt-Operation an, die in der Abbildung unten dargestellt ist. Zunächst sehenSie die Punktprodukt-Operation zwischen den Vektoren A und B.



Um den Winkel zwischen den beiden Vektoren zu ermitteln, können wir dieses Punktprodukt verwenden.
|A| und |B| bezeichnen die Größen der Vektoren A und B, so gegeben |A| und |B| und A Punkt B, was unbekannt bleibt, ist Theta. Mit ein wenig Algebra (im Bild gezeigt) wird die endgültige Gleichung erzeugt, mit der wir Theta finden können.
Weitere Informationen zum Vektorpunktprodukt finden Sie auf der folgenden Wolfram-Seite.
Die andere nützliche Operation ist das Kreuzprodukt. Überprüfen Sie die Operation unten:

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 darauf eingehen. Für den Fall des obigen Diagramms erfolgt die Drehung von A nach B im Uhrzeigersinn, sodass das Kreuz B negativ ist. Die Drehung von B nach A erfolgt gegen den Uhrzeigersinn, sodass das B-Kreuz A positiv ist. Beachten Sie, dass dieser Vorgang sequenzabhängig ist. Ein Kreuz B führt zu einem anderen Ergebnis als Kreuz A.
Das ist nicht alles. Es kommt vor, dass im Koordinatenraum vieler Spieleentwicklungsplattformen die y-Achse invertiert ist (y nimmt zu, wenn wir uns nach unten bewegen). Unsere Analyse ist also umgekehrt, und A-Kreuz B ist positiv, während B-Kreuz A negativ ist.
Das ist genug Überarbeitung. Kommen wir zu unserem Algorithmus.
Schritt 2: Kreisinteraktion
Die Spieler müssen die richtigen Details auf dem Bild einkreisen. Wie machen wir das jetzt? Bevor wir diese Frage beantworten, sollten wir den Winkel zwischen zwei Vektoren berechnen. Wie Sie sich jetzt erinnern werden, können wir das Punktprodukt dafür verwenden, daher werden wir diese Gleichung hier implementieren.
Hier ist eine Demo, um zu veranschaulichen, was wir tun. Ziehen Sie einen der Pfeile, um das Feedback anzuzeigen.
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.
1 |
public function Demo1() |
2 |
{
|
3 |
feedback = new TextField; addChild(feedback); |
4 |
feedback.selectable = false; |
5 |
feedback.autoSize = TextFieldAutoSize.LEFT; |
6 |
|
7 |
a1 = new Arrow; addChild(a1); |
8 |
a2 = new Arrow; addChild(a2); a2.rotation = 90 |
9 |
|
10 |
center = new Point(stage.stageWidth >> 1, stage.stageHeight >> 1) |
11 |
a1.x = center.x; a1.y = center.y; a1.name = "a1"; |
12 |
a2.x = center.x; a2.y = center.y; a2.name = "a2"; |
13 |
|
14 |
a1.transform.colorTransform = new ColorTransform(0, 0, 0, 1, 255); |
15 |
a2.transform.colorTransform = new ColorTransform(0, 0, 0, 1, 0, 255); |
16 |
|
17 |
a1.addEventListener(MouseEvent.MOUSE_DOWN, handleMouse); |
18 |
a2.addEventListener(MouseEvent.MOUSE_DOWN, handleMouse); |
19 |
stage.addEventListener(MouseEvent.MOUSE_UP, handleMouse); |
20 |
|
21 |
v1 = new Vector2d(1, 0); |
22 |
v2 = new Vector2d(0, 1); |
23 |
curr_vec = new Vector2d(1, 0); |
24 |
|
25 |
t = new Timer(50); |
26 |
}
|
Alle 50 Millisekunden wird die folgende Funktion ausgeführt und zum Aktualisieren des Grafik- und Textfeedbacks verwendet:
1 |
private function update(e:TimerEvent):void |
2 |
{
|
3 |
var curr_angle:Number = Math.atan2(mouseY - center.y, mouseX - center.x); |
4 |
curr_vec.angle = curr_angle; |
5 |
|
6 |
if (item == 1) { |
7 |
//update arrow's rotation visually
|
8 |
a1.rotation = Math2.degreeOf(curr_angle); |
9 |
|
10 |
//measuring the angle from a1 to b1
|
11 |
v1 = curr_vec.clone(); |
12 |
direction = v2.crossProduct(v1); |
13 |
feedback.text = "You are now moving the red vector, A \n"; |
14 |
feedback.appendText("Angle measured from green to red: "); |
15 |
}
|
16 |
else if (item == 2) { |
17 |
a2.rotation = Math2.degreeOf(curr_angle); |
18 |
|
19 |
v2 = curr_vec.clone(); |
20 |
direction = v1.crossProduct(v2); |
21 |
feedback.text = "You are now moving the green vector, B\n"; |
22 |
feedback.appendText("Angle measured from red to green: "); |
23 |
}
|
24 |
|
25 |
theta_rad = Math.acos(v1.dotProduct(v2)); //theta is in radians |
26 |
theta_deg = Math2.degreeOf(theta_rad); |
27 |
if (direction < 0) { |
28 |
feedback.appendText("-" + theta_deg.toPrecision(4) + "\n"); |
29 |
feedback.appendText("rotation is anti clockwise") |
30 |
}
|
31 |
else { |
32 |
feedback.appendText(theta_deg.toPrecision(4) + "\n"); |
33 |
feedback.appendText("rotation is clockwise") |
34 |
}
|
35 |
drawSector(); |
36 |
}
|
Sie werden feststellen, dass die Größe für v1 und v2 in diesem Szenario beide 1 Einheit beträgt (siehe oben hervorgehobene Zeilen 52 und 53). Daher habe ich die Notwendigkeit, die Größe der Vektoren zu berechnen, vorerst übersprungen.
Wenn Sie den vollständigen Quellcode anzeigen möchten, lesen Sie Demo1.as
im Quelldownload.
Schritt 3: Erkennen Sie einen vollen Kreis
Ok, jetzt, da wir die Grundidee verstanden haben, werden wir sie verwenden, um zu überprüfen, ob der Spieler einen Punkt erfolgreich eingekreist hat.

Ich hoffe das Diagramm spricht für sich. Der Beginn der Interaktion ist, wenn die Maustaste gedrückt wird, und das Ende der Interaktion ist, wenn die Maustaste losgelassen wird.
In jedem Intervall (z. B. 0,01 Sekunden) während der Interaktion berechnen wir den Winkel zwischen aktuellen und vorherigen Vektoren. Diese Vektoren werden von der Markierungsposition (wo der Unterschied ist) zur Mausposition in dieser Instanz konstruiert. Addiere alle diese Winkel (in diesem Fall t1, t2, t3) 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 Vollkreises auf 300 bis 340 Grad einstellen, um Platz für Spielerfehler bei der Ausführung der Mausbewegung zu schaffen.
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
Lassen Sie uns die Implementierung für die Demo untersuchen. Wir werden uns hier nur die wichtigen Berechnungen ansehen.
Schauen Sie sich den hervorgehobenen Code unten an und stimmen Sie ihn mit der mathematischen Gleichung in Schritt 1 ab. Sie werden feststellen, dass der Wert für Arccos manchmal Not a Number
(NaN) ergibt, wenn Sie Zeile 92 überspringen. Außerdem überschreitet constants_value
manchmal 1 aufgrund von Rundungsungenauigkeiten, sodass wir es manuell auf maximal 1 zurücksetzen müssen. Jede Eingabenummer für Arccos größer als 1 erzeugt eine NaN.
1 |
private function update(e:TimerEvent):void |
2 |
{
|
3 |
graphics.clear(); |
4 |
graphics.lineStyle(1) |
5 |
graphics.moveTo(marker.x, marker.y); |
6 |
graphics.lineTo(mouseX, mouseY); |
7 |
|
8 |
prev_vec = curr_vec.clone(); |
9 |
curr_vec = new Vector2d(mouseX - marker.x, mouseY - marker.y); |
10 |
//value of calculation sometimes exceed 1 need to manually handle the precission
|
11 |
var constants_value:Number = Math.min(1, |
12 |
prev_vec.dotProduct(curr_vec) / (prev_vec.magnitude * curr_vec.magnitude) |
13 |
); |
14 |
var delta_angle:Number = Math.acos(constants_value) //angle made |
15 |
var direction:Number = prev_vec.crossProduct(curr_vec) > 0? 1: -1; //checking the direction of rotation |
16 |
total_angle += direction * delta_angle; //add to the cumulative angle made during the interaction |
17 |
}
|
Die vollständige Quelle hierfür finden Sie in Demo2.as
Schritt 5: Der Fehler
Ein Problem, das Sie möglicherweise sehen, ist, dass der Marker als eingekreist betrachtet wird, solange ich einen großen Kreis um die Leinwand zeichne. Ich muss nicht unbedingt wissen, wo sich der Marker befindet.
Um diesem Problem entgegenzuwirken, können wir die Nähe der Kreisbewegung überprüfen. Wenn der Kreis innerhalb der Grenzen eines bestimmten Bereichs gezeichnet wird (dessen Wert unter Ihrer Kontrolle steht), wird er nur dann als Erfolg gewertet.
Überprüfen Sie den Code unten. Wenn der Benutzer jemals MIN_DIST
überschreitet (in diesem Fall mit einem Wert von 60), wird dies als zufällige Vermutung angesehen.
1 |
private function update(e:TimerEvent):void |
2 |
{
|
3 |
graphics.clear(); |
4 |
graphics.lineStyle(1) |
5 |
graphics.moveTo(marker.x, marker.y); |
6 |
graphics.lineTo(mouseX, mouseY); |
7 |
|
8 |
prev_vec = curr_vec.clone(); |
9 |
curr_vec = new Vector2d(mouseX - marker.x, mouseY - marker.y); |
10 |
|
11 |
if (curr_vec.magnitude > MIN_DIST) within_bound = false; |
12 |
|
13 |
//value of calculation sometimes exceed 1 need to manually handle the precission
|
14 |
var constants_value:Number = Math.min(1, |
15 |
prev_vec.dotProduct(curr_vec) / (prev_vec.magnitude * curr_vec.magnitude) |
16 |
); |
17 |
var delta_angle:Number = Math.acos(constants_value) //angle made |
18 |
var direction:Number = prev_vec.crossProduct(curr_vec) > 0? 1: -1; //checking the direction of rotation |
19 |
total_angle += direction * delta_angle; //add to the cumulative angle made during the interaction |
20 |
|
21 |
mag_box.text = "Distance from marker: " + curr_vec.magnitude.toPrecision(4); |
22 |
mag_box.x = mouseX + 10; |
23 |
mag_box.y = mouseY + 10; |
24 |
feedback.text = "Do not go beyond "+MIN_DIST |
25 |
}
|
Versuchen Sie erneut, die Markierung zu umkreisen. Wenn Sie der Meinung sind, dass der MIN_DIST
etwas unversöhnlich ist, kann er jederzeit an das Bild angepasst werden.
Schritt 6: Verschiedene 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 anstelle nur eines Markers einige aufstellen:



In der obigen Abbildung sind oben zwei Mauscursor dargestellt. Beginnend mit dem Cursor ganz rechts machen wir eine kreisförmige Bewegung im Uhrzeigersinn zum anderen Ende links. Beachten Sie, dass der Pfad alle drei Markierungen umgibt.
Ich habe auch die Winkel gezeichnet, die dieser Pfad auf jeder der Markierungen verstrichen ist (helle Striche bis dunkle Striche). Wenn alle drei Winkel mehr als 360 Grad betragen (oder welcher Wert auch immer Sie wählen), zählen wir ihn nur dann als Kreis.
Das reicht aber nicht. Erinnern Sie sich an den Fehler in Schritt 4? Das gleiche gilt hier: Wir müssen nach der Nähe suchen. Anstatt zu verlangen, dass die Geste einen bestimmten Radius eines bestimmten Markers nicht überschreitet, prüfen wir nur, ob der Mauszeiger für mindestens eine kurze Instanz allen Markierungen nahe gekommen ist. Ich werde Pseudocode verwenden, um diese Idee zu erklären:
1 |
Calculate angle elapsed by path for marker1, marker2 and marker3 |
2 |
|
3 |
if each angle is more than 360 |
4 |
if each marker's proximity was crossed by mouse cursor |
5 |
then the circle made is surrounding the area marked by markers |
6 |
endif |
7 |
endif |
Schritt 7: Demo für die Idee
Hier verwenden wir drei Punkte, um ein Dreieck darzustellen.
Versuchen Sie zu kreisen:
- 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 befindet sich in Demo4.as
.
1 |
private function handleMouse(e:MouseEvent):void |
2 |
{
|
3 |
if (e.type == "mouseDown") { |
4 |
t.addEventListener(TimerEvent.TIMER, update); |
5 |
t.start(); |
6 |
update_curr_vecs(); |
7 |
}
|
8 |
else if (e.type == "mouseUp") { |
9 |
t.stop(); |
10 |
t.removeEventListener(TimerEvent.TIMER, update); |
11 |
|
12 |
//check if conditions were met
|
13 |
condition1 = true //all angles are meeting MIN_ANGLE |
14 |
condition2 = true //all proximities are meeting MIN_DIST |
15 |
|
16 |
for (var i:int = 0; i < markers.length; i++) { |
17 |
if (Math.abs(angles[i])< MIN_ANGLE) { |
18 |
condition1 = false; |
19 |
break; |
20 |
}
|
21 |
if (proximity[i] == false) { |
22 |
condition2 = false; |
23 |
break
|
24 |
}
|
25 |
}
|
26 |
|
27 |
if (condition1 && condition2) { |
28 |
box.text="Attempt to circle the item is successful" |
29 |
}
|
30 |
else { |
31 |
box.text="Failure" |
32 |
}
|
33 |
|
34 |
reset_vecs(); |
35 |
reset_values(); |
36 |
}
|
37 |
}
|
38 |
|
39 |
private function update(e:TimerEvent):void |
40 |
{
|
41 |
update_prev_vecs(); |
42 |
update_curr_vecs(); |
43 |
update_values(); |
44 |
}
|
Schritt 8: Zeichnen der Kreise
Die beste Methode zum Zeichnen der Linie, die Sie verfolgen, hängt von Ihrer Entwicklungsplattform ab. Daher werde ich hier nur die Methode skizzieren, die wir in Flash verwenden würden.



Es gibt zwei Möglichkeiten, in AS3 Linien zu zeichnen, wie im obigen Bild gezeigt.
Der erste Ansatz ist ziemlich einfach: Verwenden Sie moveTo()
, um die Zeichnungsposition auf die Koordinate (10, 20) zu verschieben. Zeichnen Sie dann eine Linie, um (10, 20) mit (80, 70) mit lineTo()
zu verbinden.
Der zweite Ansatz besteht darin, alle Details in zwei Arrays, commands[]
und coords[]
(mit Koordinaten, die in (x, y) Paaren innerhalb von coords[]
gespeichert sind) zu speichern und später alle grafischen Details mit drawPath()
in einem einzigen auf Leinwand zu zeichnen Schuss. Ich habe mich in meiner Demo für den zweiten Ansatz entschieden.
Probieren Sie es aus: Versuchen Sie, mit der Maus auf die Leinwand zu klicken und sie zu ziehen, um eine Linie zu zeichnen.
Und hier ist der AS3-Code für diese Demo. Schauen Sie sich die vollständige Quelle in Drawing1.as
an.
1 |
public class Drawing1 extends Sprite |
2 |
{
|
3 |
private var cmd:Vector.<int>; |
4 |
private var coords:Vector.<Number>; |
5 |
private var _thickness:Number = 2, _col:Number = 0, _alpha:Number = 1; |
6 |
|
7 |
public function Drawing1() |
8 |
{
|
9 |
//assign event handlerst to mouse up and mouse down
|
10 |
stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseHandler); |
11 |
stage.addEventListener(MouseEvent.MOUSE_UP, mouseHandler); |
12 |
}
|
13 |
|
14 |
/**
|
15 |
* Mouse event handler
|
16 |
* @param e mouse event
|
17 |
*/
|
18 |
private function mouseHandler(e:MouseEvent):void |
19 |
{
|
20 |
if (e.type == "mouseDown") { |
21 |
//randomise the line properties
|
22 |
_thickness = Math.random() * 5; |
23 |
_col = Math.random() * 0xffffff; |
24 |
_alpha = Math.random() * 0.5 + 0.5 |
25 |
|
26 |
//initiate the variables
|
27 |
cmd = new Vector.<int>; |
28 |
coords = new Vector.<Number>; |
29 |
|
30 |
//first registration of line beginning
|
31 |
cmd[0] = 1; |
32 |
coords[0] = mouseX; |
33 |
coords[1] = mouseY; |
34 |
|
35 |
//start the drawing when mouse move
|
36 |
stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseHandler); |
37 |
}
|
38 |
else if (e.type == "mouseUp") { |
39 |
//remove the mouse move handler once mouse button is released
|
40 |
stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseHandler); |
41 |
}
|
42 |
else if (e.type == "mouseMove") { |
43 |
//pushing into the mouse move the
|
44 |
cmd.push(2); //draw command |
45 |
coords.push(mouseX); //coordinates to draw line to |
46 |
coords.push(mouseY); |
47 |
redraw(); //execute the drawing command |
48 |
}
|
49 |
}
|
50 |
/**
|
51 |
* Method to draw the line(s) as defined by mouse movement
|
52 |
*/
|
53 |
private function redraw():void { |
54 |
graphics.clear(); //clearing all previous drawing |
55 |
graphics.lineStyle(_thickness, _col, _alpha); |
56 |
graphics.drawPath(cmd, coords); |
57 |
}
|
58 |
|
59 |
}
|
Wenn Sie in Flash das graphics
zum Zeichnen wie dieses verwenden, wird das Rendering im beibehaltenen Modus verwendet. Dies bedeutet, dass die Eigenschaften der einzelnen Linien separat gespeichert werden - im Gegensatz zum Rendern im Sofortmodus, bei dem nur das endgültige Bild gespeichert wird. (Dieselben Konzepte gibt es auch auf anderen Entwicklungsplattformen. In HTML5 wird beispielsweise beim Zeichnen in SVG der beibehaltene Modus verwendet, während beim Zeichnen auf Leinwand der Sofortmodus verwendet wird.)
Wenn auf dem Bildschirm viele Zeilen angezeigt werden, kann das Speichern und erneute Rendern aller Zeilen Ihr Spiel langsam und verzögert machen. 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: Probenstufe
Hier habe ich eine Demo für das Beispiellevel eines Spot the Difference-Spiels erstellt. Hör zu! Die vollständige Quelle befindet sich in Sample2.as
des Quelldownloads.
Abschluss
Vielen Dank für das Lesen dieses Artikels; Ich hoffe, es hat dir eine Idee gegeben, dein eigenes Spiel zu bauen. Hinterlassen Sie einige Kommentare, wenn es Probleme mit dem Code gibt, und ich werde mich so schnell wie möglich bei Ihnen melden.