Advertisement
  1. Code
  2. Coding Fundamentals
  3. Game Development

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

Scroll to top
Read Time: 13 min

() translation by (you can also view the original English article)

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 se podría usar 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.

Vector definitionVector definitionVector definition

La imagen de arriba muestra el vector A desglosado en sus componentes horizontal y vertical (Ax y Ay, respectivamente).

Ahora veamos la operación del producto punto, ilustrada en la siguiente imagen. Primero, verá la operación del producto de puntos entre los vectores A y B.

Vector mathVector mathVector math

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 vector dot, consulte la siguiente página de Wolfram.

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

Cross vector

Esta operación es útil para encontrar 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 B cruz 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 A cruz B será positivo mientras que B cruz A es negativo.

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 punto para esto, así que implementaremos esa ecuación aquí.

Aquí hay una demostración para ilustrar lo que estamos haciendo. Arrastre 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.

1
public function Demo1() 
2
{
3
    feedback = new TextField; addChild(feedback);
4
    feedback.selectable = false;
5
    feedback.autoSize = TextFieldAutoSize.LEFT;
6
    
7
    a1 = new Arrow; addChild(a1); 
8
    a2 = new Arrow; addChild(a2); a2.rotation = 90
9
    
10
    center = new Point(stage.stageWidth >> 1, stage.stageHeight >> 1)
11
    a1.x = center.x;  a1.y = center.y; a1.name = "a1";
12
    a2.x = center.x;  a2.y = center.y; a2.name = "a2";
13
    
14
    a1.transform.colorTransform = new ColorTransform(0, 0, 0, 1, 255);
15
    a2.transform.colorTransform = new ColorTransform(0, 0, 0, 1, 0, 255);
16
    
17
    a1.addEventListener(MouseEvent.MOUSE_DOWN, handleMouse);
18
    a2.addEventListener(MouseEvent.MOUSE_DOWN, handleMouse);
19
    stage.addEventListener(MouseEvent.MOUSE_UP, handleMouse);
20
    
21
    v1 = new Vector2d(1, 0);
22
    v2 = new Vector2d(0, 1);
23
    curr_vec = new Vector2d(1, 0);
24
    
25
    t = new Timer(50);
26
}

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

1
private function update(e:TimerEvent):void 
2
{
3
    var curr_angle:Number = Math.atan2(mouseY - center.y, mouseX - center.x);
4
    curr_vec.angle = curr_angle;
5
    
6
    if (item == 1) {
7
        //update arrow's rotation visually

8
        a1.rotation = Math2.degreeOf(curr_angle);
9
        
10
        //measuring the angle from a1 to b1

11
        v1 = curr_vec.clone();
12
        direction = v2.crossProduct(v1);
13
        feedback.text = "You are now moving the red vector, A \n";
14
        feedback.appendText("Angle measured from green to red: ");
15
    }
16
    else if (item == 2) {
17
        a2.rotation = Math2.degreeOf(curr_angle);
18
        
19
        v2 = curr_vec.clone();
20
        direction = v1.crossProduct(v2);
21
        feedback.text = "You are now moving the green vector, B\n";
22
        feedback.appendText("Angle measured from red to green: ");
23
    }
24
    
25
    theta_rad = Math.acos(v1.dotProduct(v2));  //theta is in radians

26
    theta_deg = Math2.degreeOf(theta_rad);
27
    if (direction < 0) {
28
        feedback.appendText("-" + theta_deg.toPrecision(4) + "\n");
29
        feedback.appendText("rotation is anti clockwise")
30
    }
31
    else {
32
        feedback.appendText(theta_deg.toPrecision(4) + "\n");
33
        feedback.appendText("rotation is clockwise")
34
    }
35
    drawSector();
36
}

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

Si desea ver el código fuente completo, consulte Demo1.as en la descarga de la fuente.


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.

Calculate the angles

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, entonces 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 del jugador 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í.

Verifique 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 Not a Number (NaN) si salta la línea 92. Además, constants_value a veces 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.

1
private function update(e:TimerEvent):void 
2
{
3
    graphics.clear();
4
    graphics.lineStyle(1)
5
    graphics.moveTo(marker.x, marker.y);
6
    graphics.lineTo(mouseX, mouseY);
7
    
8
    prev_vec = curr_vec.clone();
9
    curr_vec = new Vector2d(mouseX - marker.x, mouseY - marker.y);
10
    //value of calculation sometimes exceed 1 need to manually handle the precission

11
    var constants_value:Number = Math.min(1, 
12
                                prev_vec.dotProduct(curr_vec) / (prev_vec.magnitude * curr_vec.magnitude)
13
                                );
14
    var delta_angle:Number = Math.acos(constants_value)	//angle made

15
    var direction:Number = prev_vec.crossProduct(curr_vec) > 0? 1: -1;	//checking the direction of rotation

16
    total_angle += direction * delta_angle;	//add to the cumulative angle made during the interaction

17
}

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.

1
private function update(e:TimerEvent):void 
2
{
3
    graphics.clear();
4
    graphics.lineStyle(1)
5
    graphics.moveTo(marker.x, marker.y);
6
    graphics.lineTo(mouseX, mouseY);
7
    
8
    prev_vec = curr_vec.clone();
9
    curr_vec = new Vector2d(mouseX - marker.x, mouseY - marker.y);
10
    
11
    if (curr_vec.magnitude > MIN_DIST) within_bound = false;
12
    
13
    //value of calculation sometimes exceed 1 need to manually handle the precission

14
    var constants_value:Number = Math.min(1, 
15
                                prev_vec.dotProduct(curr_vec) / (prev_vec.magnitude * curr_vec.magnitude)
16
                                );
17
    var delta_angle:Number = Math.acos(constants_value)	//angle made

18
    var direction:Number = prev_vec.crossProduct(curr_vec) > 0? 1: -1;	//checking the direction of rotation

19
    total_angle += direction * delta_angle;	//add to the cumulative angle made during the interaction

20
    
21
    mag_box.text = "Distance from marker: " + curr_vec.magnitude.toPrecision(4);
22
    mag_box.x = mouseX + 10;
23
    mag_box.y = mouseY + 10;
24
    feedback.text = "Do not go beyond "+MIN_DIST
25
}

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:

Circling multiple cirlceCircling multiple cirlceCircling multiple cirlce

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:

1
Calculate angle elapsed by path for marker1, marker2 and marker3
2
3
if each angle is more than 360
4
	if each marker's proximity was crossed by mouse cursor
5
		then the circle made is surrounding the area marked by markers
6
	endif
7
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 script completo está en Demo4.as.

1
private function handleMouse(e:MouseEvent):void 
2
{
3
    if (e.type == "mouseDown") {
4
        t.addEventListener(TimerEvent.TIMER, update);
5
        t.start();
6
        update_curr_vecs();
7
    }
8
    else if (e.type == "mouseUp") {
9
        t.stop();
10
        t.removeEventListener(TimerEvent.TIMER, update);
11
        
12
        //check if conditions were met

13
        condition1 = true	//all angles are meeting MIN_ANGLE

14
        condition2 = true	//all proximities are meeting MIN_DIST

15
        
16
        for (var i:int = 0; i < markers.length; i++) {
17
            if (Math.abs(angles[i])< MIN_ANGLE) {
18
                condition1 = false;
19
                break;
20
            }
21
            if (proximity[i] == false) {
22
                condition2 = false;
23
                break
24
            }
25
        }
26
        
27
        if (condition1 && condition2) {
28
            box.text="Attempt to circle the item is successful"
29
        }
30
        else {
31
            box.text="Failure"
32
        }
33
        
34
        reset_vecs();
35
        reset_values();
36
    }
37
}
38
39
private function update(e:TimerEvent):void 
40
{
41
    update_prev_vecs();
42
    update_curr_vecs();
43
    update_values();
44
}

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 solo describiré el método que usaríamos en Flash aquí.

Actionscript drawing APIActionscript drawing APIActionscript drawing API

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

El primer enfoque es bastante simple: use moveTo () 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 matrices, commands [] y coords [] (con las coordenadas almacenadas en pares (x, y) dentro de coords []) y luego dibujar todos los detalles gráficos en el lienzo usando drawPath () en un sola Disparo. He optado por el segundo enfoque en mi demo.

Compruébelo: intente hacer clic y arrastrar el mouse 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.

1
public class Drawing1 extends Sprite
2
{
3
    private var cmd:Vector.<int>;
4
    private var coords:Vector.<Number>;
5
    private var _thickness:Number = 2, _col:Number = 0, _alpha:Number = 1;
6
    
7
    public function Drawing1() 
8
    {
9
        //assign event handlerst to mouse up and mouse down

10
        stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseHandler);
11
        stage.addEventListener(MouseEvent.MOUSE_UP, mouseHandler);
12
    }
13
    
14
    /**

15
     * Mouse event handler

16
     * @param	e	mouse event

17
     */
18
    private function mouseHandler(e:MouseEvent):void 
19
    {
20
        if (e.type == "mouseDown") {
21
            //randomise the line properties

22
            _thickness = Math.random() * 5;
23
            _col = Math.random() * 0xffffff;
24
            _alpha = Math.random() * 0.5 + 0.5
25
            
26
            //initiate the variables

27
            cmd = new Vector.<int>;
28
            coords = new Vector.<Number>;
29
            
30
            //first registration of line beginning

31
            cmd[0] = 1;
32
            coords[0] = mouseX;
33
            coords[1] = mouseY;
34
            
35
            //start the drawing when mouse move

36
            stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseHandler);
37
        }
38
        else if (e.type == "mouseUp") {
39
            //remove the mouse move handler once mouse button is released

40
            stage.removeEventListener(MouseEvent.MOUSE_MOVE, mouseHandler);
41
        }
42
        else if (e.type == "mouseMove") {
43
            //pushing into the mouse move the

44
            cmd.push(2);			//draw command

45
            coords.push(mouseX);	//coordinates to draw line to

46
            coords.push(mouseY);
47
            redraw();				//execute the drawing command

48
        }
49
    }
50
    /**

51
     * Method to draw the line(s) as defined by mouse movement

52
     */
53
    private function redraw():void {
54
        graphics.clear();	//clearing all previous drawing 

55
        graphics.lineStyle(_thickness, _col, _alpha);
56
        graphics.drawPath(cmd, coords);
57
    }
58
    
59
}

En Flash, el uso del objeto graphics para dibujar de esta manera utiliza el procesamiento en modo retenido, lo que significa que las propiedades de las líneas individuales se almacenan por separado, a diferencia del procesamiento en 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 Sample2.as de la descarga de la fuente.

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.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.