Come rilevare quando un oggetto è stato cerchiato da un gesto

Non sei mai troppo vecchio per un gioco di Spot the Difference, ricordo che lo interpretavo da bambino e ora trovo che mia moglie lo suona occasionalmente! In questo tutorial, vedremo come rilevare quando un anello è stato disegnato attorno a un oggetto, con un algoritmo che potrebbe essere utilizzato con il mouse, lo stilo o l'input del touchscreen.

Nota: Sebbene le demo e il codice sorgente di questo tutorial utilizzino Flash e AS3, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi.


Anteprima del risultato finale

Diamo un'occhiata al risultato finale su cui lavoreremo. Lo schermo è diviso in due immagini, che sono quasi identiche ma non del tutto. Cerca di individuare le sei differenze e cerchia quelle nell'immagine a sinistra. In bocca al lupo!

Nota: non è necessario disegnare un cerchio perfetto! Hai solo bisogno di disegnare un anello ruvido o fare un giro attorno a ciascuna differenza.

Non hai Flash? Guarda questa demo video:


Step 1: The Circling Motion

Useremo alcuni calcoli vettoriali nell'algoritmo. Come sempre, è bene comprendere la matematica sottostante prima di applicarla, quindi ecco un breve aggiornamento della matematica vettoriale.

L'immagine sopra mostra il vettore UN suddiviso in componenti orizzontali e verticali (Ascia e Ay, rispettivamente).

Ora diamo un'occhiata al prodotto punto operazione, illustrata nell'immagine qui sotto. Per prima cosa, vedrai il funzionamento del prodotto con punti tra i vettori A e B.

Per trovare l'angolo inserito tra i due vettori, possiamo utilizzare questo prodotto con punti.

| A | e | B | denota le grandezze dei vettori A e B, così dati | A | e | B | e Un punto B, ciò che rimane sconosciuto è theta. Con una piccola algebra (mostrata nell'immagine), viene prodotta l'equazione finale, che possiamo usare per trovare theta.

Per ulteriori informazioni sul prodotto con punti vettoriali, fare riferimento alla seguente pagina Wolfram.

L'altra operazione utile è prodotto incrociato. Controlla l'operazione di seguito:

Questa operazione è utile per scoprire se l'angolo a sandwich è in senso orario o antiorario rispetto a un vettore specifico.

Lasciami elaborare ulteriormente. Per il caso del diagramma sopra, la rotazione da A a B è in senso orario, quindi A croce B è negativa. La rotazione di B a A è antioraria, quindi la croce B è positiva. Notare che questa operazione è sensibile alla sequenza. Una croce B produrrà risultati diversi dalla croce B A.

Non è tutto. Succede che nello spazio delle coordinate di molte piattaforme di sviluppo di giochi, l'asse y è invertito (y aumenta man mano che ci spostiamo verso il basso). Quindi la nostra analisi è invertita, e una croce B sarà positiva mentre la croce B è negativa.

Questa è una revisione sufficiente. Andiamo al nostro algoritmo.


Passaggio 2: interazione circolare

I giocatori dovranno circondare i dettagli corretti sull'immagine. Ora come lo facciamo? Prima di rispondere a questa domanda, dovremmo calcolare l'angolo tra due vettori. Come ora ricorderai, possiamo usare il prodotto dot per questo, quindi applicheremo questa equazione qui.

Ecco una demo per illustrare cosa stiamo facendo. Trascina una freccia per vedere il feedback.

Vediamo come funziona. Nel codice qui sotto, ho semplicemente inizializzato i vettori e un timer, e ho messo alcune frecce interattive sullo schermo.

funzione pubblica Demo1 () feedback = new TextField; addChild (feedback); feedback.selectable = false; feedback.autoSize = TextFieldAutoSize.LEFT; a1 = new Arrow; addChild (a1); a2 = new Arrow; 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 = new Vector2d (1, 0); v2 = new Vector2d (0, 1); curr_vec = new Vector2d (1, 0); t = new Timer (50); 

Ogni 50 millisecondi, viene eseguita la funzione seguente e utilizzata per aggiornare il feedback grafico e di testo:

aggiornamento della funzione privata (e: TimerEvent): void var curr_angle: Number = Math.atan2 (mouseY - center.y, mouseX - center.x); curr_vec.angle = curr_angle; if (item == 1) // aggiorna la rotazione della freccia visivamente a1.rotation = Math2.degreeOf (curr_angle); // misurazione dell'angolo da a1 a b1 v1 = curr_vec.clone (); direction = v2.crossProduct (v1); feedback.text = "Ora stai spostando il vettore rosso, A \ n"; feedback.appendText ("Angolo misurato da verde a rosso:");  else if (item == 2) a2.rotation = Math2.degreeOf (curr_angle); v2 = curr_vec.clone (); direction = v1.crossProduct (v2); feedback.text = "Ora stai spostando il vettore verde, B \ n"; feedback.appendText ("Angolo misurato da rosso a verde:");  theta_rad = Math.acos (v1.dotProduct (v2)); // theta è in radianti theta_deg = Math2.degreeOf (theta_rad); se (direzione < 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(); 

Noterai che la magnitudine per v1 e v2 sono entrambe 1 unità in questo scenario (controlla le righe 52 e 53 sopra evidenziate), quindi ho saltato la necessità di calcolare la magnitudine dei vettori per ora.

Se vuoi vedere il codice sorgente completo, controlla Demo1.as nel download di origine.


Passaggio 3: Rileva un cerchio completo

Ok, ora che abbiamo capito l'idea di base, ora la useremo per verificare se il giocatore ha cerchiato con successo un punto.

Spero che lo schema parli da solo. L'inizio dell'interazione è quando si preme il pulsante del mouse e la fine dell'interazione è quando il pulsante del mouse viene rilasciato.

Ad ogni intervallo (di, per esempio, 0,01 secondi) durante l'interazione, calcoleremo l'angolo inserito tra i vettori attuali e quelli precedenti. Questi vettori sono costruiti dalla posizione del marcatore (dove la differenza è) alla posizione del mouse in quell'istanza. Sommare tutti questi angoli (t1, t2, t3 in questo caso) e se l'angolo fatto è di 360 gradi alla fine dell'interazione, allora il giocatore ha disegnato un cerchio.

Naturalmente, si può modificare la definizione di un cerchio completo di 300-340 gradi, dando spazio agli errori del giocatore quando si esegue il gesto del mouse.

Ecco una demo per questa idea. Trascina un gesto circolare attorno all'indicatore rosso al centro. È possibile spostare la posizione dell'indicatore rosso usando i tasti W, A, S, D.


Passaggio 4: l'implementazione

Esaminiamo l'implementazione per la demo. Vedremo solo i calcoli importanti qui.

Controlla il codice evidenziato in basso e confrontalo con l'equazione matematica al punto 1. Noterai che il valore per arccos a volte produce Non un numero (NaN) se salti la linea 92. Inoltre, constants_value a volte supera 1 a causa di imprecisioni di arrotondamento, quindi è necessario riportarlo manualmente a un massimo di 1. Qualsiasi numero di input per arccos superiore a 1 produrrà un NaN.

aggiornamento della funzione privata (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); // valore del calcolo a volte supera 1 necessità di gestire manualmente la precission 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) // angolo fatto var direction: Number = prev_vec.crossProduct (curr_vec)> 0? 1: -1; // controlla la direzione di rotazione total_angle + = direction * delta_angle; // aggiungi all'angolo cumulativo effettuato durante l'interazione

La fonte completa per questo può essere trovato in Demo2.as


Step 5: The Flaw

Un problema che potresti vedere è che fintanto che disegno un grande cerchio che racchiude la tela, il marcatore sarà considerato cerchiato. Non ho bisogno di sapere dove si trova il marcatore.

Bene, per contrastare questo problema, possiamo verificare la vicinanza del movimento circolare. Se il cerchio viene disegnato entro i confini di un certo intervallo (il cui valore è sotto il tuo controllo), solo allora viene considerato un successo.

Controlla il codice qui sotto. Se mai l'utente supera MIN_DIST (con un valore di 60 in questo caso), quindi è considerato un'ipotesi casuale.

aggiornamento della funzione privata (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; // valore del calcolo a volte supera 1 necessità di gestire manualmente la precission 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) // angolo fatto var direction: Number = prev_vec.crossProduct (curr_vec)> 0? 1: -1; // controlla la direzione di rotazione total_angle + = direction * delta_angle; // aggiungi all'angolo cumulativo creato durante l'interazione mag_box.text = "Distanza dal marker:" + curr_vec.magnitude.toPrecision (4); mag_box.x = mouseX + 10; mag_box.y = mouseY + 10; feedback.text = "Non andare oltre" + MIN_DIST

Di nuovo, prova a circondare il marker. Se pensi che MIN_DIST è un po 'spietato, può sempre essere regolato per adattarsi all'immagine.


Passaggio 6: forme diverse

Cosa succede se la "differenza" non è un cerchio esatto? Alcuni potrebbero essere rettangolari, triangolari o di qualsiasi altra forma.
In questi casi, invece di usare un solo marker, possiamo metterne alcuni:

Nel diagramma sopra, due cursori del mouse sono mostrati in alto. Iniziando con il cursore più a destra, faremo un movimento circolare in senso orario verso l'altra estremità a sinistra. Nota che il percorso circonda tutti e tre i marcatori.

Ho anche disegnato gli angoli trascorsi da questo percorso su ciascuno dei segnalini (tratti chiari a tratti scuri). Se tutti e tre gli angoli sono più di 360 gradi (o qualsiasi valore tu scelga), solo allora lo consideriamo come un cerchio.

Ma non è abbastanza. Ricorda l'errore nel passaggio 4? Bene, lo stesso vale qui: dovremo controllare la vicinanza. Invece di richiedere che il gesto non superi un certo raggio di un marker specifico, controlleremo solo se il cursore del mouse si è avvicinato a tutti i marker per almeno una breve istanza. Userò lo pseudo-codice per spiegare questa idea:

Calcola l'angolo trascorso dal percorso per marker1, marker2 e marker3 se ogni angolo è maggiore di 360 se la prossimità di ogni marker è stata attraversata dal cursore del mouse, quindi il cerchio creato circonda l'area contrassegnata dai marker endif endif

Step 7: Demo per l'idea

Qui, stiamo usando tre punti per rappresentare un triangolo.

Prova a girare intorno:

  • un punto
  • due punti
  • tre punti

... nell'immagine qui sotto. Prendi nota che il gesto ha esito positivo solo se contiene tutti e tre i punti.

Controlliamo il codice per questa demo. Ho evidenziato le linee chiave per l'idea di seguito; lo script completo è in Demo4.as.

funzione privata 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, aggiornamento); // controlla se le condizioni sono state soddisfatte condition1 = true // tutti gli angoli si incontrano MIN_ANGLE condition2 = true // tutte le distanze incontrano MIN_DIST per (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(); 

Step 8: Disegnare i cerchi

Il metodo migliore per disegnare effettivamente la linea che tracciamo dipenderà dalla tua piattaforma di sviluppo, quindi descriverò il metodo che useremo in Flash qui.

Ci sono due modi per disegnare linee in AS3, come indicato dall'immagine sopra.

Il primo approccio è piuttosto semplice: usare moveTo () per spostare la posizione del disegno da coordinare (10, 20). Quindi tracciare una linea per connettere (10, 20) a (80, 70) usando lineTo ().

Il secondo approccio consiste nel memorizzare tutti i dettagli in due matrici, comandi [] e coords [] (con coordinate memorizzate in coppie (x, y) all'interno coords []) e successivamente disegnare tutti i dettagli grafici sulla tela utilizzando drawPath () in un solo colpo. Ho optato per il secondo approccio nella mia demo.

Dai un'occhiata: prova a fare clic e trascina il mouse su una tela per tracciare una linea.

Ed ecco il codice AS3 per questa demo. Guarda l'origine completa in Drawing1.as.

public class Drawing1 estende Sprite private var cmd: Vector.; private var coords: Vector.; private var _thickness: Number = 2, _col: Number = 0, _alpha: Number = 1; public function Drawing1 () // assegna handlerst di eventi al mouse e al mouse down stage.addEventListener (MouseEvent.MOUSE_DOWN, mouseHandler); stage.addEventListener (MouseEvent.MOUSE_UP, mouseHandler);  / ** * Gestore di eventi del mouse * @param e mouse event * / funzione privata mouseHandler (e: MouseEvent): void if (e.type == "mouseDown") // randomizza le proprietà della linea _thickness = Math.random () * 5; _col = Math.random () * 0xffffff; _alpha = Math.random () * 0.5 + 0.5 // avvia le variabili cmd = new Vector.; coords = new Vector.; // prima registrazione della riga che inizia cmd [0] = 1; coords [0] = mouseX; coords [1] = mouseY; // avvia il disegno quando il mouse sposta stage.addEventListener (MouseEvent.MOUSE_MOVE, mouseHandler);  else if (e.type == "mouseUp") // rimuove il gestore di spostamento del mouse quando viene rilasciato il pulsante del mouse stage.removeEventListener (MouseEvent.MOUSE_MOVE, mouseHandler);  else if (e.type == "mouseMove") // spingendo nel mouse sposta cmd.push (2); // disegna comando coords.push (mouseX); // coordinate per disegnare la linea su coords.push (mouseY); ridisegnare (); // esegue il comando di disegno / ** * Metodo per disegnare la / e linea / e come definito dal movimento del mouse * / private function redraw (): void graphics.clear (); // cancella tutto il disegno precedente graphics.lineStyle (_thickness, _col, _alpha); graphics.drawPath (cmd, coords); 

In Flash, usando il grafica oggetto per disegnare come questo rendering in modalità conservata, il che significa che le proprietà delle singole righe sono memorizzate separatamente - al contrario di rendering in modalità immediata, dove viene archiviata solo l'immagine finale. (Gli stessi concetti esistono in altre piattaforme di sviluppo: ad esempio, in HTML5, il disegno in SVG utilizza la modalità conservata, mentre il disegno su tela utilizza la modalità immediata.)

Se ci sono molte righe sullo schermo, quindi memorizzarle e renderle nuovamente tutte separatamente potrebbe rendere il gioco lento e lento. La soluzione a questo dipenderà dalla tua piattaforma: in Flash, puoi utilizzare BitmapData.draw () per memorizzare ogni riga in una bitmap singola dopo che è stata disegnata.


Passaggio 9: Livello di esempio

Qui ho creato una demo per il livello di esempio di un gioco Spot the Difference. Controlla! La fonte completa è in Sample2.as del download sorgente.

Conclusione

Grazie per aver letto questo articolo; Spero ti abbia dato un'idea per costruire il tuo gioco. Lascia alcuni commenti in caso di problemi con il codice e ti risponderemo il prima possibile.