Cómo detectar cuando un objeto ha sido rodeado por un gesto

Nunca eres demasiado viejo para un juego de Spot the Difference. Recuerdo jugarlo de niño, ¡y ahora encuentro que mi esposa todavía juega ocasionalmente! En este tutorial, veremos cómo detectar cuándo se ha dibujado un anillo alrededor de un objeto, con un algoritmo que podría usarse con el mouse, el lápiz óptico o la entrada de la pantalla táctil..

Nota: Aunque las demostraciones y el código fuente de este tutorial utilizan Flash y AS3, debería poder usar las mismas técnicas y conceptos en casi cualquier entorno de desarrollo de juegos..


Vista previa del resultado final

Echemos un vistazo al resultado final en el que trabajaremos. La pantalla está dividida en dos imágenes, que son casi idénticas pero no del todo. Intenta detectar las seis diferencias y encierra en un círculo las de la imagen de la izquierda. Buena suerte!

Nota: ¡No tienes que dibujar un círculo perfecto! Solo necesitas dibujar un anillo aproximado o un bucle alrededor de cada diferencia..

¿No tienes flash? Echa un vistazo a esta demostración de vídeo:


Paso 1: El movimiento circular

Usaremos algunos cálculos vectoriales en el algoritmo. Como siempre, es bueno entender las matemáticas subyacentes antes de aplicarlas, así que aquí hay un breve repaso de las matemáticas vectoriales..

La imagen de arriba muestra el vector. UNA Desglosado a sus componentes horizontales y verticales (Hacha y , respectivamente).

Ahora veamos el producto puntual Operación, ilustrada en la imagen de abajo. Primero, verás la operación del producto punto entre los vectores A y B.

Para encontrar el ángulo intercalado entre los dos vectores, podemos hacer uso de este producto de puntos..

| A | y | B | denota las magnitudes de los vectores A y B, así dados | A | y | B | y A punto B, lo que queda desconocido es theta. Con un poco de álgebra (como se muestra en la imagen), se produce la ecuación final, que podemos usar para encontrar theta.

Para obtener más información sobre el producto vectorial de puntos, consulte la siguiente página de Wolfram.

La otra operación útil es producto cruzado. Echa un vistazo a la operación a continuación:

Esta operación es útil para determinar si el ángulo intercalado es hacia la derecha o hacia la izquierda con respecto a un vector específico.

Déjame elaborar más. Para el caso del diagrama anterior, la rotación de A a B es en el sentido de las agujas del reloj, por lo que A es B negativo. La rotación de B a A es en sentido contrario a las agujas del reloj, por lo que B en A es positivo. Tenga en cuenta que esta operación es sensible a la secuencia. Una cruz B producirá un resultado diferente de la cruz B A.

Eso no es todo. Sucede que en el espacio de coordenadas de muchas plataformas de desarrollo de juegos, el eje y se invierte (y aumenta a medida que avanzamos hacia abajo). Por lo tanto, nuestro análisis se invierte, y la cruz A es positiva mientras que la cruz A la A es negativa.

Eso es suficiente revisión. Vayamos a nuestro algoritmo..


Paso 2: Interacción en círculos

Los jugadores deberán rodear el detalle correcto en la imagen. Ahora, ¿cómo hacemos eso? Antes de responder a esta pregunta, debemos calcular el ángulo entre dos vectores. Como recordará ahora, podemos usar el producto de puntos para esto, así que implementaremos esa ecuación aquí.

Aquí hay una demostración para ilustrar lo que estamos haciendo. Arrastra cualquiera de las flechas para ver los comentarios..

Vamos a ver cómo funciona esto. En el código a continuación, simplemente he inicializado los vectores y un temporizador, y puse algunas flechas interactivas en la pantalla.

función pública Demo1 () feedback = new TextField; addChild (retroalimentación); feedback.selectable = false; feedback.autoSize = TextFieldAutoSize.LEFT; a1 = nueva flecha; addChild (a1); a2 = nueva flecha; addChild (a2); a2.rotation = 90 center = punto nuevo (stage.stageWidth >> 1, stage.stageHeight >> 1) a1.x = center.x; a1.y = centro.y; a1.name = "a1"; a2.x = center.x; a2.y = centro.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 = nuevo Vector2d (1, 0); v2 = nuevo Vector2d (0, 1); curr_vec = nuevo Vector2d (1, 0); t = nuevo temporizador (50); 

Cada 50 milisegundos, la siguiente función se ejecuta y se usa para actualizar la retroalimentación gráfica y de texto:

actualización de la función privada (e: TimerEvent): void var curr_angle: Number = Math.atan2 (mouseY - center.y, mouseX - center.x); curr_vec.angle = curr_angle; if (item == 1) // actualizar la rotación de la flecha visualmente a1.rotation = Math2.degreeOf (curr_angle); // midiendo el ángulo de a1 a b1 v1 = curr_vec.clone (); direction = v2.crossProduct (v1); feedback.text = "Ahora estás moviendo el vector rojo, A \ n"; feedback.appendText ("Ángulo medido de verde a rojo:");  else if (item == 2) a2.rotation = Math2.degreeOf (curr_angle); v2 = curr_vec.clone (); direction = v1.crossProduct (v2); feedback.text = "Ahora estás moviendo el vector verde, B \ n"; feedback.appendText ("Ángulo medido de rojo a verde:");  theta_rad = Math.acos (v1.dotProduct (v2)); // theta está en radianes theta_deg = Math2.degreeOf (theta_rad); si (direccion < 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(); 

Notará que la magnitud para v1 y v2 son 1 unidad en este escenario (verifique las líneas 52 y 53 resaltadas arriba), así que omití la necesidad de calcular la magnitud de los vectores por ahora..

Si quieres ver el código fuente completo, echa un vistazo a Demo1.as en la fuente de descarga.


Paso 3: Detectar un círculo completo

Ok, ahora que hemos entendido la idea básica, ahora la usaremos para verificar si el jugador circuló un punto con éxito.

Espero que el diagrama habla por sí mismo. El inicio de la interacción es cuando se presiona el botón del mouse, y el final de la interacción es cuando se suelta el botón del mouse.

En cada intervalo (de, por ejemplo, 0,01 segundos) durante la interacción, calcularemos el ángulo intercalado entre los vectores actuales y anteriores. Estos vectores se construyen desde la ubicación del marcador (donde está la diferencia) a la ubicación del mouse en esa instancia. Sume todos estos ángulos (t1, t2, t3 en este caso) y si el ángulo formado es de 360 ​​grados al final de la interacción, el jugador ha dibujado un círculo.

Por supuesto, puede ajustar la definición de un círculo completo para que sea de 300-340 grados, dando espacio a los errores de los jugadores al realizar el gesto del mouse..

Aquí hay una demostración de esta idea. Arrastra un gesto circular alrededor del marcador rojo en el medio. Puede mover la posición del marcador rojo usando las teclas W, A, S, D.


Paso 4: La Implementación

Examinemos la implementación de la demo. Veremos los cálculos importantes aquí..

Revise el código resaltado a continuación y cópielo con la ecuación matemática en el Paso 1. Notará que el valor para arccos a veces produce No un número (NaN) si omite la línea 92. También, constantes_valor a veces se excede de 1 debido a imprecisiones de redondeo, por lo que necesitamos devolverlo manualmente a un máximo de 1. Cualquier número de entrada para arccos más de 1 producirá un NaN.

actualización de la función privada (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 = nuevo Vector2d (mouseX - marker.x, mouseY - marker.y); // el valor del cálculo a veces excede 1 necesidad de manejar manualmente la precisión 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; // verificando la dirección de rotación total_angle + = direction * delta_angle; // agregar al ángulo acumulado hecho durante la interacción

La fuente completa para esto se puede encontrar en Demo2.as


Paso 5: El defecto

Un problema que puede ver es que mientras dibuje un círculo grande que encierre el lienzo, el marcador se considerará en un círculo. No necesito saber dónde está el marcador..

Bueno, para contrarrestar este problema, podemos verificar la proximidad del movimiento circular. Si el círculo se dibuja dentro de los límites de un cierto rango (cuyo valor está bajo su control), solo entonces se considera un éxito.

Echa un vistazo al código de abajo. Si alguna vez el usuario supera MIN_DIST (con un valor de 60 en este caso), entonces se considera una suposición aleatoria.

actualización de la función privada (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 = nuevo Vector2d (mouseX - marker.x, mouseY - marker.y); if (curr_vec.magnitude> MIN_DIST) within_bound = false; // el valor del cálculo a veces excede 1 necesidad de manejar manualmente la precisión 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; // verificando la dirección de rotación total_angle + = direction * delta_angle; // agregar al ángulo acumulado realizado durante la interacción mag_box.text = "Distancia desde el marcador:" + curr_vec.magnitude.toPrecision (4); mag_box.x = mouseX + 10; mag_box.y = mouseY + 10; feedback.text = "No vayas más allá de" + MIN_DIST

De nuevo, trata de rodear el marcador. Si crees que el MIN_DIST es un poco implacable, siempre se puede ajustar para adaptarse a la imagen.


Paso 6: Diferentes formas

¿Qué pasa si la "diferencia" no es un círculo exacto? Algunos pueden ser rectangulares, triangulares o cualquier otra forma..
En estos casos, en lugar de utilizar un solo marcador, podemos colocar algunos:

En el diagrama de arriba, se muestran dos cursores de ratón en la parte superior. Comenzando con el cursor situado más a la derecha, realizaremos un movimiento circular en el sentido de las agujas del reloj hacia el otro extremo de la izquierda. Tenga en cuenta que la ruta rodea los tres marcadores.

También he dibujado los ángulos transcurridos por este camino en cada uno de los marcadores (guiones claros a guiones oscuros). Si los tres ángulos son más de 360 ​​grados (o el valor que elija), solo entonces lo contamos como un círculo.

Pero eso no es suficiente. ¿Recuerdas la falla en el paso 4? Bueno, lo mismo ocurre aquí: tendremos que verificar la proximidad. En lugar de requerir que el gesto no exceda un radio determinado de un marcador específico, solo verificaremos si el cursor del mouse se acercó a todos los marcadores para al menos una breve instancia. Usaré pseudocódigo para explicar esta idea:

Calcule el ángulo transcurrido por el camino para el marcador 1, el marcador 2 y el marcador 3 si cada ángulo es más de 360 ​​si la proximidad de cada marcador se cruzó con el cursor del mouse, entonces el círculo formado rodea el área marcada con los marcadores endif endif

Paso 7: Demo para la Idea

Aquí, estamos usando tres puntos para representar un triángulo.

Trate de rodear alrededor:

  • un punto
  • dos puntos
  • tres puntos

... en la imagen de abajo. Tenga en cuenta que el gesto solo tiene éxito si contiene los tres puntos.

Veamos el código de esta demo. He resaltado las líneas clave para la idea a continuación; el guión completo está en Demo4.as.

Función privada handleMouse (e: MouseEvent): void if (e.type == "mouseDown") t.addEventListener (TimerEvent.TIMER, actualizar); t.start (); update_curr_vecs ();  else if (e.type == "mouseUp") t.stop (); t.removeEventListener (TimerEvent.TIMER, actualización); // verificar si se cumplieron las condiciones condition1 = true // todos los ángulos cumplen con MIN_ANGLE condition2 = true // todas las proximidades cumplen con MIN_DIST para (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(); 

Paso 8: Dibujando los círculos

El mejor método para dibujar realmente la línea que traza dependerá de su plataforma de desarrollo, por lo que simplemente describiré el método que usaríamos en Flash aquí..

Hay dos formas de dibujar líneas en AS3, como lo indica la imagen de arriba.

El primer enfoque es bastante simple: uso mover a() para mover la posición de dibujo a coordinar (10, 20). Luego dibuja una línea para conectar (10, 20) a (80, 70) usando lineTo ().

El segundo enfoque es almacenar todos los detalles en dos arreglos, comandos [] y acordes [] (con coordenadas almacenadas en (x, y) pares dentro de acordes []) y luego dibuje todos los detalles gráficos en el lienzo usando drawPath () en un solo disparo He optado por el segundo enfoque en mi demo..

Compruébalo: intenta hacer clic y arrastrar el ratón sobre el lienzo para dibujar una línea.

Y aquí está el código AS3 para esta demo. Echa un vistazo a la fuente completa en Drawing1.as.

clase pública Drawing1 extiende Sprite private var cmd: Vector.; var coords privados: vector.; privado var _thickness: Number = 2, _col: Number = 0, _alpha: Number = 1; función pública Drawing1 () // asignar controladores de eventos a mouse up y mouse down stage.addEventListener (MouseEvent.MOUSE_DOWN, mouseHandler); stage.addEventListener (MouseEvent.MOUSE_UP, mouseHandler);  / ** * Controlador de eventos del mouse * @param e evento del mouse * / private function mouseHandler (e: MouseEvent): void if (e.type == "mouseDown") // aleatoriza las propiedades de línea _thickness = Math.random () * 5; _col = Math.random () * 0xffffff; _alpha = Math.random () * 0.5 + 0.5 // inicia las variables cmd = nuevo Vector.; coords = nuevo Vector.; // primer registro de principio de línea cmd [0] = 1; coords [0] = mouseX; coords [1] = mouseY; // comienza el dibujo cuando el mouse mueve stage.addEventListener (MouseEvent.MOUSE_MOVE, mouseHandler);  else if (e.type == "mouseUp") // eliminar el controlador de movimiento del mouse una vez que se suelta el botón del mouse stage.removeEventListener (MouseEvent.MOUSE_MOVE, mouseHandler);  else if (e.type == "mouseMove") // presionando el mouse mueve el cmd.push (2); // dibujar el comando coords.push (mouseX); // coordenadas para dibujar una línea a coords.push (mouseY); redibujar (); // ejecuta el comando de dibujo / ** * Método para dibujar la (s) línea (s) como se define por el movimiento del mouse * / private function redraw (): void graphics.clear (); // borrando todos los dibujos de gráficos anteriores. estilo de línea (grosor, color, _alpha); graphics.drawPath (cmd, coords); 

En Flash, utilizando el gráficos Objeto para dibujar como este usa. modo de reproducción retenido, lo que significa que las propiedades de las líneas individuales se almacenan por separado, a diferencia de reproducción de modo inmediato, donde solo se almacena la imagen final. (Los mismos conceptos existen en otras plataformas de desarrollo; por ejemplo, en HTML5, dibujar en SVG usa el modo retenido, mientras que dibujar en lienzo usa el modo inmediato).

Si hay muchas líneas en la pantalla, entonces almacenarlas y volver a renderizarlas todas por separado puede hacer que su juego sea lento y lento. La solución dependerá de su plataforma: en Flash, puede usar BitmapData.draw () para almacenar cada línea en un solo mapa de bits una vez que se haya dibujado.


Paso 9: Nivel de muestra

Aquí he creado una demostración para el nivel de muestra de un juego Spot the Difference. ¡Echale un vistazo! La fuente completa está en Muestra2.as de la fuente de descarga.

Conclusión

Gracias por leer este artículo; Espero que te haya dado una idea para construir tu propio juego. Deje algunos comentarios si hay algún problema con el código y le responderé lo antes posible..