Hoe te detecteren wanneer een object is omcirkeld door een gebaar

Je bent nooit te oud voor een spelletje Spot the Difference - ik herinner me het als een kind te spelen en ik merk nu dat mijn vrouw het nog steeds af en toe speelt! In deze zelfstudie zullen we kijken hoe we kunnen detecteren wanneer een ring rond een object is getrokken, met een algoritme dat kan worden gebruikt met muis, pen of touchscreen.

Notitie: Hoewel de demo's en broncode van deze tutorial Flash en AS3 gebruiken, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken.


Eindresultaat voorbeeld

Laten we eens kijken naar het uiteindelijke resultaat waar we naartoe zullen werken. Het scherm is verdeeld in twee afbeeldingen, die bijna identiek maar niet helemaal zijn. Probeer de zes verschillen te herkennen en omcirkel die op de linker afbeelding. Succes!

Opmerking: je hoeft geen perfecte cirkel te tekenen! Je hoeft alleen maar een ruwe ring of lus rond elk verschil te tekenen.

Heb je geen flits? Bekijk deze videodemo:


Stap 1: De rondgaande beweging

We zullen enkele vectorberekeningen in het algoritme gebruiken. Zoals altijd is het goed om de onderliggende wiskunde te begrijpen voordat je het toepast, dus hier is een korte opfriscursus van vector wiskunde.

De afbeelding hierboven toont de vector EEN uitgesplitst naar horizontale en verticale componenten (Bijl en ja, respectievelijk).

Laten we nu kijken naar de punt product werking, geïllustreerd in de afbeelding hieronder. Eerst ziet u de bewerking van het puntproduct tussen de vectoren A en B.

Om de hoek tussen de twee vectoren te vinden, kunnen we gebruik maken van dit puntproduct.

| A | en | B | geven de magnitudes van vectoren A en B aan, dus gegeven | A | en | B | en een punt B, wat theta onbekend blijft, is onbekend. Met een beetje algebra (weergegeven in de afbeelding) wordt de laatste vergelijking geproduceerd, die we kunnen gebruiken om theta te vinden.

Raadpleeg de volgende Wolfram-pagina voor meer informatie over vectorpixels.

De andere nuttige bewerking is kruisproduct. Bekijk de onderstaande bewerking:

Deze bewerking is handig om te bepalen of de ingeklemd hoek met de klok mee of tegen de klok in is ten opzichte van een specifieke vector.

Ik zal het verder uitwerken. In het bovenstaande diagram is rotatie van A naar B met de klok mee, dus A-kruis B is negatief. Rotatie van B naar A is tegen de klok in, dus B-kruising A is positief. Merk op dat deze bewerking sequentiegevoelig is. Een kruising B zal een ander resultaat opleveren dan B-kruising A..

Dat is niet alles. Het komt voor dat in veel coördinatiereeksen van ontwikkelplatforms de y-as wordt omgekeerd (y neemt toe naarmate we naar beneden gaan). Dus onze analyse is omgekeerd en A kruis B zal positief zijn terwijl B kruis A negatief is.

Dat is genoeg revisie. Laten we naar ons algoritme gaan.


Stap 2: Circuit interactie

Spelers moeten het juiste detail omcirkelen in de afbeelding. Hoe doen we dat? Voordat we deze vraag beantwoorden, moeten we de hoek tussen twee vectoren berekenen. Zoals u zich nu herinnert, kunnen we het puntproduct hiervoor gebruiken, dus we zullen die vergelijking hier implementeren.

Hier is een demo om te illustreren wat we doen. Sleep een van de pijlen rond om de feedback te zien.

Laten we kijken hoe dit werkt. In de onderstaande code heb ik eenvoudig de vectoren en een timer geïnitialiseerd en een aantal interactieve pijlen op het scherm geplaatst.

openbare functie Demo1 () feedback = new TextField; addChild (feedback); feedback.selectable = false; feedback.autoSize = TextFieldAutoSize.LEFT; a1 = nieuwe pijl; addChild (a1); a2 = nieuwe pijl; 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 = nieuwe Vector2d (1, 0); v2 = nieuwe Vector2d (0, 1); curr_vec = new Vector2d (1, 0); t = nieuwe timer (50); 

Elke 50 milliseconden wordt de onderstaande functie uitgevoerd en gebruikt om de grafische en tekstfeedback bij te werken:

persoonlijke functie-update (e: TimerEvent): void var curr_angle: Number = Math.atan2 (mouseY - center.y, mouseX - center.x); curr_vec.angle = curr_angle; if (item == 1) // de rotatie van de update visueel bijwerken a1.rotation = Math2.degreeOf (curr_angle); // de hoek meten van a1 tot b1 v1 = curr_vec.clone (); richting = v2.crossProduct (v1); feedback.text = "U verplaatst nu de rode vector, A \ n"; feedback.appendText ("Hoek gemeten van groen naar rood:");  else if (item == 2) a2.rotation = Math2.degreeOf (curr_angle); v2 = curr_vec.clone (); richting = v1.crossProduct (v2); feedback.text = "U verplaatst nu de groene vector, B \ n"; feedback.appendText ("Hoek gemeten van rood naar groen:");  theta_rad = Math.acos (v1.dotProduct (v2)); // theta is in radialen theta_deg = Math2.degreeOf (theta_rad); als (richting < 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(); 

U zult opmerken dat de magnitude voor v1 en v2 in dit scenario beide 1 eenheid zijn (bekijk lijn 52 en 53 hierboven gemarkeerd), dus ik sloeg de noodzaak over om de grootte van de vectoren nu te berekenen.

Als je de volledige broncode wilt zien, kijk dan eens Demo1.as in de brondownload.


Stap 3: Detecteer een volledige cirkel

Ok, nu we het basisidee hebben begrepen, zullen we het nu gebruiken om te controleren of de speler met succes een punt omcirkelde.

Ik hoop dat het diagram voor zichzelf spreekt. Het begin van de interactie is wanneer de muisknop wordt ingedrukt en het einde van de interactie is wanneer de muisknop wordt losgelaten.

Bij elk interval (van bijvoorbeeld 0,01 seconde) tijdens de interactie, berekenen we de hoek ingeklemd tussen huidige en vorige vectoren. Deze vectoren worden geconstrueerd vanaf de markeringslocatie (waar het verschil is) naar de muislocatie bij die instantie. Tel hierbij alle hoeken op (t1, t2, t3) en als de gemaakte hoek 360 graden is aan het einde van de interactie, dan heeft de speler een cirkel getekend.

Natuurlijk kun je de definitie van een volledige cirkel aanpassen tot 300-340 graden, waardoor er ruimte is voor spelerfouten bij het uitvoeren van muisbewegingen.

Hier is een demo voor dit idee. Sleep een cirkelvormig gebaar rond de rode markering in het midden. U kunt de positie van de rode markering verplaatsen met de toetsen W, A, S, D.


Stap 4: De implementatie

Laten we de implementatie voor de demo bekijken. We zullen hier alleen de belangrijke berekeningen bekijken.

Bekijk de gemarkeerde code hieronder en vergelijk deze met de wiskundige vergelijking in stap 1. Je zult merken dat de waarde voor arccos soms produceert Geen nummer (NaN) als u regel 92 overslaat, constants_value soms overschrijdt het 1 vanwege afrondingsonnauwkeurigheden, dus moeten we het handmatig terugbrengen naar een maximum van 1. Elk ingangsnummer voor arco's van meer dan 1 levert een NaN op.

persoonlijke functie-update (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); // de waarde van de berekening overschrijdt soms 1 behoefte om handmatig de 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) // angle made var direction: Number = prev_vec.crossProduct (curr_vec)> 0? 1: -1; // controle van de draairichting total_angle + = richting * delta_angle; // toevoegen aan de cumulatieve hoek gemaakt tijdens de interactie

De volledige bron hiervoor is te vinden in Demo2.as


Stap 5: De fout

Een probleem dat je misschien zult zien, is dat zolang ik een grote cirkel teken die het canvas omsluit, de marker als omcirkeld wordt beschouwd. Ik hoef niet echt te weten waar de marker is.

Welnu, om dit probleem tegen te gaan, kunnen we de nabijheid van de cirkelvormige beweging controleren. Als de cirkel binnen de grenzen van een bepaald bereik wordt getrokken (waarvan de waarde onder uw controle is), alleen dan wordt het als een succes beschouwd.

Bekijk de onderstaande code. Als ooit de gebruiker overschrijdt MIN_DIST (in dit geval een waarde van 60), dan wordt het beschouwd als een willekeurige schatting.

persoonlijke functie-update (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; // de waarde van de berekening overschrijdt soms 1 behoefte om handmatig de 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) // angle made var direction: Number = prev_vec.crossProduct (curr_vec)> 0? 1: -1; // controle van de draairichting total_angle + = richting * delta_angle; // toevoegen aan de cumulatieve hoek gemaakt tijdens de interactie mag_box.text = "Afstand van markering:" + curr_vec.magnitude.toPrecision (4); mag_box.x = mouseX + 10; mag_box.y = mouseY + 10; feedback.text = "Ga niet verder dan" + MIN_DIST

Nogmaals, probeer de marker te omcirkelen. Als je denkt dat het MIN_DIST is een beetje meedogenloos, het kan altijd worden aangepast aan de afbeelding.


Stap 6: Verschillende vormen

Wat als het "verschil" geen exacte cirkel is? Sommige kunnen rechthoekig of driehoekig zijn of een andere vorm hebben.
In deze gevallen kunnen we, in plaats van slechts één markering te gebruiken, er een paar plaatsen:

In het bovenstaande diagram worden bovenaan twee muisaanwijzers weergegeven. Te beginnen met de meest rechtse cursor, maken we een cirkelvormige beweging met de klok mee naar het andere uiteinde aan de linkerkant. Merk op dat het pad alle drie markeringen omcirkelt.

Ik heb ook de hoeken getekend die zijn verstreken door dit pad op elk van de markeringen (lichte streepjes naar donkere streepjes). Als alle drie de hoeken meer dan 360 graden zijn (of welke waarde u ook kiest), tellen we het alleen als een cirkel.

Maar dat is niet voldoende. Denk aan de fout in stap 4? Welnu, hetzelfde geldt hier: we moeten controleren of we in de buurt zijn. In plaats van te vereisen dat het gebaar een bepaalde straal van een specifieke markering niet overschrijdt, controleren we alleen of de muiscursor althans een korte instantie dicht bij alle markeringen kwam. Ik zal pseudo-code gebruiken om dit idee uit te leggen:

Bereken de hoek die door het pad is verstreken voor marker1, marker2 en marker3 als elke hoek meer dan 360 is als de nabijheid van de marker werd overschreden door de muiscursor en de gemaakte cirkel het gebied omringt dat is gemarkeerd met markeringen endif endif

Stap 7: demo voor het idee

Hier gebruiken we drie punten om een ​​driehoek weer te geven.

Probeer rond te cirkelen:

  • een punt
  • twee punten
  • drie punten

... in de onderstaande afbeelding. Houd er rekening mee dat het gebaar alleen slaagt als het alle drie de stippen bevat.

Laten we eens kijken naar de code voor deze demo. Ik heb de belangrijkste lijnen voor het onderstaande idee belicht; het volledige script is binnen Demo4.as.

private function 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); // controleer of aan voorwaarden is voldaan condition1 = true // alle hoeken voldoen aan MIN_ANGLE condition2 = true // alle proximiteiten komen overeen met MIN_DIST voor (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(); 

Stap 8: De cirkels tekenen

De beste methode om de regel die u volgt daadwerkelijk te tekenen, is afhankelijk van uw ontwikkelplatform, dus ik beschrijf alleen de methode die we hier in Flash zouden gebruiken.

Er zijn twee manieren om lijnen in AS3 te tekenen, zoals aangegeven door de afbeelding hierboven.

De eerste benadering is vrij simpel: gebruik moveTo () om de tekenpositie naar coördinaat te verplaatsen (10, 20). Teken vervolgens een lijn om verbinding te maken (10, 20) tot (80, 70) met lineTo ().

De tweede benadering is om alle details in twee arrays op te slaan, commando's [] en coords [] (met coördinaten opgeslagen in (x, y) paren binnen coords []) en later alle grafische details op canvas tekenen met drawPath () in een enkele opname. Ik heb gekozen voor de tweede benadering in mijn demo.

Bekijk het: probeer de muis op het canvas te klikken en te slepen om de lijn te tekenen.

En hier is de AS3-code voor deze demo. Bekijk de volledige bron in Drawing1.as.

public class Drawing1 breidt Sprite uit private var cmd: Vector.; private var coords: Vector.; private var _thickness: Number = 2, _col: Number = 0, _alpha: Number = 1; openbare functie Drawing1 () // wijst eventhandlerst toe aan muis omhoog en muis omlaag stage.addEventListener (MouseEvent.MOUSE_DOWN, mouseHandler); stage.addEventListener (MouseEvent.MOUSE_UP, mouseHandler);  / ** * Muisgebeurtenishandler * @param e mouse -gebeurtenis * / persoonlijke functie mouseHandler (e: MouseEvent): void if (e.type == "mouseDown") // randomiseer de lijneigenschappen _thickness = Math.random () * 5; _col = Math.random () * 0xffffff; _alpha = Math.random () * 0.5 + 0.5 // start de variabelen cmd = new Vector.; coords = nieuwe Vector.; // eerste registratie van het begin van de regel cmd [0] = 1; coords [0] = mouseX; coords [1] = mouseY; // start de tekening wanneer de muis move stage.addEventListener (MouseEvent.MOUSE_MOVE, mouseHandler);  else if (e.type == "mouseUp") // verwijder de muisverplaatsingshandler zodra de muisknop wordt losgelaten stage.removeEventListener (MouseEvent.MOUSE_MOVE, mouseHandler);  else if (e.type == "mouseMove") // duw in de muis verplaats de cmd.push (2); // tekenopdracht coords.push (mouseX); // coördinaten om een ​​lijn te tekenen naar coords.push (mouseY); terugtrekken(); // voer het tekenopdracht uit / ** * Methode om de lijn (en) te tekenen zoals gedefinieerd door muisbeweging * / private function redraw (): void graphics.clear (); // wissen van alle voorgaande tekening graphics.lineStyle (_dikte, _col, _alpha); graphics.drawPath (cmd, coords); 

In Flash gebruikt u de grafiek object voor tekenen zoals dit gebruikt bewaarde modus rendering, wat betekent dat de eigenschappen van de individuele regels afzonderlijk worden opgeslagen - in tegenstelling tot onmiddellijke weergave van de modus, waar alleen de uiteindelijke afbeelding wordt opgeslagen. (Dezelfde concepten bestaan ​​ook in andere ontwikkelingsplatforms, bijvoorbeeld in HTML5 maakt tekenen naar SVG gebruik van de behouden modus, terwijl het tekenen naar canvas de directe modus gebruikt.)

Als er veel lijnen op het scherm staan, kan het langzaam en laggy worden als u ze allemaal apart opslaat en opnieuw genereert. De oplossing hiervoor hangt af van uw platform - in Flash kunt u BitmapData.draw () gebruiken om elke regel in één bitmap op te slaan nadat deze is getekend.


Stap 9: Voorbeeldniveau

Hier heb ik een demo gemaakt voor het voorbeeldniveau van een Spot the Difference-game. Bekijken! De volledige bron is binnen Sample2.as van de brondownload.

Conclusie

Bedankt voor het lezen van dit artikel; Ik hoop dat het je een idee heeft gegeven voor het bouwen van je eigen spel. Laat enkele opmerkingen achter als er een probleem is met de code en ik neem zo snel mogelijk contact met u op.