Advertisement
  1. Code
  2. Coding Fundamentals
  3. Game Development

So erkennen Sie, wann ein Objekt von einer Geste eingekreist wurde

Scroll to top
Read Time: 13 min

() 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.

Vector definitionVector definitionVector definition

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.

Vector mathVector mathVector math

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:

Cross vector

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.

Calculate the angles

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:

Circling multiple cirlceCircling multiple cirlceCircling multiple cirlce

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.

Actionscript drawing APIActionscript drawing APIActionscript drawing API

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.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.