Hvordan oppdage når et objekt har blitt omkranset av en gest

Du er aldri for gammel til et spill av Spot the Difference-jeg husker å spille det som et barn, og jeg finner nå at min kone fortsatt spiller det noen ganger! I denne opplæringen ser vi på hvordan man oppdager når en ring er trukket rundt et objekt, med en algoritme som kan brukes med mus, pekepennen eller berøringsskjerminngang.

Merk: Selv om demoene og kildekoden til denne opplæringen bruker Flash og AS3, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø.


Endelig resultatforhåndsvisning

La oss se på det endelige resultatet vi skal jobbe for. Skjermen er delt inn i to bilder, som er nesten identiske, men ikke helt. Prøv å oppdage de seks forskjellene, og sirkel dem på venstre bilde. Lykke til!

Merk: Du trenger ikke å tegne en perfekt sirkel! Du trenger bare å tegne en grov ring eller sløyfe rundt hver forskjell.

Har du ikke Flash? Sjekk ut denne videodemoen:


Trinn 1: The Circling Motion

Vi bruker noen vektorberegninger i algoritmen. Som alltid er det godt å forstå den underliggende matematikken før du bruker den, så her er en kort oppdatering av vektormatematikk.

Bildet over viser vektoren EN brutt ned til sine horisontale og vertikale komponenter (Øks og Ay, henholdsvis).

La oss nå se på prikkprodukt operasjon, illustrert i bildet nedenfor. Først vil du se punktproduktoperasjonen mellom vektorer A og B.

For å finne vinkelen sandwichet mellom de to vektorene, kan vi gjøre bruk av dette punktproduktet.

| A | og | B | betegner størrelsene av vektorer A og B, så gitt | A | og | B | og en prikk B, hva som er igjen ukjent er theta. Med litt algebra (vist på bildet), er den endelige ligningen produsert, som vi kan bruke til å finne theta.

For mer informasjon om vektorpunktsprodukt, se følgende Wolfram-side.

Den andre nyttige operasjonen er kryss produkt. Sjekk ut operasjonen nedenfor:

Denne operasjonen er nyttig for å finne ut om den smeltevinkelen er med klokken eller mot klokken i forhold til en bestemt vektor.

La meg utdype videre. For tilfelle av diagrammet ovenfor er rotasjon fra A til B med klokken, så Et kryss B er negativt. Rotasjon av B til A er mot klokka, så B kryss A er positiv. Merk at denne operasjonen er sekvensfølsom. Et kryss B vil produsere forskjellig resultat fra B kryss A.

Det er ikke alt. Det skjer at y-aksen er i invertert i mange spillutviklingsplattformers koordinaterom (y øker når vi beveger oss nedover). Så vår analyse er reversert, og et kryss B vil være positivt mens B kryss A er negativt.

Det er nok revisjon. La oss komme til vår algoritme.


Trinn 2: Circling Interaction

Spillerne må sirkle riktig detalj på bildet. Nå hvordan gjør vi det? Før du svarer på dette spørsmålet, bør vi beregne vinkelen mellom to vektorer. Som du vil huske, kan vi bruke prikkproduktet for dette, så vi skal implementere denne ligningen her.

Her er en demonstrasjon for å illustrere hva vi gjør. Dra enten en pil rundt for å se tilbakemeldingen.

La oss se hvordan dette virker. I koden nedenfor har jeg ganske enkelt initialisert vektorer og en timer, og sett noen interaktive piler på skjermen.

offentlig funksjon Demo1 () tilbakemelding = nytt TextField; addChild (feedback); feedback.selectable = false; feedback.autoSize = TextFieldAutoSize.LEFT; a1 = ny pil; addChild (a1); a2 = ny pil; addChild (a2); a2.rotation = 90 center = nytt punkt (scene.stagewidth >> 1, scene.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 = ny ColorTransform (0, 0, 0, 1, 255); a2.transform.colorTransform = ny 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 = ny Vector2d (1, 0); v2 = ny Vector2d (0, 1); curr_vec = ny Vector2d (1, 0); t = ny timer (50); 

Hver 50 millisekunder kjøres funksjonen under, og brukes til å oppdatere grafisk og tekst tilbakemelding:

privat funksjon oppdatering (e: TimerEvent): void var curr_angle: Number = Math.atan2 (mouseY - center.y, mouseX - center.x); curr_vec.angle = curr_angle; hvis (element == 1) // oppdater pilens rotasjon visuelt a1.rotation = Math2.degreeOf (curr_angle); // måle vinkelen fra a1 til b1 v1 = curr_vec.clone (); retning = v2.crossProdukt (v1); feedback.text = "Du flytter nå den røde vektoren, A \ n"; feedback.appendText ("Vinkel målt fra grønt til rødt:");  annet hvis (element == 2) a2.rotation = Math2.degreeOf (curr_angle); v2 = curr_vec.clone (); retning = v1.crossProdukt (v2); feedback.text = "Du flytter nå den grønne vektoren, B \ n"; feedback.appendText ("Vinkel målt fra rød til grønn:");  theta_rad = Math.acos (v1.dotProdukt (v2)); // theta er i radianer theta_deg = Math2.degreeOf (theta_rad); hvis (retning < 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(); 

Du vil legge merke til at størrelsen for v1 og v2 er begge 1 enhet i dette scenariet (sjekk ut linjene 52 og 53 fremhevet ovenfor), så jeg hoppet over behovet for å beregne vektorens størrelsesorden for nå.

Hvis du vil se full kildekode, sjekk ut Demo1.as i kilden nedlasting.


Trinn 3: Oppdag en full sirkel

Ok, nå som vi har forstått grunnleggende ideen, bruker vi den nå for å sjekke om spilleren sirkulerer et poeng.

Jeg håper diagrammet snakker for seg selv. Samspillets start er når museknappen trykkes, og slutten av samspillet er når museknappen slippes.

Ved hvert intervall (for eksempel 0,01 sekunder) i løpet av samspillet, beregner vi vinkelen som er sandwichet mellom nåværende og tidligere vektorer. Disse vektorene er konstruert fra markørplasseringen (hvor forskjellen er) til musens plassering ved det aktuelle tilfellet. Legg opp alle disse vinklene (t1, t2, t3 i dette tilfellet) og hvis vinkelen er 360 grader ved slutten av samspillet, har spilleren trukket en sirkel.

Selvfølgelig kan du finjustere definisjonen av en full sirkel til 300-340 grader, noe som gir plass til spillerfeil når du utfører musebevegelse.

Her er en demonstrasjon for denne ideen. Dra en sirkelbevegelse rundt den røde markøren i midten. Du kan flytte den røde markørens posisjon med W, A, S, D taster.


Trinn 4: Implementeringen

La oss undersøke implementeringen av demoen. Vi ser bare på de viktige beregningene her.

Sjekk ut den uthevede koden nedenfor og sammenlign den med matematisk ligning i trinn 1. Du vil legge merke til at verdien for arccos noen ganger produserer Ikke et nummer (NaN) hvis du hopper over linje 92. Også, constants_value noen ganger overstiger 1 på grunn av avrundingsfeil, slik at vi må manuelt bringe det tilbake til maksimalt 1. Eventuelt inntaksnummer for arccos mer enn 1 vil produsere en NaN.

privat funksjon oppdatering (e: TimerEvent): void graphics.clear (); graphics.lineStyle (1) graphics.moveTo (markør.x, markør.y); graphics.lineTo (mouseX, mouseY); prev_vec = curr_vec.clone (); curr_vec = ny Vector2d (mouseX - markør.x, mouseY - markør.y); // verdien av beregningen overstiger en gang manuelt å håndtere precisjonen var constants_value: Number = Math.min (1, prev_vec.dotProduct (curr_vec) / (prev_vec.magnitude * curr_vec.magnitude)); var delta_angle: Nummer = Math.acos (constants_value) // vinkellaget varetning: Nummer = prev_vec.crossProdukt (curr_vec)> 0? 1: -1; // kontroller rotasjonsretningen total_angle + = retning * delta_angle; // legg til den kumulative vinkelen som ble gjort under samspillet

Den fullstendige kilden til dette finnes i Demo2.as


Trinn 5: Feil

Et problem du kan se er at så lenge jeg tegner en stor sirkel som omgir lerretet, vil markøren bli vurdert som omkranset. Jeg trenger ikke nødvendigvis å vite hvor markøren er.

Vel, for å motvirke dette problemet, kan vi sjekke nærheten til den sirkulære bevegelsen. Hvis sirkelen er trukket innenfor rammen av et bestemt område (verdien av denne er under din kontroll), er det bare da det regnes som en suksess.

Sjekk koden nedenfor. Om noensinne overgår brukeren MIN_DIST (med en verdi på 60 i dette tilfellet), så regnes det som en tilfeldig gjetning.

privat funksjon oppdatering (e: TimerEvent): void graphics.clear (); graphics.lineStyle (1) graphics.moveTo (markør.x, markør.y); graphics.lineTo (mouseX, mouseY); prev_vec = curr_vec.clone (); curr_vec = ny Vector2d (mouseX - markør.x, mouseY - markør.y); hvis (curr_vec.magnitude> MIN_DIST) within_bound = false; // verdien av beregningen overstiger en gang manuelt å håndtere precisjonen var constants_value: Number = Math.min (1, prev_vec.dotProduct (curr_vec) / (prev_vec.magnitude * curr_vec.magnitude)); var delta_angle: Nummer = Math.acos (constants_value) // vinkellaget varetning: Nummer = prev_vec.crossProdukt (curr_vec)> 0? 1: -1; // kontroller rotasjonsretningen total_angle + = retning * delta_angle; // legg til den kumulative vinkelen som ble gjort under samspillet mag_box.text = "Avstand fra markør:" + curr_vec.magnitude.toPrecision (4); mag_box.x = mouseX + 10; mag_box.y = mouseY + 10; feedback.text = "Ikke gå utover" + MIN_DIST

Igjen, prøv å sirkel markøren. Hvis du tenker på MIN_DIST er litt uforgivende, det kan alltid justeres for å passe på bildet.


Trinn 6: Forskjellige former

Hva om "forskjellen" ikke er en eksakt sirkel? Noen kan være rektangulære, eller trekantede eller andre former.
I disse tilfellene, i stedet for å bruke bare en markør, kan vi sette opp noen få:

I diagrammet ovenfor er to musepekere vist øverst. Fra den høyeste markøren starter vi en sirkulær retning med klokken til den andre enden til venstre. Merk at banen omkranser alle tre markørene.

Jeg har også trukket vinklene som er gått av denne banen på hver av markørene (lyse bindestreker til mørke bindestreker). Hvis alle tre vinklene er mer enn 360 grader (eller hvilken verdi du velger), så regner vi det som en sirkel.

Men det er ikke nok. Husk feilen i trinn 4? Vel, det samme gjelder her: Vi må sjekke for nærhet. I stedet for å kreve at gesten ikke overskrider en bestemt radius av en bestemt markør, kontrollerer vi bare om musemarkøren kom i nærheten av alle markørene i minst en kort stund. Jeg bruker pseudokoden for å forklare denne ideen:

Beregn vinkel forløpt med sti for markør1, markør2 og markør3 hvis hver vinkel er mer enn 360 hvis hver markør nærhet ble krysset med musepekeren, så er sirkelen laget rundt området merket med markører endif endif

Trinn 7: Demo for ideen

Her bruker vi tre prikker for å representere en trekant.

Prøv å sirkle rundt:

  • en prikk
  • to punkter
  • tre prikker

... i bildet nedenfor. Legg merke til at gesturen bare lykkes hvis den inneholder alle tre punktene.

La oss sjekke koden for denne demoen. Jeg har fremhevet nøkkellinjene for ideen nedenfor; hele skriptet er inne Demo4.as.

privat funksjon handleMouse (e: MouseEvent): void if (e.type == "mouseDown") t.addEventListener (TimerEvent.TIMER, update); t.start (); update_curr_vecs ();  annet hvis (e.type == "mouseUp") t.stop (); t.removeEventListener (TimerEvent.TIMER, update); // sjekk om betingelsene ble oppfylt condition1 = true // alle vinkler møtes MIN_ANGLE condition2 = true // alle nærhetene møtes MIN_DIST for (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(); 

Trinn 8: Tegne sirkler

Den beste metoden for å tegne linjen som du sporer, vil avhenge av utviklingsplattformen din, så jeg skal bare skissere metoden vi vil bruke i Flash her.

Det er to måter å tegne linjer i AS3, som angitt av bildet ovenfor.

Den første tilnærmingen er ganske enkel: bruk flytte til() å flytte tegneposisjonen til å koordinere (10, 20). Deretter tegner du en linje for å koble til (10, 20) til (80, 70) ved hjelp av lineTo ().

Den andre tilnærmingen er å lagre alle detaljer i to arrays, kommandoer [] og coords [] (med koordinater lagret i (x, y) par innenfor coords []) og senere tegne alle grafiske detaljer på lerret ved bruk av drawPath () i ett enkelt skudd. Jeg har valgt den andre tilnærmingen i min demo.

Sjekk det ut: Prøv å klikke og dra musen på lerret for å tegne linje.

Og her er AS3-koden for denne demoen. Sjekk ut hele kilden i Drawing1.as.

offentlig klasse Tegning1 strekker seg Sprite private var cmd: Vector.; private var koordinater: Vector.; privat var _thickness: Nummer = 2, _col: Nummer = 0, _alpha: Nummer = 1; offentlig funksjon Tegning1 () // tilordne hendelsen mest håndtert til musen opp og musen ned stage.addEventListener (MouseEvent.MOUSE_DOWN, mouseHandler); stage.addEventListener (MouseEvent.MOUSE_UP, mouseHandler);  / ** * Hendelse for hendelser i musen * @param e mouse event * / privatfunksjon mouseHandler (e: MouseEvent): void if (e.type == "mouseDown") // slår linjeegenskapene _thickness = Math.random () * 5; _col = Math.random () * 0xffffff; _alpha = Math.random () * 0.5 + 0.5 // initier variablene cmd = ny vektor.; koordinat = ny vektor.; // første registrering av linje begynner cmd [0] = 1; koordinater [0] = mouseX; koordinater [1] = mouseY; // start tegningen når musen beveger stadium.addEventListener (MouseEvent.MOUSE_MOVE, mouseHandler);  ellers hvis (e.type == "mouseUp") // fjern musen flytte handler når museknappen er utgitt stage.removeEventListener (MouseEvent.MOUSE_MOVE, mouseHandler);  ellers hvis (e.type == "mouseMove") // å skyve inn i musen flytter cmd.push (2); // tegne kommando coords.push (mouseX); // koordinater for å tegne linje til coords.push (mouseY); tegne (); // Kjør tegningskommandoen / ** * Metode for å tegne linjen / linjene som definert av musebevegelsen * / Private funksjonen redraw (): void graphics.clear (); // clearing alle tidligere tegning graphics.lineStyle (_thickness, _col, _alpha); grafikk.drawPath (cmd, koordinater); 

I Flash bruker du grafikk gjenstand for tegning som denne bruken beholdt modus gjengivelse, noe som betyr at egenskapene til de enkelte linjene lagres separat - i motsetning til umiddelbar modus gjengivelse, hvor bare det endelige bildet er lagret. (De samme konseptene finnes i andre utviklingsplattformer, for eksempel i HTML5, tegning til SVG bruker beholdt modus, mens tegning til lerret bruker umiddelbar modus.)

Hvis det er mange linjer på skjermen, kan lagring og gjenoppretting av dem alle separat, gjøre spillet sakte og laggy. Løsningen på dette vil avhenge av plattformen din - i Flash kan du bruke BitmapData.draw () for å lagre hver linje i en enkelt bitmap etter at den er tegnet.


Trinn 9: Eksempelnivå

Her har jeg laget en demo for prøvenivået til et Spot the Difference-spill. Sjekk det ut! Den fulle kilden er i Sample2.as av kilde nedlasting.

Konklusjon

Takk for at du lest denne artikkelen; Jeg håper det ga deg en ide for å bygge ditt eget spill. Gi noen kommentarer hvis det er noe problem med koden, og jeg kommer tilbake til deg så snart som mulig.