Nu sunteți niciodată prea bătrân pentru un joc de "Spot the Difference" - Îmi amintesc că am jucat-o ca un copil și acum găsesc că soția mea o mai joacă ocazional! În acest tutorial vom examina cum să detectăm momentul în care un inel a fost desenat în jurul unui obiect, cu un algoritm care ar putea fi utilizat cu mouse-ul, stylus-ul sau touchscreen-ul.
Notă: Deși demonstrațiile și codul sursă al acestui tutorial utilizează Flash și AS3, ar trebui să puteți utiliza aceleași tehnici și concepte în aproape orice mediu de dezvoltare a jocului.
Să aruncăm o privire asupra rezultatului final la care vom lucra. Ecranul este împărțit în două imagini, care sunt aproape identice, dar nu destul. Încercați să observați cele șase diferențe și să rotiți cele din imaginea din stânga. Mult noroc!
Notă: Nu trebuie să desenați un cerc perfect! Trebuie doar să desenați un inel sau o buclă în jurul fiecărei diferențe.
Nu aveți Flash? Consultați această demonstrație video:
În algoritm vom folosi câteva calcule vectoriale. Ca întotdeauna, este bine să înțelegem matematica de bază înainte de ao aplica, deci iată o scurtă actualizare a matematicii vectoriale.
Imaginea de mai sus arată vectorul A defalcate pe componentele sale orizontale și verticale (Topor și Ay, respectiv).
Acum, să ne uităm la dot produs , ilustrat în imaginea de mai jos. În primul rând, veți vedea operațiunea punctului dintre vectorii A și B.
Pentru a găsi unghiul între cele două vectori, putem folosi acest produs punct.
| A | și | B | denotă magnitudinea vectorilor A și B, astfel dat | A | și | B | și un punct B, ceea ce a rămas necunoscut este theta. Cu o algebră mică (prezentată în imagine), ecuația finală este produsă, pe care o putem folosi pentru a găsi theta.
Pentru mai multe informații despre produsul vectorial, consultați următoarea pagină Wolfram.
Cealaltă operație utilă este produs încrucișat. Verificați operația de mai jos:
Această operație este utilă pentru a afla dacă unghiul de tip sandwich este în sens orar sau în sens invers acelor de ceasornic față de un vector specific.
Permiteți-mi să-mi explic mai departe. În cazul diagramei de mai sus, rotația de la A la B este în sensul acelor de ceasornic, deci A este negativă. Rotația lui B în A este în sens contrar acelor de ceasornic, deci B crucea A este pozitivă. Rețineți că această operațiune este sensibilă la secvențe. O cruce B va produce rezultate diferite de la B cross A.
Asta nu e tot. Se întâmplă că în multe spații de coordonate ale platformelor de dezvoltare a jocurilor, axa y este inversată (y crește pe măsură ce ne mișcăm în jos). Deci, analiza noastră este inversată și o cruce B va fi pozitivă în timp ce B crucea A este negativă.
Asta e suficientă revizuire. Să ajungem la algoritmul nostru.
Jucătorii vor trebui să cuprindă detaliile corecte ale imaginii. Acum cum facem asta? Înainte de a răspunde la această întrebare, trebuie să calculați unghiul dintre doi vectori. După cum vă veți aminti acum, putem folosi produsul dot pentru acest lucru, deci vom implementa această ecuație aici.
Iată un demo pentru a ilustra ceea ce facem. Glisați fie săgeata în jurul pentru a vedea feedbackul.
Să vedem cum funcționează acest lucru. În codul de mai jos, am inițializat pur și simplu vectorii și un cronometru, și am pus niște săgeți interactive pe ecran.
funcția publică Demo1 () feedback = new TextField; addChild (feedback); feedback.selectable = false; feedback.autoSize = TextFieldAutoSize.LEFT; a1 = Arrow nou; addChild (a1); a2 = Arrow nou; addChild (a2); a2.ro = 90 centru = punct nou (stadiul. stadiul Lățime >> 1, etapa.clasa înălțime >> 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 = noul ColorTransform (0, 0, 0, 1, 255); a2.transform.colorTransform = noul ColorTransform (0, 0, 0, 1, 0, 255); a1.addEventListener (MouseEvent.MOUSE_DOWN, handleMouse); a2.addEventListener (MouseEvent.MOUSE_DOWN, handleMouse); starea.addEventListener (MouseEvent.MOUSE_UP, handleMouse); v1 = Vector2d nou (1, 0); v2 = Vector2d nou (0, 1); curr_vec = nou Vector2d (1, 0); t = timer nou (50);
La fiecare 50 de milisecunde, funcția de mai jos este rulată și utilizată pentru a actualiza feedback-ul grafic și text:
actualizarea funcției private (e: TimerEvent): void var curr_angle: Număr = Math.atan2 (mouseY - center.y, mouseX - center.x); curr_vec.angle = curr_angle; dacă item == 1) // actualizează rotația săgeții a1.rotation = Math2.degreeOf (curr_angle); // măsurarea unghiului de la a1 la b1 v1 = curr_vec.clone (); direcție = v2.crossProduct (v1); feedback.text = "Acum mutați vectorul roșu, A \ n"; feedback.appendText ("Unghiul măsurat de la verde la roșu:"); altfel dacă (item == 2) a2.rotation = Math2.degreeOf (curr_angle); v2 = curr_vec.clone (); direcție = v1.crossProduct (v2); feedback.text = "Acum mișcați vectorul verde, B \ n"; feedback.appendText ("Unghiul măsurat de la roșu la verde:"); theta_rad = Math.acos (v1.dotProduct (v2)); // theta este în radiani theta_deg = Math2.degreeOf (theta_rad); dacă (direcția < 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();
Veți observa că magnitudinea v1 și v2 sunt ambele unități în acest scenariu (verificați liniile 52 și 53 evidențiate mai sus), așa că am depășit necesitatea de a calcula magnitudinea vectorilor pentru moment.
Dacă doriți să vedeți codul sursă complet, verificați Demo1.as
în descărcarea sursei.
Ok, acum că am înțeles ideea de bază, o vom folosi acum pentru a verifica dacă jucătorul a încercat să obțină un punct.
Sper că diagrama vorbește de la sine. Începerea interacțiunii este atunci când butonul mouse-ului este apăsat, iar sfârșitul interacțiunii este când butonul mouse-ului este eliberat.
La fiecare interval (de exemplu, 0,01 secunde) în timpul interacțiunii, vom calcula unghiul întins între vectorii curenți și cei anteriori. Aceste vectori sunt construite din locația markerului (unde este diferența) față de locația mouse-ului la acel exemplu. Adăugați toate aceste unghiuri (t1, t2, t3 în acest caz) și dacă unghiul făcut este la 360 de grade la sfârșitul interacțiunii, atunci jucătorul a desenat un cerc.
Desigur, puteți modifica definiția unui cerc complet de 300-340 de grade, oferind spațiu pentru erorile jucătorului atunci când efectuați gestul mouse-ului.
Iată un demo pentru această idee. Trageți un gest circular în jurul marcatorului roșu în mijloc. Puteți deplasa poziția marcatorului roșu folosind tastele W, A, S, D.
Să examinăm implementarea demo-ului. Vom analiza doar calculele importante aici.
Verificați codul evidențiat mai jos și potriviți-l cu ecuația matematică din Pasul 1. Veți observa că valoarea pentru arccos uneori produce Nu este un număr
(NaN) dacă renunțați la linia 92. De asemenea, constants_value
uneori depășește 1 datorită inexactităților de rotunjire, așa că trebuie să-l readusem manual la un maxim de 1. Orice număr de intrare pentru arccos mai mare de 1 va produce un NaN.
actualizarea funcției private (e: TimerEvent): void graphics.clear (); graphics.lineStyle (1) grafic.moveTo (marker.x, marker.y); graphics.lineTo (mouseX, mouseY); prev_vec = curr_vec.clone (); curr_vec = Vector2d nou (mouseX - marker.x, mouseY - marker.y); // Valoarea calculului uneori depășește 1 necesitatea de a manevra manual precizia var constants_value: Number = Math.min (1, prev_vec.dotProduct (curr_vec) / (prev_vec.magnitude * curr_vec.magnitude)); var delta_angle: Numărul = Math.acos (constants_value) // unghiul făcut direcția var: Număr = prev_vec.crossProduct (curr_vec)> 0? 1: -1; // verificarea direcției de rotație total_angle + = direcția * delta_angle; // adăugați unghiului cumulat efectuat în timpul interacțiunii
Sursa completă pentru acest lucru poate fi găsită în Demo2.as
O problemă pe care o puteți vedea este că, atâta timp cât desenez un cerc mare care închide pânza, marcatorul va fi considerat în formă de cerc. Nu trebuie să știu unde este markerul.
Păi, pentru a contracara această problemă, putem verifica apropierea mișcării circulare. Dacă cercul este tras în limitele unui anumit interval (a cărui valoare este sub controlul dvs.), numai atunci este considerat un succes.
Verificați codul de mai jos. Dacă vreodată utilizatorul depășește MIN_DIST
(cu o valoare de 60 în acest caz), atunci se consideră o estimare aleatorie.
actualizarea funcției private (e: TimerEvent): void graphics.clear (); graphics.lineStyle (1) grafic.moveTo (marker.x, marker.y); graphics.lineTo (mouseX, mouseY); prev_vec = curr_vec.clone (); curr_vec = Vector2d nou (mouseX - marker.x, mouseY - marker.y); dacă (curr_vec.magnitude> MIN_DIST) within_bound = false; // Valoarea calculului uneori depășește 1 necesitatea de a manevra manual precizia var constants_value: Number = Math.min (1, prev_vec.dotProduct (curr_vec) / (prev_vec.magnitude * curr_vec.magnitude)); var delta_angle: Numărul = Math.acos (constants_value) // unghiul făcut direcția var: Număr = prev_vec.crossProduct (curr_vec)> 0? 1: -1; // verificarea direcției de rotație total_angle + = direcția * delta_angle; // adăugați la unghiul cumulativ efectuat în timpul interacțiunii mag_box.text = "Distanță față de marcator:" + curr_vec.magnitude.toPrecision (4); mag_box.x = mouseX + 10; mag_box.y = mouseY + 10; feedback.text = "Nu depășiți" + MIN_DIST
Din nou, încercați să apropiați marcatorul. Dacă credeți că MIN_DIST
este puțin neiertător, poate fi întotdeauna adaptat pentru a se potrivi imaginii.
Ce se întâmplă dacă "diferența" nu este un cerc exact? Unele ar putea fi dreptunghiulare, triunghiulare sau orice altă formă.
În aceste cazuri, în loc să folosim doar un singur marcator, putem pune câteva:
În diagrama de mai sus, în partea de sus sunt afișate două cursoare de mouse. Începând cu cel mai bun cursor, vom face o mișcare circulară în sens orar spre celălalt capăt din stânga. Rețineți că traseul cuprinde toate cele trei marcatori.
De asemenea, am tras unghiurile scurs de această cale pe fiecare dintre markere (linii luminoase până la linii întunecate). Dacă toate cele trei unghiuri sunt mai mari de 360 de grade (sau oricare dintre valorile pe care le alegeți), numai atunci o număram ca un cerc.
Dar nu este suficient. Amintiți-vă de defectul de la pasul 4? Ei bine, același lucru se întâmplă aici: va trebui să verificăm apropierea. În loc să cerem ca gestul să nu depășească o anumită rază a unui marker specific, vom verifica dacă cursorul mouse-ului sa apropiat de toate marcatorii pentru cel puțin o scurtă instanță. Voi folosi pseudocod pentru a explica această idee:
Calculați unghiul scurs de calea pentru marker1, marker2 și marker3 dacă fiecare unghi este mai mare de 360 dacă proximitatea fiecărui marker a fost traversată de cursorul mouse-ului atunci cercul făcut este înconjurat de zona marcată de markeri endif endif
Aici, folosim trei puncte pentru a reprezenta un triunghi.
Încercați să faceți cerc:
... în imaginea de mai jos. Luați notă că gestul reușește doar dacă conține toate cele trei puncte.
Să verificăm codul pentru această demonstrație. Am subliniat liniile cheie pentru ideea de mai jos; scriptul complet este în Demo4.as
.
funcția privată handleMouse (e: MouseEvent): void if (e.type == "mouseDown") t.addEventListener (TimerEvent.TIMER, actualizare); t.start (); update_curr_vecs (); altfel dacă (e.type == "mouseUp") t.stop (); t.removeEventListener (TimerEvent.TIMER, actualizare); // verificați dacă condițiile au fost îndeplinite condiție1 = adevărată // toate unghiurile sunt întâlnite MIN_ANGLE condiție2 = adevărat // toate proximitățile sunt întâlnite MIN_DIST pentru (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();
Cea mai bună metodă pentru trasarea liniei pe care o urmăriți depinde de platforma dvs. de dezvoltare, așa că voi sublinia metoda pe care o vom folosi în Flash aici.
Există două moduri de a desena linii în AS3, după cum se arată în imaginea de mai sus.
Prima abordare este destul de simplă: folosiți MoveTo ()
pentru a deplasa poziția de desen pentru a coordona (10, 20). Apoi trageți o linie pentru a conecta (10, 20) la (80, 70) utilizând lineTo ()
.
A doua abordare este de a stoca toate detaliile în două tablouri, comenzi []
și coords []
(cu coordonatele stocate în perechi (x, y) în interiorul coords []
) și ulterior să desenați toate detaliile grafice pe panza folosind drawPath ()
într-o singură lovitură. Am optat pentru cea de-a doua abordare în demo-ul meu.
Verificați-l: încercați să faceți clic și să glisați mouse-ul pe panza pentru a desena linia.
Iată codul AS3 pentru această demonstrație. Verificați sursa completă în Drawing1.as
.
clasa publică Drawing1 extinde Sprite private var cmd: Vector.; private var coords: Vector. ; privat var _thickness: Number = 2, _col: Number = 0, _alpha: Number = 1; funcția publică Drawing1 () // atribuiți managerului evenimentului mouse-ului sus și mouse-ului în jos stage.addEventListener (MouseEvent.MOUSE_DOWN, mouseHandler); stage.addEventListener (MouseEvent.MOUSE_UP, mouseHandler); / ** * Mouse-ul evenimentului de manipulare * @ param e mouse-ul de eveniment * / funcția privată mouseHandler (e: MouseEvent): void if (e.type == "mouseDown") // randomizeaza proprietățile liniei _thickness = () * 5; _col = Math.random () * 0xffffff; _alpha = Math.random () * 0.5 + 0.5 // inițiază variabilele cmd = Vector nou. ; coords = Vector nou. ; // prima înregistrare a liniei de început cmd [0] = 1; coords [0] = mouseX; coords [1] = mouseY; // începe desenul când mutați mouse-ul stage.addEventListener (MouseEvent.MOUSE_MOVE, mouseHandler); altfel dacă (e.type == "mouseUp") // îndepărtați butonul de mutare a mouse-ului odată ce butonul mouse-ului este eliberat stage.removeEventListener (MouseEvent.MOUSE_MOVE, mouseHandler); altfel dacă (e.type == "mouseMove") // împingând mouse-ul mișcați cmd.push (2); // trageți comanda coords.push (mouseX); // coordonează pentru a tras linie la coords.push (mouseY); redraw (); // executați comanda desenului / ** * Metoda de a desena linia (liniile) așa cum este definită prin mișcarea mouse-ului * / redraw funcția privată (): void graphic.clear (); // ștergerea tuturor graficelor anterioare de tip drawing.lineStyle (_thickness, _col, _alpha); grafică (cale, cale);
În Flash, folosind grafică
obiect pentru desen ca aceasta folosește modul de redare retransmisie, ceea ce înseamnă că proprietățile liniilor individuale sunt stocate separat - spre deosebire de redarea modului imediat, unde este stocată doar imaginea finală. (Aceleași concepte există și în alte platforme de dezvoltare, de exemplu, în HTML5, desenul spre SVG folosește modul retratat, în timp ce desenul pentru panza folosește modul imediat.)
Dacă pe ecran sunt mai multe linii, atunci stocarea și redarea acestora în mod separat pot face jocul dvs. lent și lent. Soluția pentru aceasta va depinde de platforma dvs. - în Flash, puteți utiliza BitmapData.draw () pentru a stoca fiecare linie într-un singur bitmap după ce a fost desenată.
Aici am creat un demo pentru nivelul de eșantion al unui joc Spot the Difference. Verifică! Sursa completă este în Sample2.as
din descărcarea sursei.
Vă mulțumim că ați citit acest articol; Sper că ți-a dat o idee pentru a-ți construi propriul joc. Lăsați niște comentarii dacă există vreo problemă cu codul și vă voi întoarce cât mai curând posibil.